From 4ceac4f167308448404bb625a1dea9b638a7586f Mon Sep 17 00:00:00 2001 From: "R. Rajesh Jeba Anbiah" Date: Mon, 20 Jul 2015 13:43:47 +0530 Subject: [PATCH] Restyaboard v0.1.1 launched --- Gruntfile.js | 221 + LICENSE.txt | 47 + README.md | 82 +- api_explorer/api-docs/activities_listing.json | 53 + api_explorer/api-docs/boards.json | 668 + api_explorer/api-docs/card_attachments.json | 67 + api_explorer/api-docs/card_voters.json | 210 + api_explorer/api-docs/cards.json | 748 + api_explorer/api-docs/cards_labels.json | 67 + api_explorer/api-docs/checklist_items.json | 225 + api_explorer/api-docs/checklists.json | 204 + api_explorer/api-docs/comments.json | 204 + api_explorer/api-docs/index.php | 94 + api_explorer/api-docs/lists.json | 323 + api_explorer/api-docs/organizations.json | 329 + api_explorer/api-docs/users.json | 418 + api_explorer/css/highlight.default.css | 135 + api_explorer/css/screen.css | 1070 ++ api_explorer/images/logo_small.png | Bin 0 -> 770 bytes api_explorer/images/pet_store_api.png | Bin 0 -> 824 bytes api_explorer/images/throbber.gif | Bin 0 -> 9257 bytes api_explorer/images/wordnik_api.png | Bin 0 -> 980 bytes api_explorer/index.html | 111 + api_explorer/lib/backbone-min.js | 38 + api_explorer/lib/handlebars-1.0.0.js | 2278 ++++ api_explorer/lib/highlight.7.3.pack.js | 1 + api_explorer/lib/jquery-1.8.0.min.js | 2 + api_explorer/lib/jquery.ba-bbq.min.js | 18 + api_explorer/lib/jquery.slideto.min.js | 1 + api_explorer/lib/jquery.wiggle.min.js | 8 + api_explorer/lib/shred.bundle.js | 2765 ++++ api_explorer/lib/shred/content.js | 193 + api_explorer/lib/swagger.js | 1291 ++ api_explorer/lib/underscore-min.js | 32 + api_explorer/swagger-ui.js | 2121 +++ api_explorer/swagger-ui.min.js | 1 + client/.gitignore | 0 client/apple-touch-icon-114x114.png | Bin 0 -> 2555 bytes client/apple-touch-icon-72x72.png | Bin 0 -> 1734 bytes client/apple-touch-icon.png | Bin 0 -> 1226 bytes client/css/alerts.less | 67 + client/css/animate.less | 3813 ++++++ client/css/badges.less | 55 + client/css/bootstrap-datetimepicker.min.css | 418 + client/css/bootstrap.less | 60 + client/css/breadcrumbs.less | 26 + client/css/button-groups.less | 226 + client/css/buttons.less | 159 + client/css/carousel.less | 232 + client/css/chosen.css | 408 + client/css/close.less | 33 + client/css/code.less | 63 + client/css/component-animations.less | 29 + client/css/css-avatars.less | 102 + client/css/custom-responsive.less | 190 + client/css/custom.less | 1354 ++ client/css/dropdowns.less | 213 + client/css/flag.css | 260 + client/css/font-awesome-ie7.less | 1200 ++ client/css/font-awesome.less | 1479 ++ client/css/forms.less | 438 + client/css/fullcalendar.css | 633 + client/css/glyphicons.less | 233 + client/css/grid.less | 84 + client/css/input-groups.less | 162 + client/css/jquery-ui.css | 642 + client/css/jquery.dockmodal.css | 131 + client/css/jquery.fileupload-ui.css | 62 + client/css/jumbotron.less | 44 + client/css/labels.less | 64 + client/css/list-group.less | 110 + client/css/media.less | 56 + client/css/mixins.less | 929 ++ client/css/modals.less | 139 + client/css/navbar.less | 616 + client/css/navs.less | 242 + client/css/normalize.less | 423 + client/css/pager.less | 55 + client/css/pagination.less | 88 + client/css/panels.less | 241 + client/css/popovers.less | 133 + client/css/print.less | 101 + client/css/progress-bars.less | 80 + client/css/responsive-utilities.less | 92 + client/css/scaffolding.less | 134 + client/css/select2-bootstrap.css | 687 + client/css/tables.less | 233 + client/css/theme.less | 247 + client/css/thumbnails.less | 36 + client/css/tooltip.less | 95 + client/css/type.less | 293 + client/css/utilities.less | 56 + client/css/variables.less | 829 ++ client/css/wells.less | 29 + client/font/FontAwesome.otf | Bin 0 -> 61896 bytes client/font/fontawesome-corp-webfont.eot | Bin 0 -> 5854 bytes client/font/fontawesome-corp-webfont.svg | 179 + client/font/fontawesome-corp-webfont.ttf | Bin 0 -> 5640 bytes client/font/fontawesome-corp-webfont.woff | Bin 0 -> 9864 bytes client/font/fontawesome-webfont.eot | Bin 0 -> 37405 bytes client/font/fontawesome-webfont.svg | 399 + client/font/fontawesome-webfont.ttf | Bin 0 -> 79076 bytes client/font/fontawesome-webfont.woff | Bin 0 -> 43572 bytes client/img/default-organization.png | Bin 0 -> 5304 bytes client/img/favicon.ico | Bin 0 -> 32988 bytes client/img/flags.png | Bin 0 -> 83188 bytes client/img/logo-icon-offline.png | Bin 0 -> 755 bytes client/img/logo-icon-sync.gif | Bin 0 -> 1068 bytes client/img/logo-icon.png | Bin 0 -> 558 bytes client/img/logo.png | Bin 0 -> 4461 bytes client/img/logo.svg | 120 + client/img/select2-spinner.gif | Bin 0 -> 1849 bytes client/img/select2.png | Bin 0 -> 613 bytes client/img/select2x2.png | Bin 0 -> 845 bytes client/img/split.gif | Bin 0 -> 830 bytes client/img/star-load.gif | Bin 0 -> 1060 bytes client/index.html | 297 + client/js/application.js | 445 + client/js/collections/acl_collection.js | 12 + client/js/collections/activity_collection.js | 12 + .../js/collections/attachment_collection.js | 17 + client/js/collections/board_collection.js | 22 + .../js/collections/board_star_collection.js | 12 + .../board_subscriber_collection.js | 12 + .../js/collections/boards_user_collection.js | 28 + .../collections/card_attachment_collection.js | 12 + .../collections/card_checklist_collection.js | 35 + client/js/collections/card_collection.js | 53 + .../js/collections/card_label_collection.js | 12 + .../collections/card_position_collection.js | 12 + .../collections/card_subscriber_collection.js | 12 + client/js/collections/card_user_collection.js | 12 + .../js/collections/card_voter_collection.js | 12 + .../collections/checklist_item_collection.js | 35 + .../js/collections/closed_board_collection.js | 12 + .../collections/elasticsearch_collection.js | 12 + .../collections/email_template_collection.js | 12 + client/js/collections/flickr_collection.js | 13 + client/js/collections/list_collection.js | 33 + .../collections/list_subscriber_collection.js | 12 + .../js/collections/organization_collection.js | 47 + .../organizations_user_collection.js | 17 + client/js/collections/role_collection.js | 12 + .../collections/role_settings_collection.js | 12 + .../setting_category_collection.js | 16 + client/js/collections/user_collection.js | 52 + .../workflow_template_collection.js | 12 + client/js/common.js | 125 + client/js/libs/ImageSelect.jquery.js | 175 + client/js/libs/Markdown.Converter.js | 1332 ++ client/js/libs/affix.js | 137 + .../js/libs/backbone.defered-view-loader.js | 174 + client/js/libs/backbone.dualstorage.js | 846 ++ client/js/libs/backbone.js | 1616 +++ client/js/libs/backbone.rails.js | 71 + client/js/libs/backbone.stickit.js | 537 + client/js/libs/backbone.upload-manager.js | 479 + client/js/libs/bootstrap-alert.js | 88 + client/js/libs/bootstrap-carousel.js | 205 + client/js/libs/bootstrap-collapse.js | 170 + client/js/libs/bootstrap-datepicker.js | 401 + .../js/libs/bootstrap-datetimepicker.min.js | 1789 +++ client/js/libs/bootstrap-dropdown.js | 165 + client/js/libs/bootstrap-modal.js | 245 + client/js/libs/bootstrap-popover.js | 114 + client/js/libs/bootstrap-tab.js | 144 + client/js/libs/bootstrap-tooltip.js | 353 + client/js/libs/bootstrap-transition.js | 48 + client/js/libs/bootstrap-twipsy.js | 307 + client/js/libs/chosen.jquery.min.js | 2 + client/js/libs/date.format.js | 125 + client/js/libs/favico-0.3.8.min.js | 7 + client/js/libs/fullcalendar.min.js | 113 + client/js/libs/i18n.js | 531 + client/js/libs/jquery-1.8.3.js | 9472 +++++++++++++ client/js/libs/jquery-ui-1.8.23.js | 11371 ++++++++++++++++ client/js/libs/jquery.bootstrap-growl.js | 77 + client/js/libs/jquery.dockmodal.js | 446 + client/js/libs/jquery.drawDoughnutChart.js | 301 + client/js/libs/jquery.fileupload-image.js | 309 + client/js/libs/jquery.fileupload-process.js | 164 + client/js/libs/jquery.fileupload-ui.js | 647 + client/js/libs/jquery.fileupload-validate.js | 117 + client/js/libs/jquery.fileupload.js | 1315 ++ client/js/libs/jquery.gritter.min.js | 29 + client/js/libs/jquery.iframe-transport.js | 205 + client/js/libs/jquery.scrollTo-min.js | 11 + client/js/libs/jquery.timeago.js | 214 + client/js/libs/jquery.ui.touch-punch.min.js | 11 + client/js/libs/jquery.ui.widget.js | 530 + client/js/libs/less-1.3.0.min.js | 9 + client/js/libs/load-image.min.js | 1 + client/js/libs/locale.js | 29 + client/js/libs/md5.js | 174 + client/js/libs/musical.js | 1700 +++ client/js/libs/select2.js | 3485 +++++ client/js/libs/select2.min.js | 23 + client/js/libs/showdown.js | 1457 ++ client/js/libs/splitter.js | 5939 ++++++++ client/js/libs/tag-it.js | 386 + client/js/libs/tmpl.min.js | 86 + client/js/libs/underscore.js | 1396 ++ client/js/models/acl.js | 14 + client/js/models/activity.js | 18 + client/js/models/board.js | 53 + client/js/models/boards_star.js | 14 + client/js/models/boards_subscriber.js | 14 + client/js/models/boards_user.js | 12 + client/js/models/card.js | 75 + client/js/models/card_attachment.js | 14 + client/js/models/card_subscriber.js | 12 + client/js/models/card_user.js | 12 + client/js/models/card_voter.js | 12 + client/js/models/checklist.js | 53 + client/js/models/checklist_item.js | 52 + client/js/models/elasticsearch.js | 10 + client/js/models/email_template.js | 10 + client/js/models/flickr.js | 15 + client/js/models/instant_card_add.js | 10 + client/js/models/label.js | 15 + client/js/models/list.js | 67 + client/js/models/list_subscriber.js | 10 + client/js/models/oauth.js | 14 + client/js/models/organization.js | 16 + client/js/models/organizations_user.js | 10 + client/js/models/role.js | 10 + client/js/models/role_setting.js | 10 + client/js/models/setting_category.js | 10 + client/js/models/user.js | 24 + client/js/models/workflow_template.js | 10 + client/js/templates/about_us.jst.ejs | 31 + client/js/templates/activity.jst.ejs | 106 + client/js/templates/activity_add_form.jst.ejs | 1 + .../js/templates/activity_card_search.jst.ejs | 5 + .../templates/activity_delete_confirm.jst.ejs | 1 + client/js/templates/activity_index.jst.ejs | 5 + .../js/templates/activity_reply_form.jst.ejs | 1 + .../activity_user_add_search_result.jst.ejs | 6 + .../js/templates/admin_activity_index.jst.ejs | 100 + client/js/templates/admin_user_add.jst.ejs | 25 + client/js/templates/archived_card.jst.ejs | 5 + client/js/templates/archived_cards.jst.ejs | 1 + client/js/templates/archived_items.jst.ejs | 1 + client/js/templates/archived_list.jst.ejs | 5 + client/js/templates/archived_lists.jst.ejs | 1 + client/js/templates/attachment.jst.ejs | 40 + .../attachment_delete_confirm.jst.ejs | 1 + .../attachment_delete_confirm_form.jst.ejs | 1 + client/js/templates/board.jst.ejs | 54 + client/js/templates/board_404.jst.ejs | 5 + client/js/templates/board_add.jst.ejs | 41 + .../board_add_organization_form.jst.ejs | 2 + .../board_additional_settings.jst.ejs | 1 + client/js/templates/board_background.jst.ejs | 207 + .../templates/board_custom_background.jst.ejs | 1 + client/js/templates/board_filter.jst.ejs | 51 + client/js/templates/board_header.jst.ejs | 288 + client/js/templates/board_index.jst.ejs | 7 + .../js/templates/board_index_header.jst.ejs | 21 + .../board_member_add_search_result.jst.ejs | 13 + .../templates/board_organization_form.jst.ejs | 22 + client/js/templates/board_sidebar.jst.ejs | 73 + client/js/templates/board_simple_view.jst.ejs | 82 + .../js/templates/board_user_actions.jst.ejs | 47 + .../js/templates/board_user_activity.jst.ejs | 1 + .../board_user_remove_confirm.jst.ejs | 19 + client/js/templates/board_users_view.jst.ejs | 17 + client/js/templates/board_visibility.jst.ejs | 19 + client/js/templates/card.jst.ejs | 94 + client/js/templates/card_actions.jst.ejs | 1 + client/js/templates/card_add.jst.ejs | 23 + client/js/templates/card_attachment.jst.ejs | 31 + client/js/templates/card_checklist.jst.ejs | 39 + .../js/templates/card_checklist_item.jst.ejs | 10 + client/js/templates/card_copy.jst.ejs | 1 + client/js/templates/card_duedate_from.jst.ejs | 1 + client/js/templates/card_label.jst.ejs | 1 + client/js/templates/card_label_form.jst.ejs | 4 + client/js/templates/card_labels_form.jst.ejs | 1 + client/js/templates/card_list_view.jst.ejs | 32 + client/js/templates/card_member_form.jst.ejs | 39 + .../js/templates/card_positions_form.jst.ejs | 46 + .../js/templates/card_search_result.jst.ejs | 1 + .../card_search_users_result.jst.ejs | 18 + client/js/templates/card_voters_list.jst.ejs | 25 + client/js/templates/change_password.jst.ejs | 25 + client/js/templates/chat.jst.ejs | 1 + client/js/templates/checklist_actions.jst.ejs | 1 + .../js/templates/checklist_add_form.jst.ejs | 32 + .../checklist_delete_confirm_form.jst.ejs | 8 + .../js/templates/checklist_edit_form.jst.ejs | 12 + .../templates/checklist_item_actions.jst.ejs | 9 + .../templates/checklist_item_add_form.jst.ejs | 16 + .../templates/checklist_item_add_link.jst.ejs | 4 + ...checklist_item_delete_confirm_form.jst.ejs | 8 + .../checklist_item_edit_form.jst.ejs | 44 + .../checklist_item_mention_member.jst.ejs | 16 + ...st_item_mention_member_search_form.jst.ejs | 21 + .../js/templates/closed_boards_index.jst.ejs | 6 + .../templates/closed_boards_listing.jst.ejs | 45 + .../templates/copy_board_visibility.jst.ejs | 10 + client/js/templates/copy_card.jst.ejs | 96 + .../templates/copy_from_existing_card.jst.ejs | 66 + client/js/templates/copy_list.jst.ejs | 17 + .../js/templates/edit_activity_form.jst.ejs | 8 + ...t_board_member_permission_to_admin.jst.ejs | 13 + ..._board_member_permission_to_normal.jst.ejs | 14 + client/js/templates/email_templates.jst.ejs | 75 + client/js/templates/error_404.jst.ejs | 1 + client/js/templates/flickr.jst.ejs | 10 + client/js/templates/footer.jst.ejs | 199 + client/js/templates/header.jst.ejs | 143 + client/js/templates/instant_card_add.jst.ejs | 97 + .../instant_card_add_labels_form.jst.ejs | 7 + .../instant_card_add_members_form.jst.ejs | 1 + client/js/templates/list.jst.ejs | 35 + client/js/templates/list_actions.jst.ejs | 42 + client/js/templates/list_add.jst.ejs | 15 + .../js/templates/list_archive_confirm.jst.ejs | 8 + .../list_cards_archive_confirm.jst.ejs | 8 + .../js/templates/list_delete_confirm.jst.ejs | 8 + client/js/templates/login.jst.ejs | 44 + .../js/templates/modal_activity_view.jst.ejs | 18 + .../templates/modal_card_member_form.jst.ejs | 17 + client/js/templates/modal_card_view.jst.ejs | 524 + .../templates/modal_flickr_photo_view.jst.ejs | 25 + client/js/templates/modal_list_view.jst.ejs | 13 + client/js/templates/modal_music_view.jst.ejs | 37 + .../modal_user_activities_list_view.jst.ejs | 17 + client/js/templates/move_card.jst.ejs | 51 + .../js/templates/move_cards_from_list.jst.ejs | 17 + client/js/templates/move_list.jst.ejs | 43 + client/js/templates/my_boards_listing.jst.ejs | 43 + client/js/templates/notification_menu.jst.ejs | 2 + client/js/templates/organization_add.jst.ejs | 20 + .../js/templates/organization_board.jst.ejs | 76 + .../organization_delete_form.jst.ejs | 14 + .../js/templates/organization_header.jst.ejs | 69 + ...ization_member_confirm_remove_form.jst.ejs | 13 + ...rganization_member_permission_form.jst.ejs | 49 + .../organization_member_remove_form.jst.ejs | 6 + client/js/templates/organization_view.jst.ejs | 48 + .../organization_visibility_form.jst.ejs | 17 + .../organizations_board_form_view.jst.ejs | 33 + .../templates/organizations_list_view.jst.ejs | 111 + .../organizations_lists_header.jst.ejs | 23 + .../organizations_lists_view.jst.ejs | 9 + .../templates/organizations_user_view.jst.ejs | 157 + client/js/templates/role_settings.jst.ejs | 249 + client/js/templates/roles.jst.ejs | 15 + client/js/templates/search_result.jst.ejs | 18 + .../templates/select_board_visibility.jst.ejs | 10 + .../selected_board_visibility.jst.ejs | 12 + client/js/templates/setting_list.jst.ejs | 85 + .../js/templates/show_all_visibility.jst.ejs | 29 + .../show_board_member_permission_form.jst.ejs | 18 + .../templates/show_board_visibility.jst.ejs | 5 + client/js/templates/show_copy_board.jst.ejs | 57 + .../js/templates/show_search_boards.jst.ejs | 16 + .../js/templates/show_search_message.jst.ejs | 1 + .../show_sync_google_calendar.jst.ejs | 16 + .../js/templates/starred_boards_index.jst.ejs | 6 + .../templates/started_boards_listing.jst.ejs | 27 + .../js/templates/switch_to_list_form.jst.ejs | 30 + client/js/templates/user.jst.ejs | 145 + client/js/templates/user_activity.jst.ejs | 77 + .../js/templates/user_activity_menu.jst.ejs | 3 + client/js/templates/user_board_list.jst.ejs | 20 + .../user_boards_listing_menu.jst.ejs | 15 + client/js/templates/user_cards.jst.ejs | 66 + .../js/templates/user_index_container.jst.ejs | 5 + .../js/templates/user_search_result.jst.ejs | 15 + client/js/templates/user_view.jst.ejs | 89 + client/js/templates/user_view_header.jst.ejs | 13 + .../templates/users_forgot_password.jst.ejs | 23 + client/js/templates/users_register.jst.ejs | 39 + client/js/views/about_us_view.js | 38 + client/js/views/activity_add_form_view.js | 45 + client/js/views/activity_card_search_view.js | 43 + .../js/views/activity_delete_confirm_view.js | 43 + client/js/views/activity_index_view.js | 126 + client/js/views/activity_reply_form_view.js | 45 + .../activity_user_add_search_result_view.js | 44 + client/js/views/activity_view.js | 162 + client/js/views/admin_activity_index_view.js | 75 + client/js/views/admin_user_add_view.js | 80 + client/js/views/application_view.js | 772 ++ client/js/views/archived_card_view.js | 69 + client/js/views/archived_cards_view.js | 44 + client/js/views/archived_items_view.js | 43 + client/js/views/archived_list_view.js | 69 + client/js/views/archived_lists_view.js | 44 + .../attachment_delete_confirm_form_view.js | 43 + .../views/attachment_delete_confirm_view.js | 43 + client/js/views/attachment_view.js | 86 + client/js/views/board_404_view.js | 42 + .../views/board_add_organization_form_view.js | 44 + client/js/views/board_add_view.js | 125 + .../js/views/board_additional_setting_view.js | 45 + client/js/views/board_background_view.js | 43 + .../js/views/board_custom_background_view.js | 44 + client/js/views/board_filter_view.js | 48 + client/js/views/board_header_view.js | 1705 +++ client/js/views/board_index_header_view.js | 68 + .../board_member_add_search_result_view.js | 70 + .../js/views/board_organization_form_view.js | 48 + client/js/views/board_sidebar_view.js | 46 + client/js/views/board_simple_view.js | 341 + client/js/views/board_user_actions_view.js | 158 + client/js/views/board_user_activity_view.js | 43 + .../views/board_user_remove_confirm_view.js | 43 + client/js/views/board_view.js | 1154 ++ client/js/views/board_visibility_view.js | 44 + client/js/views/boards_index_view.js | 74 + client/js/views/boards_user_view.js | 85 + client/js/views/card_actions_view.js | 44 + client/js/views/card_attachment_view.js | 93 + client/js/views/card_checklist_item_view.js | 431 + client/js/views/card_checklist_view.js | 586 + client/js/views/card_copy_view.js | 44 + client/js/views/card_duedate_from_view.js | 45 + client/js/views/card_label_form_view.js | 43 + client/js/views/card_label_view.js | 63 + client/js/views/card_labels_form_view.js | 43 + client/js/views/card_member_form_view.js | 43 + client/js/views/card_positions_form_view.js | 45 + client/js/views/card_search_result_view.js | 44 + .../js/views/card_search_users_result_view.js | 46 + client/js/views/card_view.js | 563 + client/js/views/card_voters_list_view.js | 43 + client/js/views/chat_view.js | 42 + client/js/views/checklist_actions_view.js | 48 + client/js/views/checklist_add_form_view.js | 47 + .../checklist_delete_confirm_form_view.js | 43 + client/js/views/checklist_edit_form_view.js | 47 + .../js/views/checklist_item_actions_view.js | 43 + .../js/views/checklist_item_add_form_view.js | 48 + .../js/views/checklist_item_add_link_view.js | 42 + ...checklist_item_delete_confirm_form_view.js | 43 + .../js/views/checklist_item_edit_form_view.js | 47 + ...st_item_mention_member_search_form_view.js | 41 + .../checklist_item_mention_member_view.js | 44 + client/js/views/closed_boards_index_view.js | 74 + client/js/views/closed_boards_listing_view.js | 100 + client/js/views/copy_board_visibility_view.js | 43 + client/js/views/copy_card_view.js | 45 + .../js/views/copy_from_existing_card_view.js | 46 + client/js/views/copy_list_view.js | 43 + client/js/views/edit_activity_form_view.js | 44 + ..._board_member_permission_to_adminl_view.js | 44 + ..._board_member_permission_to_normal_view.js | 44 + client/js/views/email_template_view.js | 88 + client/js/views/error_404_view.js | 42 + client/js/views/flickr_view.js | 83 + client/js/views/footer_view.js | 1513 ++ client/js/views/header_view.js | 121 + .../instant_card_add_labels_form_view.js | 44 + .../instant_card_add_members_form_view.js | 42 + client/js/views/instant_card_add_view.js | 438 + client/js/views/list_actions_view.js | 48 + client/js/views/list_archive_confirm_view.js | 43 + .../views/list_cards_archive_confirm_view.js | 43 + client/js/views/list_delete_confirm_view.js | 43 + client/js/views/list_view.js | 1271 ++ client/js/views/login_view.js | 108 + client/js/views/modal_activity_view.js | 162 + client/js/views/modal_board_view.js | 114 + .../js/views/modal_card_member_form_view.js | 48 + client/js/views/modal_card_view.js | 2593 ++++ client/js/views/modal_flickr_photo_view.js | 251 + client/js/views/modal_list_view.js | 107 + client/js/views/modal_music_view.js | 113 + .../views/modal_user_activities_list_view.js | 109 + client/js/views/move_card_view.js | 45 + client/js/views/move_cards_from_list_view.js | 47 + client/js/views/move_list_view.js | 47 + client/js/views/music_repeat_view.js | 47 + client/js/views/my_boards_listing_view.js | 44 + client/js/views/notification_menu_view.js | 44 + client/js/views/organization_add_view.js | 69 + client/js/views/organization_board_view.js | 260 + .../js/views/organization_delete_form_view.js | 86 + client/js/views/organization_header_view.js | 167 + ...ization_member_confirm_remove_form_view.js | 43 + ...rganization_member_permission_form_view.js | 43 + .../organization_member_remove_form_view.js | 44 + client/js/views/organization_view.js | 351 + .../organization_visibility_form_view.js | 44 + .../js/views/organizations_board_form_view.js | 48 + client/js/views/organizations_list_view.js | 103 + .../views/organizations_lists_header_view.js | 78 + client/js/views/organizations_lists_view.js | 73 + client/js/views/organizations_user_view.js | 183 + client/js/views/register_view.js | 74 + client/js/views/role_index_view.js | 48 + client/js/views/role_settings_view.js | 72 + .../js/views/search_board_subscribe_view.js | 39 + client/js/views/search_result_view.js | 44 + .../js/views/select_board_visibility_view.js | 43 + .../views/selected_board_visibility_view.js | 44 + client/js/views/setting_view.js | 94 + client/js/views/show_all_visibility_view.js | 45 + .../show_board_member_permission_form_view.js | 43 + client/js/views/show_board_visibility_view.js | 66 + client/js/views/show_boards_list_view.js | 40 + client/js/views/show_copy_board_view.js | 76 + client/js/views/show_search_boards_view.js | 46 + client/js/views/show_search_message_view.js | 44 + .../views/show_sync_google_calendar_view.js | 44 + client/js/views/starred_boards_index_view.js | 74 + .../js/views/started_boards_listing_view.js | 45 + client/js/views/switch_to_list_form_view.js | 157 + client/js/views/user_activity_menu_view.js | 44 + client/js/views/user_activity_view.js | 81 + client/js/views/user_board_list_view.js | 43 + .../js/views/user_boards_listing_menu_view.js | 43 + client/js/views/user_cards_view.js | 46 + client/js/views/user_index_container_view.js | 67 + client/js/views/user_index_view.js | 186 + client/js/views/user_search_result_view.js | 45 + client/js/views/user_view.js | 312 + client/js/views/user_view_header_view.js | 44 + client/js/views/users_activation_view.js | 64 + client/js/views/users_change_password_view.js | 77 + client/js/views/users_forgot_password_view.js | 77 + client/js/workflow_templates/bug.json | 12 + client/js/workflow_templates/crm.json | 11 + client/js/workflow_templates/scrum.json | 13 + client/js/workflow_templates/todo.json | 8 + media/User/1/default-admin-user.png | Bin 0 -> 4750 bytes package.json | 35 + restyaboard.conf | 48 + server/.gitignore | 0 server/java/play/.gitignore | 0 server/js/NODE/.gitignore | 0 server/php/R/README.md | 4 + server/php/R/config.inc.php | 69 + server/php/R/download.php | 47 + server/php/R/ical.php | 61 + server/php/R/image.php | 177 + server/php/R/libs/core.php | 932 ++ .../php/R/libs/vendors/OAuth2/Autoloader.php | 48 + .../ClientAssertionTypeInterface.php | 15 + .../OAuth2/ClientAssertionType/HttpBasic.php | 124 + .../OAuth2/Controller/AuthorizeController.php | 375 + .../AuthorizeControllerInterface.php | 43 + .../OAuth2/Controller/ResourceController.php | 111 + .../ResourceControllerInterface.php | 26 + .../OAuth2/Controller/TokenController.php | 220 + .../Controller/TokenControllerInterface.php | 32 + .../OAuth2/Encryption/EncryptionInterface.php | 11 + .../R/libs/vendors/OAuth2/Encryption/Jwt.php | 152 + .../OAuth2/GrantType/AuthorizationCode.php | 101 + .../OAuth2/GrantType/ClientCredentials.php | 67 + .../OAuth2/GrantType/GrantTypeInterface.php | 20 + .../vendors/OAuth2/GrantType/JwtBearer.php | 221 + .../vendors/OAuth2/GrantType/RefreshToken.php | 102 + .../OAuth2/GrantType/UserCredentials.php | 84 + .../OpenID/Controller/AuthorizeController.php | 86 + .../AuthorizeControllerInterface.php | 12 + .../OpenID/Controller/UserInfoController.php | 57 + .../UserInfoControllerInterface.php | 23 + .../OpenID/GrantType/AuthorizationCode.php | 36 + .../OpenID/ResponseType/AuthorizationCode.php | 60 + .../AuthorizationCodeInterface.php | 32 + .../OAuth2/OpenID/ResponseType/IdToken.php | 125 + .../OpenID/ResponseType/IdTokenInterface.php | 29 + .../OpenID/ResponseType/TokenIdToken.php | 28 + .../ResponseType/TokenIdTokenInterface.php | 9 + .../Storage/AuthorizationCodeInterface.php | 44 + .../OpenID/Storage/UserClaimsInterface.php | 38 + server/php/R/libs/vendors/OAuth2/Request.php | 212 + .../libs/vendors/OAuth2/RequestInterface.php | 16 + server/php/R/libs/vendors/OAuth2/Response.php | 369 + .../libs/vendors/OAuth2/ResponseInterface.php | 24 + .../OAuth2/ResponseType/AccessToken.php | 154 + .../ResponseType/AccessTokenInterface.php | 27 + .../OAuth2/ResponseType/AuthorizationCode.php | 100 + .../AuthorizationCodeInterface.php | 34 + .../OAuth2/ResponseType/CryptoToken.php | 118 + .../ResponseType/ResponseTypeInterface.php | 8 + server/php/R/libs/vendors/OAuth2/Scope.php | 103 + .../R/libs/vendors/OAuth2/ScopeInterface.php | 40 + server/php/R/libs/vendors/OAuth2/Server.php | 784 ++ .../OAuth2/Storage/AccessTokenInterface.php | 53 + .../Storage/AuthorizationCodeInterface.php | 92 + .../libs/vendors/OAuth2/Storage/Cassandra.php | 367 + .../Storage/ClientCredentialsInterface.php | 49 + .../OAuth2/Storage/ClientInterface.php | 66 + .../vendors/OAuth2/Storage/CryptoToken.php | 77 + .../OAuth2/Storage/CryptoTokenInterface.php | 14 + .../OAuth2/Storage/JwtBearerInterface.php | 74 + .../R/libs/vendors/OAuth2/Storage/Memory.php | 364 + .../R/libs/vendors/OAuth2/Storage/Mongo.php | 339 + .../php/R/libs/vendors/OAuth2/Storage/Pdo.php | 444 + .../OAuth2/Storage/PublicKeyInterface.php | 16 + .../R/libs/vendors/OAuth2/Storage/Redis.php | 312 + .../OAuth2/Storage/RefreshTokenInterface.php | 82 + .../vendors/OAuth2/Storage/ScopeInterface.php | 46 + .../Storage/UserCredentialsInterface.php | 52 + .../libs/vendors/OAuth2/TokenType/Bearer.php | 130 + .../R/libs/vendors/OAuth2/TokenType/Mac.php | 22 + .../OAuth2/TokenType/TokenTypeInterface.php | 21 + server/php/R/libs/vendors/finediff.php | 688 + server/php/R/r.php | 3270 +++++ server/php/R/resource.php | 24 + server/php/R/server.php | 49 + server/php/R/shell/cron.php | 163 + server/php/R/token.php | 16 + server/py/Bottle/.gitignore | 0 sql/restyaboard_with_empty_data.sql | 5956 ++++++++ 611 files changed, 130509 insertions(+), 4 deletions(-) create mode 100644 Gruntfile.js create mode 100644 LICENSE.txt create mode 100644 api_explorer/api-docs/activities_listing.json create mode 100644 api_explorer/api-docs/boards.json create mode 100644 api_explorer/api-docs/card_attachments.json create mode 100644 api_explorer/api-docs/card_voters.json create mode 100644 api_explorer/api-docs/cards.json create mode 100644 api_explorer/api-docs/cards_labels.json create mode 100644 api_explorer/api-docs/checklist_items.json create mode 100644 api_explorer/api-docs/checklists.json create mode 100644 api_explorer/api-docs/comments.json create mode 100644 api_explorer/api-docs/index.php create mode 100644 api_explorer/api-docs/lists.json create mode 100644 api_explorer/api-docs/organizations.json create mode 100644 api_explorer/api-docs/users.json create mode 100644 api_explorer/css/highlight.default.css create mode 100644 api_explorer/css/screen.css create mode 100644 api_explorer/images/logo_small.png create mode 100644 api_explorer/images/pet_store_api.png create mode 100644 api_explorer/images/throbber.gif create mode 100644 api_explorer/images/wordnik_api.png create mode 100644 api_explorer/index.html create mode 100644 api_explorer/lib/backbone-min.js create mode 100644 api_explorer/lib/handlebars-1.0.0.js create mode 100644 api_explorer/lib/highlight.7.3.pack.js create mode 100644 api_explorer/lib/jquery-1.8.0.min.js create mode 100644 api_explorer/lib/jquery.ba-bbq.min.js create mode 100644 api_explorer/lib/jquery.slideto.min.js create mode 100644 api_explorer/lib/jquery.wiggle.min.js create mode 100644 api_explorer/lib/shred.bundle.js create mode 100644 api_explorer/lib/shred/content.js create mode 100644 api_explorer/lib/swagger.js create mode 100644 api_explorer/lib/underscore-min.js create mode 100644 api_explorer/swagger-ui.js create mode 100644 api_explorer/swagger-ui.min.js create mode 100644 client/.gitignore create mode 100644 client/apple-touch-icon-114x114.png create mode 100644 client/apple-touch-icon-72x72.png create mode 100644 client/apple-touch-icon.png create mode 100644 client/css/alerts.less create mode 100644 client/css/animate.less create mode 100644 client/css/badges.less create mode 100644 client/css/bootstrap-datetimepicker.min.css create mode 100644 client/css/bootstrap.less create mode 100644 client/css/breadcrumbs.less create mode 100644 client/css/button-groups.less create mode 100644 client/css/buttons.less create mode 100644 client/css/carousel.less create mode 100644 client/css/chosen.css create mode 100644 client/css/close.less create mode 100644 client/css/code.less create mode 100644 client/css/component-animations.less create mode 100644 client/css/css-avatars.less create mode 100644 client/css/custom-responsive.less create mode 100644 client/css/custom.less create mode 100644 client/css/dropdowns.less create mode 100644 client/css/flag.css create mode 100644 client/css/font-awesome-ie7.less create mode 100644 client/css/font-awesome.less create mode 100644 client/css/forms.less create mode 100644 client/css/fullcalendar.css create mode 100644 client/css/glyphicons.less create mode 100644 client/css/grid.less create mode 100644 client/css/input-groups.less create mode 100644 client/css/jquery-ui.css create mode 100644 client/css/jquery.dockmodal.css create mode 100644 client/css/jquery.fileupload-ui.css create mode 100644 client/css/jumbotron.less create mode 100644 client/css/labels.less create mode 100644 client/css/list-group.less create mode 100644 client/css/media.less create mode 100644 client/css/mixins.less create mode 100644 client/css/modals.less create mode 100644 client/css/navbar.less create mode 100644 client/css/navs.less create mode 100644 client/css/normalize.less create mode 100644 client/css/pager.less create mode 100644 client/css/pagination.less create mode 100644 client/css/panels.less create mode 100644 client/css/popovers.less create mode 100644 client/css/print.less create mode 100644 client/css/progress-bars.less create mode 100644 client/css/responsive-utilities.less create mode 100644 client/css/scaffolding.less create mode 100644 client/css/select2-bootstrap.css create mode 100644 client/css/tables.less create mode 100644 client/css/theme.less create mode 100644 client/css/thumbnails.less create mode 100644 client/css/tooltip.less create mode 100644 client/css/type.less create mode 100644 client/css/utilities.less create mode 100644 client/css/variables.less create mode 100644 client/css/wells.less create mode 100644 client/font/FontAwesome.otf create mode 100644 client/font/fontawesome-corp-webfont.eot create mode 100644 client/font/fontawesome-corp-webfont.svg create mode 100644 client/font/fontawesome-corp-webfont.ttf create mode 100644 client/font/fontawesome-corp-webfont.woff create mode 100644 client/font/fontawesome-webfont.eot create mode 100644 client/font/fontawesome-webfont.svg create mode 100644 client/font/fontawesome-webfont.ttf create mode 100644 client/font/fontawesome-webfont.woff create mode 100644 client/img/default-organization.png create mode 100644 client/img/favicon.ico create mode 100644 client/img/flags.png create mode 100644 client/img/logo-icon-offline.png create mode 100644 client/img/logo-icon-sync.gif create mode 100644 client/img/logo-icon.png create mode 100644 client/img/logo.png create mode 100644 client/img/logo.svg create mode 100644 client/img/select2-spinner.gif create mode 100644 client/img/select2.png create mode 100644 client/img/select2x2.png create mode 100644 client/img/split.gif create mode 100644 client/img/star-load.gif create mode 100644 client/index.html create mode 100644 client/js/application.js create mode 100644 client/js/collections/acl_collection.js create mode 100644 client/js/collections/activity_collection.js create mode 100644 client/js/collections/attachment_collection.js create mode 100644 client/js/collections/board_collection.js create mode 100644 client/js/collections/board_star_collection.js create mode 100644 client/js/collections/board_subscriber_collection.js create mode 100644 client/js/collections/boards_user_collection.js create mode 100644 client/js/collections/card_attachment_collection.js create mode 100644 client/js/collections/card_checklist_collection.js create mode 100644 client/js/collections/card_collection.js create mode 100644 client/js/collections/card_label_collection.js create mode 100644 client/js/collections/card_position_collection.js create mode 100644 client/js/collections/card_subscriber_collection.js create mode 100644 client/js/collections/card_user_collection.js create mode 100644 client/js/collections/card_voter_collection.js create mode 100644 client/js/collections/checklist_item_collection.js create mode 100644 client/js/collections/closed_board_collection.js create mode 100644 client/js/collections/elasticsearch_collection.js create mode 100644 client/js/collections/email_template_collection.js create mode 100644 client/js/collections/flickr_collection.js create mode 100644 client/js/collections/list_collection.js create mode 100644 client/js/collections/list_subscriber_collection.js create mode 100644 client/js/collections/organization_collection.js create mode 100644 client/js/collections/organizations_user_collection.js create mode 100644 client/js/collections/role_collection.js create mode 100644 client/js/collections/role_settings_collection.js create mode 100644 client/js/collections/setting_category_collection.js create mode 100644 client/js/collections/user_collection.js create mode 100644 client/js/collections/workflow_template_collection.js create mode 100644 client/js/common.js create mode 100644 client/js/libs/ImageSelect.jquery.js create mode 100644 client/js/libs/Markdown.Converter.js create mode 100644 client/js/libs/affix.js create mode 100644 client/js/libs/backbone.defered-view-loader.js create mode 100644 client/js/libs/backbone.dualstorage.js create mode 100644 client/js/libs/backbone.js create mode 100644 client/js/libs/backbone.rails.js create mode 100644 client/js/libs/backbone.stickit.js create mode 100644 client/js/libs/backbone.upload-manager.js create mode 100644 client/js/libs/bootstrap-alert.js create mode 100644 client/js/libs/bootstrap-carousel.js create mode 100644 client/js/libs/bootstrap-collapse.js create mode 100644 client/js/libs/bootstrap-datepicker.js create mode 100644 client/js/libs/bootstrap-datetimepicker.min.js create mode 100644 client/js/libs/bootstrap-dropdown.js create mode 100644 client/js/libs/bootstrap-modal.js create mode 100644 client/js/libs/bootstrap-popover.js create mode 100644 client/js/libs/bootstrap-tab.js create mode 100644 client/js/libs/bootstrap-tooltip.js create mode 100644 client/js/libs/bootstrap-transition.js create mode 100644 client/js/libs/bootstrap-twipsy.js create mode 100644 client/js/libs/chosen.jquery.min.js create mode 100644 client/js/libs/date.format.js create mode 100644 client/js/libs/favico-0.3.8.min.js create mode 100644 client/js/libs/fullcalendar.min.js create mode 100644 client/js/libs/i18n.js create mode 100644 client/js/libs/jquery-1.8.3.js create mode 100644 client/js/libs/jquery-ui-1.8.23.js create mode 100644 client/js/libs/jquery.bootstrap-growl.js create mode 100644 client/js/libs/jquery.dockmodal.js create mode 100644 client/js/libs/jquery.drawDoughnutChart.js create mode 100644 client/js/libs/jquery.fileupload-image.js create mode 100644 client/js/libs/jquery.fileupload-process.js create mode 100644 client/js/libs/jquery.fileupload-ui.js create mode 100644 client/js/libs/jquery.fileupload-validate.js create mode 100644 client/js/libs/jquery.fileupload.js create mode 100644 client/js/libs/jquery.gritter.min.js create mode 100644 client/js/libs/jquery.iframe-transport.js create mode 100644 client/js/libs/jquery.scrollTo-min.js create mode 100644 client/js/libs/jquery.timeago.js create mode 100644 client/js/libs/jquery.ui.touch-punch.min.js create mode 100644 client/js/libs/jquery.ui.widget.js create mode 100644 client/js/libs/less-1.3.0.min.js create mode 100644 client/js/libs/load-image.min.js create mode 100644 client/js/libs/locale.js create mode 100644 client/js/libs/md5.js create mode 100644 client/js/libs/musical.js create mode 100644 client/js/libs/select2.js create mode 100644 client/js/libs/select2.min.js create mode 100644 client/js/libs/showdown.js create mode 100644 client/js/libs/splitter.js create mode 100644 client/js/libs/tag-it.js create mode 100644 client/js/libs/tmpl.min.js create mode 100644 client/js/libs/underscore.js create mode 100644 client/js/models/acl.js create mode 100644 client/js/models/activity.js create mode 100644 client/js/models/board.js create mode 100644 client/js/models/boards_star.js create mode 100644 client/js/models/boards_subscriber.js create mode 100644 client/js/models/boards_user.js create mode 100644 client/js/models/card.js create mode 100644 client/js/models/card_attachment.js create mode 100644 client/js/models/card_subscriber.js create mode 100644 client/js/models/card_user.js create mode 100644 client/js/models/card_voter.js create mode 100644 client/js/models/checklist.js create mode 100644 client/js/models/checklist_item.js create mode 100644 client/js/models/elasticsearch.js create mode 100644 client/js/models/email_template.js create mode 100644 client/js/models/flickr.js create mode 100644 client/js/models/instant_card_add.js create mode 100644 client/js/models/label.js create mode 100644 client/js/models/list.js create mode 100644 client/js/models/list_subscriber.js create mode 100644 client/js/models/oauth.js create mode 100644 client/js/models/organization.js create mode 100644 client/js/models/organizations_user.js create mode 100644 client/js/models/role.js create mode 100644 client/js/models/role_setting.js create mode 100644 client/js/models/setting_category.js create mode 100644 client/js/models/user.js create mode 100644 client/js/models/workflow_template.js create mode 100644 client/js/templates/about_us.jst.ejs create mode 100644 client/js/templates/activity.jst.ejs create mode 100644 client/js/templates/activity_add_form.jst.ejs create mode 100644 client/js/templates/activity_card_search.jst.ejs create mode 100644 client/js/templates/activity_delete_confirm.jst.ejs create mode 100644 client/js/templates/activity_index.jst.ejs create mode 100644 client/js/templates/activity_reply_form.jst.ejs create mode 100644 client/js/templates/activity_user_add_search_result.jst.ejs create mode 100644 client/js/templates/admin_activity_index.jst.ejs create mode 100644 client/js/templates/admin_user_add.jst.ejs create mode 100644 client/js/templates/archived_card.jst.ejs create mode 100644 client/js/templates/archived_cards.jst.ejs create mode 100644 client/js/templates/archived_items.jst.ejs create mode 100644 client/js/templates/archived_list.jst.ejs create mode 100644 client/js/templates/archived_lists.jst.ejs create mode 100644 client/js/templates/attachment.jst.ejs create mode 100644 client/js/templates/attachment_delete_confirm.jst.ejs create mode 100644 client/js/templates/attachment_delete_confirm_form.jst.ejs create mode 100644 client/js/templates/board.jst.ejs create mode 100644 client/js/templates/board_404.jst.ejs create mode 100644 client/js/templates/board_add.jst.ejs create mode 100644 client/js/templates/board_add_organization_form.jst.ejs create mode 100644 client/js/templates/board_additional_settings.jst.ejs create mode 100644 client/js/templates/board_background.jst.ejs create mode 100644 client/js/templates/board_custom_background.jst.ejs create mode 100644 client/js/templates/board_filter.jst.ejs create mode 100644 client/js/templates/board_header.jst.ejs create mode 100644 client/js/templates/board_index.jst.ejs create mode 100644 client/js/templates/board_index_header.jst.ejs create mode 100644 client/js/templates/board_member_add_search_result.jst.ejs create mode 100644 client/js/templates/board_organization_form.jst.ejs create mode 100644 client/js/templates/board_sidebar.jst.ejs create mode 100644 client/js/templates/board_simple_view.jst.ejs create mode 100644 client/js/templates/board_user_actions.jst.ejs create mode 100644 client/js/templates/board_user_activity.jst.ejs create mode 100644 client/js/templates/board_user_remove_confirm.jst.ejs create mode 100644 client/js/templates/board_users_view.jst.ejs create mode 100644 client/js/templates/board_visibility.jst.ejs create mode 100644 client/js/templates/card.jst.ejs create mode 100644 client/js/templates/card_actions.jst.ejs create mode 100644 client/js/templates/card_add.jst.ejs create mode 100644 client/js/templates/card_attachment.jst.ejs create mode 100644 client/js/templates/card_checklist.jst.ejs create mode 100644 client/js/templates/card_checklist_item.jst.ejs create mode 100644 client/js/templates/card_copy.jst.ejs create mode 100644 client/js/templates/card_duedate_from.jst.ejs create mode 100644 client/js/templates/card_label.jst.ejs create mode 100644 client/js/templates/card_label_form.jst.ejs create mode 100644 client/js/templates/card_labels_form.jst.ejs create mode 100644 client/js/templates/card_list_view.jst.ejs create mode 100644 client/js/templates/card_member_form.jst.ejs create mode 100644 client/js/templates/card_positions_form.jst.ejs create mode 100644 client/js/templates/card_search_result.jst.ejs create mode 100644 client/js/templates/card_search_users_result.jst.ejs create mode 100644 client/js/templates/card_voters_list.jst.ejs create mode 100644 client/js/templates/change_password.jst.ejs create mode 100644 client/js/templates/chat.jst.ejs create mode 100644 client/js/templates/checklist_actions.jst.ejs create mode 100644 client/js/templates/checklist_add_form.jst.ejs create mode 100644 client/js/templates/checklist_delete_confirm_form.jst.ejs create mode 100644 client/js/templates/checklist_edit_form.jst.ejs create mode 100644 client/js/templates/checklist_item_actions.jst.ejs create mode 100644 client/js/templates/checklist_item_add_form.jst.ejs create mode 100644 client/js/templates/checklist_item_add_link.jst.ejs create mode 100644 client/js/templates/checklist_item_delete_confirm_form.jst.ejs create mode 100644 client/js/templates/checklist_item_edit_form.jst.ejs create mode 100644 client/js/templates/checklist_item_mention_member.jst.ejs create mode 100644 client/js/templates/checklist_item_mention_member_search_form.jst.ejs create mode 100644 client/js/templates/closed_boards_index.jst.ejs create mode 100644 client/js/templates/closed_boards_listing.jst.ejs create mode 100644 client/js/templates/copy_board_visibility.jst.ejs create mode 100644 client/js/templates/copy_card.jst.ejs create mode 100644 client/js/templates/copy_from_existing_card.jst.ejs create mode 100644 client/js/templates/copy_list.jst.ejs create mode 100644 client/js/templates/edit_activity_form.jst.ejs create mode 100644 client/js/templates/edit_board_member_permission_to_admin.jst.ejs create mode 100644 client/js/templates/edit_board_member_permission_to_normal.jst.ejs create mode 100644 client/js/templates/email_templates.jst.ejs create mode 100644 client/js/templates/error_404.jst.ejs create mode 100644 client/js/templates/flickr.jst.ejs create mode 100644 client/js/templates/footer.jst.ejs create mode 100644 client/js/templates/header.jst.ejs create mode 100644 client/js/templates/instant_card_add.jst.ejs create mode 100644 client/js/templates/instant_card_add_labels_form.jst.ejs create mode 100644 client/js/templates/instant_card_add_members_form.jst.ejs create mode 100644 client/js/templates/list.jst.ejs create mode 100644 client/js/templates/list_actions.jst.ejs create mode 100644 client/js/templates/list_add.jst.ejs create mode 100644 client/js/templates/list_archive_confirm.jst.ejs create mode 100644 client/js/templates/list_cards_archive_confirm.jst.ejs create mode 100644 client/js/templates/list_delete_confirm.jst.ejs create mode 100644 client/js/templates/login.jst.ejs create mode 100644 client/js/templates/modal_activity_view.jst.ejs create mode 100644 client/js/templates/modal_card_member_form.jst.ejs create mode 100644 client/js/templates/modal_card_view.jst.ejs create mode 100644 client/js/templates/modal_flickr_photo_view.jst.ejs create mode 100644 client/js/templates/modal_list_view.jst.ejs create mode 100644 client/js/templates/modal_music_view.jst.ejs create mode 100644 client/js/templates/modal_user_activities_list_view.jst.ejs create mode 100644 client/js/templates/move_card.jst.ejs create mode 100644 client/js/templates/move_cards_from_list.jst.ejs create mode 100644 client/js/templates/move_list.jst.ejs create mode 100644 client/js/templates/my_boards_listing.jst.ejs create mode 100644 client/js/templates/notification_menu.jst.ejs create mode 100644 client/js/templates/organization_add.jst.ejs create mode 100644 client/js/templates/organization_board.jst.ejs create mode 100644 client/js/templates/organization_delete_form.jst.ejs create mode 100644 client/js/templates/organization_header.jst.ejs create mode 100644 client/js/templates/organization_member_confirm_remove_form.jst.ejs create mode 100644 client/js/templates/organization_member_permission_form.jst.ejs create mode 100644 client/js/templates/organization_member_remove_form.jst.ejs create mode 100644 client/js/templates/organization_view.jst.ejs create mode 100644 client/js/templates/organization_visibility_form.jst.ejs create mode 100644 client/js/templates/organizations_board_form_view.jst.ejs create mode 100644 client/js/templates/organizations_list_view.jst.ejs create mode 100644 client/js/templates/organizations_lists_header.jst.ejs create mode 100644 client/js/templates/organizations_lists_view.jst.ejs create mode 100644 client/js/templates/organizations_user_view.jst.ejs create mode 100644 client/js/templates/role_settings.jst.ejs create mode 100644 client/js/templates/roles.jst.ejs create mode 100644 client/js/templates/search_result.jst.ejs create mode 100644 client/js/templates/select_board_visibility.jst.ejs create mode 100644 client/js/templates/selected_board_visibility.jst.ejs create mode 100644 client/js/templates/setting_list.jst.ejs create mode 100644 client/js/templates/show_all_visibility.jst.ejs create mode 100644 client/js/templates/show_board_member_permission_form.jst.ejs create mode 100644 client/js/templates/show_board_visibility.jst.ejs create mode 100644 client/js/templates/show_copy_board.jst.ejs create mode 100644 client/js/templates/show_search_boards.jst.ejs create mode 100644 client/js/templates/show_search_message.jst.ejs create mode 100644 client/js/templates/show_sync_google_calendar.jst.ejs create mode 100644 client/js/templates/starred_boards_index.jst.ejs create mode 100644 client/js/templates/started_boards_listing.jst.ejs create mode 100644 client/js/templates/switch_to_list_form.jst.ejs create mode 100644 client/js/templates/user.jst.ejs create mode 100644 client/js/templates/user_activity.jst.ejs create mode 100644 client/js/templates/user_activity_menu.jst.ejs create mode 100644 client/js/templates/user_board_list.jst.ejs create mode 100644 client/js/templates/user_boards_listing_menu.jst.ejs create mode 100644 client/js/templates/user_cards.jst.ejs create mode 100644 client/js/templates/user_index_container.jst.ejs create mode 100644 client/js/templates/user_search_result.jst.ejs create mode 100644 client/js/templates/user_view.jst.ejs create mode 100644 client/js/templates/user_view_header.jst.ejs create mode 100644 client/js/templates/users_forgot_password.jst.ejs create mode 100644 client/js/templates/users_register.jst.ejs create mode 100644 client/js/views/about_us_view.js create mode 100644 client/js/views/activity_add_form_view.js create mode 100644 client/js/views/activity_card_search_view.js create mode 100644 client/js/views/activity_delete_confirm_view.js create mode 100644 client/js/views/activity_index_view.js create mode 100644 client/js/views/activity_reply_form_view.js create mode 100644 client/js/views/activity_user_add_search_result_view.js create mode 100644 client/js/views/activity_view.js create mode 100644 client/js/views/admin_activity_index_view.js create mode 100644 client/js/views/admin_user_add_view.js create mode 100644 client/js/views/application_view.js create mode 100644 client/js/views/archived_card_view.js create mode 100644 client/js/views/archived_cards_view.js create mode 100644 client/js/views/archived_items_view.js create mode 100644 client/js/views/archived_list_view.js create mode 100644 client/js/views/archived_lists_view.js create mode 100644 client/js/views/attachment_delete_confirm_form_view.js create mode 100644 client/js/views/attachment_delete_confirm_view.js create mode 100644 client/js/views/attachment_view.js create mode 100644 client/js/views/board_404_view.js create mode 100644 client/js/views/board_add_organization_form_view.js create mode 100644 client/js/views/board_add_view.js create mode 100644 client/js/views/board_additional_setting_view.js create mode 100644 client/js/views/board_background_view.js create mode 100644 client/js/views/board_custom_background_view.js create mode 100644 client/js/views/board_filter_view.js create mode 100644 client/js/views/board_header_view.js create mode 100644 client/js/views/board_index_header_view.js create mode 100644 client/js/views/board_member_add_search_result_view.js create mode 100644 client/js/views/board_organization_form_view.js create mode 100644 client/js/views/board_sidebar_view.js create mode 100644 client/js/views/board_simple_view.js create mode 100644 client/js/views/board_user_actions_view.js create mode 100644 client/js/views/board_user_activity_view.js create mode 100644 client/js/views/board_user_remove_confirm_view.js create mode 100644 client/js/views/board_view.js create mode 100644 client/js/views/board_visibility_view.js create mode 100644 client/js/views/boards_index_view.js create mode 100644 client/js/views/boards_user_view.js create mode 100644 client/js/views/card_actions_view.js create mode 100644 client/js/views/card_attachment_view.js create mode 100644 client/js/views/card_checklist_item_view.js create mode 100644 client/js/views/card_checklist_view.js create mode 100644 client/js/views/card_copy_view.js create mode 100644 client/js/views/card_duedate_from_view.js create mode 100644 client/js/views/card_label_form_view.js create mode 100644 client/js/views/card_label_view.js create mode 100644 client/js/views/card_labels_form_view.js create mode 100644 client/js/views/card_member_form_view.js create mode 100644 client/js/views/card_positions_form_view.js create mode 100644 client/js/views/card_search_result_view.js create mode 100644 client/js/views/card_search_users_result_view.js create mode 100644 client/js/views/card_view.js create mode 100644 client/js/views/card_voters_list_view.js create mode 100644 client/js/views/chat_view.js create mode 100644 client/js/views/checklist_actions_view.js create mode 100644 client/js/views/checklist_add_form_view.js create mode 100644 client/js/views/checklist_delete_confirm_form_view.js create mode 100644 client/js/views/checklist_edit_form_view.js create mode 100644 client/js/views/checklist_item_actions_view.js create mode 100644 client/js/views/checklist_item_add_form_view.js create mode 100644 client/js/views/checklist_item_add_link_view.js create mode 100644 client/js/views/checklist_item_delete_confirm_form_view.js create mode 100644 client/js/views/checklist_item_edit_form_view.js create mode 100644 client/js/views/checklist_item_mention_member_search_form_view.js create mode 100644 client/js/views/checklist_item_mention_member_view.js create mode 100644 client/js/views/closed_boards_index_view.js create mode 100644 client/js/views/closed_boards_listing_view.js create mode 100644 client/js/views/copy_board_visibility_view.js create mode 100644 client/js/views/copy_card_view.js create mode 100644 client/js/views/copy_from_existing_card_view.js create mode 100644 client/js/views/copy_list_view.js create mode 100644 client/js/views/edit_activity_form_view.js create mode 100644 client/js/views/edit_board_member_permission_to_adminl_view.js create mode 100644 client/js/views/edit_board_member_permission_to_normal_view.js create mode 100644 client/js/views/email_template_view.js create mode 100644 client/js/views/error_404_view.js create mode 100644 client/js/views/flickr_view.js create mode 100644 client/js/views/footer_view.js create mode 100644 client/js/views/header_view.js create mode 100644 client/js/views/instant_card_add_labels_form_view.js create mode 100644 client/js/views/instant_card_add_members_form_view.js create mode 100644 client/js/views/instant_card_add_view.js create mode 100644 client/js/views/list_actions_view.js create mode 100644 client/js/views/list_archive_confirm_view.js create mode 100644 client/js/views/list_cards_archive_confirm_view.js create mode 100644 client/js/views/list_delete_confirm_view.js create mode 100644 client/js/views/list_view.js create mode 100644 client/js/views/login_view.js create mode 100644 client/js/views/modal_activity_view.js create mode 100644 client/js/views/modal_board_view.js create mode 100644 client/js/views/modal_card_member_form_view.js create mode 100644 client/js/views/modal_card_view.js create mode 100644 client/js/views/modal_flickr_photo_view.js create mode 100644 client/js/views/modal_list_view.js create mode 100644 client/js/views/modal_music_view.js create mode 100644 client/js/views/modal_user_activities_list_view.js create mode 100644 client/js/views/move_card_view.js create mode 100644 client/js/views/move_cards_from_list_view.js create mode 100644 client/js/views/move_list_view.js create mode 100644 client/js/views/music_repeat_view.js create mode 100644 client/js/views/my_boards_listing_view.js create mode 100644 client/js/views/notification_menu_view.js create mode 100644 client/js/views/organization_add_view.js create mode 100644 client/js/views/organization_board_view.js create mode 100644 client/js/views/organization_delete_form_view.js create mode 100644 client/js/views/organization_header_view.js create mode 100644 client/js/views/organization_member_confirm_remove_form_view.js create mode 100644 client/js/views/organization_member_permission_form_view.js create mode 100644 client/js/views/organization_member_remove_form_view.js create mode 100644 client/js/views/organization_view.js create mode 100644 client/js/views/organization_visibility_form_view.js create mode 100644 client/js/views/organizations_board_form_view.js create mode 100644 client/js/views/organizations_list_view.js create mode 100644 client/js/views/organizations_lists_header_view.js create mode 100644 client/js/views/organizations_lists_view.js create mode 100644 client/js/views/organizations_user_view.js create mode 100644 client/js/views/register_view.js create mode 100644 client/js/views/role_index_view.js create mode 100644 client/js/views/role_settings_view.js create mode 100644 client/js/views/search_board_subscribe_view.js create mode 100644 client/js/views/search_result_view.js create mode 100644 client/js/views/select_board_visibility_view.js create mode 100644 client/js/views/selected_board_visibility_view.js create mode 100644 client/js/views/setting_view.js create mode 100644 client/js/views/show_all_visibility_view.js create mode 100644 client/js/views/show_board_member_permission_form_view.js create mode 100644 client/js/views/show_board_visibility_view.js create mode 100644 client/js/views/show_boards_list_view.js create mode 100644 client/js/views/show_copy_board_view.js create mode 100644 client/js/views/show_search_boards_view.js create mode 100644 client/js/views/show_search_message_view.js create mode 100644 client/js/views/show_sync_google_calendar_view.js create mode 100644 client/js/views/starred_boards_index_view.js create mode 100644 client/js/views/started_boards_listing_view.js create mode 100644 client/js/views/switch_to_list_form_view.js create mode 100644 client/js/views/user_activity_menu_view.js create mode 100644 client/js/views/user_activity_view.js create mode 100644 client/js/views/user_board_list_view.js create mode 100644 client/js/views/user_boards_listing_menu_view.js create mode 100644 client/js/views/user_cards_view.js create mode 100644 client/js/views/user_index_container_view.js create mode 100644 client/js/views/user_index_view.js create mode 100644 client/js/views/user_search_result_view.js create mode 100644 client/js/views/user_view.js create mode 100644 client/js/views/user_view_header_view.js create mode 100644 client/js/views/users_activation_view.js create mode 100644 client/js/views/users_change_password_view.js create mode 100644 client/js/views/users_forgot_password_view.js create mode 100644 client/js/workflow_templates/bug.json create mode 100644 client/js/workflow_templates/crm.json create mode 100644 client/js/workflow_templates/scrum.json create mode 100644 client/js/workflow_templates/todo.json create mode 100644 media/User/1/default-admin-user.png create mode 100644 package.json create mode 100644 restyaboard.conf create mode 100644 server/.gitignore create mode 100644 server/java/play/.gitignore create mode 100644 server/js/NODE/.gitignore create mode 100644 server/php/R/README.md create mode 100644 server/php/R/config.inc.php create mode 100644 server/php/R/download.php create mode 100644 server/php/R/ical.php create mode 100644 server/php/R/image.php create mode 100644 server/php/R/libs/core.php create mode 100644 server/php/R/libs/vendors/OAuth2/Autoloader.php create mode 100644 server/php/R/libs/vendors/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/ClientAssertionType/HttpBasic.php create mode 100644 server/php/R/libs/vendors/OAuth2/Controller/AuthorizeController.php create mode 100644 server/php/R/libs/vendors/OAuth2/Controller/AuthorizeControllerInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/Controller/ResourceController.php create mode 100644 server/php/R/libs/vendors/OAuth2/Controller/ResourceControllerInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/Controller/TokenController.php create mode 100644 server/php/R/libs/vendors/OAuth2/Controller/TokenControllerInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/Encryption/EncryptionInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/Encryption/Jwt.php create mode 100644 server/php/R/libs/vendors/OAuth2/GrantType/AuthorizationCode.php create mode 100644 server/php/R/libs/vendors/OAuth2/GrantType/ClientCredentials.php create mode 100644 server/php/R/libs/vendors/OAuth2/GrantType/GrantTypeInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/GrantType/JwtBearer.php create mode 100644 server/php/R/libs/vendors/OAuth2/GrantType/RefreshToken.php create mode 100644 server/php/R/libs/vendors/OAuth2/GrantType/UserCredentials.php create mode 100644 server/php/R/libs/vendors/OAuth2/OpenID/Controller/AuthorizeController.php create mode 100644 server/php/R/libs/vendors/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/OpenID/Controller/UserInfoController.php create mode 100644 server/php/R/libs/vendors/OAuth2/OpenID/Controller/UserInfoControllerInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/OpenID/GrantType/AuthorizationCode.php create mode 100644 server/php/R/libs/vendors/OAuth2/OpenID/ResponseType/AuthorizationCode.php create mode 100644 server/php/R/libs/vendors/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/OpenID/ResponseType/IdToken.php create mode 100644 server/php/R/libs/vendors/OAuth2/OpenID/ResponseType/IdTokenInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/OpenID/ResponseType/TokenIdToken.php create mode 100644 server/php/R/libs/vendors/OAuth2/OpenID/ResponseType/TokenIdTokenInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/OpenID/Storage/AuthorizationCodeInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/OpenID/Storage/UserClaimsInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/Request.php create mode 100644 server/php/R/libs/vendors/OAuth2/RequestInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/Response.php create mode 100644 server/php/R/libs/vendors/OAuth2/ResponseInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/ResponseType/AccessToken.php create mode 100644 server/php/R/libs/vendors/OAuth2/ResponseType/AccessTokenInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/ResponseType/AuthorizationCode.php create mode 100644 server/php/R/libs/vendors/OAuth2/ResponseType/AuthorizationCodeInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/ResponseType/CryptoToken.php create mode 100644 server/php/R/libs/vendors/OAuth2/ResponseType/ResponseTypeInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/Scope.php create mode 100644 server/php/R/libs/vendors/OAuth2/ScopeInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/Server.php create mode 100644 server/php/R/libs/vendors/OAuth2/Storage/AccessTokenInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/Storage/AuthorizationCodeInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/Storage/Cassandra.php create mode 100644 server/php/R/libs/vendors/OAuth2/Storage/ClientCredentialsInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/Storage/ClientInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/Storage/CryptoToken.php create mode 100644 server/php/R/libs/vendors/OAuth2/Storage/CryptoTokenInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/Storage/JwtBearerInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/Storage/Memory.php create mode 100644 server/php/R/libs/vendors/OAuth2/Storage/Mongo.php create mode 100644 server/php/R/libs/vendors/OAuth2/Storage/Pdo.php create mode 100644 server/php/R/libs/vendors/OAuth2/Storage/PublicKeyInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/Storage/Redis.php create mode 100644 server/php/R/libs/vendors/OAuth2/Storage/RefreshTokenInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/Storage/ScopeInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/Storage/UserCredentialsInterface.php create mode 100644 server/php/R/libs/vendors/OAuth2/TokenType/Bearer.php create mode 100644 server/php/R/libs/vendors/OAuth2/TokenType/Mac.php create mode 100644 server/php/R/libs/vendors/OAuth2/TokenType/TokenTypeInterface.php create mode 100644 server/php/R/libs/vendors/finediff.php create mode 100644 server/php/R/r.php create mode 100644 server/php/R/resource.php create mode 100644 server/php/R/server.php create mode 100644 server/php/R/shell/cron.php create mode 100644 server/php/R/token.php create mode 100644 server/py/Bottle/.gitignore create mode 100644 sql/restyaboard_with_empty_data.sql diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 000000000..7e43b1dda --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,221 @@ +module.exports = function(grunt) { + var css_files = new Array('client/css/fullcalendar.css', 'client/css/flag.css', 'client/css/jquery-ui.css', 'client/css/bootstrap-datetimepicker.min.css', 'client/css/bootstrap.css', 'client/css/jquery.dockmodal.css', 'client/css/select2-bootstrap.css'); + var js_files = new Array('client/js/libs/jquery-1.8.3.js', 'client/js/libs/jquery-ui-1.8.23.js', 'client/js/libs/jquery.ui.touch-punch.min.js', 'client/js/libs/jquery.timeago.js', 'client/js/libs/jquery.drawDoughnutChart.js', 'client/js/libs/underscore.js', 'client/js/libs/backbone.js', 'client/js/libs/backbone.stickit.js', 'client/js/libs/backbone.dualstorage.js', 'client/js/libs/affix.js', 'client/js/libs/bootstrap-twipsy.js', 'client/js/libs/bootstrap-tooltip.js', 'client/js/libs/bootstrap-popover.js', 'client/js/libs/bootstrap-dropdown.js', 'client/js/libs/bootstrap-datetimepicker.min.js', 'client/js/libs/bootstrap-collapse.js', 'client/js/libs/bootstrap-alert.js', 'client/js/libs/bootstrap-transition.js', 'client/js/libs/bootstrap-tab.js', 'client/js/libs/bootstrap-modal.js', 'client/js/libs/md5.js', 'client/js/libs/select2.js', 'client/js/libs/ImageSelect.jquery.js', 'client/js/libs/date.format.js', 'client/js/libs/i18n.js', 'client/js/libs/jquery.gritter.min.js', 'client/js/libs/jquery.scrollTo-min.js', 'client/js/libs/jquery.dockmodal.js', 'client/js/libs/Markdown.Converter.js', 'client/js/libs/tag-it.js', 'client/js/libs/jquery.iframe-transport.js', 'client/js/libs/showdown.js', 'client/js/libs/fullcalendar.min.js', 'client/js/libs/load-image.min.js', 'client/js/libs/canvas-to-blob.min.js', 'client/js/libs/tmpl.min.js', 'client/js/libs/jquery.ui.widget.js', 'client/js/libs/jquery.fileupload.js', 'client/js/libs/jquery.fileupload-process.js', 'client/js/libs/jquery.fileupload-image.js', 'client/js/libs/jquery.fileupload-validate.js', 'client/js/libs/jquery.bootstrap-growl.js', 'client/js/libs/backbone.defered-view-loader.js', 'client/js/libs/backbone.upload-manager.js', 'client/js/libs/locale.js', 'client/js/libs/splitter.js', 'client/js/libs/favico-0.3.8.min.js', 'client/js/libs/musical.js', 'client/js/models/oauth.js', 'client/js/models/user.js', 'client/js/models/board.js', 'client/js/models/boards_subscriber.js', 'client/js/models/list.js', 'client/js/models/flickr.js', 'client/js/models/organization.js', 'client/js/models/list_subscriber.js', 'client/js/models/card.js', 'client/js/models/organizations_user.js', 'client/js/models/boards_user.js', 'client/js/models/activity.js', 'client/js/models/card_voter.js', 'client/js/models/card_subscriber.js', 'client/js/models/card_attachment.js', 'client/js/models/label.js', 'client/js/models/checklist.js', 'client/js/models/checklist_item.js', 'client/js/models/card_user.js', 'client/js/models/elasticsearch.js', 'client/js/models/workflow_template.js', 'client/js/models/acl.js', 'client/js/models/role.js', 'client/js/models/role_setting.js', 'client/js/models/setting_category.js', 'client/js/models/boards_star.js', 'client/js/models/instant_card_add.js', 'client/js/models/email_template.js', 'client/js/collections/user_collection.js', 'client/js/collections/attachment_collection.js', 'client/js/collections/list_collection.js', 'client/js/collections/flickr_collection.js', 'client/js/collections/organization_collection.js', 'client/js/collections/organizations_user_collection.js', 'client/js/collections/boards_user_collection.js', 'client/js/collections/activity_collection.js', 'client/js/collections/board_subscriber_collection.js', 'client/js/collections/card_collection.js', 'client/js/collections/board_collection.js', 'client/js/collections/card_attachment_collection.js', 'client/js/collections/card_label_collection.js', 'client/js/collections/card_position_collection.js', 'client/js/collections/card_checklist_collection.js', 'client/js/collections/checklist_item_collection.js', 'client/js/collections/card_voter_collection.js', 'client/js/collections/elasticsearch_collection.js', 'client/js/collections/workflow_template_collection.js', 'client/js/collections/list_subscriber_collection.js', 'client/js/collections/card_subscriber_collection.js', 'client/js/collections/card_user_collection.js', 'client/js/collections/acl_collection.js', 'client/js/collections/role_collection.js', 'client/js/collections/setting_category_collection.js', 'client/js/collections/board_star_collection.js', 'client/js/collections/my_closed_board_collection.js', 'client/js/collections/role_settings_collection.js', 'client/js/collections/email_template_collection.js', 'client/js/templates/templates.js', 'client/js/views/application_view.js', 'client/js/views/admin_user_add_view.js', 'client/js/views/register_view.js', 'client/js/views/login_view.js', 'client/js/views/list_view.js', 'client/js/views/activity_view.js', 'client/js/views/board_view.js', 'client/js/views/header_view.js', 'client/js/views/footer_view.js', 'client/js/views/boards_index_view.js', 'client/js/views/users_forgot_password_view.js', 'client/js/views/users_change_password_view.js', 'client/js/views/organization_view.js', 'client/js/views/organizations_user_view.js', 'client/js/views/boards_user_view.js', 'client/js/views/chat_view.js', 'client/js/views/card_view.js', 'client/js/views/modal_card_view.js', 'client/js/views/modal_list_view.js', 'client/js/views/modal_board_view.js', 'client/js/views/attachment_view.js', 'client/js/views/card_attachment_view.js', 'client/js/views/card_label_view.js', 'client/js/views/card_position_view.js', 'client/js/views/card_checklist_view.js', 'client/js/views/card_checklist_item_view.js', 'client/js/views/user_view.js', 'client/js/views/user_index_view.js', 'client/js/views/board_simple_view.js', 'client/js/views/instant_card_add_view.js', 'client/js/views/role_index_view.js', 'client/js/views/role_settings_view.js', 'client/js/views/user_cards_view.js', 'client/js/views/user_activity_menu_view.js', 'client/js/views/user_boards_listing_menu_view.js', 'client/js/views/user_search_result_view.js', 'client/js/views/organization_visibility_form_view.js', 'client/js/views/organization_member_permission_form_view.js', 'client/js/views/organization_member_remove_form_view.js', 'client/js/views/organization_member_confirm_remove_form_view.js', 'client/js/views/attachment_delete_confirm_form_view.js', 'client/js/views/attachment_delete_confirm_form_view.js', 'client/js/views/board_organization_form_view.js', 'client/js/views/board_custom_background_view.js', 'client/js/views/board_sidebar_view.js', 'client/js/views/archived_items_view.js', 'client/js/views/board_background_view.js', 'client/js/views/board_filter_view.js', 'client/js/views/board_user_activity_view.js', 'client/js/views/board_user_remove_confirm_view.js', 'client/js/views/card_copy_view.js', 'client/js/views/list_archive_confirm_view.js', 'client/js/views/list_cards_archive_confirm_view.js', 'client/js/views/move_cards_from_list_view.js', 'client/js/views/move_list_view.js', 'client/js/views/copy_list_view.js', 'client/js/views/list_delete_confirm_view.js', 'client/js/views/list_actions_view.js', 'client/js/views/card_labels_form_view.js', 'client/js/views/card_positions_form_view.js', 'client/js/views/card_member_form_view.js', 'client/js/views/card_actions_view.js', 'client/js/views/activity_user_add_search_result_view.js', 'client/js/views/card_voters_list_view.js', 'client/js/views/activity_delete_confirm_view.js', 'client/js/views/edit_activity_form_view.js', 'client/js/views/activity_reply_form_view.js', 'client/js/views/activity_add_form_view.js', 'client/js/views/card_duedate_from_view.js', 'client/js/views/card_label_form_view.js', 'client/js/views/card_position_form_view.js', 'client/js/views/card_search_result_view.js', 'client/js/views/copy_from_existing_card_view.js', 'client/js/views/move_card_view.js', 'client/js/views/copy_card_view.js', 'client/js/views/activity_card_search_view.js', 'client/js/views/checklist_add_form_view.js', 'client/js/views/modal_card_member_form_view.js', 'client/js/views/card_search_users_result_view.js', 'client/js/views/notification_menu_view.js', 'client/js/views/organization_add_view.js', 'client/js/views/board_add_view.js', 'client/js/views/organizations_board_form_view.js', 'client/js/views/user_cards_view.js', 'client/js/views/checklist_item_add_form_view.js', 'client/js/views/checklist_delete_confirm_form_view.js', 'client/js/views/checklist_actions_view.js', 'client/js/views/checklist_item_actions_view.js', 'client/js/views/checklist_item_delete_confirm_form_view.js', 'client/js/views/checklist_item_edit_form_view.js', 'client/js/views/checklist_item_add_link_view.js', 'client/js/views/checklist_edit_form_view.js', 'client/js/views/attachment_delete_confirm_view.js', 'client/js/views/setting_view.js', 'client/js/views/instant_card_add_labels_form_view.js', 'client/js/views/instant_card_add_members_form_view.js', 'client/js/views/switch_to_list_form_view.js', 'client/js/views/user_activity_menu_view.js', 'client/js/views/user_board_list_view.js', 'client/js/views/archived_lists_view.js', 'client/js/views/archived_list_view.js', 'client/js/views/archived_cards_view.js', 'client/js/views/archived_card_view.js', 'client/js/views/edit_board_member_permission_to_normal_view.js', 'client/js/views/edit_board_member_permission_to_admin_view.js', 'client/js/views/copy_board_visibility_view.js', 'client/js/views/show_all_visibility_view.js', 'client/js/views/show_board_member_permission_form_view.js', 'client/js/views/show_board_visibility_view.js', 'client/js/views/show_search_message_view.js', 'client/js/views/search_result_view.js', 'client/js/views/show_search_boards_view.js', 'client/js/views/search_board_subscribe_view.js', 'client/js/views/show_boards_list_view.js', 'client/js/views/my_boards_listing_view.js', 'client/js/views/started_boards_listing_view.js', 'client/js/views/closed_boards_listing_view.js', 'client/js/views/board_additional_setting_view.js', 'client/js/views/select_board_visibility_view.js', 'client/js/views/board_visibility_view.js', 'client/js/views/board_add_organization_form_view.js', 'client/js/views/board_member_add_search_result_view.js', 'client/js/views/checklist_item_mention_member_view.js', 'client/js/views/checklist_item_mention_member_search_form_view.js', 'client/js/views/organization_board_view.js', 'client/js/views/user_boards_listing_menu_view.js', 'client/js/views/board_user_actions_view.js', 'client/js/views/modal_user_activities_list_view.js', 'client/js/views/organizations_lists_view.js', 'client/js/views/organizations_list_view.js', 'client/js/views/email_template_view.js', 'client/js/views/user_activity_view.js', 'client/js/views/user_index_container_view.js', 'client/js/views/selected_board_visibility_view.js', 'client/js/views/modal_activity_view.js', 'client/js/views/modal_flickr_photo_view.js', 'client/js/views/modal_music_view.js', 'client/js/views/flickr_view.js', 'client/js/views/board_404_view.js', 'client/js/views/organization_header_view.js', 'client/js/views/user_view_header_view.js', 'client/js/views/organizations_lists_header_view.js', 'client/js/views/starred_boards_index_view.js', 'client/js/views/board_header_view.js', 'client/js/views/organization_delete_form_view.js', 'client/js/views/error_404_view.js', 'client/js/views/board_index_header_view.js', 'client/js/views/about_us_view.js', 'client/js/views/closed_boards_index_view.js', 'client/js/views/activity_index_view.js', 'client/js/views/admin_activity_index_view.js', 'client/js/views/users_activation_view.js', 'client/js/views/show_sync_google_calendar_view.js', 'client/js/views/show_copy_board_view.js', 'client/js/application.js', 'client/js/views/music_repeat_view.js', 'client/js/common.js'); + var source_js_files = new Array('Gruntfile.js', 'client/js/collections/**/*.js', 'client/js/models/**/*.js', 'client/js/views/**/*.js', 'client/js/application.js', 'client/js/common.js'); + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + jshint: { + all: source_js_files + }, + phplint: { + all: ['server/php/R/*.php'] + }, + less: { + development: { + files: { + 'client/css/bootstrap.css': 'client/css/bootstrap.less' + } + } + }, + jst: { + compile: { + options: { + processName: function(filepath) { + return filepath.replace('client/js/', '').replace('.jst.ejs', ''); + } + }, + files: { + 'client/js/templates/templates.js': 'client/js/templates/**/*.ejs', + } + } + }, + concat: { + css: { + src: css_files, + dest: 'client/css/default.cache.css' + }, + js: { + src: js_files, + dest: 'client/js/default.cache.js' + } + }, + jsbeautifier: { + default: { + src: source_js_files + } + }, + prettify: { + options: { + indent: 1 + }, + index: { + src: 'client/index.html', + dest: 'client/index.html' + } + }, + cssmin: { + css: { + src: 'client/css/default.cache.css', + dest: 'client/css/default.cache.css' + } + }, + uglify: { + js: { + files: { + 'client/js/default.cache.js': ['client/js/default.cache.js'] + } + } + }, + filerev: { + live: { + src: ['client/js/default.cache.js', 'client/css/default.cache.css'] + } + }, + usemin: { + html: 'client/index.html' + }, + htmlmin: { + main: { + options: { + removeComments: true, + collapseWhitespace: true + }, + files: { + 'client/index.html': 'client/index.html' + } + } + }, + 'regex-replace': { + build: { + src: ['client/index.html', 'server/php/R/config.inc.php'], + actions: [{ + name: '', + search: '/restyaboard/', + replace: '/', + flags: 'g' + }, { + name: 'DB User', + search: '\'restya\'', + replace: '\'REPLACE_ME\'', + flags: 'g' + }, { + name: 'DB Password', + search: 'hjVl2!rGd', + replace: 'REPLACE_ME', + flags: 'g' + }, { + name: 'Manifest Replace', + search: '', + replace: '', + flags: 'g' + }] + } + }, + manifest: { + build: { + options: { + basePath: 'client', + timestamp: true, + hash: true + }, + src: [ + 'img/*.*', + 'css/default.cache.*.css', + 'js/default.cache.*.js', + 'font/*.*', + '*.*' + ], + dest: 'client/default.appcache' + } + }, + zip: { + 'using-cwd': { + src: ['manifest.xml', 'api_explorer/**/*.*', 'server/php/**/*.*', 'media/**/*.*', 'client/*.*', 'client/css/default.cache.*.css', 'client/js/default.cache.*.js', 'client/font/**/*.*', 'client/img/**/*.*'], + dest: 'restyaboard.zip' + } + }, + jasmine: { + pivotal: { + src: js_files, + options: { + specs: [], + helpers: [] + } + }, + coverage: { + src: js_files, + options: { + specs: 'client/spec/**/*.js', + template: require('grunt-template-jasmine-istanbul'), + templateOptions: { + report: [{ + type: 'html', + options: { + dir: 'client/html' + } + }, { + type: 'text-summary' + }] + } + } + } + }, + plato: { + default: { + files: { + 'reports': source_js_files + } + }, + }, + complexity: { + generic: { + src: source_js_files, + options: { + breakOnErrors: true, + jsLintXML: 'report.xml', + checkstyleXML: 'checkstyle.xml', + errorsOnly: false, + cyclomatic: [3, 7, 12], + halstead: [8, 13, 20], + maintainability: 100, + hideComplexFunctions: false, + broadcast: false + } + } + }, + docco: { + debug: { + src: source_js_files, + options: { + output: 'docs/' + } + } + }, + watch: { + files: ['client/js/**/*.js', 'client/js/templates/*.ejs', 'client/css/**/*.css', '!client/css/bootstrap.css', '!client/css/default.cache.css', '!client/js/templates/templates.js', '!client/js/default.cache.js'], + tasks: ['jshint', 'less', 'jst'] + } + }); + grunt.loadNpmTasks('grunt-contrib-jshint'); + grunt.loadNpmTasks('grunt-phplint'); + grunt.loadNpmTasks('grunt-contrib-less'); + grunt.loadNpmTasks('grunt-contrib-jst'); + grunt.loadNpmTasks('grunt-contrib-concat'); + grunt.loadNpmTasks('grunt-jsbeautifier'); + grunt.loadNpmTasks('grunt-prettify'); + grunt.loadNpmTasks('grunt-contrib-cssmin'); + grunt.loadNpmTasks('grunt-contrib-uglify'); + grunt.loadNpmTasks('grunt-filerev'); + grunt.loadNpmTasks('grunt-usemin'); + grunt.loadNpmTasks('grunt-contrib-htmlmin'); + grunt.loadNpmTasks('grunt-regex-replace'); + grunt.loadNpmTasks('grunt-manifest'); + grunt.loadNpmTasks('grunt-zip'); + grunt.loadNpmTasks('grunt-contrib-jasmine'); + grunt.loadNpmTasks('grunt-plato'); + grunt.loadNpmTasks('grunt-complexity'); + grunt.loadNpmTasks('grunt-docco'); + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.registerTask('default', ['jshint', 'phplint', 'less', 'jst', 'jsbeautifier', 'prettify']); + grunt.registerTask('build', ['jshint', 'phplint', 'less', 'jst', 'concat', 'cssmin', 'uglify', 'filerev', 'usemin', 'htmlmin', 'regex-replace:build', 'manifest', 'zip']); +}; diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 000000000..1e1c2182e --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,47 @@ +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/README.md b/README.md index 0c4b9e1da..398e4d200 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,81 @@ -# Restyaboard +### Restyaboard -http://restya.com/board/ +Trello like kanban board. [Restyaboard](http://restya.com/board/) is based on [Restya platform] (http://restya.com/). -Demo available now. +### Demo -Source code will be pushed once AWS AMI is ready for sale (One of our revenue options) +[Demo](http://restya.com/board/demo.html) + +### Install + +* [Install](http://restya.com/board/install.html) +* [Configure](http://restya.com/board/install.html#configure) +* [Importing from trello](http://restya.com/board/install.html#import-trello) + +### Known Issues + +[Known Issues](http://restya.com/board/issues.html) + +------------ + +### Current Status / Plans / Roadamap + +For speed, our codebase is in our local GitLab repo. We have pushed our `master` branch alone here and will sync periodically. So, to give you some idea about of our plans: + +#### Immediate (cooking) + +* Chat +* App + * Simple app architecture + * "Import from GitHub" sample app + * Current API tweaks to accept other oAuth clients "properly" +* UI enhancements +* Better responsive mobile view +* Address some [known issues](http://restya.com/board/issues.html) +* Notifier iOS App (Possibly, Free and non-open source) + + +#### Next + +* Email notifications + * Brainstorm for "best" approach +* Refactor R framework + * Our focus on shipping this somewhat bloated "ultra thin" R framework. Better use new "REST URL to DB Query builder" code once that is tested (?). +* Merge caching layer works (Or, only in commerical?) +* Marketplace for ecosystem + * Allow developers to make money +* Apps listing platform + * Find apps easily + +------------ + +### Contributing + +Our approach is similar to Magento. If anything is not clear, please [contact us](http://restya.com/contact.html?category=contributing). + +All Submissions you make to Restya through GitHub are subject to the following terms and conditions: + +* You grant Restya a perpetual, worldwide, non-exclusive, no charge, royalty free, irrevocable license under your applicable copyrights and patents to reproduce, prepare derivative works of, display, publicly perform, sublicense and distribute any feedback, ideas, code, or other information ("Submission") you submit through GitHub. +* Your Submission is an original work of authorship and you are the owner or are legally entitled to grant the license stated above. + + + +### Build + +Required sofware: nginx, php-fpm (with mbstring), PostgreSQL, ElasticSearch, Grunt + +* `grunt less` - Converts LESS to CSS +* `grunt jst` - Converts EJS to JS +* `grunt watch` - Converts LESS to CSS and EJS to JS, automatically by "watching" for file changes +* `restyaboard_with_empty_data.sql` - Database generation script +* `server/php/R/config.inc.php` - Database and other configurations +* `media` - Need write permission for php; can be `chmod 655` or `755` or `777` depending upon server configuration +* `grunt build` - Generates restyaboard.zip, deployable code + +------------ + +### License + +Copyright (c) 2014-2015 [Restya](http://restya.com/). + +Dual License ([OSL 3.0](LICENSE.txt) & [Commercial License](http://restya.com/contact.html)) \ No newline at end of file diff --git a/api_explorer/api-docs/activities_listing.json b/api_explorer/api-docs/activities_listing.json new file mode 100644 index 000000000..5c905e553 --- /dev/null +++ b/api_explorer/api-docs/activities_listing.json @@ -0,0 +1,53 @@ +{ + "apiVersion": "1.0.0", + "swaggerVersion": "1.2", + "basePath": "/api", + "resourcePath": "/checklist_items", + "produces": [ + "application/json", + "application/xml", + "text/plain", + "text/html" + ], + "apis": [ + { + "path": "/activities/undo/{activityId}.json", + "operations": [ + { + "method": "PUT", + "summary": "Undo activities", + "notes": "Update the activities", + "type": "activities", + "nickname": "editActivities", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "activityId", + "description": "Activity ID to undo", + "required": true, + "type": "string", + "paramType": "path" + } + ], + "responseMessages": [ + { + "code": 400, + "message": "Invalid ID supplied" + }, + { + "code": 404, + "message": "Page not found" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/api_explorer/api-docs/boards.json b/api_explorer/api-docs/boards.json new file mode 100644 index 000000000..3fba62e15 --- /dev/null +++ b/api_explorer/api-docs/boards.json @@ -0,0 +1,668 @@ +{ + "apiVersion": "1.0.0", + "swaggerVersion": "1.2", + "basePath": "/api", + "resourcePath": "/Board", + "produces": [ + "application/json", + "application/xml", + "text/plain", + "text/html" + ], + "apis": [ + { + "path": "/v1/boards.json", + "operations": [ + { + "method": "POST", + "summary": "Add a new Board", + "notes": "", + "type": "void", + "nickname": "addBoard", + "consumes": [ + "application/json", + "application/xml" + ], + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "body", + "description": "Created Board", + "required": true, + "type": "addBoard", + "paramType": "body" + } + ], + "responseMessages": [ + { + "code": 405, + "message": "Invalid input" + } + ] + }, + { + "method": "GET", + "summary": "Find Board by userId", + "notes": "Returns a Board based on userID", + "type": "Board", + "nickname": "findBoardsByUserID", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "userId", + "description": "Current Login user ID", + "required": true, + "type": "string", + "paramType": "path" + } + ], + "responseMessages": [ + { + "code": 400, + "message": "Invalid ID supplied" + }, + { + "code": 404, + "message": "Pet not found" + } + ] + } + ] + }, + { + "path": "/v1/boards/{boardId}.json", + "operations": [ + { + "method": "GET", + "summary": "View Board", + "notes": "Returns a Board based on ID", + "type": "Board", + "nickname": "getBoardById", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "ID of Board that needs to be fetched", + "required": true, + "type": "integer", + "format": "int64", + "paramType": "path", + "minimum": "1.0", + "maximum": "100000.0" + } + ], + "responseMessages": [ + { + "code": 400, + "message": "Invalid ID supplied" + }, + { + "code": 404, + "message": "Pet not found" + } + ] + }, + { + "method": "PUT", + "summary": "Edit Board", + "notes": "Update the board details", + "type": "Board", + "nickname": "editBoard", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "ID of Board that needs to be fetched", + "required": true, + "type": "integer", + "format": "int64", + "paramType": "path", + "minimum": "1.0", + "maximum": "100000.0" + }, + { + "name": "body", + "description": "Edit Board", + "required": true, + "type": "editBoard", + "paramType": "body" + } + ], + "responseMessages": [ + { + "code": 400, + "message": "Invalid ID supplied" + }, + { + "code": 404, + "message": "Pet not found" + } + ] + }, + { + "method": "DELETE", + "summary": "Delete the board", + "notes": "", + "type": "void", + "nickname": "deleteBoard", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Board ID to delete", + "required": true, + "type": "string", + "paramType": "path" + } + ] + } + ] + }, + { + "path": "/v1/boards/{boardId}/activities.json", + "operations": [ + { + "method": "GET", + "summary": "Activities", + "notes": "Returns Board Activities", + "type": "Activities", + "nickname": "Activities listing", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Selected board ID", + "required": true, + "type": "string", + "paramType": "path" + } + ] + } + ] + }, + { + "path": "/v1/boards/{boardId}/board_subscribers.json", + "operations": [ + { + "method": "POST", + "summary": "Add subscriber to Board", + "notes": "", + "type": "void", + "nickname": "addBoardSubscriber", + "consumes": [ + "application/json", + "application/xml" + ], + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "ID of Board that needs add Subscriber into the Board", + "required": true, + "type": "integer", + "format": "int64", + "paramType": "path", + "minimum": "1.0", + "maximum": "100000.0" + } + ], + "responseMessages": [ + { + "code": 405, + "message": "Invalid input" + } + ] + } + ] + }, + { + "path": "/v1/boards/{boardId}/board_subscribers/{userId}.json", + "operations": [ + { + "method": "PUT", + "summary": "Unsubscribe the Board", + "notes": "", + "type": "void", + "nickname": "unSubscribeBoard", + "consumes": [ + "application/json", + "application/xml" + ], + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "ID of Board that needs Unsubscribe from the Board", + "required": true, + "type": "integer", + "format": "int64", + "paramType": "path", + "minimum": "1.0", + "maximum": "100000.0" + }, + { + "name": "userId", + "description": "ID of User that needs to Unsubscribe from the Board", + "required": true, + "type": "integer", + "format": "int64", + "paramType": "path", + "minimum": "1.0", + "maximum": "100000.0" + } + ], + "responseMessages": [ + { + "code": 405, + "message": "Invalid input" + } + ] + } + ] + }, + { + "path": "/v1/boards_users/{id}.json", + "operations": [ + { + "method": "DELETE", + "summary": "Remove member from the Board", + "notes": "", + "type": "void", + "nickname": "removeBoardMember", + "consumes": [ + "application/json", + "application/xml" + ], + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "id", + "description": "ID of BoardUser Table needs to Remove", + "required": true, + "type": "integer", + "format": "int64", + "paramType": "path", + "minimum": "1.0", + "maximum": "100000.0" + } + ], + "responseMessages": [ + { + "code": 405, + "message": "Invalid input" + } + ] + } + ] + }, + { + "path": "/v1/boards/{boardId}/board_subscribers.json", + "operations": [ + { + "method": "GET", + "summary": "listing the board Subscribers", + "notes": "", + "type": "void", + "nickname": "listBoardSubscribers", + "consumes": [ + "application/json", + "application/xml" + ], + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "ID of Board that needs to be fetched", + "required": true, + "type": "integer", + "format": "int64", + "paramType": "path", + "minimum": "1.0", + "maximum": "100000.0" + } + ], + "responseMessages": [ + { + "code": 405, + "message": "Invalid input" + } + ] + } + ] + }, + { + "path": "/v1/boards/{boardId}/visibility.json", + "operations": [ + { + "method": "GET", + "summary": "Get the board visibility", + "notes": "", + "type": "void", + "nickname": "boardVisibility", + "consumes": [ + "application/json", + "application/xml" + ], + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "ID of Board that needs to be fetched", + "required": true, + "type": "integer", + "format": "int64", + "paramType": "path", + "minimum": "1.0", + "maximum": "100000.0" + } + ], + "responseMessages": [ + { + "code": 405, + "message": "Invalid input" + } + ] + } + ] + }, + { + "path": "/v1/boards/{boardId}/users.json", + "operations": [ + { + "method": "POST", + "summary": "Add board member", + "notes": "", + "type": "void", + "nickname": "addBoardMember", + "consumes": [ + "application/json", + "application/xml" + ], + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "ID of Board that needs to be fetched", + "required": true, + "type": "integer", + "format": "int64", + "paramType": "path", + "minimum": "1.0", + "maximum": "100000.0" + }, + { + "name": "body", + "description": "Add Board Member", + "required": true, + "type": "addBoardMember", + "paramType": "body" + } + ], + "responseMessages": [ + { + "code": 405, + "message": "Invalid input" + } + ] + } + ] + }, + { + "path": "/v1/boards/{boardId}/copy.json", + "operations": [ + { + "method": "POST", + "summary": "Copy board", + "notes": "Copy board", + "type": "void", + "nickname": "copy_board", + "consumes": [ + "application/json", + "application/xml" + ], + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "ID of Board that needs to be fetched", + "required": true, + "type": "integer", + "format": "int64", + "paramType": "path", + "minimum": "1.0", + "maximum": "100000.0" + }, + { + "name": "body", + "description": "Copy Board", + "required": true, + "type": "copy_board", + "paramType": "body" + } + ], + "responseMessages": [ + { + "code": 405, + "message": "Invalid input" + } + ] + } + ] + }, + { + "path": "/v1/boards_users/{userId}.json", + "operations": [ + { + "method": "PUT", + "summary": "Update board member permissions", + "notes": "Update board member permissions", + "type": "void", + "nickname": "edit_board_user", + "consumes": [ + "application/json", + "application/xml" + ], + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "userId", + "description": "ID of Board user that needs to be update", + "required": true, + "type": "integer", + "format": "int64", + "paramType": "path", + "minimum": "1.0", + "maximum": "100000.0" + } + ], + "responseMessages": [ + { + "code": 405, + "message": "Invalid input" + } + ] + } + ] + } + ], + "models": { + "Board": { + "id": "Board", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64", + "description": "unique identifier for the board" + }, + "description": { + "type": "string" + } + } + }, + "addBoard": { + "id": "addBoard", + "required": [ + "name", + "board_visibility" + ], + "properties": { + "name": { + "type": "string" + }, + "board_visibility": { + "type": "integer" + } + } + }, + "editBoard": { + "id": "editBoard", + "required": [ + "name", + "board_visibility" + ], + "properties": { + "name": { + "type": "string" + }, + "board_visibility": { + "type": "integer" + }, + "organization_id": { + "type": "integer" + } + } + }, + "addBoardMember": { + "id": "addBoardMember", + "required": [ + "user_id" + ], + "properties": { + "user_id": { + "type": "integer" + } + } + }, + "copy_board": { + "id": "copy_board", + "required": [ + "board_visibility", + "organization_id", + "keepCards" + ], + "properties": { + "board_visibility": { + "type": "integer" + }, + "organization_id": { + "type": "integer" + }, + "keepCards": { + "type": "boolean" + } + } + } + } +} \ No newline at end of file diff --git a/api_explorer/api-docs/card_attachments.json b/api_explorer/api-docs/card_attachments.json new file mode 100644 index 000000000..2f0615a4e --- /dev/null +++ b/api_explorer/api-docs/card_attachments.json @@ -0,0 +1,67 @@ +{ + "apiVersion": "1.0.0", + "swaggerVersion": "1.2", + "basePath": "/api", + "resourcePath": "/card_attachments", + "produces": [ + "application/json", + "application/xml", + "text/plain", + "text/html" + ], + "apis": [ + { + "path": "/v1/boards/{boardId}/lists/{listId}/cards/{cardId}/attachments.json", + "operations": [ + { + "method": "POST", + "summary": "Add a Label", + "notes": "", + "type": "void", + "nickname": "addLabel", + "consumes": [ + "application/json", + "application/xml" + ], + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Selected board ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "listId", + "description": "Selected list ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "cardId", + "description": "Selected card ID", + "required": true, + "type": "string", + "paramType": "path" + } + ], + "responseMessages": [ + { + "code": 405, + "message": "Invalid input" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/api_explorer/api-docs/card_voters.json b/api_explorer/api-docs/card_voters.json new file mode 100644 index 000000000..3b2aadfe3 --- /dev/null +++ b/api_explorer/api-docs/card_voters.json @@ -0,0 +1,210 @@ +{ + "apiVersion": "1.0.0", + "swaggerVersion": "1.2", + "basePath": "/api", + "resourcePath": "/card_voters", + "produces": [ + "application/json", + "application/xml", + "text/plain", + "text/html" + ], + "apis": [ + { + "path": "/v1/boards/{boardId}/lists/{listId}/cards/{cardId}/card_voters.json", + "operations": [ + { + "method": "POST", + "summary": "Add a Vote", + "notes": "", + "type": "void", + "nickname": "addVote", + "consumes": [ + "application/json", + "application/xml" + ], + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Selected board ID", + "required": true, + "type": "integer", + "paramType": "path" + }, + { + "name": "listId", + "description": "Selected list ID", + "required": true, + "type": "integer", + "paramType": "path" + }, + { + "name": "cardId", + "description": "Selected card ID", + "required": true, + "type": "integer", + "paramType": "path" + } + ], + "responseMessages": [ + { + "code": 405, + "message": "Invalid input" + } + ] + }, + { + "method": "DELETE", + "summary": "Delete all Card voters", + "notes": "", + "type": "void", + "nickname": "deleteCardVoter", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Selected board ID", + "required": true, + "type": "integer", + "paramType": "path" + }, + { + "name": "listId", + "description": "Selected list ID", + "required": true, + "type": "integer", + "paramType": "path" + }, + { + "name": "cardId", + "description": "Card ID to delete", + "required": true, + "type": "integer", + "paramType": "path" + } + ] + } + ] + }, + { + "path": "/v1/boards/{boardId}/lists/{listId}/cards/{cardId}/card_voters/{cardVoterId}.json", + "operations": [ + { + "method": "PUT", + "summary": "Update a Vote", + "notes": "", + "type": "void", + "nickname": "updateVote", + "consumes": [ + "application/json", + "application/xml" + ], + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Selected board ID", + "required": true, + "type": "integer", + "paramType": "path" + }, + { + "name": "listId", + "description": "Selected list ID", + "required": true, + "type": "integer", + "paramType": "path" + }, + { + "name": "cardId", + "description": "Card ID to delete", + "required": true, + "type": "integer", + "paramType": "path" + }, + { + "name": "cardVoterId", + "description": "Selected card voter ID", + "required": true, + "type": "string", + "paramType": "path" + } + ], + "responseMessages": [ + { + "code": 405, + "message": "Invalid input" + } + ] + }, + { + "method": "DELETE", + "summary": "Delete the Card voter", + "notes": "", + "type": "void", + "nickname": "deleteCardVoter", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Selected board ID", + "required": true, + "type": "integer", + "paramType": "path" + }, + { + "name": "listId", + "description": "Selected list ID", + "required": true, + "type": "integer", + "paramType": "path" + }, + { + "name": "cardId", + "description": "Card ID to delete", + "required": true, + "type": "integer", + "paramType": "path" + }, + { + "name": "cardVoterId", + "description": "Card voter ID to delete", + "required": true, + "type": "string", + "paramType": "path" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/api_explorer/api-docs/cards.json b/api_explorer/api-docs/cards.json new file mode 100644 index 000000000..8c280e8df --- /dev/null +++ b/api_explorer/api-docs/cards.json @@ -0,0 +1,748 @@ +{ + "apiVersion": "1.0.0", + "swaggerVersion": "1.2", + "basePath": "/api", + "resourcePath": "/Card", + "produces": [ + "application/json", + "application/xml", + "text/plain", + "text/html" + ], + "apis": [ + { + "path": "/v1/boards/{boardId}/lists/{listId}/cards.json", + "operations": [ + { + "method": "POST", + "summary": "Add a new Card", + "notes": "", + "type": "void", + "nickname": "addCard", + "consumes": [ + "application/json", + "application/xml" + ], + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Selected board ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "listId", + "description": "Selected list ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "body", + "description": "Created Card", + "required": true, + "type": "addCard", + "paramType": "body" + } + ], + "responseMessages": [ + { + "code": 405, + "message": "Invalid input" + } + ] + } + ] + }, + { + "path": "/v1/boards/{boardId}/lists/{listId}/cards/{cardId}.json", + "operations": [ + { + "method": "PUT", + "summary": "Edit Card", + "notes": "Update the Card details", + "type": "Card", + "nickname": "editCard", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Selected board ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "listId", + "description": "Selected list ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "cardId", + "description": "Selected card ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "body", + "description": "Edit Card", + "required": true, + "type": "editCard", + "paramType": "body" + } + ], + "responseMessages": [ + { + "code": 400, + "message": "Invalid ID supplied" + }, + { + "code": 404, + "message": "Pet not found" + } + ] + }, + { + "method": "DELETE", + "summary": "Delete the Card", + "notes": "", + "type": "void", + "nickname": "deleteCard", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Board ID to delete", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "listId", + "description": "List ID to delete", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "cardId", + "description": "Card ID to delete", + "required": true, + "type": "string", + "paramType": "query" + } + ] + } + ] + }, + { + "path": "/v1/boards/{boardId}/lists/{listId}/cards/{cardId}/activities.json", + "operations": [ + { + "method": "GET", + "summary": "Activities", + "notes": "Returns Card Activities", + "type": "Activities", + "nickname": "Activities listing", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Selected board ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "listId", + "description": "Selected list ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "cardId", + "description": "Selected card ID", + "required": true, + "type": "string", + "paramType": "path" + } + ] + } + ] + }, + { + "path": "/v1/boards/{boardId}/lists/{listId}/cards/{cardId}/card_subscribers.json", + "operations": [ + { + "method": "POST", + "summary": "Subscribe Card", + "notes": "Subscribe Card", + "type": "void", + "nickname": "subscribeCard", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Selected board ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "listId", + "description": "Selected list ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "cardId", + "description": "Selected card ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "body", + "description": "Subscribe Card", + "required": true, + "type": "subscribeCard", + "paramType": "body" + } + ] + } + ] + }, + { + "path": "/v1/boards/{boardId}/lists/{listId}/cards/{cardId}/checklists/{checkListId}/items/{itemId}/convert_to_card.json", + "operations": [ + { + "method": "POST", + "summary": "Convert item to card", + "notes": "Convert item to card", + "type": "void", + "nickname": "convert_item_to_card", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Selected board ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "listId", + "description": "Selected list ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "cardId", + "description": "Selected card ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "checkListId", + "description": "Selected Checklist ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "itemId", + "description": "Selected Checklist Item ID", + "required": true, + "type": "string", + "paramType": "path" + } + ] + } + ] + }, + { + "path": "/v1/boards/{boardId}/lists/{listId}/cards/{cardId}/users/{userId}.json", + "operations": [ + { + "method": "POST", + "summary": "Assign member to card", + "notes": "Assign member to card", + "type": "void", + "nickname": "add_card_user", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Selected board ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "listId", + "description": "Selected list ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "cardId", + "description": "Selected card ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "userId", + "description": "Selected User ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "body", + "description": "Assign member to card", + "required": true, + "type": "add_card_user", + "paramType": "body" + } + ] + } + ] + }, + { + "path": "/v1/boards/{boardId}/lists/{listId}/cards/{cardId}/copy.json", + "operations": [ + { + "method": "POST", + "summary": "Copy card", + "notes": "Copy card", + "type": "void", + "nickname": "copy_card", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Selected board ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "listId", + "description": "Selected list ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "cardId", + "description": "Selected card ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "body", + "description": "Assign member to card", + "required": true, + "type": "add_card_user", + "paramType": "body" + } + ] + } + ] + }, + { + "path": "/v1/boards/{boardId}/lists/{listId}/cards/{cardId}/attachments/{attachmentId}.json", + "operations": [ + { + "method": "DELETE", + "summary": "Remove attachment from card", + "notes": "Remove attachment from card", + "type": "void", + "nickname": "remove_card_attachment", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Selected board ID", + "required": true, + "type": "integer", + "paramType": "path" + }, + { + "name": "listId", + "description": "Selected list ID", + "required": true, + "type": "integer", + "paramType": "path" + }, + { + "name": "cardId", + "description": "Selected card ID", + "required": true, + "type": "integer", + "paramType": "path" + }, + { + "name": "attachmentId", + "description": "attachment ID to remove", + "required": true, + "type": "integer", + "paramType": "path" + } + ] + } + ] + }, + { + "path": "/v1/users/{userId}/cards.json", + "operations": [ + { + "method": "GET", + "summary": "View user cards", + "notes": "View user cards", + "type": "void", + "nickname": "view_user_cards", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "userId", + "description": "Selected User ID", + "required": true, + "type": "integer", + "paramType": "path" + } + ] + } + ] + }, + { + "path": "/v1/boards/{boardId}/lists/{listId}/cards.json", + "operations": [ + { + "method": "GET", + "summary": "Archived card send back to board", + "notes": "Archived card send back to board", + "type": "void", + "nickname": "send_back_to_archived_card", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Selected Board ID", + "required": true, + "type": "integer", + "paramType": "path" + }, + { + "name": "listId", + "description": "Selected List ID", + "required": true, + "type": "integer", + "paramType": "path" + }, + { + "name": "body", + "description": "Archived card send back to board", + "required": true, + "type": "send_back_to_archived_card", + "paramType": "body" + } + ] + } + ] + }, + { + "path": "/v1/boards/{boardId}/lists/{listId}/cards.json", + "operations": [ + { + "method": "PUT", + "summary": "Move list cards", + "notes": "Move list cards", + "type": "void", + "nickname": "move_list_cards", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Selected Board ID", + "required": true, + "type": "integer", + "paramType": "path" + }, + { + "name": "listId", + "description": "Selected List ID", + "required": true, + "type": "integer", + "paramType": "path" + }, + { + "name": "body", + "description": "Move list cards", + "required": true, + "type": "move_list_cards", + "paramType": "body" + } + ] + } + ] + } + ], + "models": { + "addCard": { + "id": "addCard", + "required": [ + "board_id", + "list_id", + "name", + "position" + ], + "properties": { + "board_id": { + "type": "integer" + }, + "list_id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "position": { + "type": "integer" + } + } + }, + "editCard": { + "id": "editCard", + "required": [ + "name", + "position", + "archive", + "due date" + ], + "properties": { + "name": { + "type": "string" + }, + "position": { + "type": "string" + }, + "archive": { + "type": "string" + }, + "due date": { + "type": "string" + } + } + }, + "subscribeCard": { + "id": "subscribeCard", + "required": [ + "board_id", + "list_id", + "is_subscribe" + ], + "properties": { + "board_id": { + "type": "integer" + }, + "list_id": { + "type": "integer" + }, + "is_subscribe": { + "type": "boolean" + } + } + }, + "add_card_user": { + "id": "add_card_user", + "properties": { + "board_id": { + "type": "integer" + }, + "list_id": { + "type": "integer" + }, + "is_offline": { + "type": "boolean" + }, + "profile_picture_path": { + "type": "string" + }, + "username": { + "type": "string" + }, + "initials": { + "type": "string" + } + } + }, + "copy_card": { + "id": "copy_card", + "required": [ + "keep_attachments", + "keep_activities", + "keep_labels", + "keep_users", + "keep_checklists", + "copied_card_id", + "name" + ], + "properties": { + "keep_attachments": { + "type": "boolean" + }, + "keep_activities": { + "type": "boolean" + }, + "keep_labels": { + "type": "boolean" + }, + "keep_users": { + "type": "boolean" + }, + "keep_checklists": { + "type": "boolean" + }, + "copied_card_id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "send_back_to_archived_card": { + "id": "send_back_to_archived_card", + "required": [ + "board_id", + "list_id", + "position" + ], + "properties": { + "board_id": { + "type": "integer" + }, + "list_id": { + "type": "integer" + }, + "position": { + "type": "integer" + }, + "due_date": { + "type": "date" + }, + "user_ids": { + "type": "string" + } + } + }, + "move_list_cards": { + "id": "move_list_cards", + "properties": { + "list_id": { + "type": "integer" + }, + "is_archived": { + "type": "boolean" + } + } + } + } +} \ No newline at end of file diff --git a/api_explorer/api-docs/cards_labels.json b/api_explorer/api-docs/cards_labels.json new file mode 100644 index 000000000..cabbd8a7d --- /dev/null +++ b/api_explorer/api-docs/cards_labels.json @@ -0,0 +1,67 @@ +{ + "apiVersion": "1.0.0", + "swaggerVersion": "1.2", + "basePath": "/api", + "resourcePath": "/cards_labels", + "produces": [ + "application/json", + "application/xml", + "text/plain", + "text/html" + ], + "apis": [ + { + "path": "/v1/boards/{boardId}/lists/{listId}/cards/{cardId}/labels.json", + "operations": [ + { + "method": "POST", + "summary": "Add a Label", + "notes": "", + "type": "void", + "nickname": "addLabel", + "consumes": [ + "application/json", + "application/xml" + ], + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Selected board ID", + "required": true, + "type": "integer", + "paramType": "path" + }, + { + "name": "listId", + "description": "Selected list ID", + "required": true, + "type": "integer", + "paramType": "path" + }, + { + "name": "cardId", + "description": "Card ID to delete", + "required": true, + "type": "integer", + "paramType": "path" + } + ], + "responseMessages": [ + { + "code": 405, + "message": "Invalid input" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/api_explorer/api-docs/checklist_items.json b/api_explorer/api-docs/checklist_items.json new file mode 100644 index 000000000..15aaa2b80 --- /dev/null +++ b/api_explorer/api-docs/checklist_items.json @@ -0,0 +1,225 @@ +{ + "apiVersion": "1.0.0", + "swaggerVersion": "1.2", + "basePath": "/api", + "resourcePath": "/checklist_items", + "produces": [ + "application/json", + "application/xml", + "text/plain", + "text/html" + ], + "apis": [ + { + "path": "/v1/boards/{boardId}/lists/{listId}/cards/{cardId}/checklists/{checklistId}/items.json", + "operations": [ + { + "method": "POST", + "summary": "Add a checklist item", + "notes": "", + "type": "void", + "nickname": "addChecklistItem", + "consumes": [ + "application/json", + "application/xml" + ], + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Selected board ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "listId", + "description": "Selected board ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "cardId", + "description": "Selected board ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "checklistId", + "description": "Selected checklist ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "body", + "description": "Created checklist object", + "required": true, + "type": "addChecklistItem", + "paramType": "body" + } + ], + "responseMessages": [ + { + "code": 405, + "message": "Invalid input" + } + ] + } + ] + }, + { + "path": "/v1/boards/{boardId}/lists/{listId}/cards/{cardId}/checklists/{checklistId}/items/{checklistItemId}.json", + "operations": [ + { + "method": "PUT", + "summary": "Edit Checklist", + "notes": "Update the Checklist", + "type": "Comment", + "nickname": "editChecklistItem", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Board ID to edit", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "listId", + "description": "List ID to edit", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "cardId", + "description": "Card ID to edit", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "checklistId", + "description": "Checklist ID to edit", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "checklistItemId", + "description": "Checklist item ID to edit", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "body", + "description": "Edit Checklist", + "required": true, + "type": "editChecklistItem", + "paramType": "body" + } + ], + "responseMessages": [ + { + "code": 400, + "message": "Invalid ID supplied" + }, + { + "code": 404, + "message": "Page not found" + } + ] + }, + { + "method": "DELETE", + "summary": "Delete comment", + "notes": "", + "type": "void", + "nickname": "deleteCardVoter", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Board ID to delete", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "listId", + "description": "List ID to delete", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "cardId", + "description": "Card ID to delete", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "checklistId", + "description": "Checklist ID to delete", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "checklistItemId", + "description": "Checklist item ID to edit", + "required": true, + "type": "string", + "paramType": "path" + } + ] + } + ] + } + ], + "models": { + "addChecklistItem": { + "id": "addChecklistItem", + "properties": { + "name": { + "type": "string" + } + } + }, + "editChecklistItem": { + "id": "editChecklistItem", + "properties": { + "name": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/api_explorer/api-docs/checklists.json b/api_explorer/api-docs/checklists.json new file mode 100644 index 000000000..7c400f89e --- /dev/null +++ b/api_explorer/api-docs/checklists.json @@ -0,0 +1,204 @@ +{ + "apiVersion": "1.0.0", + "swaggerVersion": "1.2", + "basePath": "/api", + "resourcePath": "/checklists", + "produces": [ + "application/json", + "application/xml", + "text/plain", + "text/html" + ], + "apis": [ + { + "path": "/v1/boards/{boardId}/lists/{listId}/cards/{cardId}/checklists.json", + "operations": [ + { + "method": "POST", + "summary": "Add a checklist", + "notes": "", + "type": "void", + "nickname": "addChecklist", + "consumes": [ + "application/json", + "application/xml" + ], + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Selected board ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "listId", + "description": "Selected board ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "cardId", + "description": "Selected board ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "body", + "description": "Created checklist object", + "required": true, + "type": "addChecklist", + "paramType": "body" + } + ], + "responseMessages": [ + { + "code": 405, + "message": "Invalid input" + } + ] + } + ] + }, + { + "path": "/v1/boards/{boardId}/lists/{listId}/cards/{cardId}/checklists/{checklistId}.json", + "operations": [ + { + "method": "PUT", + "summary": "Edit Checklist", + "notes": "Update the Checklist", + "type": "Comment", + "nickname": "editChecklist", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Board ID to edit", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "listId", + "description": "List ID to edit", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "cardId", + "description": "Card ID to edit", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "checklistId", + "description": "Checklist ID to edit", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "body", + "description": "Edit Checklist", + "required": true, + "type": "editChecklist", + "paramType": "body" + } + ], + "responseMessages": [ + { + "code": 400, + "message": "Invalid ID supplied" + }, + { + "code": 404, + "message": "Page not found" + } + ] + }, + { + "method": "DELETE", + "summary": "Delete comment", + "notes": "", + "type": "void", + "nickname": "deleteCardVoter", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Board ID to delete", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "listId", + "description": "List ID to delete", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "cardId", + "description": "Card ID to delete", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "checklistId", + "description": "Checklist ID to delete", + "required": true, + "type": "string", + "paramType": "path" + } + ] + } + ] + } + ], + "models": { + "addChecklist": { + "id": "addChecklist", + "properties": { + "name": { + "type": "string" + } + } + }, + "editChecklist": { + "id": "editChecklist", + "properties": { + "name": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/api_explorer/api-docs/comments.json b/api_explorer/api-docs/comments.json new file mode 100644 index 000000000..d58e85167 --- /dev/null +++ b/api_explorer/api-docs/comments.json @@ -0,0 +1,204 @@ +{ + "apiVersion": "1.0.0", + "swaggerVersion": "1.2", + "basePath": "/api", + "resourcePath": "/activities", + "produces": [ + "application/json", + "application/xml", + "text/plain", + "text/html" + ], + "apis": [ + { + "path": "/v1/boards/{boardId}/lists/{listId}/cards/{cardId}/comment.json", + "operations": [ + { + "method": "POST", + "summary": "Add a comment", + "notes": "", + "type": "void", + "nickname": "addComment", + "consumes": [ + "application/json", + "application/xml" + ], + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Selected board ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "listId", + "description": "Selected list ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "cardId", + "description": "Selected card ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "body", + "description": "Created comment object", + "required": true, + "type": "addComment", + "paramType": "body" + } + ], + "responseMessages": [ + { + "code": 405, + "message": "Invalid input" + } + ] + } + ] + }, + { + "path": "/v1/boards/{boardId}/lists/{listId}/cards/{cardId}/comment/{commentId}.json", + "operations": [ + { + "method": "PUT", + "summary": "Edit Comment", + "notes": "Update the Comment", + "type": "Comment", + "nickname": "editComment", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Board ID to edit", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "listId", + "description": "List ID to edit", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "cardId", + "description": "Card ID to edit", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "commentId", + "description": "Comment ID to edit", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "body", + "description": "Edit Comment", + "required": true, + "type": "editComment", + "paramType": "body" + } + ], + "responseMessages": [ + { + "code": 400, + "message": "Invalid ID supplied" + }, + { + "code": 404, + "message": "Page not found" + } + ] + }, + { + "method": "DELETE", + "summary": "Delete comment", + "notes": "", + "type": "void", + "nickname": "deleteCardVoter", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Board ID to delete", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "listId", + "description": "List ID to delete", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "cardId", + "description": "Card ID to delete", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "commentId", + "description": "Comment ID to delete", + "required": true, + "type": "string", + "paramType": "path" + } + ] + } + ] + } + ], + "models": { + "addComment": { + "id": "addComment", + "properties": { + "comment": { + "type": "string" + } + } + }, + "editComment": { + "id": "editComment", + "properties": { + "comment": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/api_explorer/api-docs/index.php b/api_explorer/api-docs/index.php new file mode 100644 index 000000000..2fe04491a --- /dev/null +++ b/api_explorer/api-docs/index.php @@ -0,0 +1,94 @@ + \ No newline at end of file diff --git a/api_explorer/api-docs/lists.json b/api_explorer/api-docs/lists.json new file mode 100644 index 000000000..830c7c91d --- /dev/null +++ b/api_explorer/api-docs/lists.json @@ -0,0 +1,323 @@ +{ + "apiVersion": "1.0.0", + "swaggerVersion": "1.2", + "basePath": "/api", + "resourcePath": "/List", + "produces": [ + "application/json", + "application/xml", + "text/plain", + "text/html" + ], + "apis": [ + { + "path": "/v1/boards/{boardId}/lists.json", + "operations": [ + { + "method": "POST", + "summary": "Add a new List", + "notes": "", + "type": "void", + "nickname": "addList", + "consumes": [ + "application/json", + "application/xml" + ], + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "ID of Board that needs to be fetched", + "required": true, + "type": "integer", + "format": "int64", + "paramType": "path", + "minimum": "1.0", + "maximum": "100000.0" + }, + { + "name": "body", + "description": "Created List", + "required": true, + "type": "addList", + "paramType": "body" + } + ], + "responseMessages": [ + { + "code": 405, + "message": "Invalid input" + } + ] + } + ] + }, + { + "path": "/v1/boards/{boardId}/lists/{listId}.json", + "operations": [ + { + "method": "PUT", + "summary": "Edit List", + "notes": "Update the List details", + "type": "List", + "nickname": "editList", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "ID of Board that needs to be fetched", + "required": true, + "type": "integer", + "format": "int64", + "paramType": "path", + "minimum": "1.0", + "maximum": "100000.0" + }, + { + "name": "listId", + "description": "ID of Board that needs to be fetched", + "required": true, + "type": "integer", + "format": "int64", + "paramType": "path", + "minimum": "1.0", + "maximum": "100000.0" + }, + { + "name": "body", + "description": "Edit List", + "required": true, + "type": "editList", + "paramType": "body" + } + ], + "responseMessages": [ + { + "code": 400, + "message": "Invalid ID supplied" + }, + { + "code": 404, + "message": "Pet not found" + } + ] + }, + { + "method": "DELETE", + "summary": "Delete the list", + "notes": "", + "type": "void", + "nickname": "deleteList", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "Board ID to delete", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "listId", + "description": "List ID to delete", + "required": true, + "type": "string", + "paramType": "path" + } + ] + } + ] + }, + { + "path": "/v1/boards/{boardId}/lists/{listId}/list_subscribers.json", + "operations": [ + { + "method": "POST", + "summary": "Add List Subscribers", + "notes": "Add the List Subscribers", + "type": "List", + "nickname": "addListSubscriber", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "ID of Board that needs to be fetched", + "required": true, + "type": "integer", + "format": "int64", + "paramType": "path", + "minimum": "1.0", + "maximum": "100000.0" + }, + { + "name": "listId", + "description": "ID of Board that needs to be fetched", + "required": true, + "type": "integer", + "format": "int64", + "paramType": "path", + "minimum": "1.0", + "maximum": "100000.0" + }, + { + "name": "body", + "description": "Edit List", + "required": true, + "type": "addListSubscriber", + "paramType": "body" + } + ], + "responseMessages": [ + { + "code": 400, + "message": "Invalid ID supplied" + } + ] + } + ] + }, + { + "path": "/v1/boards/{boardId}/lists/{listId}/list_subscribers/{subscriberId}.json", + "operations": [ + { + "method": "PUT", + "summary": "Unsubscribe the List", + "notes": "Unsubscribe the List", + "type": "List", + "nickname": "unSubscribeList", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "boardId", + "description": "ID of Board that needs to be fetched", + "required": true, + "type": "integer", + "format": "int64", + "paramType": "path", + "minimum": "1.0", + "maximum": "100000.0" + }, + { + "name": "listId", + "description": "ID of Board that needs to be fetched", + "required": true, + "type": "integer", + "format": "int64", + "paramType": "path", + "minimum": "1.0", + "maximum": "100000.0" + }, + { + "name": "subscriberId", + "description": "ID of User that needs to be Unsubscribe", + "required": true, + "type": "integer", + "format": "int64", + "paramType": "path", + "minimum": "1.0", + "maximum": "100000.0" + } + ], + "responseMessages": [ + { + "code": 400, + "message": "Invalid ID supplied" + } + ] + } + ] + } + ], + "models": { + "addList": { + "id": "addList", + "required": [ + "board_id", + "name", + "uuid" + ], + "properties": { + "board_id": { + "type": "integer" + }, + "is_archived": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "position": { + "type": "integer" + }, + "uuid": { + "type": "integer" + } + } + }, + "editList": { + "id": "editList", + "required": [ + "name", + "position", + "archive" + ], + "properties": { + "name": { + "type": "string" + }, + "position": { + "type": "string" + }, + "archive": { + "type": "string" + } + } + }, + "addListSubscriber": { + "id": "addListSubscriber", + "required": [ + "is_subscribed" + ], + "properties": { + "is_subscribed": { + "type": "boolean" + } + } + } + } +} \ No newline at end of file diff --git a/api_explorer/api-docs/organizations.json b/api_explorer/api-docs/organizations.json new file mode 100644 index 000000000..6dfa01ed3 --- /dev/null +++ b/api_explorer/api-docs/organizations.json @@ -0,0 +1,329 @@ +{ + "apiVersion": "1.0.0", + "swaggerVersion": "1.2", + "basePath": "/api", + "resourcePath": "/checklists", + "produces": [ + "application/json", + "application/xml", + "text/plain", + "text/html" + ], + "apis": [ + { + "path": "/v1/organizations.json", + "operations": [ + { + "method": "POST", + "summary": "Add a organization", + "notes": "", + "type": "void", + "nickname": "addOrganization", + "consumes": [ + "application/json", + "application/xml" + ], + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "body", + "description": "Created Organization object", + "required": true, + "type": "addOrganization", + "paramType": "body" + } + ], + "responseMessages": [ + { + "code": 405, + "message": "Invalid input" + } + ] + }, + { + "method": "GET", + "summary": "Boards Listing", + "notes": "Returns boards list", + "type": "Organizations", + "nickname": "Boards listing", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + } + ] + } + ] + }, + { + "path": "/v1/organizations/{organizationId}.json", + "operations": [ + { + "method": "PUT", + "summary": "Edit Organization", + "notes": "Update the Organization", + "type": "Organization", + "nickname": "editOrganization", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "organizationId", + "description": "Organization ID to edit", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "body", + "description": "Edit Organization", + "required": true, + "type": "editOrganization", + "paramType": "body" + } + ], + "responseMessages": [ + { + "code": 400, + "message": "Invalid ID supplied" + }, + { + "code": 404, + "message": "Page not found" + } + ] + }, + { + "method": "DELETE", + "summary": "Delete Organization", + "notes": "", + "type": "void", + "nickname": "deleteOrganization", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "organizationId", + "description": "Organization ID to delete", + "required": true, + "type": "string", + "paramType": "path" + } + ] + } + ] + }, + { + "path": "/v1/organizations_users/{organizationId}.json", + "operations": [ + { + "method": "GET", + "summary": "Member Listing", + "notes": "Returns member list", + "type": "Organizations", + "nickname": "Members listing", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "organizationId", + "description": "Organization ID to get listing", + "required": true, + "type": "string", + "paramType": "path" + } + ] + }, + { + "method": "PUT", + "summary": "Change member permission", + "notes": "Change member permission", + "type": "ChangeMemberPermission", + "nickname": "changeMemberPermission", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "organizationId", + "description": "Organization ID to get listing", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "body", + "description": "Change Member Permission", + "required": true, + "type": "changeMemberPermission", + "paramType": "body" + } + ], + "responseMessages": [ + { + "code": 400, + "message": "Invalid ID supplied" + }, + { + "code": 404, + "message": "Page not found" + } + ] + } + ] + }, + { + "path": "/v1/organizations/{organizationId}/users/{userId}.json", + "operations": [ + { + "method": "POST", + "summary": "Add a member", + "notes": "", + "type": "void", + "nickname": "addMember", + "consumes": [ + "application/json", + "application/xml" + ], + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "organizationId", + "description": "Organization ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "userId", + "description": "User ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "body", + "description": "Created Member object", + "required": true, + "type": "addMember", + "paramType": "body" + } + ], + "responseMessages": [ + { + "code": 405, + "message": "Invalid input" + } + ] + } + ] + }, + { + "path": "/v1/organizations_users/{userId}.json", + "operations": [ + { + "method": "DELETE", + "summary": "Remove organization member", + "notes": "Remove organization member", + "type": "void", + "nickname": "remove_organization_user", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "userId", + "description": "User ID to Remove from Organization members list", + "required": true, + "type": "integer", + "paramType": "path" + } + ] + } + ] + } + ], + "models": { + "addOrganization": { + "id": "addOrganization", + "properties": { + "name": { + "type": "string" + } + } + }, + "editOrganization": { + "id": "editOrganization", + "properties": { + "name": { + "type": "string" + } + } + }, + "addMember": { + "id": "addMember", + "properties": { + "email": { + "type": "string" + } + } + }, + "changeMemberPermission": { + "id": "changeMemberPermission", + "properties": { + "user ID": { + "type": "string" + }, + "is_admin": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/api_explorer/api-docs/users.json b/api_explorer/api-docs/users.json new file mode 100644 index 000000000..886364fde --- /dev/null +++ b/api_explorer/api-docs/users.json @@ -0,0 +1,418 @@ +{ + "apiVersion": "1.0.0", + "swaggerVersion": "1.2", + "basePath": "/api", + "resourcePath": "/user", + "produces": [ + "application/json" + ], + "apis": [ + { + "path": "/v1/users.json", + "operations": [ + { + "method": "GET", + "summary": "Users", + "notes": "Returns Users listing", + "type": "Users", + "nickname": "usersListing", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + } + ], + "responseMessages": [ + { + "code": 404, + "message": "User not found" + } + ] + } + ] + }, + { + "path": "/v1/users/{userId}.json", + "operations": [ + { + "method": "GET", + "summary": "UserView", + "notes": "Returns Users view", + "type": "Users", + "nickname": "usersView", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "userId", + "description": "Selected user ID", + "required": true, + "type": "string", + "paramType": "path" + } + ], + "responseMessages": [ + { + "code": 404, + "message": "User not found" + } + ] + } + ] + }, + { + "path": "/v1/oauth.json", + "operations": [ + { + "method": "GET", + "summary": "Get Oauth token", + "notes": "", + "type": "void", + "nickname": "oauth", + "authorizations": {}, + "parameters": [] + } + ] + }, + { + "path": "/v1/users/logout.json", + "operations": [ + { + "method": "GET", + "summary": "Logs out current logged in user session", + "notes": "", + "type": "void", + "nickname": "logoutUser", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + } + ] + } + ] + }, + { + "path": "/v1/users/forgotpassword.json", + "operations": [ + { + "method": "POST", + "summary": "Enter your Email, and we will send resetting your password.", + "notes": "", + "type": "void", + "nickname": "forgotPassword", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "body", + "description": "Created Forgot Password object", + "required": true, + "type": "forgotPassword", + "paramType": "body" + } + ] + } + ] + }, + { + "path": "/v1/users/login.json", + "operations": [ + { + "method": "POST", + "summary": "Logs user into the system", + "notes": "", + "type": "void", + "nickname": "loginUser", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "body", + "description": "Created user object", + "required": true, + "type": "Login", + "paramType": "body" + } + ] + } + ] + }, + { + "path": "/v1/users/register.json", + "operations": [ + { + "method": "POST", + "summary": "Creates user", + "notes": "", + "type": "void", + "nickname": "createUsers", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "body", + "description": "Created user object", + "required": true, + "type": "User", + "paramType": "body" + } + ] + } + ] + }, + { + "path": "/v1/users/{userId}.json", + "operations": [ + { + "method": "PUT", + "summary": "Edit User", + "notes": "Update the users details", + "type": "User", + "nickname": "editUsers", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "userId", + "description": "Current Login user ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "body", + "description": "Edit user object", + "required": true, + "type": "editUser", + "paramType": "body" + } + ], + "responseMessages": [ + { + "code": 404, + "message": "User not found" + } + ] + } + ] + }, + { + "path": "/v1/users/{userId}/changepassword.json", + "operations": [ + { + "method": "PUT", + "summary": "Change Password", + "notes": "This can only be done by the logged in user.", + "type": "void", + "nickname": "updateUser", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "userId", + "description": "Current Login user ID", + "required": true, + "type": "string", + "paramType": "path" + }, + { + "name": "body", + "description": "Change Password user object", + "required": true, + "type": "changepassword", + "paramType": "body" + } + ], + "responseMessages": [ + { + "code": 400, + "message": "Invalid username supplied" + }, + { + "code": 404, + "message": "User not found" + } + ] + } + ] + }, + { + "path": "/v1/users/{userId}/activities.json", + "operations": [ + { + "method": "GET", + "summary": "Activities", + "notes": "Returns User Activities", + "type": "Activities", + "nickname": "Activities listing", + "authorizations": {}, + "parameters": [ + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "userId", + "description": "Selected user ID", + "required": true, + "type": "string", + "paramType": "path" + } + ] + } + ] + }, + { + "path": "/v1/users/{userId}.json", + "operations": [ + { + "method": "DELETE", + "summary": "Delete the user", + "notes": "", + "type": "void", + "nickname": "deleteUser", + "authorizations": {}, + "parameters": [ + { + "name": "userId", + "description": "User ID to delete", + "required": true, + "type": "string", + "paramType": "query" + }, + { + "name": "token", + "description": "OAuth token", + "required": true, + "type": "string", + "paramType": "query" + } + ] + } + ] + } + ], + "models": { + "User": { + "id": "User", + "properties": { + "username": { + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "is_agree_terms_and_conditions": { + "type": "string" + } + } + }, + "forgotPassword": { + "id": "forgotPassword", + "properties": { + "email": { + "type": "string" + } + } + }, + "Login": { + "id": "Login", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "changepassword": { + "id": "changepassword", + "properties": { + "id": { + "type": "string" + }, + "oldpassword": { + "type": "string" + }, + "password": { + "type": "string" + }, + "confirmpassword": { + "type": "string" + } + } + }, + "editUser": { + "id": "editUser", + "properties": { + "username": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "dob": { + "type": "string" + }, + "gender_id": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/api_explorer/css/highlight.default.css b/api_explorer/css/highlight.default.css new file mode 100644 index 000000000..e417fc179 --- /dev/null +++ b/api_explorer/css/highlight.default.css @@ -0,0 +1,135 @@ +/* + +Original style from softwaremaniacs.org (c) Ivan Sagalaev + +*/ + +pre code { + display: block; padding: 0.5em; + background: #F0F0F0; +} + +pre code, +pre .subst, +pre .tag .title, +pre .lisp .title, +pre .clojure .built_in, +pre .nginx .title { + color: black; +} + +pre .string, +pre .title, +pre .constant, +pre .parent, +pre .tag .value, +pre .rules .value, +pre .rules .value .number, +pre .preprocessor, +pre .ruby .symbol, +pre .ruby .symbol .string, +pre .aggregate, +pre .template_tag, +pre .django .variable, +pre .smalltalk .class, +pre .addition, +pre .flow, +pre .stream, +pre .bash .variable, +pre .apache .tag, +pre .apache .cbracket, +pre .tex .command, +pre .tex .special, +pre .erlang_repl .function_or_atom, +pre .markdown .header { + color: #800; +} + +pre .comment, +pre .annotation, +pre .template_comment, +pre .diff .header, +pre .chunk, +pre .markdown .blockquote { + color: #888; +} + +pre .number, +pre .date, +pre .regexp, +pre .literal, +pre .smalltalk .symbol, +pre .smalltalk .char, +pre .go .constant, +pre .change, +pre .markdown .bullet, +pre .markdown .link_url { + color: #080; +} + +pre .label, +pre .javadoc, +pre .ruby .string, +pre .decorator, +pre .filter .argument, +pre .localvars, +pre .array, +pre .attr_selector, +pre .important, +pre .pseudo, +pre .pi, +pre .doctype, +pre .deletion, +pre .envvar, +pre .shebang, +pre .apache .sqbracket, +pre .nginx .built_in, +pre .tex .formula, +pre .erlang_repl .reserved, +pre .prompt, +pre .markdown .link_label, +pre .vhdl .attribute, +pre .clojure .attribute, +pre .coffeescript .property { + color: #88F +} + +pre .keyword, +pre .id, +pre .phpdoc, +pre .title, +pre .built_in, +pre .aggregate, +pre .css .tag, +pre .javadoctag, +pre .phpdoc, +pre .yardoctag, +pre .smalltalk .class, +pre .winutils, +pre .bash .variable, +pre .apache .tag, +pre .go .typename, +pre .tex .command, +pre .markdown .strong, +pre .request, +pre .status { + font-weight: bold; +} + +pre .markdown .emphasis { + font-style: italic; +} + +pre .nginx .built_in { + font-weight: normal; +} + +pre .coffeescript .javascript, +pre .javascript .xml, +pre .tex .formula, +pre .xml .javascript, +pre .xml .vbscript, +pre .xml .css, +pre .xml .cdata { + opacity: 0.5; +} diff --git a/api_explorer/css/screen.css b/api_explorer/css/screen.css new file mode 100644 index 000000000..1627ecd0d --- /dev/null +++ b/api_explorer/css/screen.css @@ -0,0 +1,1070 @@ +/* http://meyerweb.com/eric/tools/css/reset/ v2.0 | 20110126 */ +html, +body, +div, +span, +applet, +object, +iframe, +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote, +pre, +a, +abbr, +acronym, +address, +big, +cite, +code, +del, +dfn, +em, +img, +ins, +kbd, +q, +s, +samp, +small, +strike, +strong, +sub, +sup, +tt, +var, +b, +u, +i, +center, +dl, +dt, +dd, +ol, +ul, +li, +fieldset, +form, +label, +legend, +table, +caption, +tbody, +tfoot, +thead, +tr, +th, +td, +article, +aside, +canvas, +details, +embed, +figure, +figcaption, +footer, +header, +hgroup, +menu, +nav, +output, +ruby, +section, +summary, +time, +mark, +audio, +video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +menu, +nav, +section { + display: block; +} +body { + line-height: 1; +} +ol, +ul { + list-style: none; +} +blockquote, +q { + quotes: none; +} +blockquote:before, +blockquote:after, +q:before, +q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} +.swagger-ui-wrap { + line-height: 1; + font-family: "Droid Sans", sans-serif; + max-width: 960px; + margin-left: auto; + margin-right: auto; +} +.swagger-ui-wrap b, +.swagger-ui-wrap strong { + font-family: "Droid Sans", sans-serif; + font-weight: bold; +} +.swagger-ui-wrap q, +.swagger-ui-wrap blockquote { + quotes: none; +} +.swagger-ui-wrap p { + line-height: 1.4em; + padding: 0 0 10px; + color: #333333; +} +.swagger-ui-wrap q:before, +.swagger-ui-wrap q:after, +.swagger-ui-wrap blockquote:before, +.swagger-ui-wrap blockquote:after { + content: none; +} +.swagger-ui-wrap .heading_with_menu h1, +.swagger-ui-wrap .heading_with_menu h2, +.swagger-ui-wrap .heading_with_menu h3, +.swagger-ui-wrap .heading_with_menu h4, +.swagger-ui-wrap .heading_with_menu h5, +.swagger-ui-wrap .heading_with_menu h6 { + display: block; + clear: none; + float: left; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; + width: 60%; +} +.swagger-ui-wrap table { + border-collapse: collapse; + border-spacing: 0; +} +.swagger-ui-wrap table thead tr th { + padding: 5px; + font-size: 0.9em; + color: #666666; + border-bottom: 1px solid #999999; +} +.swagger-ui-wrap table tbody tr:last-child td { + border-bottom: none; +} +.swagger-ui-wrap table tbody tr.offset { + background-color: #f0f0f0; +} +.swagger-ui-wrap table tbody tr td { + padding: 6px; + font-size: 0.9em; + border-bottom: 1px solid #cccccc; + vertical-align: top; + line-height: 1.3em; +} +.swagger-ui-wrap ol { + margin: 0px 0 10px; + padding: 0 0 0 18px; + list-style-type: decimal; +} +.swagger-ui-wrap ol li { + padding: 5px 0px; + font-size: 0.9em; + color: #333333; +} +.swagger-ui-wrap ol, +.swagger-ui-wrap ul { + list-style: none; +} +.swagger-ui-wrap h1 a, +.swagger-ui-wrap h2 a, +.swagger-ui-wrap h3 a, +.swagger-ui-wrap h4 a, +.swagger-ui-wrap h5 a, +.swagger-ui-wrap h6 a { + text-decoration: none; +} +.swagger-ui-wrap h1 a:hover, +.swagger-ui-wrap h2 a:hover, +.swagger-ui-wrap h3 a:hover, +.swagger-ui-wrap h4 a:hover, +.swagger-ui-wrap h5 a:hover, +.swagger-ui-wrap h6 a:hover { + text-decoration: underline; +} +.swagger-ui-wrap h1 span.divider, +.swagger-ui-wrap h2 span.divider, +.swagger-ui-wrap h3 span.divider, +.swagger-ui-wrap h4 span.divider, +.swagger-ui-wrap h5 span.divider, +.swagger-ui-wrap h6 span.divider { + color: #aaaaaa; +} +.swagger-ui-wrap a { + color: #547f00; +} +.swagger-ui-wrap a img { + border: none; +} +.swagger-ui-wrap article, +.swagger-ui-wrap aside, +.swagger-ui-wrap details, +.swagger-ui-wrap figcaption, +.swagger-ui-wrap figure, +.swagger-ui-wrap footer, +.swagger-ui-wrap header, +.swagger-ui-wrap hgroup, +.swagger-ui-wrap menu, +.swagger-ui-wrap nav, +.swagger-ui-wrap section, +.swagger-ui-wrap summary { + display: block; +} +.swagger-ui-wrap pre { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + background-color: #fcf6db; + border: 1px solid #e5e0c6; + padding: 10px; +} +.swagger-ui-wrap pre code { + line-height: 1.6em; + background: none; +} +.swagger-ui-wrap .content > .content-type > div > label { + clear: both; + display: block; + color: #0F6AB4; + font-size: 1.1em; + margin: 0; + padding: 15px 0 5px; +} +.swagger-ui-wrap .content pre { + font-size: 12px; + margin-top: 5px; + padding: 5px; +} +.swagger-ui-wrap .icon-btn { + cursor: pointer; +} +.swagger-ui-wrap .info_title { + padding-bottom: 10px; + font-weight: bold; + font-size: 25px; +} +.swagger-ui-wrap p.big, +.swagger-ui-wrap div.big p { + font-size: 1em; + margin-bottom: 10px; +} +.swagger-ui-wrap form.fullwidth ol li.string input, +.swagger-ui-wrap form.fullwidth ol li.url input, +.swagger-ui-wrap form.fullwidth ol li.text textarea, +.swagger-ui-wrap form.fullwidth ol li.numeric input { + width: 500px !important; +} +.swagger-ui-wrap .info_license { + padding-bottom: 5px; +} +.swagger-ui-wrap .info_tos { + padding-bottom: 5px; +} +.swagger-ui-wrap .message-fail { + color: #cc0000; +} +.swagger-ui-wrap .info_contact { + padding-bottom: 5px; +} +.swagger-ui-wrap .info_description { + padding-bottom: 10px; + font-size: 15px; +} +.swagger-ui-wrap .markdown ol li, +.swagger-ui-wrap .markdown ul li { + padding: 3px 0px; + line-height: 1.4em; + color: #333333; +} +.swagger-ui-wrap form.formtastic fieldset.inputs ol li.string input, +.swagger-ui-wrap form.formtastic fieldset.inputs ol li.url input, +.swagger-ui-wrap form.formtastic fieldset.inputs ol li.numeric input { + display: block; + padding: 4px; + width: auto; + clear: both; +} +.swagger-ui-wrap form.formtastic fieldset.inputs ol li.string input.title, +.swagger-ui-wrap form.formtastic fieldset.inputs ol li.url input.title, +.swagger-ui-wrap form.formtastic fieldset.inputs ol li.numeric input.title { + font-size: 1.3em; +} +.swagger-ui-wrap table.fullwidth { + width: 100%; +} +.swagger-ui-wrap .model-signature { + font-family: "Droid Sans", sans-serif; + font-size: 1em; + line-height: 1.5em; +} +.swagger-ui-wrap .model-signature .signature-nav a { + text-decoration: none; + color: #AAA; +} +.swagger-ui-wrap .model-signature .signature-nav a:hover { + text-decoration: underline; + color: black; +} +.swagger-ui-wrap .model-signature .signature-nav .selected { + color: black; + text-decoration: none; +} +.swagger-ui-wrap .model-signature .propType { + color: #5555aa; +} +.swagger-ui-wrap .model-signature pre:hover { + background-color: #ffffdd; +} +.swagger-ui-wrap .model-signature pre { + font-size: .85em; + line-height: 1.2em; + overflow: auto; + max-height: 200px; + cursor: pointer; +} +.swagger-ui-wrap .model-signature ul.signature-nav { + display: block; + margin: 0; + padding: 0; +} +.swagger-ui-wrap .model-signature ul.signature-nav li:last-child { + padding-right: 0; + border-right: none; +} +.swagger-ui-wrap .model-signature ul.signature-nav li { + float: left; + margin: 0 5px 5px 0; + padding: 2px 5px 2px 0; + border-right: 1px solid #ddd; +} +.swagger-ui-wrap .model-signature .propOpt { + color: #555; +} +.swagger-ui-wrap .model-signature .snippet small { + font-size: 0.75em; +} +.swagger-ui-wrap .model-signature .propOptKey { + font-style: italic; +} +.swagger-ui-wrap .model-signature .description .strong { + font-weight: bold; + color: #000; + font-size: .9em; +} +.swagger-ui-wrap .model-signature .description div { + font-size: 0.9em; + line-height: 1.5em; + margin-left: 1em; +} +.swagger-ui-wrap .model-signature .description .stronger { + font-weight: bold; + color: #000; +} +.swagger-ui-wrap .model-signature .propName { + font-weight: bold; +} +.swagger-ui-wrap .model-signature .signature-container { + clear: both; +} +.swagger-ui-wrap .body-textarea { + width: 300px; + height: 100px; + border: 1px solid #aaa; +} +.swagger-ui-wrap .markdown p code, +.swagger-ui-wrap .markdown li code { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + background-color: #f0f0f0; + color: black; + padding: 1px 3px; +} +.swagger-ui-wrap .required { + font-weight: bold; +} +.swagger-ui-wrap input.parameter { + width: 300px; + border: 1px solid #aaa; +} +.swagger-ui-wrap h1 { + color: black; + font-size: 1.5em; + line-height: 1.3em; + padding: 10px 0 10px 0; + font-family: "Droid Sans", sans-serif; + font-weight: bold; +} +.swagger-ui-wrap .heading_with_menu { + float: none; + clear: both; + overflow: hidden; + display: block; +} +.swagger-ui-wrap .heading_with_menu ul { + display: block; + clear: none; + float: right; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; + margin-top: 10px; +} +.swagger-ui-wrap h2 { + color: black; + font-size: 1.3em; + padding: 10px 0 10px 0; +} +.swagger-ui-wrap h2 a { + color: black; +} +.swagger-ui-wrap h2 span.sub { + font-size: 0.7em; + color: #999999; + font-style: italic; +} +.swagger-ui-wrap h2 span.sub a { + color: #777777; +} +.swagger-ui-wrap span.weak { + color: #666666; +} +.swagger-ui-wrap .message-success { + color: #89BF04; +} +.swagger-ui-wrap caption, +.swagger-ui-wrap th, +.swagger-ui-wrap td { + text-align: left; + font-weight: normal; + vertical-align: middle; +} +.swagger-ui-wrap .code { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; +} +.swagger-ui-wrap form.formtastic fieldset.inputs ol li.text textarea { + font-family: "Droid Sans", sans-serif; + height: 250px; + padding: 4px; + display: block; + clear: both; +} +.swagger-ui-wrap form.formtastic fieldset.inputs ol li.select select { + display: block; + clear: both; +} +.swagger-ui-wrap form.formtastic fieldset.inputs ol li.boolean { + float: none; + clear: both; + overflow: hidden; + display: block; +} +.swagger-ui-wrap form.formtastic fieldset.inputs ol li.boolean label { + display: block; + float: left; + clear: none; + margin: 0; + padding: 0; +} +.swagger-ui-wrap form.formtastic fieldset.inputs ol li.boolean input { + display: block; + float: left; + clear: none; + margin: 0 5px 0 0; +} +.swagger-ui-wrap form.formtastic fieldset.inputs ol li.required label { + color: black; +} +.swagger-ui-wrap form.formtastic fieldset.inputs ol li label { + display: block; + clear: both; + width: auto; + padding: 0 0 3px; + color: #666666; +} +.swagger-ui-wrap form.formtastic fieldset.inputs ol li label abbr { + padding-left: 3px; + color: #888888; +} +.swagger-ui-wrap form.formtastic fieldset.inputs ol li p.inline-hints { + margin-left: 0; + font-style: italic; + font-size: 0.9em; + margin: 0; +} +.swagger-ui-wrap form.formtastic fieldset.buttons { + margin: 0; + padding: 0; +} +.swagger-ui-wrap span.blank, +.swagger-ui-wrap span.empty { + color: #888888; + font-style: italic; +} +.swagger-ui-wrap .markdown h3 { + color: #547f00; +} +.swagger-ui-wrap .markdown h4 { + color: #666666; +} +.swagger-ui-wrap .markdown pre { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + background-color: #fcf6db; + border: 1px solid #e5e0c6; + padding: 10px; + margin: 0 0 10px 0; +} +.swagger-ui-wrap .markdown pre code { + line-height: 1.6em; +} +.swagger-ui-wrap div.gist { + margin: 20px 0 25px 0 !important; +} +.swagger-ui-wrap ul#resources { + font-family: "Droid Sans", sans-serif; + font-size: 0.9em; +} +.swagger-ui-wrap ul#resources li.resource { + border-bottom: 1px solid #dddddd; +} +.swagger-ui-wrap ul#resources li.resource:hover div.heading h2 a, +.swagger-ui-wrap ul#resources li.resource.active div.heading h2 a { + color: black; +} +.swagger-ui-wrap ul#resources li.resource:hover div.heading ul.options li a, +.swagger-ui-wrap ul#resources li.resource.active div.heading ul.options li a { + color: #555555; +} +.swagger-ui-wrap ul#resources li.resource:last-child { + border-bottom: none; +} +.swagger-ui-wrap ul#resources li.resource div.heading { + border: 1px solid transparent; + float: none; + clear: both; + overflow: hidden; + display: block; +} +.swagger-ui-wrap ul#resources li.resource div.heading ul.options { + overflow: hidden; + padding: 0; + display: block; + clear: none; + float: right; + margin: 14px 10px 0 0; +} +.swagger-ui-wrap ul#resources li.resource div.heading ul.options li { + float: left; + clear: none; + margin: 0; + padding: 2px 10px; + border-right: 1px solid #dddddd; + color: #666666; + font-size: 0.9em; +} +.swagger-ui-wrap ul#resources li.resource div.heading ul.options li a { + color: #aaaaaa; + text-decoration: none; +} +.swagger-ui-wrap ul#resources li.resource div.heading ul.options li a:hover { + text-decoration: underline; + color: black; +} +.swagger-ui-wrap ul#resources li.resource div.heading ul.options li a:hover, +.swagger-ui-wrap ul#resources li.resource div.heading ul.options li a:active, +.swagger-ui-wrap ul#resources li.resource div.heading ul.options li a.active { + text-decoration: underline; +} +.swagger-ui-wrap ul#resources li.resource div.heading ul.options li:first-child, +.swagger-ui-wrap ul#resources li.resource div.heading ul.options li.first { + padding-left: 0; +} +.swagger-ui-wrap ul#resources li.resource div.heading ul.options li:last-child, +.swagger-ui-wrap ul#resources li.resource div.heading ul.options li.last { + padding-right: 0; + border-right: none; +} +.swagger-ui-wrap ul#resources li.resource div.heading ul.options:first-child, +.swagger-ui-wrap ul#resources li.resource div.heading ul.options.first { + padding-left: 0; +} +.swagger-ui-wrap ul#resources li.resource div.heading h2 { + color: #999999; + padding-left: 0; + display: block; + clear: none; + float: left; + font-family: "Droid Sans", sans-serif; + font-weight: bold; +} +.swagger-ui-wrap ul#resources li.resource div.heading h2 a { + color: #999999; +} +.swagger-ui-wrap ul#resources li.resource div.heading h2 a:hover { + color: black; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 10px; + padding: 0; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0; + padding: 0; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading h3 { + display: block; + clear: none; + float: left; + width: auto; + margin: 0; + padding: 0; + line-height: 1.1em; + color: black; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading h3 span.path { + padding-left: 10px; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading h3 span.path a { + color: black; + text-decoration: none; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading h3 span.path a:hover { + text-decoration: underline; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading h3 span.http_method a { + text-transform: uppercase; + text-decoration: none; + color: white; + display: inline-block; + width: 50px; + font-size: 0.7em; + text-align: center; + padding: 7px 0 4px; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + -o-border-radius: 2px; + -ms-border-radius: 2px; + -khtml-border-radius: 2px; + border-radius: 2px; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading h3 span { + margin: 0; + padding: 0; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading ul.options { + overflow: hidden; + padding: 0; + display: block; + clear: none; + float: right; + margin: 6px 10px 0 0; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading ul.options li { + float: left; + clear: none; + margin: 0; + padding: 2px 10px; + font-size: 0.9em; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.heading ul.options li a { + text-decoration: none; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content { + border-top: none; + padding: 10px; + -moz-border-radius-bottomleft: 6px; + -webkit-border-bottom-left-radius: 6px; + -o-border-bottom-left-radius: 6px; + -ms-border-bottom-left-radius: 6px; + -khtml-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -moz-border-radius-bottomright: 6px; + -webkit-border-bottom-right-radius: 6px; + -o-border-bottom-right-radius: 6px; + -ms-border-bottom-right-radius: 6px; + -khtml-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + margin: 0 0 20px; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content h4 { + font-size: 1.1em; + margin: 0; + padding: 15px 0 5px; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content div.sandbox_header { + float: none; + clear: both; + overflow: hidden; + display: block; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content div.sandbox_header a { + padding: 4px 0 0 10px; + display: inline-block; + font-size: 0.9em; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content div.sandbox_header img { + display: block; + clear: none; + float: right; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content div.sandbox_header input.submit { + display: block; + clear: none; + float: left; + padding: 6px 8px; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content form input[type='text'].error { + outline: 2px solid black; + outline-color: #cc0000; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation div.content div.response div.block pre { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + padding: 10px; + font-size: 0.9em; + max-height: 400px; + overflow-y: auto; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading { + background-color: #f9f2e9; + border: 1px solid #f0e0ca; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading h3 span.http_method a { + background-color: #c5862b; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li { + border-right: 1px solid #dddddd; + border-right-color: #f0e0ca; + color: #c5862b; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li a { + color: #c5862b; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content { + background-color: #faf5ee; + border: 1px solid #f0e0ca; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content h4 { + color: #c5862b; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content div.sandbox_header a { + color: #dcb67f; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.heading { + background-color: #fcffcd; + border: 1px solid black; + border-color: #ffd20f; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.heading h3 span.http_method a { + text-transform: uppercase; + background-color: #ffd20f; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.heading ul.options li { + border-right: 1px solid #dddddd; + border-right-color: #ffd20f; + color: #ffd20f; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.heading ul.options li a { + color: #ffd20f; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.content { + background-color: #fcffcd; + border: 1px solid black; + border-color: #ffd20f; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.content h4 { + color: #ffd20f; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.content div.sandbox_header a { + color: #6fc992; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading { + background-color: #f5e8e8; + border: 1px solid #e8c6c7; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading h3 span.http_method a { + text-transform: uppercase; + background-color: #a41e22; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li { + border-right: 1px solid #dddddd; + border-right-color: #e8c6c7; + color: #a41e22; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li a { + color: #a41e22; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content { + background-color: #f7eded; + border: 1px solid #e8c6c7; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content h4 { + color: #a41e22; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content div.sandbox_header a { + color: #c8787a; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading { + background-color: #e7f6ec; + border: 1px solid #c3e8d1; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading h3 span.http_method a { + background-color: #10a54a; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li { + border-right: 1px solid #dddddd; + border-right-color: #c3e8d1; + color: #10a54a; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li a { + color: #10a54a; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content { + background-color: #ebf7f0; + border: 1px solid #c3e8d1; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content h4 { + color: #10a54a; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content div.sandbox_header a { + color: #6fc992; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading { + background-color: #FCE9E3; + border: 1px solid #F5D5C3; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading h3 span.http_method a { + background-color: #D38042; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li { + border-right: 1px solid #dddddd; + border-right-color: #f0cecb; + color: #D38042; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li a { + color: #D38042; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content { + background-color: #faf0ef; + border: 1px solid #f0cecb; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content h4 { + color: #D38042; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content div.sandbox_header a { + color: #dcb67f; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading { + background-color: #e7f0f7; + border: 1px solid #c3d9ec; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading h3 span.http_method a { + background-color: #0f6ab4; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li { + border-right: 1px solid #dddddd; + border-right-color: #c3d9ec; + color: #0f6ab4; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li a { + color: #0f6ab4; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content { + background-color: #ebf3f9; + border: 1px solid #c3d9ec; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content h4 { + color: #0f6ab4; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content div.sandbox_header a { + color: #6fa5d2; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content, +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content, +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.content, +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content, +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content, +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content { + border-top: none; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li:last-child, +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li:last-child, +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.heading ul.options li:last-child, +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li:last-child, +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li:last-child, +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li:last-child, +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li.last, +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li.last, +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.heading ul.options li.last, +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li.last, +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li.last, +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li.last { + padding-right: 0; + border-right: none; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations ul.options li a:hover, +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations ul.options li a:active, +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations ul.options li a.active { + text-decoration: underline; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations ul.options li:first-child, +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations ul.options li.first { + padding-left: 0; +} +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations:first-child, +.swagger-ui-wrap ul#resources li.resource ul.endpoints li.endpoint ul.operations.first { + padding-left: 0; +} +.swagger-ui-wrap p#colophon { + margin: 0 15px 40px 15px; + padding: 10px 0; + font-size: 0.8em; + border-top: 1px solid #dddddd; + font-family: "Droid Sans", sans-serif; + color: #999999; + font-style: italic; +} +.swagger-ui-wrap p#colophon a { + text-decoration: none; + color: #547f00; +} +.swagger-ui-wrap h3 { + color: black; + font-size: 1.1em; + padding: 10px 0 10px 0; +} +.swagger-ui-wrap .markdown ol, +.swagger-ui-wrap .markdown ul { + font-family: "Droid Sans", sans-serif; + margin: 5px 0 10px; + padding: 0 0 0 18px; + list-style-type: disc; +} +.swagger-ui-wrap form.form_box { + background-color: #ebf3f9; + border: 1px solid #c3d9ec; + padding: 10px; +} +.swagger-ui-wrap form.form_box label { + color: #0f6ab4 !important; +} +.swagger-ui-wrap form.form_box input[type=submit] { + display: block; + padding: 10px; +} +.swagger-ui-wrap form.form_box p.weak { + font-size: 0.8em; +} +.swagger-ui-wrap form.form_box p { + font-size: 0.9em; + padding: 0 0 15px; + color: #7e7b6d; +} +.swagger-ui-wrap form.form_box p a { + color: #646257; +} +.swagger-ui-wrap form.form_box p strong { + color: black; +} +#header { + background-color: #89bf04; + padding: 14px; +} +#header a#logo { + font-size: 1.5em; + font-weight: bold; + text-decoration: none; + background: transparent url(../images/logo_small.png) no-repeat left center; + padding: 20px 0 20px 40px; + color: white; +} +#header form#api_selector { + display: block; + clear: none; + float: right; +} +#header form#api_selector .input { + display: block; + clear: none; + float: left; + margin: 0 10px 0 0; +} +#header form#api_selector .input input#input_apiKey { + width: 200px; +} +#header form#api_selector .input input#input_baseUrl { + width: 400px; +} +#header form#api_selector .input a#explore { + display: block; + text-decoration: none; + font-weight: bold; + padding: 6px 8px; + font-size: 0.9em; + color: white; + background-color: #547f00; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + -o-border-radius: 4px; + -ms-border-radius: 4px; + -khtml-border-radius: 4px; + border-radius: 4px; +} +#header form#api_selector .input a#explore:hover { + background-color: #547f00; +} +#header form#api_selector .input input { + font-size: 0.9em; + padding: 3px; + margin: 0; +} +#content_message { + margin: 10px 15px; + font-style: italic; + color: #999999; +} +#message-bar { + min-height: 30px; + text-align: center; + padding-top: 10px; +} diff --git a/api_explorer/images/logo_small.png b/api_explorer/images/logo_small.png new file mode 100644 index 0000000000000000000000000000000000000000..5496a65579ae903d4008f9d268fac422ef9d3679 GIT binary patch literal 770 zcmV+d1O5DoP)K11rQipnJ)eVnTSzHNF zN8ab&RhE5cC$$4FI-PZXx$pga@8yN)KS}L2Us~^y$(x-xioWbnFcV+~b9ig=!ft8Q z0RD+rpA8910Smyc0GviVUOPGiY6YM@-r6Nn8S&~cxHl27$l)-R$1(!Xx045RDy;_& zeXkG{;_#i9rz0B6149#Ddj=KM6MV^rTD%ylzGdCBX<^=^@I0X3SCR7OMbn}sUKdeF zKO-flaJa%@kJ27@Rod?J9=+Qx5|=PtG8n> zy~9rIu}+48M}FW5Bbqw3t#po?c?kmG!FX32W(dOjzTb+U@64MzHItoeB!M0Jcd}|E z>ekW`<~FjR_ZVVJkF|_htH&v!({Oad?xax?0K0sLwBY%nr46DpCmIIaa?@|Y&?n0q z@kJlMy`pE2HtEgASNd~xNzt$Kn7w#^Fy5oi`e$bUE*+f>Vk5z7=-2pj68afrqli$_ zvqe##5V?a)QU_-s9+s?mJYT5m`MQDRH4cYs^L1lCW;Dua5Ln9lG0BC@9DJQHA(}y&Z}$apb{kU zbezR}b^|O%6i+$BFsT3zqAe8wg9`vfiRp#{)z2bsJw`vBQL7Bt!IexM3$Hsf0tHK3 z+R=x{lR$K`s;7__?ASPW=3?*xgCpGaiadSEpoi0pw-_V#OXM8Ap{4qlG08x0ig9IY z3Ijqh(t1_=g#jocuqyJO=729e9OSiNDSrhR0Gc5G)(QGH?*IS*07*qoM6N<$f<~fU A82|tP literal 0 HcmV?d00001 diff --git a/api_explorer/images/pet_store_api.png b/api_explorer/images/pet_store_api.png new file mode 100644 index 0000000000000000000000000000000000000000..f9f9cd4aeb35a108c4b2f1dddb59977d56c595d8 GIT binary patch literal 824 zcmV-81IPS{P)n=Rd;8mVwQNY4k4xJQ%YT}s;WA7;r!W@XgqjG_4og} z8w>{OB9REiMa8-B85td+y}bji^~2KA`Md4j-u{zw=H%Da@83%_8qEnl9k1WK;pWX- zb-lg)pQYAreK@>)*5Clqni{IZVYGG+NY67Bp-^bn;L{Nbh44I6CIK+n7p8#U?;fCA zYMFcy%UEjup4fgnli%NyzSe*@419QuU9lJ|T$?f9w?HIQ$RwEJGK7^!y7LhxIgVJp z9c!kB{0aydM1epU1NJ=h(}2X?Y{qn70yEN$dwm~favs=VbQ+T?!AvSl{P~PE zS&zsJbTQttne>kdM4$jBhLMFy@I1)3u-4cAzrY*l!o9eK^w%+jqY!oi(Ri8sMauvK zwnCP#%3hEH#FtNqq{iT(?=_JA_8XC>5Y8Y@!wmxKb|A87ZbpHA`+%v~0pt{5Nko1L zLKR^25YExt1lH7L1{t{|P z@n)yHyZf~3>LZ@#&CNw1rA#OlY^|)UJQKUrlKKO&x%wPhH}6&e0000K^a6u zQ3;5MiU^7p6*M3qDk!2=YEcHMQ>nzEYP;R`e2C@r+U+?#XaC*&gKPcB#k$`o&;7mu zYNhYYXe|Uo84#4ZIko#rcU5K8*yFL{qT47O&^5fZH$ zVZ@%(l~vVHjnm;H@KL8@r%yUHoo;rbHI_4lIH(_nsTT>S2`DFOD~uCb9_dF4`#QgI zy7ldMcLs+A_s%|e1pRPrbX-tpeNP!9(IpMFTce`t_5U%lP99z%&i6`1d~ zWeM!Rxc50<+d$e^9LT`?B+aMK~apR zHm?q;p<7{wN2g|I^aGlSws;VP84j(z%aQwvAWv83Z$}p(% zZ^?2;gxg(ey_`V5J7{;!o;o;KslW@z5EP~JGs|U)J7dF&(ff#A=6vU?cGQ$-4+;Jf z-ggJEa!yStn`_EWvl)#yhm6XVs}UUbsi;+agri;mCfjH^Uy;lH+Zw^h)4N?oZgZz4 zJk(fTZ|Bi^;+s_M=~+d#vyoxEPzTlOS=mX@sbl*uRj>=MaMr}cFIY8i?UM61>86uB zV$DlOUCiUJwbzJMP@D$urzK|lL2-PC!p1l47V-ZG<5Ev0Z5h~Kx?`KOp7gkAjV93A z-Gc7MrlxTf?wF;CbNc@tCHJH{TB3c;#{SVu%97}tyAM2n&|9W_?qv}$*Jt*%7Yxb# zV0;d;7|lDEltJYS+U)#aiJO};?_Jyy_4%syQ(uy?-J-Yx-9O5nKRk@@XSS~X<(2u~ zV-LamWm~!iqtH9wkpf8mAXZhOD&L#aA_%)4h2M;1M5jt zIR>Us+%W-GXa_f^opKg=DSrAs)AXeRa;Hp0aC1OgbxQ%Qr_QvTleM1jkR!2mkcX$3 ztsR8~G9iqh(-FJ@F_rQBIYDXV_6s7G9SxaVF^laZqcx$!D97m|7t16j6@Jt6UdDRy49Qyvs|c>RuA|@b%}`*wU}2^7q;&Vtc6@lb zcXl)T!6nYDzmMJ~%n$KNXyNlCG)GkJ4!82;v6@d3>s5r~E+3!O?049JDr14Y^PeMI02R`0lJ^=oJ zYd|*u9|SU(j7hY?+<=(?fP*mtV*zFhOrz6%{VA?ozdm&(Jf^V zMfPZ?>l`mS3{Uq8IM;e!+1YjJy2!mzK$O|wPeU{*QSbs9m+@`f5KxO3PBnQ=%RsZg%go*fJ`*w9TL{-WgZVIA$!YV}3BRcfeXaR$x#b zW)Tpd#8E4)^MyYdkH;4_;ChJuw%n+Be7Ko4;w-nHvyo$d_0e-YiF78Df&)_)(}fcr_r0mPH(4RRYWIu+d@t0&Ss@O^s! zOKyX&13)%N@83r^;QsgN{rl(!0|RF1FA)b1{CRXAy&1ySz@>olPiR4r$aMdq&_=nK zq|cFs8phWJ1@%dZ-gXd{zDbTILD>)qEvH-NU*Rf1b2J1Ri79`rBFl@ z8E^0I)OqEi{pH(a24b9YPG;Kz@t-qZW;3Mpe`MRlmYx{7bH-XZ&`RQ7Rb^%}gc&X| zd}Q-FZf|RWxHU?PR!(C?80zu(^l>*h{#ulSiid(O!J(8P-41bNM3tnX@U6NS5yo0? zdcF)~xFE&+&|gZ$23dV5t~?$$&ymZ;F8j7GGMncGSsDo%>J`26=&l=X#rSKv_64;0 zr;k6no@=gV`P)K!=kaHl>q?!`X>(A;84tg^Md<`zA%qbRLby1Z=fn*ZRdNqs%Tq|3 zOt}lZu0q9oKJhgz&+^7PCt$=UFW=R*w?a1)ePoL*`R$Gxj?TU@12tTHsT$giHQU+sqf;fS0FpT!< z z#UR4L_rT;lfRLVo8|3$7cmuxwjY5rmYs&kR6z_LRhf9-=4QalKQYEWw^4-EBI3j$& zA>$Im_{ZA>0`)E_&m%x6a)BThkx=e|aMkOrK9zb1YzqpQ&WZ^$)2T>CwTCuYRn5y) z3fVXg-@R5&Bf4?WUTyD|hBDe2>xEh|o-y}o5Se~+Ob!5xN>CaAN!<4)F zwNh!Y7B?@AigokFYNJL`0Vz&-ekrY95-n3M<%GR<;SzXRmO7(zd+gf|$Thb%;pby2 zyd{5TJ?|JYUgpSlJ0=LB@k6#d&opuPGq^qJAIumfhigC2qAX0OEnYnT@O;bA?X1O5 zpLe9|%_H+Yki!Rv$7Kvjv8r7Z?$<>G)g*%D*V#s&kz>Z3V1 z3!ZKh9H8Nl9IdhEW_rY#oYdDCLTe+nQ{(d2pBX8%CmxL+1`|b#Vb!?IY!kT7$PDWAP9$FY=e9KSK{DEH|408! zl-$lv)U8$EB{~es&j>rYg%{{JRvIl8@NK}L=xDAEVv(o#W@3LUDc*m?yKSPR0O|nY zAh;*QuBdpja8HzP8Uw`ce-r*LrUA47ZvZ)ff3k4^>;dFcof}9eXeeM<0OVj&CKDVK zpUKKIF%hSmry!pwK68UX>zOF@dv}B4Gg)^2GQmN7@A?zG!xO6dT*Cq0+r{eY6}AfU zf`|~y!?^R*nB0!iTcg|CgM}ou^H*s~5)%h;Xh;PYOM!|Yhfk$w;@`1Dx1y!EZrM&^zMat!^Wz# z=Z{;Pa0w21oA1X3*9=`*c7o3ePa^k%Vzu>2C_7DaZJ8FW5GJv|t>`Ym;_S>7g_3XI zdRb!Ppd`ErK`pUDHRsJd9@)bu>}s1)nKsyAR7h21<1u{DX1gd_Vf;^zdUpFPeSHHR z7AMgw^{FlFlK91CGMafKt`$FLhq#^=->@Uok7pqW6&#Zs4*E(i5-jog43A*qC@!(8 z8&F}pofRcMVmcJd=f;fvlfAR!ZqeaTE?#TQ^jQM0ioaJf8m^!Kdv^`f5kEsD0=gX#4={QE1$3A4K~V$ITKEd){XVLx?i6K*D>JF6E=i znqF^X#&UX}rfB|#A9%y|sR5i6B5gyk>8@Q+xHg|^5iz7C2}YkGF)nuP4LX#k2tRBP z=!VnWnXea(K#Wvg2&0f{!mXuuWaPpsoZ)3TSaEp;i|_)CvP=4wjI; zH%7tcLM8dQXsHW*#|}%TG9yiGpyjBltpcpXkpl8zg~x zD{QG)2Z8x$vfjgDc(J6i|OHoLX&!<+m^<$S3DtA8Mf!{ z7;g1}0uqJ0Mxuy%=#BFX5;Xh9JkrA$d}neS9T;$F$kXn}ss zF{Jn}9EDk=>h)sMy$YXfhKIDxr7U@3xl+uI|N5y!>?{aVn703L1Qgb$ql%JT^lsGD%)~)(H?Spj$zNt)h)Raob z@KyVB@&ngE0rtMW4!UTqGX>{&KHJAWqb)oYq9O)e)nmN0jVa;LNbKXx04a+8&O;q) zHBzGejrqt7Dk$Z2VR%%K#`!((pXE*MR{jGtv|q$p5#v9N0f^6B9IB!Q6(y$TmHRLM zsYXm2jn3f{9T)KVVzotDx=Ng8q0Z*VDZOkd5C!p0PRoFt>NyVEc9*%YR&2>Nq~$AI zXOQfjJ&wpGMe~I8y=cC(QR4=W2GWccFK(3`d&gN+)qWtW-`*}mZI%KDRl4@rUv1%d zxFO82lhW$xQyYxJg8tOZyXm1As%kEFNn)eW{R61M>af@wr(YW{R@+eL2 zx?SovK+867$F%T;Dfeajw|kiQ81GcOnS$Y4+hp8g_w1P8_~79d9p$*M1_Ei81$H$Ti6oi?ZW)&tmsJa7RV1LKddm7R*qL54L7j zvCr1Mrb;l!=m^TbJun-C_6$7w81E1eAQC^6s4>rZ4&I5+yyu$kha%Z&d+|S7Ki#{2 zy}%Giz|eR|G?ychX%%=eL`W(aLarb(L4jd>J+wlX;xMV9H8J!l&i?~Mw7)jlIuLD% zyq+AK92j#kC`ycv$SJ|E7!FBParx#v<3_rZ-DLQ@>`#sdl5}immok8&`{YgF|+< z`tB>e%6G{=B4?V-be>`&*}0d*f?$yBX@w+rJht@O+=^zttqB2p=IiA17#YD$4-fih z@$gJ95mGmFhN!d;3Ag4#>3o`>%L{G=9<}qOJ$wDN)%)MN6bVsAPG4oKB3+8r6!Qf9 z3m8?jIpWcEJbt6|f?Y4nMXK(--YZ|GA2_aRS!do%J9S7?Q&4FYL@sPilq}e4tlYa& z?f+we^=FH^Z9|dnXZghblW!IYGIAT{``58&7vZBybh+GuIPP{h*J?&vf7i8rv6qgx zab9~l+K`tvC7pWtlS!5lt(n#Yl}PAR(v01oXjc0F?T0w>+*p#PtE?Tf_hMrEaZ!^V zbv_>=4xibc0TUxg^I>TS?HR4fdiWl`@6{7|WU9G68l7tOz2p>oIe~NNr!>Q&PHm`4 z98R?g(IT*nl#{_|*WO_h0X78;WwMp?A^Zi)W@BX5q==TdOl?~J6HK(0b(xD6?m3e3 z#+zMaSJb(W$h5+d+6vujSjyi_R80c9>7h;0YlUFDvN`iNGu&5HQ5^e>6x?&JSc4V$6_I1jJ4vnCVbkU`Gz=Uy#~OI( zlL-$UAE$pVCsD_rICM#Q!ltzcqDphp5L|ZrqUm>=H%x!RjMrF#*?BN2shvUg=H;)& zy~_xWl*k$~9Hl6PIq({dELPE-r4*YNs7?5{>dlC`EcK~lPKB_8V)G@H)UZFF8$tXT z@^raW#Hq4OJGFL2Aye|HU&_NL%dYans6?ltqEBz`Q|m=@Zh4=-p2r;}q(Nbsk$fUI zP|(Ns2>MDvZi1H7<55frlQn#%?`WY3g`+fRuC#UJx%#d!zxEu3=}zF514S=6f@?~$ zeuSB=6E7r3ya|; z@K7M3VBrls6c{M*M_{AB_fVjgQ|F(FuK(@=1eWeVMSpLglllqV6Rg-L_46;?^IskS z)x6|SR1^gGl6amWjkb1dX}^8DumNXNmhsfxKA#;bBBIZE@0gma5yQY(FX>|N~Y^mgq`xc zdxOf6r{9u#_e0gV3(fdBTdV2Sc4SN5ZmP?cB4?KRdvj&>@zN_HP5m0E=+A=efDBI*IG*Gy%%< zz@yc%2XvGm)QQv5k^ZC6!9MwX8BCmQ{3eAX|GTwn#>(PS6PoB=$Pwn*?wz?%Tx2gwJ4apoy`A15D=>?%}hj`fV*p=6XW=YR(sp))`dxTnqHE&{&; zPdeO}SVkf*6_$c45W3Z}u|Z&a8{r!6ZNY62S>5{jAd)Hkjg@h%@c)c#BvZK2lmGw| z`Vh+%ECkF{t=)XpF3Z1bj=Pe9LpHbnQwjeTU#=4hB76#52DU2P2Ouj~^lRWwRd%eN zBw_z%FL0CUlk!`s2!`>QG&H__i_)I9=AuA=jn40z>;@hRsg)>J(58cx;l;h_zE*-R7Wbz6Ff#1Mss*)zTImU4`2@?a7y;v4 zH=lJ_PM5Rkw*AU`Cmq6aa>chASJ&Z3Ebj`y;w$MM!fa6`13VU7Kc|T5Xl#7ecj?mp zREV-nBJ6C)`?&}QDe_(KM>BrlN|iF{7-90j+J>N0^vY=LK;8!^9Y_m*aRPX{!S6ag zgRw(13pJvt`;{^S-vgUk?8pV_Vh4a4P7~}uHT)ENFMqd71QIOl8Q6+24TM_+158z) z54U-*C{M)S&!2Bfu&`?Ti6;WojY;%6+I;uCof+*T2iUMz!7Eg<{}#DJSx)C$5f zP(oSf>_s1t06cJ-U3?<9poS4O{Go>H>hro^ks;r3mm1Ehfq?m(_YE8UiVUgG%W9ZY z!@O^}KR%JW*0e=66rUYj5BP~=x%$^x92-m_ + + + Restyaboard | API Explorer + + + + + + + + + + + + + + + + + + + + + + + + + +
+   +
+ +
+ +
+ + + + diff --git a/api_explorer/lib/backbone-min.js b/api_explorer/lib/backbone-min.js new file mode 100644 index 000000000..c1c0d4fff --- /dev/null +++ b/api_explorer/lib/backbone-min.js @@ -0,0 +1,38 @@ +// Backbone.js 0.9.2 + +// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. +// Backbone may be freely distributed under the MIT license. +// For all details and documentation: +// http://backbonejs.org +(function(){var l=this,y=l.Backbone,z=Array.prototype.slice,A=Array.prototype.splice,g;g="undefined"!==typeof exports?exports:l.Backbone={};g.VERSION="0.9.2";var f=l._;!f&&"undefined"!==typeof require&&(f=require("underscore"));var i=l.jQuery||l.Zepto||l.ender;g.setDomLibrary=function(a){i=a};g.noConflict=function(){l.Backbone=y;return this};g.emulateHTTP=!1;g.emulateJSON=!1;var p=/\s+/,k=g.Events={on:function(a,b,c){var d,e,f,g,j;if(!b)return this;a=a.split(p);for(d=this._callbacks||(this._callbacks= +{});e=a.shift();)f=(j=d[e])?j.tail:{},f.next=g={},f.context=c,f.callback=b,d[e]={tail:g,next:j?j.next:f};return this},off:function(a,b,c){var d,e,h,g,j,q;if(e=this._callbacks){if(!a&&!b&&!c)return delete this._callbacks,this;for(a=a?a.split(p):f.keys(e);d=a.shift();)if(h=e[d],delete e[d],h&&(b||c))for(g=h.tail;(h=h.next)!==g;)if(j=h.callback,q=h.context,b&&j!==b||c&&q!==c)this.on(d,j,q);return this}},trigger:function(a){var b,c,d,e,f,g;if(!(d=this._callbacks))return this;f=d.all;a=a.split(p);for(g= +z.call(arguments,1);b=a.shift();){if(c=d[b])for(e=c.tail;(c=c.next)!==e;)c.callback.apply(c.context||this,g);if(c=f){e=c.tail;for(b=[b].concat(g);(c=c.next)!==e;)c.callback.apply(c.context||this,b)}}return this}};k.bind=k.on;k.unbind=k.off;var o=g.Model=function(a,b){var c;a||(a={});b&&b.parse&&(a=this.parse(a));if(c=n(this,"defaults"))a=f.extend({},c,a);b&&b.collection&&(this.collection=b.collection);this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.changed={};this._silent= +{};this._pending={};this.set(a,{silent:!0});this.changed={};this._silent={};this._pending={};this._previousAttributes=f.clone(this.attributes);this.initialize.apply(this,arguments)};f.extend(o.prototype,k,{changed:null,_silent:null,_pending:null,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.get(a);return this._escapedAttributes[a]=f.escape(null== +b?"":""+b)},has:function(a){return null!=this.get(a)},set:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c||(c={});if(!d)return this;d instanceof o&&(d=d.attributes);if(c.unset)for(e in d)d[e]=void 0;if(!this._validate(d,c))return!1;this.idAttribute in d&&(this.id=d[this.idAttribute]);var b=c.changes={},h=this.attributes,g=this._escapedAttributes,j=this._previousAttributes||{};for(e in d){a=d[e];if(!f.isEqual(h[e],a)||c.unset&&f.has(h,e))delete g[e],(c.silent?this._silent: +b)[e]=!0;c.unset?delete h[e]:h[e]=a;!f.isEqual(j[e],a)||f.has(h,e)!=f.has(j,e)?(this.changed[e]=a,c.silent||(this._pending[e]=!0)):(delete this.changed[e],delete this._pending[e])}c.silent||this.change(c);return this},unset:function(a,b){(b||(b={})).unset=!0;return this.set(a,null,b)},clear:function(a){(a||(a={})).unset=!0;return this.set(f.clone(this.attributes),a)},fetch:function(a){var a=a?f.clone(a):{},b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&&c(b,d)}; +a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},save:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c=c?f.clone(c):{};if(c.wait){if(!this._validate(d,c))return!1;e=f.clone(this.attributes)}a=f.extend({},c,{silent:!0});if(d&&!this.set(d,c.wait?a:c))return!1;var h=this,i=c.success;c.success=function(a,b,e){b=h.parse(a,e);if(c.wait){delete c.wait;b=f.extend(d||{},b)}if(!h.set(b,c))return false;i?i(h,a):h.trigger("sync",h,a,c)};c.error=g.wrapError(c.error, +h,c);b=this.isNew()?"create":"update";b=(this.sync||g.sync).call(this,b,this,c);c.wait&&this.set(e,a);return b},destroy:function(a){var a=a?f.clone(a):{},b=this,c=a.success,d=function(){b.trigger("destroy",b,b.collection,a)};if(this.isNew())return d(),!1;a.success=function(e){a.wait&&d();c?c(b,e):b.trigger("sync",b,e,a)};a.error=g.wrapError(a.error,b,a);var e=(this.sync||g.sync).call(this,"delete",this,a);a.wait||d();return e},url:function(){var a=n(this,"urlRoot")||n(this.collection,"url")||t(); +return this.isNew()?a:a+("/"==a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return null==this.id},change:function(a){a||(a={});var b=this._changing;this._changing=!0;for(var c in this._silent)this._pending[c]=!0;var d=f.extend({},a.changes,this._silent);this._silent={};for(c in d)this.trigger("change:"+c,this,this.get(c),a);if(b)return this;for(;!f.isEmpty(this._pending);){this._pending= +{};this.trigger("change",this,a);for(c in this.changed)!this._pending[c]&&!this._silent[c]&&delete this.changed[c];this._previousAttributes=f.clone(this.attributes)}this._changing=!1;return this},hasChanged:function(a){return!arguments.length?!f.isEmpty(this.changed):f.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this.changed):!1;var b,c=!1,d=this._previousAttributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return!arguments.length|| +!this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},isValid:function(){return!this.validate(this.attributes)},_validate:function(a,b){if(b.silent||!this.validate)return!0;var a=f.extend({},this.attributes,a),c=this.validate(a,b);if(!c)return!0;b&&b.error?b.error(this,c,b):this.trigger("error",this,c,b);return!1}});var r=g.Collection=function(a,b){b||(b={});b.model&&(this.model=b.model);b.comparator&&(this.comparator=b.comparator); +this._reset();this.initialize.apply(this,arguments);a&&this.reset(a,{silent:!0,parse:b.parse})};f.extend(r.prototype,k,{model:o,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},add:function(a,b){var c,d,e,g,i,j={},k={},l=[];b||(b={});a=f.isArray(a)?a.slice():[a];c=0;for(d=a.length;c=b))this.iframe=i('' : ''); + inst._keyEvent = false; + return html; + }, + + /* Generate the month and year header. */ + _generateMonthYearHeader: function(inst, drawMonth, drawYear, minDate, maxDate, + secondary, monthNames, monthNamesShort) { + var changeMonth = this._get(inst, 'changeMonth'); + var changeYear = this._get(inst, 'changeYear'); + var showMonthAfterYear = this._get(inst, 'showMonthAfterYear'); + var html = '
'; + var monthHtml = ''; + // month selection + if (secondary || !changeMonth) + monthHtml += '' + monthNames[drawMonth] + ''; + else { + var inMinYear = (minDate && minDate.getFullYear() == drawYear); + var inMaxYear = (maxDate && maxDate.getFullYear() == drawYear); + monthHtml += ''; + } + if (!showMonthAfterYear) + html += monthHtml + (secondary || !(changeMonth && changeYear) ? ' ' : ''); + // year selection + if ( !inst.yearshtml ) { + inst.yearshtml = ''; + if (secondary || !changeYear) + html += '' + drawYear + ''; + else { + // determine range of years to display + var years = this._get(inst, 'yearRange').split(':'); + var thisYear = new Date().getFullYear(); + var determineYear = function(value) { + var year = (value.match(/c[+-].*/) ? drawYear + parseInt(value.substring(1), 10) : + (value.match(/[+-].*/) ? thisYear + parseInt(value, 10) : + parseInt(value, 10))); + return (isNaN(year) ? thisYear : year); + }; + var year = determineYear(years[0]); + var endYear = Math.max(year, determineYear(years[1] || '')); + year = (minDate ? Math.max(year, minDate.getFullYear()) : year); + endYear = (maxDate ? Math.min(endYear, maxDate.getFullYear()) : endYear); + inst.yearshtml += ''; + + html += inst.yearshtml; + inst.yearshtml = null; + } + } + html += this._get(inst, 'yearSuffix'); + if (showMonthAfterYear) + html += (secondary || !(changeMonth && changeYear) ? ' ' : '') + monthHtml; + html += '
'; // Close datepicker_header + return html; + }, + + /* Adjust one of the date sub-fields. */ + _adjustInstDate: function(inst, offset, period) { + var year = inst.drawYear + (period == 'Y' ? offset : 0); + var month = inst.drawMonth + (period == 'M' ? offset : 0); + var day = Math.min(inst.selectedDay, this._getDaysInMonth(year, month)) + + (period == 'D' ? offset : 0); + var date = this._restrictMinMax(inst, + this._daylightSavingAdjust(new Date(year, month, day))); + inst.selectedDay = date.getDate(); + inst.drawMonth = inst.selectedMonth = date.getMonth(); + inst.drawYear = inst.selectedYear = date.getFullYear(); + if (period == 'M' || period == 'Y') + this._notifyChange(inst); + }, + + /* Ensure a date is within any min/max bounds. */ + _restrictMinMax: function(inst, date) { + var minDate = this._getMinMaxDate(inst, 'min'); + var maxDate = this._getMinMaxDate(inst, 'max'); + var newDate = (minDate && date < minDate ? minDate : date); + newDate = (maxDate && newDate > maxDate ? maxDate : newDate); + return newDate; + }, + + /* Notify change of month/year. */ + _notifyChange: function(inst) { + var onChange = this._get(inst, 'onChangeMonthYear'); + if (onChange) + onChange.apply((inst.input ? inst.input[0] : null), + [inst.selectedYear, inst.selectedMonth + 1, inst]); + }, + + /* Determine the number of months to show. */ + _getNumberOfMonths: function(inst) { + var numMonths = this._get(inst, 'numberOfMonths'); + return (numMonths == null ? [1, 1] : (typeof numMonths == 'number' ? [1, numMonths] : numMonths)); + }, + + /* Determine the current maximum date - ensure no time components are set. */ + _getMinMaxDate: function(inst, minMax) { + return this._determineDate(inst, this._get(inst, minMax + 'Date'), null); + }, + + /* Find the number of days in a given month. */ + _getDaysInMonth: function(year, month) { + return 32 - this._daylightSavingAdjust(new Date(year, month, 32)).getDate(); + }, + + /* Find the day of the week of the first of a month. */ + _getFirstDayOfMonth: function(year, month) { + return new Date(year, month, 1).getDay(); + }, + + /* Determines if we should allow a "next/prev" month display change. */ + _canAdjustMonth: function(inst, offset, curYear, curMonth) { + var numMonths = this._getNumberOfMonths(inst); + var date = this._daylightSavingAdjust(new Date(curYear, + curMonth + (offset < 0 ? offset : numMonths[0] * numMonths[1]), 1)); + if (offset < 0) + date.setDate(this._getDaysInMonth(date.getFullYear(), date.getMonth())); + return this._isInRange(inst, date); + }, + + /* Is the given date in the accepted range? */ + _isInRange: function(inst, date) { + var minDate = this._getMinMaxDate(inst, 'min'); + var maxDate = this._getMinMaxDate(inst, 'max'); + return ((!minDate || date.getTime() >= minDate.getTime()) && + (!maxDate || date.getTime() <= maxDate.getTime())); + }, + + /* Provide the configuration settings for formatting/parsing. */ + _getFormatConfig: function(inst) { + var shortYearCutoff = this._get(inst, 'shortYearCutoff'); + shortYearCutoff = (typeof shortYearCutoff != 'string' ? shortYearCutoff : + new Date().getFullYear() % 100 + parseInt(shortYearCutoff, 10)); + return {shortYearCutoff: shortYearCutoff, + dayNamesShort: this._get(inst, 'dayNamesShort'), dayNames: this._get(inst, 'dayNames'), + monthNamesShort: this._get(inst, 'monthNamesShort'), monthNames: this._get(inst, 'monthNames')}; + }, + + /* Format the given date for display. */ + _formatDate: function(inst, day, month, year) { + if (!day) { + inst.currentDay = inst.selectedDay; + inst.currentMonth = inst.selectedMonth; + inst.currentYear = inst.selectedYear; + } + var date = (day ? (typeof day == 'object' ? day : + this._daylightSavingAdjust(new Date(year, month, day))) : + this._daylightSavingAdjust(new Date(inst.currentYear, inst.currentMonth, inst.currentDay))); + return this.formatDate(this._get(inst, 'dateFormat'), date, this._getFormatConfig(inst)); + } +}); + +/* + * Bind hover events for datepicker elements. + * Done via delegate so the binding only occurs once in the lifetime of the parent div. + * Global instActive, set by _updateDatepicker allows the handlers to find their way back to the active picker. + */ +function bindHover(dpDiv) { + var selector = 'button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a'; + return dpDiv.bind('mouseout', function(event) { + var elem = $( event.target ).closest( selector ); + if ( !elem.length ) { + return; + } + elem.removeClass( "ui-state-hover ui-datepicker-prev-hover ui-datepicker-next-hover" ); + }) + .bind('mouseover', function(event) { + var elem = $( event.target ).closest( selector ); + if ($.datepicker._isDisabledDatepicker( instActive.inline ? dpDiv.parent()[0] : instActive.input[0]) || + !elem.length ) { + return; + } + elem.parents('.ui-datepicker-calendar').find('a').removeClass('ui-state-hover'); + elem.addClass('ui-state-hover'); + if (elem.hasClass('ui-datepicker-prev')) elem.addClass('ui-datepicker-prev-hover'); + if (elem.hasClass('ui-datepicker-next')) elem.addClass('ui-datepicker-next-hover'); + }); +} + +/* jQuery extend now ignores nulls! */ +function extendRemove(target, props) { + $.extend(target, props); + for (var name in props) + if (props[name] == null || props[name] == undefined) + target[name] = props[name]; + return target; +}; + +/* Determine whether an object is an array. */ +function isArray(a) { + return (a && (($.browser.safari && typeof a == 'object' && a.length) || + (a.constructor && a.constructor.toString().match(/\Array\(\)/)))); +}; + +/* Invoke the datepicker functionality. + @param options string - a command, optionally followed by additional parameters or + Object - settings for attaching new datepicker functionality + @return jQuery object */ +$.fn.datepicker = function(options){ + + /* Verify an empty collection wasn't passed - Fixes #6976 */ + if ( !this.length ) { + return this; + } + + /* Initialise the date picker. */ + if (!$.datepicker.initialized) { + $(document).mousedown($.datepicker._checkExternalClick). + find('body').append($.datepicker.dpDiv); + $.datepicker.initialized = true; + } + + var otherArgs = Array.prototype.slice.call(arguments, 1); + if (typeof options == 'string' && (options == 'isDisabled' || options == 'getDate' || options == 'widget')) + return $.datepicker['_' + options + 'Datepicker']. + apply($.datepicker, [this[0]].concat(otherArgs)); + if (options == 'option' && arguments.length == 2 && typeof arguments[1] == 'string') + return $.datepicker['_' + options + 'Datepicker']. + apply($.datepicker, [this[0]].concat(otherArgs)); + return this.each(function() { + typeof options == 'string' ? + $.datepicker['_' + options + 'Datepicker']. + apply($.datepicker, [this].concat(otherArgs)) : + $.datepicker._attachDatepicker(this, options); + }); +}; + +$.datepicker = new Datepicker(); // singleton instance +$.datepicker.initialized = false; +$.datepicker.uuid = new Date().getTime(); +$.datepicker.version = "1.8.23"; + +// Workaround for #4055 +// Add another global to avoid noConflict issues with inline event handlers +window['DP_jQuery_' + dpuuid] = $; + +})(jQuery); + +(function( $, undefined ) { + +var uiDialogClasses = + 'ui-dialog ' + + 'ui-widget ' + + 'ui-widget-content ' + + 'ui-corner-all ', + sizeRelatedOptions = { + buttons: true, + height: true, + maxHeight: true, + maxWidth: true, + minHeight: true, + minWidth: true, + width: true + }, + resizableRelatedOptions = { + maxHeight: true, + maxWidth: true, + minHeight: true, + minWidth: true + }; + +$.widget("ui.dialog", { + options: { + autoOpen: true, + buttons: {}, + closeOnEscape: true, + closeText: 'close', + dialogClass: '', + draggable: true, + hide: null, + height: 'auto', + maxHeight: false, + maxWidth: false, + minHeight: 150, + minWidth: 150, + modal: false, + position: { + my: 'center', + at: 'center', + collision: 'fit', + // ensure that the titlebar is never outside the document + using: function(pos) { + var topOffset = $(this).css(pos).offset().top; + if (topOffset < 0) { + $(this).css('top', pos.top - topOffset); + } + } + }, + resizable: true, + show: null, + stack: true, + title: '', + width: 300, + zIndex: 1000 + }, + + _create: function() { + this.originalTitle = this.element.attr('title'); + // #5742 - .attr() might return a DOMElement + if ( typeof this.originalTitle !== "string" ) { + this.originalTitle = ""; + } + + this.options.title = this.options.title || this.originalTitle; + var self = this, + options = self.options, + + title = options.title || ' ', + titleId = $.ui.dialog.getTitleId(self.element), + + uiDialog = (self.uiDialog = $('
')) + .appendTo(document.body) + .hide() + .addClass(uiDialogClasses + options.dialogClass) + .css({ + zIndex: options.zIndex + }) + // setting tabIndex makes the div focusable + // setting outline to 0 prevents a border on focus in Mozilla + .attr('tabIndex', -1).css('outline', 0).keydown(function(event) { + if (options.closeOnEscape && !event.isDefaultPrevented() && event.keyCode && + event.keyCode === $.ui.keyCode.ESCAPE) { + + self.close(event); + event.preventDefault(); + } + }) + .attr({ + role: 'dialog', + 'aria-labelledby': titleId + }) + .mousedown(function(event) { + self.moveToTop(false, event); + }), + + uiDialogContent = self.element + .show() + .removeAttr('title') + .addClass( + 'ui-dialog-content ' + + 'ui-widget-content') + .appendTo(uiDialog), + + uiDialogTitlebar = (self.uiDialogTitlebar = $('
')) + .addClass( + 'ui-dialog-titlebar ' + + 'ui-widget-header ' + + 'ui-corner-all ' + + 'ui-helper-clearfix' + ) + .prependTo(uiDialog), + + uiDialogTitlebarClose = $('') + .addClass( + 'ui-dialog-titlebar-close ' + + 'ui-corner-all' + ) + .attr('role', 'button') + .hover( + function() { + uiDialogTitlebarClose.addClass('ui-state-hover'); + }, + function() { + uiDialogTitlebarClose.removeClass('ui-state-hover'); + } + ) + .focus(function() { + uiDialogTitlebarClose.addClass('ui-state-focus'); + }) + .blur(function() { + uiDialogTitlebarClose.removeClass('ui-state-focus'); + }) + .click(function(event) { + self.close(event); + return false; + }) + .appendTo(uiDialogTitlebar), + + uiDialogTitlebarCloseText = (self.uiDialogTitlebarCloseText = $('')) + .addClass( + 'ui-icon ' + + 'ui-icon-closethick' + ) + .text(options.closeText) + .appendTo(uiDialogTitlebarClose), + + uiDialogTitle = $('') + .addClass('ui-dialog-title') + .attr('id', titleId) + .html(title) + .prependTo(uiDialogTitlebar); + + //handling of deprecated beforeclose (vs beforeClose) option + //Ticket #4669 http://dev.jqueryui.com/ticket/4669 + //TODO: remove in 1.9pre + if ($.isFunction(options.beforeclose) && !$.isFunction(options.beforeClose)) { + options.beforeClose = options.beforeclose; + } + + uiDialogTitlebar.find("*").add(uiDialogTitlebar).disableSelection(); + + if (options.draggable && $.fn.draggable) { + self._makeDraggable(); + } + if (options.resizable && $.fn.resizable) { + self._makeResizable(); + } + + self._createButtons(options.buttons); + self._isOpen = false; + + if ($.fn.bgiframe) { + uiDialog.bgiframe(); + } + }, + + _init: function() { + if ( this.options.autoOpen ) { + this.open(); + } + }, + + destroy: function() { + var self = this; + + if (self.overlay) { + self.overlay.destroy(); + } + self.uiDialog.hide(); + self.element + .unbind('.dialog') + .removeData('dialog') + .removeClass('ui-dialog-content ui-widget-content') + .hide().appendTo('body'); + self.uiDialog.remove(); + + if (self.originalTitle) { + self.element.attr('title', self.originalTitle); + } + + return self; + }, + + widget: function() { + return this.uiDialog; + }, + + close: function(event) { + var self = this, + maxZ, thisZ; + + if (false === self._trigger('beforeClose', event)) { + return; + } + + if (self.overlay) { + self.overlay.destroy(); + } + self.uiDialog.unbind('keypress.ui-dialog'); + + self._isOpen = false; + + if (self.options.hide) { + self.uiDialog.hide(self.options.hide, function() { + self._trigger('close', event); + }); + } else { + self.uiDialog.hide(); + self._trigger('close', event); + } + + $.ui.dialog.overlay.resize(); + + // adjust the maxZ to allow other modal dialogs to continue to work (see #4309) + if (self.options.modal) { + maxZ = 0; + $('.ui-dialog').each(function() { + if (this !== self.uiDialog[0]) { + thisZ = $(this).css('z-index'); + if(!isNaN(thisZ)) { + maxZ = Math.max(maxZ, thisZ); + } + } + }); + $.ui.dialog.maxZ = maxZ; + } + + return self; + }, + + isOpen: function() { + return this._isOpen; + }, + + // the force parameter allows us to move modal dialogs to their correct + // position on open + moveToTop: function(force, event) { + var self = this, + options = self.options, + saveScroll; + + if ((options.modal && !force) || + (!options.stack && !options.modal)) { + return self._trigger('focus', event); + } + + if (options.zIndex > $.ui.dialog.maxZ) { + $.ui.dialog.maxZ = options.zIndex; + } + if (self.overlay) { + $.ui.dialog.maxZ += 1; + self.overlay.$el.css('z-index', $.ui.dialog.overlay.maxZ = $.ui.dialog.maxZ); + } + + //Save and then restore scroll since Opera 9.5+ resets when parent z-Index is changed. + // http://ui.jquery.com/bugs/ticket/3193 + saveScroll = { scrollTop: self.element.scrollTop(), scrollLeft: self.element.scrollLeft() }; + $.ui.dialog.maxZ += 1; + self.uiDialog.css('z-index', $.ui.dialog.maxZ); + self.element.attr(saveScroll); + self._trigger('focus', event); + + return self; + }, + + open: function() { + if (this._isOpen) { return; } + + var self = this, + options = self.options, + uiDialog = self.uiDialog; + + self.overlay = options.modal ? new $.ui.dialog.overlay(self) : null; + self._size(); + self._position(options.position); + uiDialog.show(options.show); + self.moveToTop(true); + + // prevent tabbing out of modal dialogs + if ( options.modal ) { + uiDialog.bind( "keydown.ui-dialog", function( event ) { + if ( event.keyCode !== $.ui.keyCode.TAB ) { + return; + } + + var tabbables = $(':tabbable', this), + first = tabbables.filter(':first'), + last = tabbables.filter(':last'); + + if (event.target === last[0] && !event.shiftKey) { + first.focus(1); + return false; + } else if (event.target === first[0] && event.shiftKey) { + last.focus(1); + return false; + } + }); + } + + // set focus to the first tabbable element in the content area or the first button + // if there are no tabbable elements, set focus on the dialog itself + $(self.element.find(':tabbable').get().concat( + uiDialog.find('.ui-dialog-buttonpane :tabbable').get().concat( + uiDialog.get()))).eq(0).focus(); + + self._isOpen = true; + self._trigger('open'); + + return self; + }, + + _createButtons: function(buttons) { + var self = this, + hasButtons = false, + uiDialogButtonPane = $('
') + .addClass( + 'ui-dialog-buttonpane ' + + 'ui-widget-content ' + + 'ui-helper-clearfix' + ), + uiButtonSet = $( "
" ) + .addClass( "ui-dialog-buttonset" ) + .appendTo( uiDialogButtonPane ); + + // if we already have a button pane, remove it + self.uiDialog.find('.ui-dialog-buttonpane').remove(); + + if (typeof buttons === 'object' && buttons !== null) { + $.each(buttons, function() { + return !(hasButtons = true); + }); + } + if (hasButtons) { + $.each(buttons, function(name, props) { + props = $.isFunction( props ) ? + { click: props, text: name } : + props; + var button = $('') + .click(function() { + props.click.apply(self.element[0], arguments); + }) + .appendTo(uiButtonSet); + // can't use .attr( props, true ) with jQuery 1.3.2. + $.each( props, function( key, value ) { + if ( key === "click" ) { + return; + } + if ( key in button ) { + button[ key ]( value ); + } else { + button.attr( key, value ); + } + }); + if ($.fn.button) { + button.button(); + } + }); + uiDialogButtonPane.appendTo(self.uiDialog); + } + }, + + _makeDraggable: function() { + var self = this, + options = self.options, + doc = $(document), + heightBeforeDrag; + + function filteredUi(ui) { + return { + position: ui.position, + offset: ui.offset + }; + } + + self.uiDialog.draggable({ + cancel: '.ui-dialog-content, .ui-dialog-titlebar-close', + handle: '.ui-dialog-titlebar', + containment: 'document', + start: function(event, ui) { + heightBeforeDrag = options.height === "auto" ? "auto" : $(this).height(); + $(this).height($(this).height()).addClass("ui-dialog-dragging"); + self._trigger('dragStart', event, filteredUi(ui)); + }, + drag: function(event, ui) { + self._trigger('drag', event, filteredUi(ui)); + }, + stop: function(event, ui) { + options.position = [ui.position.left - doc.scrollLeft(), + ui.position.top - doc.scrollTop()]; + $(this).removeClass("ui-dialog-dragging").height(heightBeforeDrag); + self._trigger('dragStop', event, filteredUi(ui)); + $.ui.dialog.overlay.resize(); + } + }); + }, + + _makeResizable: function(handles) { + handles = (handles === undefined ? this.options.resizable : handles); + var self = this, + options = self.options, + // .ui-resizable has position: relative defined in the stylesheet + // but dialogs have to use absolute or fixed positioning + position = self.uiDialog.css('position'), + resizeHandles = (typeof handles === 'string' ? + handles : + 'n,e,s,w,se,sw,ne,nw' + ); + + function filteredUi(ui) { + return { + originalPosition: ui.originalPosition, + originalSize: ui.originalSize, + position: ui.position, + size: ui.size + }; + } + + self.uiDialog.resizable({ + cancel: '.ui-dialog-content', + containment: 'document', + alsoResize: self.element, + maxWidth: options.maxWidth, + maxHeight: options.maxHeight, + minWidth: options.minWidth, + minHeight: self._minHeight(), + handles: resizeHandles, + start: function(event, ui) { + $(this).addClass("ui-dialog-resizing"); + self._trigger('resizeStart', event, filteredUi(ui)); + }, + resize: function(event, ui) { + self._trigger('resize', event, filteredUi(ui)); + }, + stop: function(event, ui) { + $(this).removeClass("ui-dialog-resizing"); + options.height = $(this).height(); + options.width = $(this).width(); + self._trigger('resizeStop', event, filteredUi(ui)); + $.ui.dialog.overlay.resize(); + } + }) + .css('position', position) + .find('.ui-resizable-se').addClass('ui-icon ui-icon-grip-diagonal-se'); + }, + + _minHeight: function() { + var options = this.options; + + if (options.height === 'auto') { + return options.minHeight; + } else { + return Math.min(options.minHeight, options.height); + } + }, + + _position: function(position) { + var myAt = [], + offset = [0, 0], + isVisible; + + if (position) { + // deep extending converts arrays to objects in jQuery <= 1.3.2 :-( + // if (typeof position == 'string' || $.isArray(position)) { + // myAt = $.isArray(position) ? position : position.split(' '); + + if (typeof position === 'string' || (typeof position === 'object' && '0' in position)) { + myAt = position.split ? position.split(' ') : [position[0], position[1]]; + if (myAt.length === 1) { + myAt[1] = myAt[0]; + } + + $.each(['left', 'top'], function(i, offsetPosition) { + if (+myAt[i] === myAt[i]) { + offset[i] = myAt[i]; + myAt[i] = offsetPosition; + } + }); + + position = { + my: myAt.join(" "), + at: myAt.join(" "), + offset: offset.join(" ") + }; + } + + position = $.extend({}, $.ui.dialog.prototype.options.position, position); + } else { + position = $.ui.dialog.prototype.options.position; + } + + // need to show the dialog to get the actual offset in the position plugin + isVisible = this.uiDialog.is(':visible'); + if (!isVisible) { + this.uiDialog.show(); + } + this.uiDialog + // workaround for jQuery bug #5781 http://dev.jquery.com/ticket/5781 + .css({ top: 0, left: 0 }) + .position($.extend({ of: window }, position)); + if (!isVisible) { + this.uiDialog.hide(); + } + }, + + _setOptions: function( options ) { + var self = this, + resizableOptions = {}, + resize = false; + + $.each( options, function( key, value ) { + self._setOption( key, value ); + + if ( key in sizeRelatedOptions ) { + resize = true; + } + if ( key in resizableRelatedOptions ) { + resizableOptions[ key ] = value; + } + }); + + if ( resize ) { + this._size(); + } + if ( this.uiDialog.is( ":data(resizable)" ) ) { + this.uiDialog.resizable( "option", resizableOptions ); + } + }, + + _setOption: function(key, value){ + var self = this, + uiDialog = self.uiDialog; + + switch (key) { + //handling of deprecated beforeclose (vs beforeClose) option + //Ticket #4669 http://dev.jqueryui.com/ticket/4669 + //TODO: remove in 1.9pre + case "beforeclose": + key = "beforeClose"; + break; + case "buttons": + self._createButtons(value); + break; + case "closeText": + // ensure that we always pass a string + self.uiDialogTitlebarCloseText.text("" + value); + + break; + case "dialogClass": + uiDialog + .removeClass(self.options.dialogClass) + .addClass(uiDialogClasses + value); + break; + case "disabled": + if (value) { + uiDialog.addClass('ui-dialog-disabled'); + } else { + uiDialog.removeClass('ui-dialog-disabled'); + } + break; + case "draggable": + var isDraggable = uiDialog.is( ":data(draggable)" ); + if ( isDraggable && !value ) { + uiDialog.draggable( "destroy" ); + } + + if ( !isDraggable && value ) { + self._makeDraggable(); + } + break; + case "position": + self._position(value); + break; + case "resizable": + // currently resizable, becoming non-resizable + var isResizable = uiDialog.is( ":data(resizable)" ); + if (isResizable && !value) { + uiDialog.resizable('destroy'); + } + + // currently resizable, changing handles + if (isResizable && typeof value === 'string') { + uiDialog.resizable('option', 'handles', value); + } + + // currently non-resizable, becoming resizable + if (!isResizable && value !== false) { + self._makeResizable(value); + } + break; + case "title": + // convert whatever was passed in o a string, for html() to not throw up + $(".ui-dialog-title", self.uiDialogTitlebar).html("" + (value || ' ')); + break; + } + + $.Widget.prototype._setOption.apply(self, arguments); + }, + + _size: function() { + /* If the user has resized the dialog, the .ui-dialog and .ui-dialog-content + * divs will both have width and height set, so we need to reset them + */ + var options = this.options, + nonContentHeight, + minContentHeight, + isVisible = this.uiDialog.is( ":visible" ); + + // reset content sizing + this.element.show().css({ + width: 'auto', + minHeight: 0, + height: 0 + }); + + if (options.minWidth > options.width) { + options.width = options.minWidth; + } + + // reset wrapper sizing + // determine the height of all the non-content elements + nonContentHeight = this.uiDialog.css({ + height: 'auto', + width: options.width + }) + .height(); + minContentHeight = Math.max( 0, options.minHeight - nonContentHeight ); + + if ( options.height === "auto" ) { + // only needed for IE6 support + if ( $.support.minHeight ) { + this.element.css({ + minHeight: minContentHeight, + height: "auto" + }); + } else { + this.uiDialog.show(); + var autoHeight = this.element.css( "height", "auto" ).height(); + if ( !isVisible ) { + this.uiDialog.hide(); + } + this.element.height( Math.max( autoHeight, minContentHeight ) ); + } + } else { + this.element.height( Math.max( options.height - nonContentHeight, 0 ) ); + } + + if (this.uiDialog.is(':data(resizable)')) { + this.uiDialog.resizable('option', 'minHeight', this._minHeight()); + } + } +}); + +$.extend($.ui.dialog, { + version: "1.8.23", + + uuid: 0, + maxZ: 0, + + getTitleId: function($el) { + var id = $el.attr('id'); + if (!id) { + this.uuid += 1; + id = this.uuid; + } + return 'ui-dialog-title-' + id; + }, + + overlay: function(dialog) { + this.$el = $.ui.dialog.overlay.create(dialog); + } +}); + +$.extend($.ui.dialog.overlay, { + instances: [], + // reuse old instances due to IE memory leak with alpha transparency (see #5185) + oldInstances: [], + maxZ: 0, + events: $.map('focus,mousedown,mouseup,keydown,keypress,click'.split(','), + function(event) { return event + '.dialog-overlay'; }).join(' '), + create: function(dialog) { + if (this.instances.length === 0) { + // prevent use of anchors and inputs + // we use a setTimeout in case the overlay is created from an + // event that we're going to be cancelling (see #2804) + setTimeout(function() { + // handle $(el).dialog().dialog('close') (see #4065) + if ($.ui.dialog.overlay.instances.length) { + $(document).bind($.ui.dialog.overlay.events, function(event) { + // stop events if the z-index of the target is < the z-index of the overlay + // we cannot return true when we don't want to cancel the event (#3523) + if ($(event.target).zIndex() < $.ui.dialog.overlay.maxZ) { + return false; + } + }); + } + }, 1); + + // allow closing by pressing the escape key + $(document).bind('keydown.dialog-overlay', function(event) { + if (dialog.options.closeOnEscape && !event.isDefaultPrevented() && event.keyCode && + event.keyCode === $.ui.keyCode.ESCAPE) { + + dialog.close(event); + event.preventDefault(); + } + }); + + // handle window resize + $(window).bind('resize.dialog-overlay', $.ui.dialog.overlay.resize); + } + + var $el = (this.oldInstances.pop() || $('
').addClass('ui-widget-overlay')) + .appendTo(document.body) + .css({ + width: this.width(), + height: this.height() + }); + + if ($.fn.bgiframe) { + $el.bgiframe(); + } + + this.instances.push($el); + return $el; + }, + + destroy: function($el) { + var indexOf = $.inArray($el, this.instances); + if (indexOf != -1){ + this.oldInstances.push(this.instances.splice(indexOf, 1)[0]); + } + + if (this.instances.length === 0) { + $([document, window]).unbind('.dialog-overlay'); + } + + $el.remove(); + + // adjust the maxZ to allow other modal dialogs to continue to work (see #4309) + var maxZ = 0; + $.each(this.instances, function() { + maxZ = Math.max(maxZ, this.css('z-index')); + }); + this.maxZ = maxZ; + }, + + height: function() { + var scrollHeight, + offsetHeight; + // handle IE 6 + if ($.browser.msie && $.browser.version < 7) { + scrollHeight = Math.max( + document.documentElement.scrollHeight, + document.body.scrollHeight + ); + offsetHeight = Math.max( + document.documentElement.offsetHeight, + document.body.offsetHeight + ); + + if (scrollHeight < offsetHeight) { + return $(window).height() + 'px'; + } else { + return scrollHeight + 'px'; + } + // handle "good" browsers + } else { + return $(document).height() + 'px'; + } + }, + + width: function() { + var scrollWidth, + offsetWidth; + // handle IE + if ( $.browser.msie ) { + scrollWidth = Math.max( + document.documentElement.scrollWidth, + document.body.scrollWidth + ); + offsetWidth = Math.max( + document.documentElement.offsetWidth, + document.body.offsetWidth + ); + + if (scrollWidth < offsetWidth) { + return $(window).width() + 'px'; + } else { + return scrollWidth + 'px'; + } + // handle "good" browsers + } else { + return $(document).width() + 'px'; + } + }, + + resize: function() { + /* If the dialog is draggable and the user drags it past the + * right edge of the window, the document becomes wider so we + * need to stretch the overlay. If the user then drags the + * dialog back to the left, the document will become narrower, + * so we need to shrink the overlay to the appropriate size. + * This is handled by shrinking the overlay before setting it + * to the full document size. + */ + var $overlays = $([]); + $.each($.ui.dialog.overlay.instances, function() { + $overlays = $overlays.add(this); + }); + + $overlays.css({ + width: 0, + height: 0 + }).css({ + width: $.ui.dialog.overlay.width(), + height: $.ui.dialog.overlay.height() + }); + } +}); + +$.extend($.ui.dialog.overlay.prototype, { + destroy: function() { + $.ui.dialog.overlay.destroy(this.$el); + } +}); + +}(jQuery)); + +(function( $, undefined ) { + +$.ui = $.ui || {}; + +var horizontalPositions = /left|center|right/, + verticalPositions = /top|center|bottom/, + center = "center", + support = {}, + _position = $.fn.position, + _offset = $.fn.offset; + +$.fn.position = function( options ) { + if ( !options || !options.of ) { + return _position.apply( this, arguments ); + } + + // make a copy, we don't want to modify arguments + options = $.extend( {}, options ); + + var target = $( options.of ), + targetElem = target[0], + collision = ( options.collision || "flip" ).split( " " ), + offset = options.offset ? options.offset.split( " " ) : [ 0, 0 ], + targetWidth, + targetHeight, + basePosition; + + if ( targetElem.nodeType === 9 ) { + targetWidth = target.width(); + targetHeight = target.height(); + basePosition = { top: 0, left: 0 }; + // TODO: use $.isWindow() in 1.9 + } else if ( targetElem.setTimeout ) { + targetWidth = target.width(); + targetHeight = target.height(); + basePosition = { top: target.scrollTop(), left: target.scrollLeft() }; + } else if ( targetElem.preventDefault ) { + // force left top to allow flipping + options.at = "left top"; + targetWidth = targetHeight = 0; + basePosition = { top: options.of.pageY, left: options.of.pageX }; + } else { + targetWidth = target.outerWidth(); + targetHeight = target.outerHeight(); + basePosition = target.offset(); + } + + // force my and at to have valid horizontal and veritcal positions + // if a value is missing or invalid, it will be converted to center + $.each( [ "my", "at" ], function() { + var pos = ( options[this] || "" ).split( " " ); + if ( pos.length === 1) { + pos = horizontalPositions.test( pos[0] ) ? + pos.concat( [center] ) : + verticalPositions.test( pos[0] ) ? + [ center ].concat( pos ) : + [ center, center ]; + } + pos[ 0 ] = horizontalPositions.test( pos[0] ) ? pos[ 0 ] : center; + pos[ 1 ] = verticalPositions.test( pos[1] ) ? pos[ 1 ] : center; + options[ this ] = pos; + }); + + // normalize collision option + if ( collision.length === 1 ) { + collision[ 1 ] = collision[ 0 ]; + } + + // normalize offset option + offset[ 0 ] = parseInt( offset[0], 10 ) || 0; + if ( offset.length === 1 ) { + offset[ 1 ] = offset[ 0 ]; + } + offset[ 1 ] = parseInt( offset[1], 10 ) || 0; + + if ( options.at[0] === "right" ) { + basePosition.left += targetWidth; + } else if ( options.at[0] === center ) { + basePosition.left += targetWidth / 2; + } + + if ( options.at[1] === "bottom" ) { + basePosition.top += targetHeight; + } else if ( options.at[1] === center ) { + basePosition.top += targetHeight / 2; + } + + basePosition.left += offset[ 0 ]; + basePosition.top += offset[ 1 ]; + + return this.each(function() { + var elem = $( this ), + elemWidth = elem.outerWidth(), + elemHeight = elem.outerHeight(), + marginLeft = parseInt( $.curCSS( this, "marginLeft", true ) ) || 0, + marginTop = parseInt( $.curCSS( this, "marginTop", true ) ) || 0, + collisionWidth = elemWidth + marginLeft + + ( parseInt( $.curCSS( this, "marginRight", true ) ) || 0 ), + collisionHeight = elemHeight + marginTop + + ( parseInt( $.curCSS( this, "marginBottom", true ) ) || 0 ), + position = $.extend( {}, basePosition ), + collisionPosition; + + if ( options.my[0] === "right" ) { + position.left -= elemWidth; + } else if ( options.my[0] === center ) { + position.left -= elemWidth / 2; + } + + if ( options.my[1] === "bottom" ) { + position.top -= elemHeight; + } else if ( options.my[1] === center ) { + position.top -= elemHeight / 2; + } + + // prevent fractions if jQuery version doesn't support them (see #5280) + if ( !support.fractions ) { + position.left = Math.round( position.left ); + position.top = Math.round( position.top ); + } + + collisionPosition = { + left: position.left - marginLeft, + top: position.top - marginTop + }; + + $.each( [ "left", "top" ], function( i, dir ) { + if ( $.ui.position[ collision[i] ] ) { + $.ui.position[ collision[i] ][ dir ]( position, { + targetWidth: targetWidth, + targetHeight: targetHeight, + elemWidth: elemWidth, + elemHeight: elemHeight, + collisionPosition: collisionPosition, + collisionWidth: collisionWidth, + collisionHeight: collisionHeight, + offset: offset, + my: options.my, + at: options.at + }); + } + }); + + if ( $.fn.bgiframe ) { + elem.bgiframe(); + } + elem.offset( $.extend( position, { using: options.using } ) ); + }); +}; + +$.ui.position = { + fit: { + left: function( position, data ) { + var win = $( window ), + over = data.collisionPosition.left + data.collisionWidth - win.width() - win.scrollLeft(); + position.left = over > 0 ? position.left - over : Math.max( position.left - data.collisionPosition.left, position.left ); + }, + top: function( position, data ) { + var win = $( window ), + over = data.collisionPosition.top + data.collisionHeight - win.height() - win.scrollTop(); + position.top = over > 0 ? position.top - over : Math.max( position.top - data.collisionPosition.top, position.top ); + } + }, + + flip: { + left: function( position, data ) { + if ( data.at[0] === center ) { + return; + } + var win = $( window ), + over = data.collisionPosition.left + data.collisionWidth - win.width() - win.scrollLeft(), + myOffset = data.my[ 0 ] === "left" ? + -data.elemWidth : + data.my[ 0 ] === "right" ? + data.elemWidth : + 0, + atOffset = data.at[ 0 ] === "left" ? + data.targetWidth : + -data.targetWidth, + offset = -2 * data.offset[ 0 ]; + position.left += data.collisionPosition.left < 0 ? + myOffset + atOffset + offset : + over > 0 ? + myOffset + atOffset + offset : + 0; + }, + top: function( position, data ) { + if ( data.at[1] === center ) { + return; + } + var win = $( window ), + over = data.collisionPosition.top + data.collisionHeight - win.height() - win.scrollTop(), + myOffset = data.my[ 1 ] === "top" ? + -data.elemHeight : + data.my[ 1 ] === "bottom" ? + data.elemHeight : + 0, + atOffset = data.at[ 1 ] === "top" ? + data.targetHeight : + -data.targetHeight, + offset = -2 * data.offset[ 1 ]; + position.top += data.collisionPosition.top < 0 ? + myOffset + atOffset + offset : + over > 0 ? + myOffset + atOffset + offset : + 0; + } + } +}; + +// offset setter from jQuery 1.4 +if ( !$.offset.setOffset ) { + $.offset.setOffset = function( elem, options ) { + // set position first, in-case top/left are set even on static elem + if ( /static/.test( $.curCSS( elem, "position" ) ) ) { + elem.style.position = "relative"; + } + var curElem = $( elem ), + curOffset = curElem.offset(), + curTop = parseInt( $.curCSS( elem, "top", true ), 10 ) || 0, + curLeft = parseInt( $.curCSS( elem, "left", true ), 10) || 0, + props = { + top: (options.top - curOffset.top) + curTop, + left: (options.left - curOffset.left) + curLeft + }; + + if ( 'using' in options ) { + options.using.call( elem, props ); + } else { + curElem.css( props ); + } + }; + + $.fn.offset = function( options ) { + var elem = this[ 0 ]; + if ( !elem || !elem.ownerDocument ) { return null; } + if ( options ) { + if ( $.isFunction( options ) ) { + return this.each(function( i ) { + $( this ).offset( options.call( this, i, $( this ).offset() ) ); + }); + } + return this.each(function() { + $.offset.setOffset( this, options ); + }); + } + return _offset.call( this ); + }; +} + +// jQuery <1.4.3 uses curCSS, in 1.4.3 - 1.7.2 curCSS = css, 1.8+ only has css +if ( !$.curCSS ) { + $.curCSS = $.css; +} + +// fraction support test (older versions of jQuery don't support fractions) +(function () { + var body = document.getElementsByTagName( "body" )[ 0 ], + div = document.createElement( "div" ), + testElement, testElementParent, testElementStyle, offset, offsetTotal; + + //Create a "fake body" for testing based on method used in jQuery.support + testElement = document.createElement( body ? "div" : "body" ); + testElementStyle = { + visibility: "hidden", + width: 0, + height: 0, + border: 0, + margin: 0, + background: "none" + }; + if ( body ) { + $.extend( testElementStyle, { + position: "absolute", + left: "-1000px", + top: "-1000px" + }); + } + for ( var i in testElementStyle ) { + testElement.style[ i ] = testElementStyle[ i ]; + } + testElement.appendChild( div ); + testElementParent = body || document.documentElement; + testElementParent.insertBefore( testElement, testElementParent.firstChild ); + + div.style.cssText = "position: absolute; left: 10.7432222px; top: 10.432325px; height: 30px; width: 201px;"; + + offset = $( div ).offset( function( _, offset ) { + return offset; + }).offset(); + + testElement.innerHTML = ""; + testElementParent.removeChild( testElement ); + + offsetTotal = offset.top + offset.left + ( body ? 2000 : 0 ); + support.fractions = offsetTotal > 21 && offsetTotal < 22; +})(); + +}( jQuery )); + +(function( $, undefined ) { + +$.widget( "ui.progressbar", { + options: { + value: 0, + max: 100 + }, + + min: 0, + + _create: function() { + this.element + .addClass( "ui-progressbar ui-widget ui-widget-content ui-corner-all" ) + .attr({ + role: "progressbar", + "aria-valuemin": this.min, + "aria-valuemax": this.options.max, + "aria-valuenow": this._value() + }); + + this.valueDiv = $( "
" ) + .appendTo( this.element ); + + this.oldValue = this._value(); + this._refreshValue(); + }, + + destroy: function() { + this.element + .removeClass( "ui-progressbar ui-widget ui-widget-content ui-corner-all" ) + .removeAttr( "role" ) + .removeAttr( "aria-valuemin" ) + .removeAttr( "aria-valuemax" ) + .removeAttr( "aria-valuenow" ); + + this.valueDiv.remove(); + + $.Widget.prototype.destroy.apply( this, arguments ); + }, + + value: function( newValue ) { + if ( newValue === undefined ) { + return this._value(); + } + + this._setOption( "value", newValue ); + return this; + }, + + _setOption: function( key, value ) { + if ( key === "value" ) { + this.options.value = value; + this._refreshValue(); + if ( this._value() === this.options.max ) { + this._trigger( "complete" ); + } + } + + $.Widget.prototype._setOption.apply( this, arguments ); + }, + + _value: function() { + var val = this.options.value; + // normalize invalid value + if ( typeof val !== "number" ) { + val = 0; + } + return Math.min( this.options.max, Math.max( this.min, val ) ); + }, + + _percentage: function() { + return 100 * this._value() / this.options.max; + }, + + _refreshValue: function() { + var value = this.value(); + var percentage = this._percentage(); + + if ( this.oldValue !== value ) { + this.oldValue = value; + this._trigger( "change" ); + } + + this.valueDiv + .toggle( value > this.min ) + .toggleClass( "ui-corner-right", value === this.options.max ) + .width( percentage.toFixed(0) + "%" ); + this.element.attr( "aria-valuenow", value ); + } +}); + +$.extend( $.ui.progressbar, { + version: "1.8.23" +}); + +})( jQuery ); + +(function( $, undefined ) { + +// number of pages in a slider +// (how many times can you page up/down to go through the whole range) +var numPages = 5; + +$.widget( "ui.slider", $.ui.mouse, { + + widgetEventPrefix: "slide", + + options: { + animate: false, + distance: 0, + max: 100, + min: 0, + orientation: "horizontal", + range: false, + step: 1, + value: 0, + values: null + }, + + _create: function() { + var self = this, + o = this.options, + existingHandles = this.element.find( ".ui-slider-handle" ).addClass( "ui-state-default ui-corner-all" ), + handle = "", + handleCount = ( o.values && o.values.length ) || 1, + handles = []; + + this._keySliding = false; + this._mouseSliding = false; + this._animateOff = true; + this._handleIndex = null; + this._detectOrientation(); + this._mouseInit(); + + this.element + .addClass( "ui-slider" + + " ui-slider-" + this.orientation + + " ui-widget" + + " ui-widget-content" + + " ui-corner-all" + + ( o.disabled ? " ui-slider-disabled ui-disabled" : "" ) ); + + this.range = $([]); + + if ( o.range ) { + if ( o.range === true ) { + if ( !o.values ) { + o.values = [ this._valueMin(), this._valueMin() ]; + } + if ( o.values.length && o.values.length !== 2 ) { + o.values = [ o.values[0], o.values[0] ]; + } + } + + this.range = $( "
" ) + .appendTo( this.element ) + .addClass( "ui-slider-range" + + // note: this isn't the most fittingly semantic framework class for this element, + // but worked best visually with a variety of themes + " ui-widget-header" + + ( ( o.range === "min" || o.range === "max" ) ? " ui-slider-range-" + o.range : "" ) ); + } + + for ( var i = existingHandles.length; i < handleCount; i += 1 ) { + handles.push( handle ); + } + + this.handles = existingHandles.add( $( handles.join( "" ) ).appendTo( self.element ) ); + + this.handle = this.handles.eq( 0 ); + + this.handles.add( this.range ).filter( "a" ) + .click(function( event ) { + event.preventDefault(); + }) + .hover(function() { + if ( !o.disabled ) { + $( this ).addClass( "ui-state-hover" ); + } + }, function() { + $( this ).removeClass( "ui-state-hover" ); + }) + .focus(function() { + if ( !o.disabled ) { + $( ".ui-slider .ui-state-focus" ).removeClass( "ui-state-focus" ); + $( this ).addClass( "ui-state-focus" ); + } else { + $( this ).blur(); + } + }) + .blur(function() { + $( this ).removeClass( "ui-state-focus" ); + }); + + this.handles.each(function( i ) { + $( this ).data( "index.ui-slider-handle", i ); + }); + + this.handles + .keydown(function( event ) { + var index = $( this ).data( "index.ui-slider-handle" ), + allowed, + curVal, + newVal, + step; + + if ( self.options.disabled ) { + return; + } + + switch ( event.keyCode ) { + case $.ui.keyCode.HOME: + case $.ui.keyCode.END: + case $.ui.keyCode.PAGE_UP: + case $.ui.keyCode.PAGE_DOWN: + case $.ui.keyCode.UP: + case $.ui.keyCode.RIGHT: + case $.ui.keyCode.DOWN: + case $.ui.keyCode.LEFT: + event.preventDefault(); + if ( !self._keySliding ) { + self._keySliding = true; + $( this ).addClass( "ui-state-active" ); + allowed = self._start( event, index ); + if ( allowed === false ) { + return; + } + } + break; + } + + step = self.options.step; + if ( self.options.values && self.options.values.length ) { + curVal = newVal = self.values( index ); + } else { + curVal = newVal = self.value(); + } + + switch ( event.keyCode ) { + case $.ui.keyCode.HOME: + newVal = self._valueMin(); + break; + case $.ui.keyCode.END: + newVal = self._valueMax(); + break; + case $.ui.keyCode.PAGE_UP: + newVal = self._trimAlignValue( curVal + ( (self._valueMax() - self._valueMin()) / numPages ) ); + break; + case $.ui.keyCode.PAGE_DOWN: + newVal = self._trimAlignValue( curVal - ( (self._valueMax() - self._valueMin()) / numPages ) ); + break; + case $.ui.keyCode.UP: + case $.ui.keyCode.RIGHT: + if ( curVal === self._valueMax() ) { + return; + } + newVal = self._trimAlignValue( curVal + step ); + break; + case $.ui.keyCode.DOWN: + case $.ui.keyCode.LEFT: + if ( curVal === self._valueMin() ) { + return; + } + newVal = self._trimAlignValue( curVal - step ); + break; + } + + self._slide( event, index, newVal ); + }) + .keyup(function( event ) { + var index = $( this ).data( "index.ui-slider-handle" ); + + if ( self._keySliding ) { + self._keySliding = false; + self._stop( event, index ); + self._change( event, index ); + $( this ).removeClass( "ui-state-active" ); + } + + }); + + this._refreshValue(); + + this._animateOff = false; + }, + + destroy: function() { + this.handles.remove(); + this.range.remove(); + + this.element + .removeClass( "ui-slider" + + " ui-slider-horizontal" + + " ui-slider-vertical" + + " ui-slider-disabled" + + " ui-widget" + + " ui-widget-content" + + " ui-corner-all" ) + .removeData( "slider" ) + .unbind( ".slider" ); + + this._mouseDestroy(); + + return this; + }, + + _mouseCapture: function( event ) { + var o = this.options, + position, + normValue, + distance, + closestHandle, + self, + index, + allowed, + offset, + mouseOverHandle; + + if ( o.disabled ) { + return false; + } + + this.elementSize = { + width: this.element.outerWidth(), + height: this.element.outerHeight() + }; + this.elementOffset = this.element.offset(); + + position = { x: event.pageX, y: event.pageY }; + normValue = this._normValueFromMouse( position ); + distance = this._valueMax() - this._valueMin() + 1; + self = this; + this.handles.each(function( i ) { + var thisDistance = Math.abs( normValue - self.values(i) ); + if ( distance > thisDistance ) { + distance = thisDistance; + closestHandle = $( this ); + index = i; + } + }); + + // workaround for bug #3736 (if both handles of a range are at 0, + // the first is always used as the one with least distance, + // and moving it is obviously prevented by preventing negative ranges) + if( o.range === true && this.values(1) === o.min ) { + index += 1; + closestHandle = $( this.handles[index] ); + } + + allowed = this._start( event, index ); + if ( allowed === false ) { + return false; + } + this._mouseSliding = true; + + self._handleIndex = index; + + closestHandle + .addClass( "ui-state-active" ) + .focus(); + + offset = closestHandle.offset(); + mouseOverHandle = !$( event.target ).parents().andSelf().is( ".ui-slider-handle" ); + this._clickOffset = mouseOverHandle ? { left: 0, top: 0 } : { + left: event.pageX - offset.left - ( closestHandle.width() / 2 ), + top: event.pageY - offset.top - + ( closestHandle.height() / 2 ) - + ( parseInt( closestHandle.css("borderTopWidth"), 10 ) || 0 ) - + ( parseInt( closestHandle.css("borderBottomWidth"), 10 ) || 0) + + ( parseInt( closestHandle.css("marginTop"), 10 ) || 0) + }; + + if ( !this.handles.hasClass( "ui-state-hover" ) ) { + this._slide( event, index, normValue ); + } + this._animateOff = true; + return true; + }, + + _mouseStart: function( event ) { + return true; + }, + + _mouseDrag: function( event ) { + var position = { x: event.pageX, y: event.pageY }, + normValue = this._normValueFromMouse( position ); + + this._slide( event, this._handleIndex, normValue ); + + return false; + }, + + _mouseStop: function( event ) { + this.handles.removeClass( "ui-state-active" ); + this._mouseSliding = false; + + this._stop( event, this._handleIndex ); + this._change( event, this._handleIndex ); + + this._handleIndex = null; + this._clickOffset = null; + this._animateOff = false; + + return false; + }, + + _detectOrientation: function() { + this.orientation = ( this.options.orientation === "vertical" ) ? "vertical" : "horizontal"; + }, + + _normValueFromMouse: function( position ) { + var pixelTotal, + pixelMouse, + percentMouse, + valueTotal, + valueMouse; + + if ( this.orientation === "horizontal" ) { + pixelTotal = this.elementSize.width; + pixelMouse = position.x - this.elementOffset.left - ( this._clickOffset ? this._clickOffset.left : 0 ); + } else { + pixelTotal = this.elementSize.height; + pixelMouse = position.y - this.elementOffset.top - ( this._clickOffset ? this._clickOffset.top : 0 ); + } + + percentMouse = ( pixelMouse / pixelTotal ); + if ( percentMouse > 1 ) { + percentMouse = 1; + } + if ( percentMouse < 0 ) { + percentMouse = 0; + } + if ( this.orientation === "vertical" ) { + percentMouse = 1 - percentMouse; + } + + valueTotal = this._valueMax() - this._valueMin(); + valueMouse = this._valueMin() + percentMouse * valueTotal; + + return this._trimAlignValue( valueMouse ); + }, + + _start: function( event, index ) { + var uiHash = { + handle: this.handles[ index ], + value: this.value() + }; + if ( this.options.values && this.options.values.length ) { + uiHash.value = this.values( index ); + uiHash.values = this.values(); + } + return this._trigger( "start", event, uiHash ); + }, + + _slide: function( event, index, newVal ) { + var otherVal, + newValues, + allowed; + + if ( this.options.values && this.options.values.length ) { + otherVal = this.values( index ? 0 : 1 ); + + if ( ( this.options.values.length === 2 && this.options.range === true ) && + ( ( index === 0 && newVal > otherVal) || ( index === 1 && newVal < otherVal ) ) + ) { + newVal = otherVal; + } + + if ( newVal !== this.values( index ) ) { + newValues = this.values(); + newValues[ index ] = newVal; + // A slide can be canceled by returning false from the slide callback + allowed = this._trigger( "slide", event, { + handle: this.handles[ index ], + value: newVal, + values: newValues + } ); + otherVal = this.values( index ? 0 : 1 ); + if ( allowed !== false ) { + this.values( index, newVal, true ); + } + } + } else { + if ( newVal !== this.value() ) { + // A slide can be canceled by returning false from the slide callback + allowed = this._trigger( "slide", event, { + handle: this.handles[ index ], + value: newVal + } ); + if ( allowed !== false ) { + this.value( newVal ); + } + } + } + }, + + _stop: function( event, index ) { + var uiHash = { + handle: this.handles[ index ], + value: this.value() + }; + if ( this.options.values && this.options.values.length ) { + uiHash.value = this.values( index ); + uiHash.values = this.values(); + } + + this._trigger( "stop", event, uiHash ); + }, + + _change: function( event, index ) { + if ( !this._keySliding && !this._mouseSliding ) { + var uiHash = { + handle: this.handles[ index ], + value: this.value() + }; + if ( this.options.values && this.options.values.length ) { + uiHash.value = this.values( index ); + uiHash.values = this.values(); + } + + this._trigger( "change", event, uiHash ); + } + }, + + value: function( newValue ) { + if ( arguments.length ) { + this.options.value = this._trimAlignValue( newValue ); + this._refreshValue(); + this._change( null, 0 ); + return; + } + + return this._value(); + }, + + values: function( index, newValue ) { + var vals, + newValues, + i; + + if ( arguments.length > 1 ) { + this.options.values[ index ] = this._trimAlignValue( newValue ); + this._refreshValue(); + this._change( null, index ); + return; + } + + if ( arguments.length ) { + if ( $.isArray( arguments[ 0 ] ) ) { + vals = this.options.values; + newValues = arguments[ 0 ]; + for ( i = 0; i < vals.length; i += 1 ) { + vals[ i ] = this._trimAlignValue( newValues[ i ] ); + this._change( null, i ); + } + this._refreshValue(); + } else { + if ( this.options.values && this.options.values.length ) { + return this._values( index ); + } else { + return this.value(); + } + } + } else { + return this._values(); + } + }, + + _setOption: function( key, value ) { + var i, + valsLength = 0; + + if ( $.isArray( this.options.values ) ) { + valsLength = this.options.values.length; + } + + $.Widget.prototype._setOption.apply( this, arguments ); + + switch ( key ) { + case "disabled": + if ( value ) { + this.handles.filter( ".ui-state-focus" ).blur(); + this.handles.removeClass( "ui-state-hover" ); + this.handles.propAttr( "disabled", true ); + this.element.addClass( "ui-disabled" ); + } else { + this.handles.propAttr( "disabled", false ); + this.element.removeClass( "ui-disabled" ); + } + break; + case "orientation": + this._detectOrientation(); + this.element + .removeClass( "ui-slider-horizontal ui-slider-vertical" ) + .addClass( "ui-slider-" + this.orientation ); + this._refreshValue(); + break; + case "value": + this._animateOff = true; + this._refreshValue(); + this._change( null, 0 ); + this._animateOff = false; + break; + case "values": + this._animateOff = true; + this._refreshValue(); + for ( i = 0; i < valsLength; i += 1 ) { + this._change( null, i ); + } + this._animateOff = false; + break; + } + }, + + //internal value getter + // _value() returns value trimmed by min and max, aligned by step + _value: function() { + var val = this.options.value; + val = this._trimAlignValue( val ); + + return val; + }, + + //internal values getter + // _values() returns array of values trimmed by min and max, aligned by step + // _values( index ) returns single value trimmed by min and max, aligned by step + _values: function( index ) { + var val, + vals, + i; + + if ( arguments.length ) { + val = this.options.values[ index ]; + val = this._trimAlignValue( val ); + + return val; + } else { + // .slice() creates a copy of the array + // this copy gets trimmed by min and max and then returned + vals = this.options.values.slice(); + for ( i = 0; i < vals.length; i+= 1) { + vals[ i ] = this._trimAlignValue( vals[ i ] ); + } + + return vals; + } + }, + + // returns the step-aligned value that val is closest to, between (inclusive) min and max + _trimAlignValue: function( val ) { + if ( val <= this._valueMin() ) { + return this._valueMin(); + } + if ( val >= this._valueMax() ) { + return this._valueMax(); + } + var step = ( this.options.step > 0 ) ? this.options.step : 1, + valModStep = (val - this._valueMin()) % step, + alignValue = val - valModStep; + + if ( Math.abs(valModStep) * 2 >= step ) { + alignValue += ( valModStep > 0 ) ? step : ( -step ); + } + + // Since JavaScript has problems with large floats, round + // the final value to 5 digits after the decimal point (see #4124) + return parseFloat( alignValue.toFixed(5) ); + }, + + _valueMin: function() { + return this.options.min; + }, + + _valueMax: function() { + return this.options.max; + }, + + _refreshValue: function() { + var oRange = this.options.range, + o = this.options, + self = this, + animate = ( !this._animateOff ) ? o.animate : false, + valPercent, + _set = {}, + lastValPercent, + value, + valueMin, + valueMax; + + if ( this.options.values && this.options.values.length ) { + this.handles.each(function( i, j ) { + valPercent = ( self.values(i) - self._valueMin() ) / ( self._valueMax() - self._valueMin() ) * 100; + _set[ self.orientation === "horizontal" ? "left" : "bottom" ] = valPercent + "%"; + $( this ).stop( 1, 1 )[ animate ? "animate" : "css" ]( _set, o.animate ); + if ( self.options.range === true ) { + if ( self.orientation === "horizontal" ) { + if ( i === 0 ) { + self.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { left: valPercent + "%" }, o.animate ); + } + if ( i === 1 ) { + self.range[ animate ? "animate" : "css" ]( { width: ( valPercent - lastValPercent ) + "%" }, { queue: false, duration: o.animate } ); + } + } else { + if ( i === 0 ) { + self.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { bottom: ( valPercent ) + "%" }, o.animate ); + } + if ( i === 1 ) { + self.range[ animate ? "animate" : "css" ]( { height: ( valPercent - lastValPercent ) + "%" }, { queue: false, duration: o.animate } ); + } + } + } + lastValPercent = valPercent; + }); + } else { + value = this.value(); + valueMin = this._valueMin(); + valueMax = this._valueMax(); + valPercent = ( valueMax !== valueMin ) ? + ( value - valueMin ) / ( valueMax - valueMin ) * 100 : + 0; + _set[ self.orientation === "horizontal" ? "left" : "bottom" ] = valPercent + "%"; + this.handle.stop( 1, 1 )[ animate ? "animate" : "css" ]( _set, o.animate ); + + if ( oRange === "min" && this.orientation === "horizontal" ) { + this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { width: valPercent + "%" }, o.animate ); + } + if ( oRange === "max" && this.orientation === "horizontal" ) { + this.range[ animate ? "animate" : "css" ]( { width: ( 100 - valPercent ) + "%" }, { queue: false, duration: o.animate } ); + } + if ( oRange === "min" && this.orientation === "vertical" ) { + this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { height: valPercent + "%" }, o.animate ); + } + if ( oRange === "max" && this.orientation === "vertical" ) { + this.range[ animate ? "animate" : "css" ]( { height: ( 100 - valPercent ) + "%" }, { queue: false, duration: o.animate } ); + } + } + } + +}); + +$.extend( $.ui.slider, { + version: "1.8.23" +}); + +}(jQuery)); + +(function( $, undefined ) { + +var tabId = 0, + listId = 0; + +function getNextTabId() { + return ++tabId; +} + +function getNextListId() { + return ++listId; +} + +$.widget( "ui.tabs", { + options: { + add: null, + ajaxOptions: null, + cache: false, + cookie: null, // e.g. { expires: 7, path: '/', domain: 'jquery.com', secure: true } + collapsible: false, + disable: null, + disabled: [], + enable: null, + event: "click", + fx: null, // e.g. { height: 'toggle', opacity: 'toggle', duration: 200 } + idPrefix: "ui-tabs-", + load: null, + panelTemplate: "
", + remove: null, + select: null, + show: null, + spinner: "Loading…", + tabTemplate: "
  • #{label}
  • " + }, + + _create: function() { + this._tabify( true ); + }, + + _setOption: function( key, value ) { + if ( key == "selected" ) { + if (this.options.collapsible && value == this.options.selected ) { + return; + } + this.select( value ); + } else { + this.options[ key ] = value; + this._tabify(); + } + }, + + _tabId: function( a ) { + return a.title && a.title.replace( /\s/g, "_" ).replace( /[^\w\u00c0-\uFFFF-]/g, "" ) || + this.options.idPrefix + getNextTabId(); + }, + + _sanitizeSelector: function( hash ) { + // we need this because an id may contain a ":" + return hash.replace( /:/g, "\\:" ); + }, + + _cookie: function() { + var cookie = this.cookie || + ( this.cookie = this.options.cookie.name || "ui-tabs-" + getNextListId() ); + return $.cookie.apply( null, [ cookie ].concat( $.makeArray( arguments ) ) ); + }, + + _ui: function( tab, panel ) { + return { + tab: tab, + panel: panel, + index: this.anchors.index( tab ) + }; + }, + + _cleanup: function() { + // restore all former loading tabs labels + this.lis.filter( ".ui-state-processing" ) + .removeClass( "ui-state-processing" ) + .find( "span:data(label.tabs)" ) + .each(function() { + var el = $( this ); + el.html( el.data( "label.tabs" ) ).removeData( "label.tabs" ); + }); + }, + + _tabify: function( init ) { + var self = this, + o = this.options, + fragmentId = /^#.+/; // Safari 2 reports '#' for an empty hash + + this.list = this.element.find( "ol,ul" ).eq( 0 ); + this.lis = $( " > li:has(a[href])", this.list ); + this.anchors = this.lis.map(function() { + return $( "a", this )[ 0 ]; + }); + this.panels = $( [] ); + + this.anchors.each(function( i, a ) { + var href = $( a ).attr( "href" ); + // For dynamically created HTML that contains a hash as href IE < 8 expands + // such href to the full page url with hash and then misinterprets tab as ajax. + // Same consideration applies for an added tab with a fragment identifier + // since a[href=#fragment-identifier] does unexpectedly not match. + // Thus normalize href attribute... + var hrefBase = href.split( "#" )[ 0 ], + baseEl; + if ( hrefBase && ( hrefBase === location.toString().split( "#" )[ 0 ] || + ( baseEl = $( "base" )[ 0 ]) && hrefBase === baseEl.href ) ) { + href = a.hash; + a.href = href; + } + + // inline tab + if ( fragmentId.test( href ) ) { + self.panels = self.panels.add( self.element.find( self._sanitizeSelector( href ) ) ); + // remote tab + // prevent loading the page itself if href is just "#" + } else if ( href && href !== "#" ) { + // required for restore on destroy + $.data( a, "href.tabs", href ); + + // TODO until #3808 is fixed strip fragment identifier from url + // (IE fails to load from such url) + $.data( a, "load.tabs", href.replace( /#.*$/, "" ) ); + + var id = self._tabId( a ); + a.href = "#" + id; + var $panel = self.element.find( "#" + id ); + if ( !$panel.length ) { + $panel = $( o.panelTemplate ) + .attr( "id", id ) + .addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" ) + .insertAfter( self.panels[ i - 1 ] || self.list ); + $panel.data( "destroy.tabs", true ); + } + self.panels = self.panels.add( $panel ); + // invalid tab href + } else { + o.disabled.push( i ); + } + }); + + // initialization from scratch + if ( init ) { + // attach necessary classes for styling + this.element.addClass( "ui-tabs ui-widget ui-widget-content ui-corner-all" ); + this.list.addClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" ); + this.lis.addClass( "ui-state-default ui-corner-top" ); + this.panels.addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" ); + + // Selected tab + // use "selected" option or try to retrieve: + // 1. from fragment identifier in url + // 2. from cookie + // 3. from selected class attribute on
  • + if ( o.selected === undefined ) { + if ( location.hash ) { + this.anchors.each(function( i, a ) { + if ( a.hash == location.hash ) { + o.selected = i; + return false; + } + }); + } + if ( typeof o.selected !== "number" && o.cookie ) { + o.selected = parseInt( self._cookie(), 10 ); + } + if ( typeof o.selected !== "number" && this.lis.filter( ".ui-tabs-selected" ).length ) { + o.selected = this.lis.index( this.lis.filter( ".ui-tabs-selected" ) ); + } + o.selected = o.selected || ( this.lis.length ? 0 : -1 ); + } else if ( o.selected === null ) { // usage of null is deprecated, TODO remove in next release + o.selected = -1; + } + + // sanity check - default to first tab... + o.selected = ( ( o.selected >= 0 && this.anchors[ o.selected ] ) || o.selected < 0 ) + ? o.selected + : 0; + + // Take disabling tabs via class attribute from HTML + // into account and update option properly. + // A selected tab cannot become disabled. + o.disabled = $.unique( o.disabled.concat( + $.map( this.lis.filter( ".ui-state-disabled" ), function( n, i ) { + return self.lis.index( n ); + }) + ) ).sort(); + + if ( $.inArray( o.selected, o.disabled ) != -1 ) { + o.disabled.splice( $.inArray( o.selected, o.disabled ), 1 ); + } + + // highlight selected tab + this.panels.addClass( "ui-tabs-hide" ); + this.lis.removeClass( "ui-tabs-selected ui-state-active" ); + // check for length avoids error when initializing empty list + if ( o.selected >= 0 && this.anchors.length ) { + self.element.find( self._sanitizeSelector( self.anchors[ o.selected ].hash ) ).removeClass( "ui-tabs-hide" ); + this.lis.eq( o.selected ).addClass( "ui-tabs-selected ui-state-active" ); + + // seems to be expected behavior that the show callback is fired + self.element.queue( "tabs", function() { + self._trigger( "show", null, + self._ui( self.anchors[ o.selected ], self.element.find( self._sanitizeSelector( self.anchors[ o.selected ].hash ) )[ 0 ] ) ); + }); + + this.load( o.selected ); + } + + // clean up to avoid memory leaks in certain versions of IE 6 + // TODO: namespace this event + $( window ).bind( "unload", function() { + self.lis.add( self.anchors ).unbind( ".tabs" ); + self.lis = self.anchors = self.panels = null; + }); + // update selected after add/remove + } else { + o.selected = this.lis.index( this.lis.filter( ".ui-tabs-selected" ) ); + } + + // update collapsible + // TODO: use .toggleClass() + this.element[ o.collapsible ? "addClass" : "removeClass" ]( "ui-tabs-collapsible" ); + + // set or update cookie after init and add/remove respectively + if ( o.cookie ) { + this._cookie( o.selected, o.cookie ); + } + + // disable tabs + for ( var i = 0, li; ( li = this.lis[ i ] ); i++ ) { + $( li )[ $.inArray( i, o.disabled ) != -1 && + // TODO: use .toggleClass() + !$( li ).hasClass( "ui-tabs-selected" ) ? "addClass" : "removeClass" ]( "ui-state-disabled" ); + } + + // reset cache if switching from cached to not cached + if ( o.cache === false ) { + this.anchors.removeData( "cache.tabs" ); + } + + // remove all handlers before, tabify may run on existing tabs after add or option change + this.lis.add( this.anchors ).unbind( ".tabs" ); + + if ( o.event !== "mouseover" ) { + var addState = function( state, el ) { + if ( el.is( ":not(.ui-state-disabled)" ) ) { + el.addClass( "ui-state-" + state ); + } + }; + var removeState = function( state, el ) { + el.removeClass( "ui-state-" + state ); + }; + this.lis.bind( "mouseover.tabs" , function() { + addState( "hover", $( this ) ); + }); + this.lis.bind( "mouseout.tabs", function() { + removeState( "hover", $( this ) ); + }); + this.anchors.bind( "focus.tabs", function() { + addState( "focus", $( this ).closest( "li" ) ); + }); + this.anchors.bind( "blur.tabs", function() { + removeState( "focus", $( this ).closest( "li" ) ); + }); + } + + // set up animations + var hideFx, showFx; + if ( o.fx ) { + if ( $.isArray( o.fx ) ) { + hideFx = o.fx[ 0 ]; + showFx = o.fx[ 1 ]; + } else { + hideFx = showFx = o.fx; + } + } + + // Reset certain styles left over from animation + // and prevent IE's ClearType bug... + function resetStyle( $el, fx ) { + $el.css( "display", "" ); + if ( !$.support.opacity && fx.opacity ) { + $el[ 0 ].style.removeAttribute( "filter" ); + } + } + + // Show a tab... + var showTab = showFx + ? function( clicked, $show ) { + $( clicked ).closest( "li" ).addClass( "ui-tabs-selected ui-state-active" ); + $show.hide().removeClass( "ui-tabs-hide" ) // avoid flicker that way + .animate( showFx, showFx.duration || "normal", function() { + resetStyle( $show, showFx ); + self._trigger( "show", null, self._ui( clicked, $show[ 0 ] ) ); + }); + } + : function( clicked, $show ) { + $( clicked ).closest( "li" ).addClass( "ui-tabs-selected ui-state-active" ); + $show.removeClass( "ui-tabs-hide" ); + self._trigger( "show", null, self._ui( clicked, $show[ 0 ] ) ); + }; + + // Hide a tab, $show is optional... + var hideTab = hideFx + ? function( clicked, $hide ) { + $hide.animate( hideFx, hideFx.duration || "normal", function() { + self.lis.removeClass( "ui-tabs-selected ui-state-active" ); + $hide.addClass( "ui-tabs-hide" ); + resetStyle( $hide, hideFx ); + self.element.dequeue( "tabs" ); + }); + } + : function( clicked, $hide, $show ) { + self.lis.removeClass( "ui-tabs-selected ui-state-active" ); + $hide.addClass( "ui-tabs-hide" ); + self.element.dequeue( "tabs" ); + }; + + // attach tab event handler, unbind to avoid duplicates from former tabifying... + this.anchors.bind( o.event + ".tabs", function() { + var el = this, + $li = $(el).closest( "li" ), + $hide = self.panels.filter( ":not(.ui-tabs-hide)" ), + $show = self.element.find( self._sanitizeSelector( el.hash ) ); + + // If tab is already selected and not collapsible or tab disabled or + // or is already loading or click callback returns false stop here. + // Check if click handler returns false last so that it is not executed + // for a disabled or loading tab! + if ( ( $li.hasClass( "ui-tabs-selected" ) && !o.collapsible) || + $li.hasClass( "ui-state-disabled" ) || + $li.hasClass( "ui-state-processing" ) || + self.panels.filter( ":animated" ).length || + self._trigger( "select", null, self._ui( this, $show[ 0 ] ) ) === false ) { + this.blur(); + return false; + } + + o.selected = self.anchors.index( this ); + + self.abort(); + + // if tab may be closed + if ( o.collapsible ) { + if ( $li.hasClass( "ui-tabs-selected" ) ) { + o.selected = -1; + + if ( o.cookie ) { + self._cookie( o.selected, o.cookie ); + } + + self.element.queue( "tabs", function() { + hideTab( el, $hide ); + }).dequeue( "tabs" ); + + this.blur(); + return false; + } else if ( !$hide.length ) { + if ( o.cookie ) { + self._cookie( o.selected, o.cookie ); + } + + self.element.queue( "tabs", function() { + showTab( el, $show ); + }); + + // TODO make passing in node possible, see also http://dev.jqueryui.com/ticket/3171 + self.load( self.anchors.index( this ) ); + + this.blur(); + return false; + } + } + + if ( o.cookie ) { + self._cookie( o.selected, o.cookie ); + } + + // show new tab + if ( $show.length ) { + + if ( $hide.length ) { + self.element.queue( "tabs", function() { + hideTab( el, $hide ); + }); + } + self.element.queue( "tabs", function() { + showTab( el, $show ); + }); + + self.load( self.anchors.index( this ) ); + } else { + throw "jQuery UI Tabs: Mismatching fragment identifier."; + } + + // Prevent IE from keeping other link focussed when using the back button + // and remove dotted border from clicked link. This is controlled via CSS + // in modern browsers; blur() removes focus from address bar in Firefox + // which can become a usability and annoying problem with tabs('rotate'). + if ( $.browser.msie ) { + this.blur(); + } + }); + + // disable click in any case + this.anchors.bind( "click.tabs", function(){ + return false; + }); + }, + + _getIndex: function( index ) { + // meta-function to give users option to provide a href string instead of a numerical index. + // also sanitizes numerical indexes to valid values. + if ( typeof index == "string" ) { + index = this.anchors.index( this.anchors.filter( "[href$='" + index + "']" ) ); + } + + return index; + }, + + destroy: function() { + var o = this.options; + + this.abort(); + + this.element + .unbind( ".tabs" ) + .removeClass( "ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible" ) + .removeData( "tabs" ); + + this.list.removeClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" ); + + this.anchors.each(function() { + var href = $.data( this, "href.tabs" ); + if ( href ) { + this.href = href; + } + var $this = $( this ).unbind( ".tabs" ); + $.each( [ "href", "load", "cache" ], function( i, prefix ) { + $this.removeData( prefix + ".tabs" ); + }); + }); + + this.lis.unbind( ".tabs" ).add( this.panels ).each(function() { + if ( $.data( this, "destroy.tabs" ) ) { + $( this ).remove(); + } else { + $( this ).removeClass([ + "ui-state-default", + "ui-corner-top", + "ui-tabs-selected", + "ui-state-active", + "ui-state-hover", + "ui-state-focus", + "ui-state-disabled", + "ui-tabs-panel", + "ui-widget-content", + "ui-corner-bottom", + "ui-tabs-hide" + ].join( " " ) ); + } + }); + + if ( o.cookie ) { + this._cookie( null, o.cookie ); + } + + return this; + }, + + add: function( url, label, index ) { + if ( index === undefined ) { + index = this.anchors.length; + } + + var self = this, + o = this.options, + $li = $( o.tabTemplate.replace( /#\{href\}/g, url ).replace( /#\{label\}/g, label ) ), + id = !url.indexOf( "#" ) ? url.replace( "#", "" ) : this._tabId( $( "a", $li )[ 0 ] ); + + $li.addClass( "ui-state-default ui-corner-top" ).data( "destroy.tabs", true ); + + // try to find an existing element before creating a new one + var $panel = self.element.find( "#" + id ); + if ( !$panel.length ) { + $panel = $( o.panelTemplate ) + .attr( "id", id ) + .data( "destroy.tabs", true ); + } + $panel.addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide" ); + + if ( index >= this.lis.length ) { + $li.appendTo( this.list ); + $panel.appendTo( this.list[ 0 ].parentNode ); + } else { + $li.insertBefore( this.lis[ index ] ); + $panel.insertBefore( this.panels[ index ] ); + } + + o.disabled = $.map( o.disabled, function( n, i ) { + return n >= index ? ++n : n; + }); + + this._tabify(); + + if ( this.anchors.length == 1 ) { + o.selected = 0; + $li.addClass( "ui-tabs-selected ui-state-active" ); + $panel.removeClass( "ui-tabs-hide" ); + this.element.queue( "tabs", function() { + self._trigger( "show", null, self._ui( self.anchors[ 0 ], self.panels[ 0 ] ) ); + }); + + this.load( 0 ); + } + + this._trigger( "add", null, this._ui( this.anchors[ index ], this.panels[ index ] ) ); + return this; + }, + + remove: function( index ) { + index = this._getIndex( index ); + var o = this.options, + $li = this.lis.eq( index ).remove(), + $panel = this.panels.eq( index ).remove(); + + // If selected tab was removed focus tab to the right or + // in case the last tab was removed the tab to the left. + if ( $li.hasClass( "ui-tabs-selected" ) && this.anchors.length > 1) { + this.select( index + ( index + 1 < this.anchors.length ? 1 : -1 ) ); + } + + o.disabled = $.map( + $.grep( o.disabled, function(n, i) { + return n != index; + }), + function( n, i ) { + return n >= index ? --n : n; + }); + + this._tabify(); + + this._trigger( "remove", null, this._ui( $li.find( "a" )[ 0 ], $panel[ 0 ] ) ); + return this; + }, + + enable: function( index ) { + index = this._getIndex( index ); + var o = this.options; + if ( $.inArray( index, o.disabled ) == -1 ) { + return; + } + + this.lis.eq( index ).removeClass( "ui-state-disabled" ); + o.disabled = $.grep( o.disabled, function( n, i ) { + return n != index; + }); + + this._trigger( "enable", null, this._ui( this.anchors[ index ], this.panels[ index ] ) ); + return this; + }, + + disable: function( index ) { + index = this._getIndex( index ); + var self = this, o = this.options; + // cannot disable already selected tab + if ( index != o.selected ) { + this.lis.eq( index ).addClass( "ui-state-disabled" ); + + o.disabled.push( index ); + o.disabled.sort(); + + this._trigger( "disable", null, this._ui( this.anchors[ index ], this.panels[ index ] ) ); + } + + return this; + }, + + select: function( index ) { + index = this._getIndex( index ); + if ( index == -1 ) { + if ( this.options.collapsible && this.options.selected != -1 ) { + index = this.options.selected; + } else { + return this; + } + } + this.anchors.eq( index ).trigger( this.options.event + ".tabs" ); + return this; + }, + + load: function( index ) { + index = this._getIndex( index ); + var self = this, + o = this.options, + a = this.anchors.eq( index )[ 0 ], + url = $.data( a, "load.tabs" ); + + this.abort(); + + // not remote or from cache + if ( !url || this.element.queue( "tabs" ).length !== 0 && $.data( a, "cache.tabs" ) ) { + this.element.dequeue( "tabs" ); + return; + } + + // load remote from here on + this.lis.eq( index ).addClass( "ui-state-processing" ); + + if ( o.spinner ) { + var span = $( "span", a ); + span.data( "label.tabs", span.html() ).html( o.spinner ); + } + + this.xhr = $.ajax( $.extend( {}, o.ajaxOptions, { + url: url, + success: function( r, s ) { + self.element.find( self._sanitizeSelector( a.hash ) ).html( r ); + + // take care of tab labels + self._cleanup(); + + if ( o.cache ) { + $.data( a, "cache.tabs", true ); + } + + self._trigger( "load", null, self._ui( self.anchors[ index ], self.panels[ index ] ) ); + try { + o.ajaxOptions.success( r, s ); + } + catch ( e ) {} + }, + error: function( xhr, s, e ) { + // take care of tab labels + self._cleanup(); + + self._trigger( "load", null, self._ui( self.anchors[ index ], self.panels[ index ] ) ); + try { + // Passing index avoid a race condition when this method is + // called after the user has selected another tab. + // Pass the anchor that initiated this request allows + // loadError to manipulate the tab content panel via $(a.hash) + o.ajaxOptions.error( xhr, s, index, a ); + } + catch ( e ) {} + } + } ) ); + + // last, so that load event is fired before show... + self.element.dequeue( "tabs" ); + + return this; + }, + + abort: function() { + // stop possibly running animations + this.element.queue( [] ); + this.panels.stop( false, true ); + + // "tabs" queue must not contain more than two elements, + // which are the callbacks for the latest clicked tab... + this.element.queue( "tabs", this.element.queue( "tabs" ).splice( -2, 2 ) ); + + // terminate pending requests from other tabs + if ( this.xhr ) { + this.xhr.abort(); + delete this.xhr; + } + + // take care of tab labels + this._cleanup(); + return this; + }, + + url: function( index, url ) { + this.anchors.eq( index ).removeData( "cache.tabs" ).data( "load.tabs", url ); + return this; + }, + + length: function() { + return this.anchors.length; + } +}); + +$.extend( $.ui.tabs, { + version: "1.8.23" +}); + +/* + * Tabs Extensions + */ + +/* + * Rotate + */ +$.extend( $.ui.tabs.prototype, { + rotation: null, + rotate: function( ms, continuing ) { + var self = this, + o = this.options; + + var rotate = self._rotate || ( self._rotate = function( e ) { + clearTimeout( self.rotation ); + self.rotation = setTimeout(function() { + var t = o.selected; + self.select( ++t < self.anchors.length ? t : 0 ); + }, ms ); + + if ( e ) { + e.stopPropagation(); + } + }); + + var stop = self._unrotate || ( self._unrotate = !continuing + ? function(e) { + if (e.clientX) { // in case of a true click + self.rotate(null); + } + } + : function( e ) { + rotate(); + }); + + // start rotation + if ( ms ) { + this.element.bind( "tabsshow", rotate ); + this.anchors.bind( o.event + ".tabs", stop ); + rotate(); + // stop rotation + } else { + clearTimeout( self.rotation ); + this.element.unbind( "tabsshow", rotate ); + this.anchors.unbind( o.event + ".tabs", stop ); + delete this._rotate; + delete this._unrotate; + } + + return this; + } +}); + +})( jQuery ); \ No newline at end of file diff --git a/client/js/libs/jquery.bootstrap-growl.js b/client/js/libs/jquery.bootstrap-growl.js new file mode 100644 index 000000000..b396ba82e --- /dev/null +++ b/client/js/libs/jquery.bootstrap-growl.js @@ -0,0 +1,77 @@ +(function() { + var $; + + $ = jQuery; + + $.bootstrapGrowl = function(message, options) { + var $alert, css, offsetAmount; + options = $.extend({}, $.bootstrapGrowl.default_options, options); + $alert = $("
    "); + $alert.attr("class", "bootstrap-growl alert"); + if (options.type) { + $alert.addClass("alert-" + options.type); + } + if (options.allow_dismiss) { + $alert.addClass("alert-dismissible"); + $alert.append(""); + } + $alert.append(message); + if (options.top_offset) { + options.offset = { + from: "top", + amount: options.top_offset + }; + } + offsetAmount = options.offset.amount; + $(".bootstrap-growl").each(function() { + return offsetAmount = Math.max(offsetAmount, parseInt($(this).css(options.offset.from)) + $(this).outerHeight() + options.stackup_spacing); + }); + css = { + "position": (options.ele === "body" ? "fixed" : "absolute"), + "margin": 0, + "z-index": "9999", + "display": "none" + }; + css[options.offset.from] = offsetAmount + "px"; + $alert.css(css); + if (options.width !== "auto") { + $alert.css("width", options.width + "px"); + } + $(options.ele).append($alert); + switch (options.align) { + case "center": + $alert.css({ + "left": "50%", + "margin-left": "-" + ($alert.outerWidth() / 2) + "px" + }); + break; + case "left": + $alert.css("left", "20px"); + break; + default: + $alert.css("right", "20px"); + } + $alert.fadeIn(); + if (options.delay > 0) { + $alert.delay(options.delay).fadeOut(function() { + return $(this).alert("close"); + }); + } + return $alert; + }; + + $.bootstrapGrowl.default_options = { + ele: "body", + type: "info", + offset: { + from: "top", + amount: 20 + }, + align: "right", + width: 250, + delay: 4000, + allow_dismiss: true, + stackup_spacing: 10 + }; + +}).call(this); diff --git a/client/js/libs/jquery.dockmodal.js b/client/js/libs/jquery.dockmodal.js new file mode 100644 index 000000000..6441cbe6b --- /dev/null +++ b/client/js/libs/jquery.dockmodal.js @@ -0,0 +1,446 @@ +/* + * jQuery.dockmodal - jQuery dockable modal dialog widget + * + * Copyright 2014, uxMine + * Dual licensed under the MIT or GPL Version 2 licenses. + * Date: 2/11/2014 + * @author Tarafder Ashek E Elahi + * @version 1.1 + * Depends: + * jquery.js + * + */ + +; +(function ($) { + var defaults = { + width: 750, + height: "65%", + minimizedWidth: 200, + gutter: 10, + poppedOutDistance: "6%", + title: "", + dialogClass: "", + buttons: [], /* id, html, buttonClass, click */ + animationSpeed: 1000, + opacity: 1, + initialState: 'modal', /* "modal", "docked", "minimized" */ + + showClose: true, + showPopout: true, + showMinimize: true, + + create: undefined, + open: undefined, + beforeClose: undefined, + close: undefined, + beforeMinimize: undefined, + minimize: undefined, + beforeRestore: undefined, + restore: undefined, + beforePopout: undefined, + popout: undefined + }; + var dClass = "dockmodal"; + var windowWidth = $(window).width(); + + function setAnimationCSS($this, $el) { + var aniSpeed = $this.options.animationSpeed / 1000; + $el.css({"transition": aniSpeed + "s right, " + aniSpeed + "s left, " + aniSpeed + "s top, " + aniSpeed + "s height, " + aniSpeed + "s width"}); + return true; + } + + function removeAnimationCSS($el) { + $el.css({"transition": "none"}); + return true; + } + + var methods = { + init: function (options) { + + return this.each(function () { + + var $this = $(this); + var data = $this.data('dockmodal'); + $this.options = $.extend({}, defaults, options); + + // If the plugin hasn't been initialized yet + if (!data) { + $this.data('dockmodal', $this); + } else { + $("body").append($this.closest("." + dClass).show()); + //methods.restore.apply($this); + methods.refreshLayout(); + setTimeout(function () { + methods.restore.apply($this); + }, $this.options.animationSpeed); + return; + } + + // create modal + var $body = $("body"); + var $window = $(window); + var $dockModal = $('
    ').addClass(dClass).addClass($this.options.dialogClass); + if ($this.options.initialState == "modal") { + $dockModal.addClass("popped-out"); + } else if ($this.options.initialState == "minimized") { + $dockModal.addClass("minimized"); + } + //$dockModal.width($this.options.width); + $dockModal.height(0); + setAnimationCSS($this, $dockModal); + + // create title + var $dockHeader = $('
    ').addClass(dClass + "-header"); + if ($this.options.showClose) { + $('').appendTo($dockHeader).click(function (e) { + methods.destroy.apply($this); + return false; + }); + } + if ($this.options.showPopout) { + $('').appendTo($dockHeader).click(function (e) { + if ($dockModal.hasClass("popped-out")) { + methods.restore.apply($this); + } else { + methods.popout.apply($this); + } + return false; + }); + } + if ($this.options.showMinimize) { + $('').appendTo($dockHeader).click(function (e) { + if ($dockModal.hasClass("minimized")) { + if ($dockModal.hasClass("popped-out")) { + methods.popout.apply($this); + } else { + methods.restore.apply($this); + } + } else { + methods.minimize.apply($this); + } + return false; + }); + } + if ($this.options.showMinimize && $this.options.showPopout) { + $dockHeader.click(function () { + if ($dockModal.hasClass("minimized")) { + if ($dockModal.hasClass("popped-out")) { + methods.popout.apply($this); + } else { + methods.restore.apply($this); + } + } else { + methods.minimize.apply($this); + } + return false; + }); + } + $dockHeader.append('
    ' + ($this.options.title || $this.attr("title")) + '
    '); + $dockModal.append($dockHeader); + + // create body section + var $placeholder = $('').insertAfter($this); + $this.placeholder = $placeholder; + var $dockBody = $('
    ').addClass(dClass + "-body").append($this); + $dockModal.append($dockBody); + + // create footer + if ($this.options.buttons.length) { + var $dockFooter = $('
    ').addClass(dClass + "-footer"); + var $dockFooterButtonset = $('
    ').addClass(dClass + "-footer-buttonset"); + $dockFooter.append($dockFooterButtonset); + $.each($this.options.buttons, function (indx, el) { + var $btn = $(''); + $btn.attr({ "id": el.id, "class": el.buttonClass }); + $btn.html(el.html); + $btn.click(function (e) { + el.click(e, $this); + return false; + }); + $dockFooterButtonset.append($btn); + }); + $dockModal.append($dockFooter); + } else { + $dockModal.addClass("no-footer"); + } + + // create overlay + var $overlay = $("." + dClass + "-overlay"); + if (!$overlay.length) { + $overlay = $('
    ').addClass(dClass + "-overlay"); + } + + // raise create event + if ($.isFunction($this.options.create)) { + $this.options.create($this); + } + + $body.append($dockModal); + $dockModal.after($overlay); + $dockBody.focus(); + + // raise open event + if ($.isFunction($this.options.open)) { + setTimeout(function () { + $this.options.open($this); + }, $this.options.animationSpeed); + } + + //methods.restore.apply($this); + if ($dockModal.hasClass("minimized")) { + $dockModal.find(".dockmodal-body, .dockmodal-footer").hide(); + methods.minimize.apply($this); + } else { + if ($dockModal.hasClass("popped-out")) { + methods.popout.apply($this); + } else { + methods.restore.apply($this); + } + } + + // attach resize event + // track width, set to window width + $body.data("windowWidth", $window.width()); + + $window.unbind("resize.dockmodal").bind("resize.dockmodal", function () { + // do nothing if the width is the same + // update new width value + if ($window.width() == $body.data("windowWidth")) { + return; + } + + $body.data("windowWidth", $window.width()); + methods.refreshLayout(); + }); + }); + }, + destroy: function () { + return this.each(function () { + + var $this = $(this).data('dockmodal'); + if (!$this) + return; + + // raise beforeClose event + if ($.isFunction($this.options.beforeClose)) { + if ($this.options.beforeClose($this) === false) { + return; + } + } + + try { + var $dockModal = $this.closest("." + dClass); + + if ($dockModal.hasClass("popped-out") && !$dockModal.hasClass("minimized")) { + $dockModal.css({ + "left": "50%", + "right": "50%", + "top": "50%", + "bottom": "50%" + }); + } else { + $dockModal.css({ + "width": "0", + "height": "0" + }); + } + setTimeout(function () { + $this.removeData('dockmodal'); + $this.placeholder.replaceWith($this); + $dockModal.remove(); + $("." + dClass + "-overlay").hide(); + methods.refreshLayout(); + + // raise close event + if ($.isFunction($this.options.close)) { + $this.options.close($this); + } + }, $this.options.animationSpeed); + + } + catch (err) { + alert(err.message); + } + // other destroy routines + + }) + }, + close: function () { + methods.destroy.apply(this); + }, + minimize: function () { + return this.each(function () { + + var $this = $(this).data('dockmodal'); + if (!$this) + return; + + // raise beforeMinimize event + if ($.isFunction($this.options.beforeMinimize)) { + if ($this.options.beforeMinimize($this) === false) { + return; + } + } + + var $dockModal = $this.closest("." + dClass); + var headerHeight = $dockModal.find(".dockmodal-header").outerHeight(); + $dockModal.addClass("minimized").css({ + "width": $this.options.minimizedWidth + "px", + "height": headerHeight + "px", + "left": "auto", + "right": "auto", + "top": "auto", + "bottom": 42 + "px" + }); + setTimeout(function () { + // for safty, hide the body and footer + $dockModal.find(".dockmodal-body, .dockmodal-footer").hide(); + + // raise minimize event + if ($.isFunction($this.options.minimize)) { + $this.options.minimize($this); + } + }, $this.options.animationSpeed); + + $("." + dClass + "-overlay").hide(); + $dockModal.find(".action-minimize").attr("title", "Restore"); + + methods.refreshLayout(); + }) + }, + restore: function () { + return this.each(function () { + + var $this = $(this).data('dockmodal'); + if (!$this) + return; + + // raise beforeRestore event + if ($.isFunction($this.options.beforeRestore)) { + if ($this.options.beforeRestore($this) === false) { + return; + } + } + + var $dockModal = $this.closest("." + dClass); + $dockModal.removeClass("minimized popped-out"); + $dockModal.find(".dockmodal-body, .dockmodal-footer").show(); + $dockModal.css({ + "width": $this.options.width + "px", + "height": $this.options.height, + "left": "auto", + "right": "auto", + "top": "auto", + "bottom": 42 + "px" + }); + + $("." + dClass + "-overlay").hide(); + $dockModal.find(".action-minimize").attr("title", "Minimize"); + $dockModal.find(".action-popout").attr("title", "Pop-out"); + + setTimeout(function () { + // raise restore event + if ($.isFunction($this.options.restore)) { + $this.options.restore($this); + } + }, $this.options.animationSpeed); + + methods.refreshLayout(); + }) + }, + popout: function () { + return this.each(function () { + + var $this = $(this).data('dockmodal'); + if (!$this) + return; + + // raise beforePopout event + if ($.isFunction($this.options.beforePopout)) { + if ($this.options.beforePopout($this) === false) { + return; + } + } + + var $dockModal = $this.closest("." + dClass); + $dockModal.find(".dockmodal-body, .dockmodal-footer").show(); + + // prepare element for animation + removeAnimationCSS($dockModal); + var offset = $dockModal.position(); + var windowWidth = $(window).width(); + $dockModal.css({ + "width": "auto", + "height": "auto", + "left": offset.left + "px", + "right": (windowWidth - offset.left - $dockModal.outerWidth(true)) + "px", + "top": offset.top + "px", + "bottom": 42 + "px" + }); + + setAnimationCSS($this, $dockModal); + setTimeout(function () { + $dockModal.removeClass("minimized").addClass("popped-out").css({ + "width": "auto", + "height": "auto", + "left": $this.options.poppedOutDistance, + "right": $this.options.poppedOutDistance, + "top": $this.options.poppedOutDistance, + "bottom": 42 + "px" + }); + $("." + dClass + "-overlay").show(); + $dockModal.find(".action-popout").attr("title", "Pop-in"); + + methods.refreshLayout(); + }, 10); + + setTimeout(function () { + // raise popout event + if ($.isFunction($this.options.popout)) { + $this.options.popout($this); + } + }, $this.options.animationSpeed); + }); + }, + refreshLayout: function () { + + var right = 0; + var windowWidth = $(window).width(); + + $.each($("." + dClass).toArray().reverse(), function (i, val) { + var $dockModal = $(this); + var $this = $dockModal.find("." + dClass + "-body > div").data("dockmodal"); + + if ($dockModal.hasClass("popped-out") && !$dockModal.hasClass("minimized")) { + return; + } + right += $this.options.gutter; + $dockModal.css({ "right": right + "px" }); + if ($dockModal.hasClass("minimized")) { + right += $this.options.minimizedWidth; + } else { + right += $this.options.width; + } + if (right > windowWidth) { + $dockModal.hide(); + } else { + setTimeout(function () { + $dockModal.show(); + }, $this.options.animationSpeed); + } + }); + } + + }; + + $.fn.dockmodal = function (method) { + if (methods[method]) { + return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); + } else if (typeof method === 'object' || !method) { + return methods.init.apply(this, arguments); + } else { + $.error('Method ' + method + ' does not exist on jQuery.dockmodal'); + } + }; +})(jQuery); diff --git a/client/js/libs/jquery.drawDoughnutChart.js b/client/js/libs/jquery.drawDoughnutChart.js new file mode 100644 index 000000000..0774b6c2c --- /dev/null +++ b/client/js/libs/jquery.drawDoughnutChart.js @@ -0,0 +1,301 @@ +/*! + * jquery.drawDoughnutChart.js + * Version: 0.4(Beta) + * Inspired by Chart.js(http://www.chartjs.org/) + * + * Copyright 2014 hiro + * https://github.com/githiro/drawDoughnutChart + * Released under the MIT license. + * + */ +;(function($, undefined) { + $.fn.drawDoughnutChart = function(data, options) { + var $this = this, + W = $this.width(), + H = $this.height(), + centerX = W/2, + centerY = H/2, + cos = Math.cos, + sin = Math.sin, + PI = Math.PI, + settings = $.extend({ + segmentShowStroke : true, + segmentStrokeColor : "#0C1013", + segmentStrokeWidth : 1, + baseColor: "rgba(0,0,0,0.5)", + baseOffset: 4, + edgeOffset : 10,//offset from edge of $this + percentageInnerCutout : 75, + animation : true, + animationSteps : 90, + animationEasing : "easeInOutExpo", + animateRotate : true, + tipOffsetX: -8, + tipOffsetY: -45, + showTip: true, + showLabel: false, + ratioFont: 1.5, + shortInt: false, + tipClass: "doughnutTip", + summaryClass: "doughnutSummary", + summaryTitle: "", + summaryTitleClass: "doughnutSummaryTitle", + summaryNumberClass: "doughnutSummaryNumber", + beforeDraw: function() { }, + afterDrawed : function() { }, + onPathEnter : function(e,data) { }, + onPathLeave : function(e,data) { } + }, options), + animationOptions = { + linear : function (t) { + return t; + }, + easeInOutExpo: function (t) { + var v = t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t; + return (v>1) ? 1 : v; + } + }, + requestAnimFrame = function() { + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function(callback) { + window.setTimeout(callback, 1000 / 60); + }; + }(); + + settings.beforeDraw.call($this); + + var $svg = $('').appendTo($this), + $paths = [], + easingFunction = animationOptions[settings.animationEasing], + doughnutRadius = Min([H / 2,W / 2]) - settings.edgeOffset, + cutoutRadius = doughnutRadius * (settings.percentageInnerCutout / 100), + segmentTotal = 0; + + //Draw base doughnut + var baseDoughnutRadius = doughnutRadius + settings.baseOffset, + baseCutoutRadius = cutoutRadius - settings.baseOffset; + $(document.createElementNS('http://www.w3.org/2000/svg', 'path')) + .attr({ + "d": getHollowCirclePath(baseDoughnutRadius, baseCutoutRadius), + "fill": settings.baseColor + }) + .appendTo($svg); + + //Set up pie segments wrapper + var $pathGroup = $(document.createElementNS('http://www.w3.org/2000/svg', 'g')); + $pathGroup.attr({opacity: 0}).appendTo($svg); + + //Set up tooltip + if (settings.showTip) { + var $tip = $('
    ').appendTo('body').hide(), + tipW = $tip.width(), + tipH = $tip.height(); + } + + //Set up center text area + var summarySize = (cutoutRadius - (doughnutRadius - cutoutRadius)) * 2, + $summary = $('
    ') + .appendTo($this) + .css({ + width: summarySize + "px", + height: summarySize + "px", + "margin-left": -(summarySize / 2) + "px", + "margin-top": -(summarySize / 2) + "px" + }); + var $summaryTitle = $('

    ' + settings.summaryTitle + '

    ').appendTo($summary); + $summaryTitle.css('font-size', getScaleFontSize( $summaryTitle, settings.summaryTitle )); // In most of case useless + var $summaryNumber = $('

    ').appendTo($summary).css({opacity: 0}); + + for (var i = 0, len = data.length; i < len; i++) { + segmentTotal += data[i].value; + $paths[i] = $(document.createElementNS('http://www.w3.org/2000/svg', 'path')) + .attr({ + "stroke-width": settings.segmentStrokeWidth, + "stroke": settings.segmentStrokeColor, + "fill": data[i].color, + "data-order": i + }) + .appendTo($pathGroup) + .on("mouseenter", pathMouseEnter) + .on("mouseleave", pathMouseLeave) + .on("mousemove", pathMouseMove) + .on("click", pathClick); + } + + //Animation start + animationLoop(drawPieSegments); + + //Functions + function getHollowCirclePath(doughnutRadius, cutoutRadius) { + //Calculate values for the path. + //We needn't calculate startRadius, segmentAngle and endRadius, because base doughnut doesn't animate. + var startRadius = -1.570,// -Math.PI/2 + segmentAngle = 6.2831,// 1 * ((99.9999/100) * (PI*2)), + endRadius = 4.7131,// startRadius + segmentAngle + startX = centerX + cos(startRadius) * doughnutRadius, + startY = centerY + sin(startRadius) * doughnutRadius, + endX2 = centerX + cos(startRadius) * cutoutRadius, + endY2 = centerY + sin(startRadius) * cutoutRadius, + endX = centerX + cos(endRadius) * doughnutRadius, + endY = centerY + sin(endRadius) * doughnutRadius, + startX2 = centerX + cos(endRadius) * cutoutRadius, + startY2 = centerY + sin(endRadius) * cutoutRadius; + var cmd = [ + 'M', startX, startY, + 'A', doughnutRadius, doughnutRadius, 0, 1, 1, endX, endY,//Draw outer circle + 'Z',//Close path + 'M', startX2, startY2,//Move pointer + 'A', cutoutRadius, cutoutRadius, 0, 1, 0, endX2, endY2,//Draw inner circle + 'Z' + ]; + cmd = cmd.join(' '); + return cmd; + }; + function pathMouseEnter(e) { + var order = $(this).data().order; + if (settings.showTip) { + $tip.text(data[order].title + ": " + data[order].value) + .fadeIn(200); + } + if(settings.showLabel) { + $summaryTitle.text(data[order].title).css('font-size', getScaleFontSize( $summaryTitle, data[order].title)); + var tmpNumber = settings.shortInt ? shortKInt(data[order].value) : data[order].value; + $summaryNumber.html(tmpNumber).css('font-size', getScaleFontSize( $summaryNumber, tmpNumber)); + } + settings.onPathEnter.apply($(this),[e,data]); + } + function pathMouseLeave(e) { + if (settings.showTip) $tip.hide(); + if(settings.showLabel) { + $summaryTitle.text(settings.summaryTitle).css('font-size', getScaleFontSize( $summaryTitle, settings.summaryTitle)); + var tmpNumber = settings.shortInt ? shortKInt(segmentTotal) : segmentTotal; + $summaryNumber.html(tmpNumber).css('font-size', getScaleFontSize( $summaryNumber, tmpNumber)); + } + settings.onPathLeave.apply($(this),[e,data]); + } + function pathMouseMove(e) { + if (settings.showTip) { + $tip.css({ + top: e.pageY + settings.tipOffsetY, + left: e.pageX - $tip.width() / 2 + settings.tipOffsetX + }); + } + } + function pathClick(e){ + var order = $(this).data().order; + if (typeof data[order].action != "undefined") + data[order].action(); + } + function drawPieSegments (animationDecimal) { + var startRadius = -PI / 2,//-90 degree + rotateAnimation = 1; + if (settings.animation && settings.animateRotate) rotateAnimation = animationDecimal;//count up between0~1 + + drawDoughnutText(animationDecimal, segmentTotal); + + $pathGroup.attr("opacity", animationDecimal); + + //If data have only one value, we draw hollow circle(#1). + if (data.length === 1 && (4.7122 < (rotateAnimation * ((data[0].value / segmentTotal) * (PI * 2)) + startRadius))) { + $paths[0].attr("d", getHollowCirclePath(doughnutRadius, cutoutRadius)); + return; + } + for (var i = 0, len = data.length; i < len; i++) { + var segmentAngle = rotateAnimation * ((data[i].value / segmentTotal) * (PI * 2)), + endRadius = startRadius + segmentAngle, + largeArc = ((endRadius - startRadius) % (PI * 2)) > PI ? 1 : 0, + startX = centerX + cos(startRadius) * doughnutRadius, + startY = centerY + sin(startRadius) * doughnutRadius, + endX2 = centerX + cos(startRadius) * cutoutRadius, + endY2 = centerY + sin(startRadius) * cutoutRadius, + endX = centerX + cos(endRadius) * doughnutRadius, + endY = centerY + sin(endRadius) * doughnutRadius, + startX2 = centerX + cos(endRadius) * cutoutRadius, + startY2 = centerY + sin(endRadius) * cutoutRadius; + var cmd = [ + 'M', startX, startY,//Move pointer + 'A', doughnutRadius, doughnutRadius, 0, largeArc, 1, endX, endY,//Draw outer arc path + 'L', startX2, startY2,//Draw line path(this line connects outer and innner arc paths) + 'A', cutoutRadius, cutoutRadius, 0, largeArc, 0, endX2, endY2,//Draw inner arc path + 'Z'//Cloth path + ]; + $paths[i].attr("d", cmd.join(' ')); + startRadius += segmentAngle; + } + } + function drawDoughnutText(animationDecimal, segmentTotal) { + $summaryNumber + .css({opacity: animationDecimal}) + .text((segmentTotal * animationDecimal).toFixed(1)); + var tmpNumber = settings.shortInt ? shortKInt(segmentTotal) : segmentTotal; + $summaryNumber.html(tmpNumber).css('font-size', getScaleFontSize( $summaryNumber, tmpNumber)); + } + function animateFrame(cnt, drawData) { + var easeAdjustedAnimationPercent =(settings.animation)? CapValue(easingFunction(cnt), null, 0) : 1; + drawData(easeAdjustedAnimationPercent); + } + function animationLoop(drawData) { + var animFrameAmount = (settings.animation)? 1 / CapValue(settings.animationSteps, Number.MAX_VALUE, 1) : 1, + cnt =(settings.animation)? 0 : 1; + requestAnimFrame(function() { + cnt += animFrameAmount; + animateFrame(cnt, drawData); + if (cnt <= 1) { + requestAnimFrame(arguments.callee); + } else { + settings.afterDrawed.call($this); + } + }); + } + function Max(arr) { + return Math.max.apply(null, arr); + } + function Min(arr) { + return Math.min.apply(null, arr); + } + function isNumber(n) { + return !isNaN(parseFloat(n)) && isFinite(n); + } + function CapValue(valueToCap, maxValue, minValue) { + if (isNumber(maxValue) && valueToCap > maxValue) return maxValue; + if (isNumber(minValue) && valueToCap < minValue) return minValue; + return valueToCap; + } + function shortKInt (int) { + int = int.toString(); + var strlen = int.length; + if(strlen<5) + return int; + if(strlen<8) + return '' + int.substring(0, strlen-3) + 'K'; + return '' + int.substring( 0, strlen-6) + 'M'; + } + function getScaleFontSize(block, newText) { + block.css('font-size', ''); + newText = newText.toString().replace(/(<([^>]+)>)/ig,""); + var newFontSize = block.width() / newText.length * settings.ratioFont; + // Not very good : http://stephensite.net/WordPressSS/2008/02/19/how-to-calculate-the-character-width-accross-fonts-and-points/ + // But best quick way the 1.5 number is to affinate in function of the police + var maxCharForDefaultFont = block.width() - newText.length * block.css('font-size').replace(/px/, '') / settings.ratioFont; + if(maxCharForDefaultFont<0) + return newFontSize+'px'; + else + return ''; + } + /** + function getScaleFontSize(block, newText) { + block.css('font-size', ''); + newText = newText.toString().replace(/(<([^>]+)>)/ig,""); + var newFontSize = block.width() / newText.length; + if(newFontSize options.maxFileSize) || + (options.fileTypes && + !options.fileTypes.test(file.type)) || + !loadImage( + file, + function (img) { + if (img.src) { + data.img = img; + } + dfd.resolveWith(that, [data]); + }, + options + )) { + return data; + } + return dfd.promise(); + }, + + // Resizes the image given as data.canvas or data.img + // and updates data.canvas or data.img with the resized image. + // Also stores the resized image as preview property. + // Accepts the options maxWidth, maxHeight, minWidth, + // minHeight, canvas and crop: + resizeImage: function (data, options) { + if (options.disabled || !(data.canvas || data.img)) { + return data; + } + options = $.extend({canvas: true}, options); + var that = this, + dfd = $.Deferred(), + img = (options.canvas && data.canvas) || data.img, + resolve = function (newImg) { + if (newImg && (newImg.width !== img.width || + newImg.height !== img.height || + options.forceResize)) { + data[newImg.getContext ? 'canvas' : 'img'] = newImg; + } + data.preview = newImg; + dfd.resolveWith(that, [data]); + }, + thumbnail; + if (data.exif) { + if (options.orientation === true) { + options.orientation = data.exif.get('Orientation'); + } + if (options.thumbnail) { + thumbnail = data.exif.get('Thumbnail'); + if (thumbnail) { + loadImage(thumbnail, resolve, options); + return dfd.promise(); + } + } + } + if (img) { + resolve(loadImage.scale(img, options)); + return dfd.promise(); + } + return data; + }, + + // Saves the processed image given as data.canvas + // inplace at data.index of data.files: + saveImage: function (data, options) { + if (!data.canvas || options.disabled) { + return data; + } + var that = this, + file = data.files[data.index], + dfd = $.Deferred(); + if (data.canvas.toBlob) { + data.canvas.toBlob( + function (blob) { + if (!blob.name) { + if (file.type === blob.type) { + blob.name = file.name; + } else if (file.name) { + blob.name = file.name.replace( + /\..+$/, + '.' + blob.type.substr(6) + ); + } + } + // Don't restore invalid meta data: + if (file.type !== blob.type) { + delete data.imageHead; + } + // Store the created blob at the position + // of the original file in the files list: + data.files[data.index] = blob; + dfd.resolveWith(that, [data]); + }, + options.type || file.type, + options.quality + ); + } else { + return data; + } + return dfd.promise(); + }, + + loadImageMetaData: function (data, options) { + if (options.disabled) { + return data; + } + var that = this, + dfd = $.Deferred(); + loadImage.parseMetaData(data.files[data.index], function (result) { + $.extend(data, result); + dfd.resolveWith(that, [data]); + }, options); + return dfd.promise(); + }, + + saveImageMetaData: function (data, options) { + if (!(data.imageHead && data.canvas && + data.canvas.toBlob && !options.disabled)) { + return data; + } + var file = data.files[data.index], + blob = new Blob([ + data.imageHead, + // Resized images always have a head size of 20 bytes, + // including the JPEG marker and a minimal JFIF header: + this._blobSlice.call(file, 20) + ], {type: file.type}); + blob.name = file.name; + data.files[data.index] = blob; + return data; + }, + + // Sets the resized version of the image as a property of the + // file object, must be called after "saveImage": + setImage: function (data, options) { + if (data.preview && !options.disabled) { + data.files[data.index][options.name || 'preview'] = data.preview; + } + return data; + }, + + deleteImageReferences: function (data, options) { + if (!options.disabled) { + delete data.img; + delete data.canvas; + delete data.preview; + delete data.imageHead; + } + return data; + } + + } + + }); + +})); diff --git a/client/js/libs/jquery.fileupload-process.js b/client/js/libs/jquery.fileupload-process.js new file mode 100644 index 000000000..87042c3d5 --- /dev/null +++ b/client/js/libs/jquery.fileupload-process.js @@ -0,0 +1,164 @@ +/* + * jQuery File Upload Processing Plugin 1.2.2 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2012, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/*jslint nomen: true, unparam: true */ +/*global define, window */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + './jquery.fileupload' + ], factory); + } else { + // Browser globals: + factory( + window.jQuery + ); + } +}(function ($) { + 'use strict'; + + var originalAdd = $.blueimp.fileupload.prototype.options.add; + + // The File Upload Processing plugin extends the fileupload widget + // with file processing functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + // The list of processing actions: + processQueue: [ + /* + { + action: 'log', + type: 'debug' + } + */ + ], + add: function (e, data) { + var $this = $(this); + data.process(function () { + return $this.fileupload('process', data); + }); + originalAdd.call(this, e, data); + } + }, + + processActions: { + /* + log: function (data, options) { + console[options.type]( + 'Processing "' + data.files[data.index].name + '"' + ); + } + */ + }, + + _processFile: function (data) { + var that = this, + dfd = $.Deferred().resolveWith(that, [data]), + chain = dfd.promise(); + this._trigger('process', null, data); + $.each(data.processQueue, function (i, settings) { + var func = function (data) { + return that.processActions[settings.action].call( + that, + data, + settings + ); + }; + chain = chain.pipe(func, settings.always && func); + }); + chain + .done(function () { + that._trigger('processdone', null, data); + that._trigger('processalways', null, data); + }) + .fail(function () { + that._trigger('processfail', null, data); + that._trigger('processalways', null, data); + }); + return chain; + }, + + // Replaces the settings of each processQueue item that + // are strings starting with an "@", using the remaining + // substring as key for the option map, + // e.g. "@autoUpload" is replaced with options.autoUpload: + _transformProcessQueue: function (options) { + var processQueue = []; + $.each(options.processQueue, function () { + var settings = {}, + action = this.action, + prefix = this.prefix === true ? action : this.prefix; + $.each(this, function (key, value) { + if ($.type(value) === 'string' && + value.charAt(0) === '@') { + settings[key] = options[ + value.slice(1) || (prefix ? prefix + + key.charAt(0).toUpperCase() + key.slice(1) : key) + ]; + } else { + settings[key] = value; + } + + }); + processQueue.push(settings); + }); + options.processQueue = processQueue; + }, + + // Returns the number of files currently in the processsing queue: + processing: function () { + return this._processing; + }, + + // Processes the files given as files property of the data parameter, + // returns a Promise object that allows to bind callbacks: + process: function (data) { + var that = this, + options = $.extend({}, this.options, data); + if (options.processQueue && options.processQueue.length) { + this._transformProcessQueue(options); + if (this._processing === 0) { + this._trigger('processstart'); + } + $.each(data.files, function (index) { + var opts = index ? $.extend({}, options) : options, + func = function () { + return that._processFile(opts); + }; + opts.index = index; + that._processing += 1; + that._processingQueue = that._processingQueue.pipe(func, func) + .always(function () { + that._processing -= 1; + if (that._processing === 0) { + that._trigger('processstop'); + } + }); + }); + } + return this._processingQueue; + }, + + _create: function () { + this._super(); + this._processing = 0; + this._processingQueue = $.Deferred().resolveWith(this) + .promise(); + } + + }); + +})); diff --git a/client/js/libs/jquery.fileupload-ui.js b/client/js/libs/jquery.fileupload-ui.js new file mode 100644 index 000000000..2360c91f0 --- /dev/null +++ b/client/js/libs/jquery.fileupload-ui.js @@ -0,0 +1,647 @@ +/* + * jQuery File Upload User Interface Plugin 8.8.5 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/*jslint nomen: true, unparam: true, regexp: true */ +/*global define, window, URL, webkitURL, FileReader */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'tmpl', + './jquery.fileupload-image', + './jquery.fileupload-audio', + './jquery.fileupload-video', + './jquery.fileupload-validate' + ], factory); + } else { + // Browser globals: + factory( + window.jQuery, + window.tmpl + ); + } +}(function ($, tmpl, loadImage) { + 'use strict'; + + $.blueimp.fileupload.prototype._specialOptions.push( + 'filesContainer', + 'uploadTemplateId', + 'downloadTemplateId' + ); + + // The UI version extends the file upload widget + // and adds complete user interface interaction: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + // By default, files added to the widget are uploaded as soon + // as the user clicks on the start buttons. To enable automatic + // uploads, set the following option to true: + autoUpload: false, + // The ID of the upload template: + uploadTemplateId: 'template-upload', + // The ID of the download template: + downloadTemplateId: 'template-download', + // The container for the list of files. If undefined, it is set to + // an element with class "files" inside of the widget element: + filesContainer: undefined, + // By default, files are appended to the files container. + // Set the following option to true, to prepend files instead: + prependFiles: false, + // The expected data type of the upload response, sets the dataType + // option of the $.ajax upload requests: + dataType: 'json', + + // Function returning the current number of files, + // used by the maxNumberOfFiles validation: + getNumberOfFiles: function () { + return this.filesContainer.children().length; + }, + + // Callback to retrieve the list of files from the server response: + getFilesFromResponse: function (data) { + if (data.result && $.isArray(data.result.files)) { + return data.result.files; + } + return []; + }, + + // The add callback is invoked as soon as files are added to the fileupload + // widget (via file input selection, drag & drop or add API call). + // See the basic file upload widget for more information: + add: function (e, data) { + var $this = $(this), + that = $this.data('blueimp-fileupload') || + $this.data('fileupload'), + options = that.options, + files = data.files; + data.process(function () { + return $this.fileupload('process', data); + }).always(function () { + data.context = that._renderUpload(files).data('data', data); + that._renderPreviews(data); + options.filesContainer[ + options.prependFiles ? 'prepend' : 'append' + ](data.context); + that._forceReflow(data.context); + that._transition(data.context).done( + function () { + if ((that._trigger('added', e, data) !== false) && + (options.autoUpload || data.autoUpload) && + data.autoUpload !== false && !data.files.error) { + data.submit(); + } + } + ); + }); + }, + // Callback for the start of each file upload request: + send: function (e, data) { + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'); + if (data.context && data.dataType && + data.dataType.substr(0, 6) === 'iframe') { + // Iframe Transport does not support progress events. + // In lack of an indeterminate progress bar, we set + // the progress to 100%, showing the full animated bar: + data.context + .find('.progress').addClass( + !$.support.transition && 'progress-animated' + ) + .attr('aria-valuenow', 100) + .children().first().css( + 'width', + '100%' + ); + } + return that._trigger('sent', e, data); + }, + // Callback for successful uploads: + done: function (e, data) { + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'), + getFilesFromResponse = data.getFilesFromResponse || + that.options.getFilesFromResponse, + files = getFilesFromResponse(data), + template, + deferred; + if (data.context) { + data.context.each(function (index) { + var file = files[index] || + {error: 'Empty file upload result'}; + deferred = that._addFinishedDeferreds(); + that._transition($(this)).done( + function () { + var node = $(this); + template = that._renderDownload([file]) + .replaceAll(node); + that._forceReflow(template); + that._transition(template).done( + function () { + data.context = $(this); + that._trigger('completed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + } + ); + } + ); + }); + } else { + template = that._renderDownload(files)[ + that.options.prependFiles ? 'prependTo' : 'appendTo' + ](that.options.filesContainer); + that._forceReflow(template); + deferred = that._addFinishedDeferreds(); + that._transition(template).done( + function () { + data.context = $(this); + that._trigger('completed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + } + ); + } + }, + // Callback for failed (abort or error) uploads: + fail: function (e, data) { + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'), + template, + deferred; + if (data.context) { + data.context.each(function (index) { + if (data.errorThrown !== 'abort') { + var file = data.files[index]; + file.error = file.error || data.errorThrown || + true; + deferred = that._addFinishedDeferreds(); + that._transition($(this)).done( + function () { + var node = $(this); + template = that._renderDownload([file]) + .replaceAll(node); + that._forceReflow(template); + that._transition(template).done( + function () { + data.context = $(this); + that._trigger('failed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + } + ); + } + ); + } else { + deferred = that._addFinishedDeferreds(); + that._transition($(this)).done( + function () { + $(this).remove(); + that._trigger('failed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + } + ); + } + }); + } else if (data.errorThrown !== 'abort') { + data.context = that._renderUpload(data.files)[ + that.options.prependFiles ? 'prependTo' : 'appendTo' + ](that.options.filesContainer) + .data('data', data); + that._forceReflow(data.context); + deferred = that._addFinishedDeferreds(); + that._transition(data.context).done( + function () { + data.context = $(this); + that._trigger('failed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + } + ); + } else { + that._trigger('failed', e, data); + that._trigger('finished', e, data); + that._addFinishedDeferreds().resolve(); + } + }, + // Callback for upload progress events: + progress: function (e, data) { + var progress = Math.floor(data.loaded / data.total * 100); + if (data.context) { + data.context.each(function () { + $(this).find('.progress') + .attr('aria-valuenow', progress) + .children().first().css( + 'width', + progress + '%' + ); + }); + } + }, + // Callback for global upload progress events: + progressall: function (e, data) { + var $this = $(this), + progress = Math.floor(data.loaded / data.total * 100), + globalProgressNode = $this.find('.fileupload-progress'), + extendedProgressNode = globalProgressNode + .find('.progress-extended'); + if (extendedProgressNode.length) { + extendedProgressNode.html( + ($this.data('blueimp-fileupload') || $this.data('fileupload')) + ._renderExtendedProgress(data) + ); + } + globalProgressNode + .find('.progress') + .attr('aria-valuenow', progress) + .children().first().css( + 'width', + progress + '%' + ); + }, + // Callback for uploads start, equivalent to the global ajaxStart event: + start: function (e) { + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'); + that._resetFinishedDeferreds(); + that._transition($(this).find('.fileupload-progress')).done( + function () { + that._trigger('started', e); + } + ); + }, + // Callback for uploads stop, equivalent to the global ajaxStop event: + stop: function (e) { + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'), + deferred = that._addFinishedDeferreds(); + $.when.apply($, that._getFinishedDeferreds()) + .done(function () { + that._trigger('stopped', e); + }); + that._transition($(this).find('.fileupload-progress')).done( + function () { + $(this).find('.progress') + .attr('aria-valuenow', '0') + .children().first().css('width', '0%'); + $(this).find('.progress-extended').html(' '); + deferred.resolve(); + } + ); + }, + processstart: function () { + $(this).addClass('fileupload-processing'); + }, + processstop: function () { + $(this).removeClass('fileupload-processing'); + }, + // Callback for file deletion: + destroy: function (e, data) { + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'), + removeNode = function () { + that._transition(data.context).done( + function () { + $(this).remove(); + that._trigger('destroyed', e, data); + } + ); + }; + if (data.url) { + $.ajax(data).done(removeNode); + } else { + removeNode(); + } + } + }, + + _resetFinishedDeferreds: function () { + this._finishedUploads = []; + }, + + _addFinishedDeferreds: function (deferred) { + if (!deferred) { + deferred = $.Deferred(); + } + this._finishedUploads.push(deferred); + return deferred; + }, + + _getFinishedDeferreds: function () { + return this._finishedUploads; + }, + + // Link handler, that allows to download files + // by drag & drop of the links to the desktop: + _enableDragToDesktop: function () { + var link = $(this), + url = link.prop('href'), + name = link.prop('download'), + type = 'application/octet-stream'; + link.bind('dragstart', function (e) { + try { + e.originalEvent.dataTransfer.setData( + 'DownloadURL', + [type, name, url].join(':') + ); + } catch (ignore) {} + }); + }, + + _formatFileSize: function (bytes) { + if (typeof bytes !== 'number') { + return ''; + } + if (bytes >= 1000000000) { + return (bytes / 1000000000).toFixed(2) + ' GB'; + } + if (bytes >= 1000000) { + return (bytes / 1000000).toFixed(2) + ' MB'; + } + return (bytes / 1000).toFixed(2) + ' KB'; + }, + + _formatBitrate: function (bits) { + if (typeof bits !== 'number') { + return ''; + } + if (bits >= 1000000000) { + return (bits / 1000000000).toFixed(2) + ' Gbit/s'; + } + if (bits >= 1000000) { + return (bits / 1000000).toFixed(2) + ' Mbit/s'; + } + if (bits >= 1000) { + return (bits / 1000).toFixed(2) + ' kbit/s'; + } + return bits.toFixed(2) + ' bit/s'; + }, + + _formatTime: function (seconds) { + var date = new Date(seconds * 1000), + days = Math.floor(seconds / 86400); + days = days ? days + 'd ' : ''; + return days + + ('0' + date.getUTCHours()).slice(-2) + ':' + + ('0' + date.getUTCMinutes()).slice(-2) + ':' + + ('0' + date.getUTCSeconds()).slice(-2); + }, + + _formatPercentage: function (floatValue) { + return (floatValue * 100).toFixed(2) + ' %'; + }, + + _renderExtendedProgress: function (data) { + return this._formatBitrate(data.bitrate) + ' | ' + + this._formatTime( + (data.total - data.loaded) * 8 / data.bitrate + ) + ' | ' + + this._formatPercentage( + data.loaded / data.total + ) + ' | ' + + this._formatFileSize(data.loaded) + ' / ' + + this._formatFileSize(data.total); + }, + + _renderTemplate: function (func, files) { + if (!func) { + return $(); + } + var result = func({ + files: files, + formatFileSize: this._formatFileSize, + options: this.options + }); + if (result instanceof $) { + return result; + } + return $(this.options.templatesContainer).html(result).children(); + }, + + _renderPreviews: function (data) { + data.context.find('.preview').each(function (index, elm) { + $(elm).append(data.files[index].preview); + }); + }, + + _renderUpload: function (files) { + return this._renderTemplate( + this.options.uploadTemplate, + files + ); + }, + + _renderDownload: function (files) { + return this._renderTemplate( + this.options.downloadTemplate, + files + ).find('a[download]').each(this._enableDragToDesktop).end(); + }, + + _startHandler: function (e) { + e.preventDefault(); + var button = $(e.currentTarget), + template = button.closest('.template-upload'), + data = template.data('data'); + if (data && data.submit && !data.jqXHR && data.submit()) { + button.prop('disabled', true); + } + }, + + _cancelHandler: function (e) { + e.preventDefault(); + var template = $(e.currentTarget) + .closest('.template-upload,.template-download'), + data = template.data('data') || {}; + if (!data.jqXHR) { + data.context = data.context || template; + data.errorThrown = 'abort'; + this._trigger('fail', e, data); + } else { + data.jqXHR.abort(); + } + }, + + _deleteHandler: function (e) { + e.preventDefault(); + var button = $(e.currentTarget); + this._trigger('destroy', e, $.extend({ + context: button.closest('.template-download'), + type: 'DELETE' + }, button.data())); + }, + + _forceReflow: function (node) { + return $.support.transition && node.length && + node[0].offsetWidth; + }, + + _transition: function (node) { + var dfd = $.Deferred(); + if ($.support.transition && node.hasClass('fade') && node.is(':visible')) { + node.bind( + $.support.transition.end, + function (e) { + // Make sure we don't respond to other transitions events + // in the container element, e.g. from button elements: + if (e.target === node[0]) { + node.unbind($.support.transition.end); + dfd.resolveWith(node); + } + } + ).toggleClass('in'); + } else { + node.toggleClass('in'); + dfd.resolveWith(node); + } + return dfd; + }, + + _initButtonBarEventHandlers: function () { + var fileUploadButtonBar = this.element.find('.fileupload-buttonbar'), + filesList = this.options.filesContainer; + this._on(fileUploadButtonBar.find('.start'), { + click: function (e) { + e.preventDefault(); + filesList.find('.start').click(); + } + }); + this._on(fileUploadButtonBar.find('.cancel'), { + click: function (e) { + e.preventDefault(); + filesList.find('.cancel').click(); + } + }); + this._on(fileUploadButtonBar.find('.delete'), { + click: function (e) { + e.preventDefault(); + filesList.find('.toggle:checked') + .closest('.template-download') + .find('.delete').click(); + fileUploadButtonBar.find('.toggle') + .prop('checked', false); + } + }); + this._on(fileUploadButtonBar.find('.toggle'), { + change: function (e) { + filesList.find('.toggle').prop( + 'checked', + $(e.currentTarget).is(':checked') + ); + } + }); + }, + + _destroyButtonBarEventHandlers: function () { + this._off( + this.element.find('.fileupload-buttonbar') + .find('.start, .cancel, .delete'), + 'click' + ); + this._off( + this.element.find('.fileupload-buttonbar .toggle'), + 'change.' + ); + }, + + _initEventHandlers: function () { + this._super(); + this._on(this.options.filesContainer, { + 'click .start': this._startHandler, + 'click .cancel': this._cancelHandler, + 'click .delete': this._deleteHandler + }); + this._initButtonBarEventHandlers(); + }, + + _destroyEventHandlers: function () { + this._destroyButtonBarEventHandlers(); + this._off(this.options.filesContainer, 'click'); + this._super(); + }, + + _enableFileInputButton: function () { + this.element.find('.fileinput-button input') + .prop('disabled', false) + .parent().removeClass('disabled'); + }, + + _disableFileInputButton: function () { + this.element.find('.fileinput-button input') + .prop('disabled', true) + .parent().addClass('disabled'); + }, + + _initTemplates: function () { + var options = this.options; + options.templatesContainer = this.document[0].createElement( + options.filesContainer.prop('nodeName') + ); + if (tmpl) { + if (options.uploadTemplateId) { + options.uploadTemplate = tmpl(options.uploadTemplateId); + } + if (options.downloadTemplateId) { + options.downloadTemplate = tmpl(options.downloadTemplateId); + } + } + }, + + _initFilesContainer: function () { + var options = this.options; + if (options.filesContainer === undefined) { + options.filesContainer = this.element.find('.files'); + } else if (!(options.filesContainer instanceof $)) { + options.filesContainer = $(options.filesContainer); + } + }, + + _initSpecialOptions: function () { + this._super(); + this._initFilesContainer(); + this._initTemplates(); + }, + + _create: function () { + this._super(); + this._resetFinishedDeferreds(); + if (!$.support.fileInput) { + this._disableFileInputButton(); + } + }, + + enable: function () { + var wasDisabled = false; + if (this.options.disabled) { + wasDisabled = true; + } + this._super(); + if (wasDisabled) { + this.element.find('input, button').prop('disabled', false); + this._enableFileInputButton(); + } + }, + + disable: function () { + if (!this.options.disabled) { + this.element.find('input, button').prop('disabled', true); + this._disableFileInputButton(); + } + this._super(); + } + + }); + +})); \ No newline at end of file diff --git a/client/js/libs/jquery.fileupload-validate.js b/client/js/libs/jquery.fileupload-validate.js new file mode 100644 index 000000000..ee1c2f2ed --- /dev/null +++ b/client/js/libs/jquery.fileupload-validate.js @@ -0,0 +1,117 @@ +/* + * jQuery File Upload Validation Plugin 1.1.1 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/*jslint nomen: true, unparam: true, regexp: true */ +/*global define, window */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + './jquery.fileupload-process' + ], factory); + } else { + // Browser globals: + factory( + window.jQuery + ); + } +}(function ($) { + 'use strict'; + + // Append to the default processQueue: + $.blueimp.fileupload.prototype.options.processQueue.push( + { + action: 'validate', + // Always trigger this action, + // even if the previous action was rejected: + always: true, + // Options taken from the global options map: + acceptFileTypes: '@', + maxFileSize: '@', + minFileSize: '@', + maxNumberOfFiles: '@', + disabled: '@disableValidation' + } + ); + + // The File Upload Validation plugin extends the fileupload widget + // with file validation functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + /* + // The regular expression for allowed file types, matches + // against either file type or file name: + acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i, + // The maximum allowed file size in bytes: + maxFileSize: 10000000, // 10 MB + // The minimum allowed file size in bytes: + minFileSize: undefined, // No minimal file size + // The limit of files to be uploaded: + maxNumberOfFiles: 10, + */ + + // Function returning the current number of files, + // has to be overriden for maxNumberOfFiles validation: + getNumberOfFiles: $.noop, + + // Error and info messages: + messages: { + maxNumberOfFiles: 'Maximum number of files exceeded', + acceptFileTypes: 'File type not allowed', + maxFileSize: 'File is too large', + minFileSize: 'File is too small' + } + }, + + processActions: { + + validate: function (data, options) { + if (options.disabled) { + return data; + } + var dfd = $.Deferred(), + settings = this.options, + file = data.files[data.index]; + if ($.type(options.maxNumberOfFiles) === 'number' && + (settings.getNumberOfFiles() || 0) + data.files.length > + options.maxNumberOfFiles) { + file.error = settings.i18n('maxNumberOfFiles'); + } else if (options.acceptFileTypes && + !(options.acceptFileTypes.test(file.type) || + options.acceptFileTypes.test(file.name))) { + file.error = settings.i18n('acceptFileTypes'); + } else if (options.maxFileSize && file.size > + options.maxFileSize) { + file.error = settings.i18n('maxFileSize'); + } else if ($.type(file.size) === 'number' && + file.size < options.minFileSize) { + file.error = settings.i18n('minFileSize'); + } else { + delete file.error; + } + if (file.error || data.files.error) { + data.files.error = true; + dfd.rejectWith(this, [data]); + } else { + dfd.resolveWith(this, [data]); + } + return dfd.promise(); + } + + } + + }); + +})); diff --git a/client/js/libs/jquery.fileupload.js b/client/js/libs/jquery.fileupload.js new file mode 100644 index 000000000..921f5aed4 --- /dev/null +++ b/client/js/libs/jquery.fileupload.js @@ -0,0 +1,1315 @@ +/* + * jQuery File Upload Plugin 5.31.6 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/*jslint nomen: true, unparam: true, regexp: true */ +/*global define, window, document, location, File, Blob, FormData */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'jquery.ui.widget' + ], factory); + } else { + // Browser globals: + factory(window.jQuery); + } +}(function ($) { + 'use strict'; + + // The FileReader API is not actually used, but works as feature detection, + // as e.g. Safari supports XHR file uploads via the FormData API, + // but not non-multipart XHR file uploads: + $.support.xhrFileUpload = !!(window.XMLHttpRequestUpload && window.FileReader); + $.support.xhrFormDataFileUpload = !!window.FormData; + + // Detect support for Blob slicing (required for chunked uploads): + $.support.blobSlice = window.Blob && (Blob.prototype.slice || + Blob.prototype.webkitSlice || Blob.prototype.mozSlice); + + // The fileupload widget listens for change events on file input fields defined + // via fileInput setting and paste or drop events of the given dropZone. + // In addition to the default jQuery Widget methods, the fileupload widget + // exposes the "add" and "send" methods, to add or directly send files using + // the fileupload API. + // By default, files added via file input selection, paste, drag & drop or + // "add" method are uploaded immediately, but it is possible to override + // the "add" callback option to queue file uploads. + $.widget('blueimp.fileupload', { + + options: { + // The drop target element(s), by the default the complete document. + // Set to null to disable drag & drop support: + dropZone: $(document), + // The paste target element(s), by the default the complete document. + // Set to null to disable paste support: + pasteZone: $(document), + // The file input field(s), that are listened to for change events. + // If undefined, it is set to the file input fields inside + // of the widget element on plugin initialization. + // Set to null to disable the change listener. + fileInput: undefined, + // By default, the file input field is replaced with a clone after + // each input field change event. This is required for iframe transport + // queues and allows change events to be fired for the same file + // selection, but can be disabled by setting the following option to false: + replaceFileInput: true, + // The parameter name for the file form data (the request argument name). + // If undefined or empty, the name property of the file input field is + // used, or "files[]" if the file input name property is also empty, + // can be a string or an array of strings: + paramName: undefined, + // By default, each file of a selection is uploaded using an individual + // request for XHR type uploads. Set to false to upload file + // selections in one request each: + singleFileUploads: true, + // To limit the number of files uploaded with one XHR request, + // set the following option to an integer greater than 0: + limitMultiFileUploads: undefined, + // Set the following option to true to issue all file upload requests + // in a sequential order: + sequentialUploads: false, + // To limit the number of concurrent uploads, + // set the following option to an integer greater than 0: + limitConcurrentUploads: undefined, + // Set the following option to true to force iframe transport uploads: + forceIframeTransport: false, + // Set the following option to the location of a redirect url on the + // origin server, for cross-domain iframe transport uploads: + redirect: undefined, + // The parameter name for the redirect url, sent as part of the form + // data and set to 'redirect' if this option is empty: + redirectParamName: undefined, + // Set the following option to the location of a postMessage window, + // to enable postMessage transport uploads: + postMessage: undefined, + // By default, XHR file uploads are sent as multipart/form-data. + // The iframe transport is always using multipart/form-data. + // Set to false to enable non-multipart XHR uploads: + multipart: true, + // To upload large files in smaller chunks, set the following option + // to a preferred maximum chunk size. If set to 0, null or undefined, + // or the browser does not support the required Blob API, files will + // be uploaded as a whole. + maxChunkSize: undefined, + // When a non-multipart upload or a chunked multipart upload has been + // aborted, this option can be used to resume the upload by setting + // it to the size of the already uploaded bytes. This option is most + // useful when modifying the options object inside of the "add" or + // "send" callbacks, as the options are cloned for each file upload. + uploadedBytes: undefined, + // By default, failed (abort or error) file uploads are removed from the + // global progress calculation. Set the following option to false to + // prevent recalculating the global progress data: + recalculateProgress: true, + // Interval in milliseconds to calculate and trigger progress events: + progressInterval: 100, + // Interval in milliseconds to calculate progress bitrate: + bitrateInterval: 500, + // By default, uploads are started automatically when adding files: + autoUpload: true, + + // Error and info messages: + messages: { + uploadedBytes: 'Uploaded bytes exceed file size' + }, + + // Translation function, gets the message key to be translated + // and an object with context specific data as arguments: + i18n: function (message, context) { + message = this.messages[message] || message.toString(); + if (context) { + $.each(context, function (key, value) { + message = message.replace('{' + key + '}', value); + }); + } + return message; + }, + + // Additional form data to be sent along with the file uploads can be set + // using this option, which accepts an array of objects with name and + // value properties, a function returning such an array, a FormData + // object (for XHR file uploads), or a simple object. + // The form of the first fileInput is given as parameter to the function: + formData: function (form) { + return form.serializeArray(); + }, + + // The add callback is invoked as soon as files are added to the fileupload + // widget (via file input selection, drag & drop, paste or add API call). + // If the singleFileUploads option is enabled, this callback will be + // called once for each file in the selection for XHR file uploads, else + // once for each file selection. + // + // The upload starts when the submit method is invoked on the data parameter. + // The data object contains a files property holding the added files + // and allows you to override plugin options as well as define ajax settings. + // + // Listeners for this callback can also be bound the following way: + // .bind('fileuploadadd', func); + // + // data.submit() returns a Promise object and allows to attach additional + // handlers using jQuery's Deferred callbacks: + // data.submit().done(func).fail(func).always(func); + add: function (e, data) { + if (data.autoUpload || (data.autoUpload !== false && + $(this).fileupload('option', 'autoUpload'))) { + data.process().done(function () { + data.submit(); + }); + } + }, + + // Other callbacks: + + // Callback for the submit event of each file upload: + // submit: function (e, data) {}, // .bind('fileuploadsubmit', func); + + // Callback for the start of each file upload request: + // send: function (e, data) {}, // .bind('fileuploadsend', func); + + // Callback for successful uploads: + // done: function (e, data) {}, // .bind('fileuploaddone', func); + + // Callback for failed (abort or error) uploads: + // fail: function (e, data) {}, // .bind('fileuploadfail', func); + + // Callback for completed (success, abort or error) requests: + // always: function (e, data) {}, // .bind('fileuploadalways', func); + + // Callback for upload progress events: + // progress: function (e, data) {}, // .bind('fileuploadprogress', func); + + // Callback for global upload progress events: + // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func); + + // Callback for uploads start, equivalent to the global ajaxStart event: + // start: function (e) {}, // .bind('fileuploadstart', func); + + // Callback for uploads stop, equivalent to the global ajaxStop event: + // stop: function (e) {}, // .bind('fileuploadstop', func); + + // Callback for change events of the fileInput(s): + // change: function (e, data) {}, // .bind('fileuploadchange', func); + + // Callback for paste events to the pasteZone(s): + // paste: function (e, data) {}, // .bind('fileuploadpaste', func); + + // Callback for drop events of the dropZone(s): + // drop: function (e, data) {}, // .bind('fileuploaddrop', func); + + // Callback for dragover events of the dropZone(s): + // dragover: function (e) {}, // .bind('fileuploaddragover', func); + + // Callback for the start of each chunk upload request: + // chunksend: function (e, data) {}, // .bind('fileuploadchunksend', func); + + // Callback for successful chunk uploads: + // chunkdone: function (e, data) {}, // .bind('fileuploadchunkdone', func); + + // Callback for failed (abort or error) chunk uploads: + // chunkfail: function (e, data) {}, // .bind('fileuploadchunkfail', func); + + // Callback for completed (success, abort or error) chunk upload requests: + // chunkalways: function (e, data) {}, // .bind('fileuploadchunkalways', func); + + // The plugin options are used as settings object for the ajax calls. + // The following are jQuery ajax settings required for the file uploads: + processData: false, + contentType: false, + cache: false + }, + + // A list of options that require reinitializing event listeners and/or + // special initialization code: + _specialOptions: [ + 'fileInput', + 'dropZone', + 'pasteZone', + 'multipart', + 'forceIframeTransport' + ], + + _blobSlice: $.support.blobSlice && function () { + var slice = this.slice || this.webkitSlice || this.mozSlice; + return slice.apply(this, arguments); + }, + + _BitrateTimer: function () { + this.timestamp = ((Date.now) ? Date.now() : (new Date()).getTime()); + this.loaded = 0; + this.bitrate = 0; + this.getBitrate = function (now, loaded, interval) { + var timeDiff = now - this.timestamp; + if (!this.bitrate || !interval || timeDiff > interval) { + this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8; + this.loaded = loaded; + this.timestamp = now; + } + return this.bitrate; + }; + }, + + _isXHRUpload: function (options) { + return !options.forceIframeTransport && + ((!options.multipart && $.support.xhrFileUpload) || + $.support.xhrFormDataFileUpload); + }, + + _getFormData: function (options) { + var formData; + if (typeof options.formData === 'function') { + return options.formData(options.form); + } + if ($.isArray(options.formData)) { + return options.formData; + } + if ($.type(options.formData) === 'object') { + formData = []; + $.each(options.formData, function (name, value) { + formData.push({name: name, value: value}); + }); + return formData; + } + return []; + }, + + _getTotal: function (files) { + var total = 0; + $.each(files, function (index, file) { + total += file.size || 1; + }); + return total; + }, + + _initProgressObject: function (obj) { + var progress = { + loaded: 0, + total: 0, + bitrate: 0 + }; + if (obj._progress) { + $.extend(obj._progress, progress); + } else { + obj._progress = progress; + } + }, + + _initResponseObject: function (obj) { + var prop; + if (obj._response) { + for (prop in obj._response) { + if (obj._response.hasOwnProperty(prop)) { + delete obj._response[prop]; + } + } + } else { + obj._response = {}; + } + }, + + _onProgress: function (e, data) { + if (e.lengthComputable) { + var now = ((Date.now) ? Date.now() : (new Date()).getTime()), + loaded; + if (data._time && data.progressInterval && + (now - data._time < data.progressInterval) && + e.loaded !== e.total) { + return; + } + data._time = now; + loaded = Math.floor( + e.loaded / e.total * (data.chunkSize || data._progress.total) + ) + (data.uploadedBytes || 0); + // Add the difference from the previously loaded state + // to the global loaded counter: + this._progress.loaded += (loaded - data._progress.loaded); + this._progress.bitrate = this._bitrateTimer.getBitrate( + now, + this._progress.loaded, + data.bitrateInterval + ); + data._progress.loaded = data.loaded = loaded; + data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate( + now, + loaded, + data.bitrateInterval + ); + // Trigger a custom progress event with a total data property set + // to the file size(s) of the current upload and a loaded data + // property calculated accordingly: + this._trigger('progress', e, data); + // Trigger a global progress event for all current file uploads, + // including ajax calls queued for sequential file uploads: + this._trigger('progressall', e, this._progress); + } + }, + + _initProgressListener: function (options) { + var that = this, + xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr(); + // Accesss to the native XHR object is required to add event listeners + // for the upload progress event: + if (xhr.upload) { + $(xhr.upload).bind('progress', function (e) { + var oe = e.originalEvent; + // Make sure the progress event properties get copied over: + e.lengthComputable = oe.lengthComputable; + e.loaded = oe.loaded; + e.total = oe.total; + that._onProgress(e, options); + }); + options.xhr = function () { + return xhr; + }; + } + }, + + _isInstanceOf: function (type, obj) { + // Cross-frame instanceof check + return Object.prototype.toString.call(obj) === '[object ' + type + ']'; + }, + + _initXHRData: function (options) { + var that = this, + formData, + file = options.files[0], + // Ignore non-multipart setting if not supported: + multipart = options.multipart || !$.support.xhrFileUpload, + paramName = options.paramName[0]; + options.headers = options.headers || {}; + if (options.contentRange) { + options.headers['Content-Range'] = options.contentRange; + } + if (!multipart || options.blob || !this._isInstanceOf('File', file)) { + options.headers['Content-Disposition'] = 'attachment; filename="' + + encodeURI(file.name) + '"'; + } + if (!multipart) { + options.contentType = file.type; + options.data = options.blob || file; + } else if ($.support.xhrFormDataFileUpload) { + if (options.postMessage) { + // window.postMessage does not allow sending FormData + // objects, so we just add the File/Blob objects to + // the formData array and let the postMessage window + // create the FormData object out of this array: + formData = this._getFormData(options); + if (options.blob) { + formData.push({ + name: paramName, + value: options.blob + }); + } else { + $.each(options.files, function (index, file) { + formData.push({ + name: options.paramName[index] || paramName, + value: file + }); + }); + } + } else { + if (that._isInstanceOf('FormData', options.formData)) { + formData = options.formData; + } else { + formData = new FormData(); + $.each(this._getFormData(options), function (index, field) { + formData.append(field.name, field.value); + }); + } + if (options.blob) { + formData.append(paramName, options.blob, file.name); + } else { + $.each(options.files, function (index, file) { + // This check allows the tests to run with + // dummy objects: + if (that._isInstanceOf('File', file) || + that._isInstanceOf('Blob', file)) { + formData.append( + options.paramName[index] || paramName, + file, + file.name + ); + } + }); + } + } + options.data = formData; + } + // Blob reference is not needed anymore, free memory: + options.blob = null; + }, + + _initIframeSettings: function (options) { + var targetHost = $('').prop('href', options.url).prop('host'); + // Setting the dataType to iframe enables the iframe transport: + options.dataType = 'iframe ' + (options.dataType || ''); + // The iframe transport accepts a serialized array as form data: + options.formData = this._getFormData(options); + // Add redirect url to form data on cross-domain uploads: + if (options.redirect && targetHost && targetHost !== location.host) { + options.formData.push({ + name: options.redirectParamName || 'redirect', + value: options.redirect + }); + } + }, + + _initDataSettings: function (options) { + if (this._isXHRUpload(options)) { + if (!this._chunkedUpload(options, true)) { + if (!options.data) { + this._initXHRData(options); + } + this._initProgressListener(options); + } + if (options.postMessage) { + // Setting the dataType to postmessage enables the + // postMessage transport: + options.dataType = 'postmessage ' + (options.dataType || ''); + } + } else { + this._initIframeSettings(options); + } + }, + + _getParamName: function (options) { + var fileInput = $(options.fileInput), + paramName = options.paramName; + if (!paramName) { + paramName = []; + fileInput.each(function () { + var input = $(this), + name = input.prop('name') || 'files[]', + i = (input.prop('files') || [1]).length; + while (i) { + paramName.push(name); + i -= 1; + } + }); + if (!paramName.length) { + paramName = [fileInput.prop('name') || 'files[]']; + } + } else if (!$.isArray(paramName)) { + paramName = [paramName]; + } + return paramName; + }, + + _initFormSettings: function (options) { + // Retrieve missing options from the input field and the + // associated form, if available: + if (!options.form || !options.form.length) { + options.form = $(options.fileInput.prop('form')); + // If the given file input doesn't have an associated form, + // use the default widget file input's form: + if (!options.form.length) { + options.form = $(this.options.fileInput.prop('form')); + } + } + options.paramName = this._getParamName(options); + if (!options.url) { + options.url = options.form.prop('action') || location.href; + } + // The HTTP request method must be "POST" or "PUT": + options.type = (options.type || options.form.prop('method') || '') + .toUpperCase(); + if (options.type !== 'POST' && options.type !== 'PUT' && + options.type !== 'PATCH') { + options.type = 'POST'; + } + if (!options.formAcceptCharset) { + options.formAcceptCharset = options.form.attr('accept-charset'); + } + }, + + _getAJAXSettings: function (data) { + var options = $.extend({}, this.options, data); + this._initFormSettings(options); + this._initDataSettings(options); + return options; + }, + + // jQuery 1.6 doesn't provide .state(), + // while jQuery 1.8+ removed .isRejected() and .isResolved(): + _getDeferredState: function (deferred) { + if (deferred.state) { + return deferred.state(); + } + if (deferred.isResolved()) { + return 'resolved'; + } + if (deferred.isRejected()) { + return 'rejected'; + } + return 'pending'; + }, + + // Maps jqXHR callbacks to the equivalent + // methods of the given Promise object: + _enhancePromise: function (promise) { + promise.success = promise.done; + promise.error = promise.fail; + promise.complete = promise.always; + return promise; + }, + + // Creates and returns a Promise object enhanced with + // the jqXHR methods abort, success, error and complete: + _getXHRPromise: function (resolveOrReject, context, args) { + var dfd = $.Deferred(), + promise = dfd.promise(); + context = context || this.options.context || promise; + if (resolveOrReject === true) { + dfd.resolveWith(context, args); + } else if (resolveOrReject === false) { + dfd.rejectWith(context, args); + } + promise.abort = dfd.promise; + return this._enhancePromise(promise); + }, + + // Adds convenience methods to the data callback argument: + _addConvenienceMethods: function (e, data) { + var that = this, + getPromise = function (data) { + return $.Deferred().resolveWith(that, [data]).promise(); + }; + data.process = function (resolveFunc, rejectFunc) { + if (resolveFunc || rejectFunc) { + data._processQueue = this._processQueue = + (this._processQueue || getPromise(this)) + .pipe(resolveFunc, rejectFunc); + } + return this._processQueue || getPromise(this); + }; + data.submit = function () { + if (this.state() !== 'pending') { + data.jqXHR = this.jqXHR = + (that._trigger('submit', e, this) !== false) && + that._onSend(e, this); + } + return this.jqXHR || that._getXHRPromise(); + }; + data.abort = function () { + if (this.jqXHR) { + return this.jqXHR.abort(); + } + return that._getXHRPromise(); + }; + data.state = function () { + if (this.jqXHR) { + return that._getDeferredState(this.jqXHR); + } + if (this._processQueue) { + return that._getDeferredState(this._processQueue); + } + }; + data.progress = function () { + return this._progress; + }; + data.response = function () { + return this._response; + }; + }, + + // Parses the Range header from the server response + // and returns the uploaded bytes: + _getUploadedBytes: function (jqXHR) { + var range = jqXHR.getResponseHeader('Range'), + parts = range && range.split('-'), + upperBytesPos = parts && parts.length > 1 && + parseInt(parts[1], 10); + return upperBytesPos && upperBytesPos + 1; + }, + + // Uploads a file in multiple, sequential requests + // by splitting the file up in multiple blob chunks. + // If the second parameter is true, only tests if the file + // should be uploaded in chunks, but does not invoke any + // upload requests: + _chunkedUpload: function (options, testOnly) { + options.uploadedBytes = options.uploadedBytes || 0; + var that = this, + file = options.files[0], + fs = file.size, + ub = options.uploadedBytes, + mcs = options.maxChunkSize || fs, + slice = this._blobSlice, + dfd = $.Deferred(), + promise = dfd.promise(), + jqXHR, + upload; + if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) || + options.data) { + return false; + } + if (testOnly) { + return true; + } + if (ub >= fs) { + file.error = options.i18n('uploadedBytes'); + return this._getXHRPromise( + false, + options.context, + [null, 'error', file.error] + ); + } + // The chunk upload method: + upload = function () { + // Clone the options object for each chunk upload: + var o = $.extend({}, options), + currentLoaded = o._progress.loaded; + o.blob = slice.call( + file, + ub, + ub + mcs, + file.type + ); + // Store the current chunk size, as the blob itself + // will be dereferenced after data processing: + o.chunkSize = o.blob.size; + // Expose the chunk bytes position range: + o.contentRange = 'bytes ' + ub + '-' + + (ub + o.chunkSize - 1) + '/' + fs; + // Process the upload data (the blob and potential form data): + that._initXHRData(o); + // Add progress listeners for this chunk upload: + that._initProgressListener(o); + jqXHR = ((that._trigger('chunksend', null, o) !== false && $.ajax(o)) || + that._getXHRPromise(false, o.context)) + .done(function (result, textStatus, jqXHR) { + ub = that._getUploadedBytes(jqXHR) || + (ub + o.chunkSize); + // Create a progress event if no final progress event + // with loaded equaling total has been triggered + // for this chunk: + if (currentLoaded + o.chunkSize - o._progress.loaded) { + that._onProgress($.Event('progress', { + lengthComputable: true, + loaded: ub - o.uploadedBytes, + total: ub - o.uploadedBytes + }), o); + } + options.uploadedBytes = o.uploadedBytes = ub; + o.result = result; + o.textStatus = textStatus; + o.jqXHR = jqXHR; + that._trigger('chunkdone', null, o); + that._trigger('chunkalways', null, o); + if (ub < fs) { + // File upload not yet complete, + // continue with the next chunk: + upload(); + } else { + dfd.resolveWith( + o.context, + [result, textStatus, jqXHR] + ); + } + }) + .fail(function (jqXHR, textStatus, errorThrown) { + o.jqXHR = jqXHR; + o.textStatus = textStatus; + o.errorThrown = errorThrown; + that._trigger('chunkfail', null, o); + that._trigger('chunkalways', null, o); + dfd.rejectWith( + o.context, + [jqXHR, textStatus, errorThrown] + ); + }); + }; + this._enhancePromise(promise); + promise.abort = function () { + return jqXHR.abort(); + }; + upload(); + return promise; + }, + + _beforeSend: function (e, data) { + if (this._active === 0) { + // the start callback is triggered when an upload starts + // and no other uploads are currently running, + // equivalent to the global ajaxStart event: + this._trigger('start'); + // Set timer for global bitrate progress calculation: + this._bitrateTimer = new this._BitrateTimer(); + // Reset the global progress values: + this._progress.loaded = this._progress.total = 0; + this._progress.bitrate = 0; + } + // Make sure the container objects for the .response() and + // .progress() methods on the data object are available + // and reset to their initial state: + this._initResponseObject(data); + this._initProgressObject(data); + data._progress.loaded = data.loaded = data.uploadedBytes || 0; + data._progress.total = data.total = this._getTotal(data.files) || 1; + data._progress.bitrate = data.bitrate = 0; + this._active += 1; + // Initialize the global progress values: + this._progress.loaded += data.loaded; + this._progress.total += data.total; + }, + + _onDone: function (result, textStatus, jqXHR, options) { + var total = options._progress.total, + response = options._response; + if (options._progress.loaded < total) { + // Create a progress event if no final progress event + // with loaded equaling total has been triggered: + this._onProgress($.Event('progress', { + lengthComputable: true, + loaded: total, + total: total + }), options); + } + response.result = options.result = result; + response.textStatus = options.textStatus = textStatus; + response.jqXHR = options.jqXHR = jqXHR; + this._trigger('done', null, options); + }, + + _onFail: function (jqXHR, textStatus, errorThrown, options) { + var response = options._response; + if (options.recalculateProgress) { + // Remove the failed (error or abort) file upload from + // the global progress calculation: + this._progress.loaded -= options._progress.loaded; + this._progress.total -= options._progress.total; + } + response.jqXHR = options.jqXHR = jqXHR; + response.textStatus = options.textStatus = textStatus; + response.errorThrown = options.errorThrown = errorThrown; + this._trigger('fail', null, options); + }, + + _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) { + // jqXHRorResult, textStatus and jqXHRorError are added to the + // options object via done and fail callbacks + this._trigger('always', null, options); + }, + + _onSend: function (e, data) { + if (!data.submit) { + this._addConvenienceMethods(e, data); + } + var that = this, + jqXHR, + aborted, + slot, + pipe, + options = that._getAJAXSettings(data), + send = function () { + that._sending += 1; + // Set timer for bitrate progress calculation: + options._bitrateTimer = new that._BitrateTimer(); + jqXHR = jqXHR || ( + ((aborted || that._trigger('send', e, options) === false) && + that._getXHRPromise(false, options.context, aborted)) || + that._chunkedUpload(options) || $.ajax(options) + ).done(function (result, textStatus, jqXHR) { + that._onDone(result, textStatus, jqXHR, options); + }).fail(function (jqXHR, textStatus, errorThrown) { + that._onFail(jqXHR, textStatus, errorThrown, options); + }).always(function (jqXHRorResult, textStatus, jqXHRorError) { + that._onAlways( + jqXHRorResult, + textStatus, + jqXHRorError, + options + ); + that._sending -= 1; + that._active -= 1; + if (options.limitConcurrentUploads && + options.limitConcurrentUploads > that._sending) { + // Start the next queued upload, + // that has not been aborted: + var nextSlot = that._slots.shift(); + while (nextSlot) { + if (that._getDeferredState(nextSlot) === 'pending') { + nextSlot.resolve(); + break; + } + nextSlot = that._slots.shift(); + } + } + if (that._active === 0) { + // The stop callback is triggered when all uploads have + // been completed, equivalent to the global ajaxStop event: + that._trigger('stop'); + } + }); + return jqXHR; + }; + this._beforeSend(e, options); + if (this.options.sequentialUploads || + (this.options.limitConcurrentUploads && + this.options.limitConcurrentUploads <= this._sending)) { + if (this.options.limitConcurrentUploads > 1) { + slot = $.Deferred(); + this._slots.push(slot); + pipe = slot.pipe(send); + } else { + this._sequence = this._sequence.pipe(send, send); + pipe = this._sequence; + } + // Return the piped Promise object, enhanced with an abort method, + // which is delegated to the jqXHR object of the current upload, + // and jqXHR callbacks mapped to the equivalent Promise methods: + pipe.abort = function () { + aborted = [undefined, 'abort', 'abort']; + if (!jqXHR) { + if (slot) { + slot.rejectWith(options.context, aborted); + } + return send(); + } + return jqXHR.abort(); + }; + return this._enhancePromise(pipe); + } + return send(); + }, + + _onAdd: function (e, data) { + var that = this, + result = true, + options = $.extend({}, this.options, data), + limit = options.limitMultiFileUploads, + paramName = this._getParamName(options), + paramNameSet, + paramNameSlice, + fileSet, + i; + if (!(options.singleFileUploads || limit) || + !this._isXHRUpload(options)) { + fileSet = [data.files]; + paramNameSet = [paramName]; + } else if (!options.singleFileUploads && limit) { + fileSet = []; + paramNameSet = []; + for (i = 0; i < data.files.length; i += limit) { + fileSet.push(data.files.slice(i, i + limit)); + paramNameSlice = paramName.slice(i, i + limit); + if (!paramNameSlice.length) { + paramNameSlice = paramName; + } + paramNameSet.push(paramNameSlice); + } + } else { + paramNameSet = paramName; + } + data.originalFiles = data.files; + $.each(fileSet || data.files, function (index, element) { + var newData = $.extend({}, data); + newData.files = fileSet ? element : [element]; + newData.paramName = paramNameSet[index]; + that._initResponseObject(newData); + that._initProgressObject(newData); + that._addConvenienceMethods(e, newData); + result = that._trigger('add', e, newData); + return result; + }); + return result; + }, + + _replaceFileInput: function (input) { + var inputClone = input.clone(true); + $('
    ').append(inputClone)[0].reset(); + // Detaching allows to insert the fileInput on another form + // without loosing the file input value: + input.after(inputClone).detach(); + // Avoid memory leaks with the detached file input: + $.cleanData(input.unbind('remove')); + // Replace the original file input element in the fileInput + // elements set with the clone, which has been copied including + // event handlers: + this.options.fileInput = this.options.fileInput.map(function (i, el) { + if (el === input[0]) { + return inputClone[0]; + } + return el; + }); + // If the widget has been initialized on the file input itself, + // override this.element with the file input clone: + if (input[0] === this.element[0]) { + this.element = inputClone; + } + }, + + _handleFileTreeEntry: function (entry, path) { + var that = this, + dfd = $.Deferred(), + errorHandler = function (e) { + if (e && !e.entry) { + e.entry = entry; + } + // Since $.when returns immediately if one + // Deferred is rejected, we use resolve instead. + // This allows valid files and invalid items + // to be returned together in one set: + dfd.resolve([e]); + }, + dirReader; + path = path || ''; + if (entry.isFile) { + if (entry._file) { + // Workaround for Chrome bug #149735 + entry._file.relativePath = path; + dfd.resolve(entry._file); + } else { + entry.file(function (file) { + file.relativePath = path; + dfd.resolve(file); + }, errorHandler); + } + } else if (entry.isDirectory) { + dirReader = entry.createReader(); + dirReader.readEntries(function (entries) { + that._handleFileTreeEntries( + entries, + path + entry.name + '/' + ).done(function (files) { + dfd.resolve(files); + }).fail(errorHandler); + }, errorHandler); + } else { + // Return an empy list for file system items + // other than files or directories: + dfd.resolve([]); + } + return dfd.promise(); + }, + + _handleFileTreeEntries: function (entries, path) { + var that = this; + return $.when.apply( + $, + $.map(entries, function (entry) { + return that._handleFileTreeEntry(entry, path); + }) + ).pipe(function () { + return Array.prototype.concat.apply( + [], + arguments + ); + }); + }, + + _getDroppedFiles: function (dataTransfer) { + dataTransfer = dataTransfer || {}; + var items = dataTransfer.items; + if (items && items.length && (items[0].webkitGetAsEntry || + items[0].getAsEntry)) { + return this._handleFileTreeEntries( + $.map(items, function (item) { + var entry; + if (item.webkitGetAsEntry) { + entry = item.webkitGetAsEntry(); + if (entry) { + // Workaround for Chrome bug #149735: + entry._file = item.getAsFile(); + } + return entry; + } + return item.getAsEntry(); + }) + ); + } + return $.Deferred().resolve( + $.makeArray(dataTransfer.files) + ).promise(); + }, + + _getSingleFileInputFiles: function (fileInput) { + fileInput = $(fileInput); + var entries = fileInput.prop('webkitEntries') || + fileInput.prop('entries'), + files, + value; + if (entries && entries.length) { + return this._handleFileTreeEntries(entries); + } + files = $.makeArray(fileInput.prop('files')); + if (!files.length) { + value = fileInput.prop('value'); + if (!value) { + return $.Deferred().resolve([]).promise(); + } + // If the files property is not available, the browser does not + // support the File API and we add a pseudo File object with + // the input value as name with path information removed: + files = [{name: value.replace(/^.*\\/, '')}]; + } else if (files[0].name === undefined && files[0].fileName) { + // File normalization for Safari 4 and Firefox 3: + $.each(files, function (index, file) { + file.name = file.fileName; + file.size = file.fileSize; + }); + } + return $.Deferred().resolve(files).promise(); + }, + + _getFileInputFiles: function (fileInput) { + if (!(fileInput instanceof $) || fileInput.length === 1) { + return this._getSingleFileInputFiles(fileInput); + } + return $.when.apply( + $, + $.map(fileInput, this._getSingleFileInputFiles) + ).pipe(function () { + return Array.prototype.concat.apply( + [], + arguments + ); + }); + }, + + _onChange: function (e) { + var that = this, + data = { + fileInput: $(e.target), + form: $(e.target.form) + }; + this._getFileInputFiles(data.fileInput).always(function (files) { + data.files = files; + if (that.options.replaceFileInput) { + that._replaceFileInput(data.fileInput); + } + if (that._trigger('change', e, data) !== false) { + that._onAdd(e, data); + } + }); + }, + + _onPaste: function (e) { + var items = e.originalEvent && e.originalEvent.clipboardData && + e.originalEvent.clipboardData.items, + data = {files: []}; + if (items && items.length) { + $.each(items, function (index, item) { + var file = item.getAsFile && item.getAsFile(); + if (file) { + data.files.push(file); + } + }); + if (this._trigger('paste', e, data) === false || + this._onAdd(e, data) === false) { + return false; + } + } + }, + + _onDrop: function (e) { + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; + var that = this, + dataTransfer = e.dataTransfer, + data = {}; + if (dataTransfer && dataTransfer.files && dataTransfer.files.length) { + e.preventDefault(); + this._getDroppedFiles(dataTransfer).always(function (files) { + data.files = files; + if (that._trigger('drop', e, data) !== false) { + that._onAdd(e, data); + } + }); + } + }, + + _onDragOver: function (e) { + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; + var dataTransfer = e.dataTransfer; + if (dataTransfer) { + if (this._trigger('dragover', e) === false) { + return false; + } + if ($.inArray('Files', dataTransfer.types) !== -1) { + dataTransfer.dropEffect = 'copy'; + e.preventDefault(); + } + } + }, + + _initEventHandlers: function () { + if (this._isXHRUpload(this.options)) { + this._on(this.options.dropZone, { + dragover: this._onDragOver, + drop: this._onDrop + }); + this._on(this.options.pasteZone, { + paste: this._onPaste + }); + } + this._on(this.options.fileInput, { + change: this._onChange + }); + }, + + _destroyEventHandlers: function () { + this._off(this.options.dropZone, 'dragover drop'); + this._off(this.options.pasteZone, 'paste'); + this._off(this.options.fileInput, 'change'); + }, + + _setOption: function (key, value) { + var reinit = $.inArray(key, this._specialOptions) !== -1; + if (reinit) { + this._destroyEventHandlers(); + } + this._super(key, value); + if (reinit) { + this._initSpecialOptions(); + this._initEventHandlers(); + } + }, + + _initSpecialOptions: function () { + var options = this.options; + if (options.fileInput === undefined) { + options.fileInput = this.element.is('input[type="file"]') ? + this.element : this.element.find('input[type="file"]'); + } else if (!(options.fileInput instanceof $)) { + options.fileInput = $(options.fileInput); + } + if (!(options.dropZone instanceof $)) { + options.dropZone = $(options.dropZone); + } + if (!(options.pasteZone instanceof $)) { + options.pasteZone = $(options.pasteZone); + } + }, + + _getRegExp: function (str) { + var parts = str.split('/'), + modifiers = parts.pop(); + parts.shift(); + return new RegExp(parts.join('/'), modifiers); + }, + + _isRegExpOption: function (key, value) { + return key !== 'url' && $.type(value) === 'string' && + /^\/.*\/[igm]{0,3}$/.test(value); + }, + + _initDataAttributes: function () { + var that = this, + options = this.options; + // Initialize options set via HTML5 data-attributes: + $.each( + $(this.element[0].cloneNode(false)).data(), + function (key, value) { + if (that._isRegExpOption(key, value)) { + value = that._getRegExp(value); + } + options[key] = value; + } + ); + }, + + _create: function () { + this._initDataAttributes(); + this._initSpecialOptions(); + this._slots = []; + this._sequence = this._getXHRPromise(true); + this._sending = this._active = 0; + this._initProgressObject(this); + this._initEventHandlers(); + }, + + // This method is exposed to the widget API and allows to query + // the number of active uploads: + active: function () { + return this._active; + }, + + // This method is exposed to the widget API and allows to query + // the widget upload progress. + // It returns an object with loaded, total and bitrate properties + // for the running uploads: + progress: function () { + return this._progress; + }, + + // This method is exposed to the widget API and allows adding files + // using the fileupload API. The data parameter accepts an object which + // must have a files property and can contain additional options: + // .fileupload('add', {files: filesList}); + add: function (data) { + var that = this; + if (!data || this.options.disabled) { + return; + } + if (data.fileInput && !data.files) { + this._getFileInputFiles(data.fileInput).always(function (files) { + data.files = files; + that._onAdd(null, data); + }); + } else { + data.files = $.makeArray(data.files); + this._onAdd(null, data); + } + }, + + // This method is exposed to the widget API and allows sending files + // using the fileupload API. The data parameter accepts an object which + // must have a files or fileInput property and can contain additional options: + // .fileupload('send', {files: filesList}); + // The method returns a Promise object for the file upload call. + send: function (data) { + if (data && !this.options.disabled) { + if (data.fileInput && !data.files) { + var that = this, + dfd = $.Deferred(), + promise = dfd.promise(), + jqXHR, + aborted; + promise.abort = function () { + aborted = true; + if (jqXHR) { + return jqXHR.abort(); + } + dfd.reject(null, 'abort', 'abort'); + return promise; + }; + this._getFileInputFiles(data.fileInput).always( + function (files) { + if (aborted) { + return; + } + data.files = files; + jqXHR = that._onSend(null, data).then( + function (result, textStatus, jqXHR) { + dfd.resolve(result, textStatus, jqXHR); + }, + function (jqXHR, textStatus, errorThrown) { + dfd.reject(jqXHR, textStatus, errorThrown); + } + ); + } + ); + return this._enhancePromise(promise); + } + data.files = $.makeArray(data.files); + if (data.files.length) { + return this._onSend(null, data); + } + } + return this._getXHRPromise(false, data && data.context); + } + + }); + +})); diff --git a/client/js/libs/jquery.gritter.min.js b/client/js/libs/jquery.gritter.min.js new file mode 100644 index 000000000..12bfa033e --- /dev/null +++ b/client/js/libs/jquery.gritter.min.js @@ -0,0 +1,29 @@ +/* + * Gritter for jQuery + * http://www.boedesign.com/ + * + * Copyright (c) 2011 Jordan Boesch + * Dual licensed under the MIT and GPL licenses. + * + * Date: March 29, 2011 + * Version: 1.7.1 + */ +(function($){$.gritter={};$.gritter.options={position:'',fade_in_speed:'medium',fade_out_speed:1000,time:6000} +$.gritter.add=function(params){try{return Gritter.add(params||{});}catch(e){var err='Gritter Error: '+e;(typeof(console)!='undefined'&&console.error)?console.error(err,params):alert(err);}} +$.gritter.remove=function(id,params){Gritter.removeSpecific(id,params||{});} +$.gritter.removeAll=function(params){Gritter.stop(params||{});} +var Gritter={position:'',fade_in_speed:'',fade_out_speed:'',time:'',_custom_timer:0,_item_count:0,_is_setup:0,_tpl_close:'
    ',_tpl_item:'',_tpl_wrap:'
    ',add:function(params){if(!params.title||!params.text){throw'You need to fill out the first 2 params: "title" and "text"';} +if(!this._is_setup){this._runSetup();} +var user=params.title,text=params.text,image=params.image||'',sticky=params.sticky||false,item_class=params.class_name||'',position=$.gritter.options.position,time_alive=params.time||'';this._verifyWrapper();this._item_count++;var number=this._item_count,tmp=this._tpl_item;$(['before_open','after_open','before_close','after_close']).each(function(i,val){Gritter['_'+val+'_'+number]=($.isFunction(params[val]))?params[val]:function(){}});this._custom_timer=0;if(time_alive){this._custom_timer=time_alive;} +var image_str=(image!='')?'':'',class_name=(image!='')?'gritter-with-image':'gritter-without-image';tmp=this._str_replace(['[[username]]','[[text]]','[[image]]','[[number]]','[[class_name]]','[[item_class]]'],[user,text,image_str,this._item_count,class_name,item_class],tmp);this['_before_open_'+number]();$('#gritter-notice-wrapper').addClass(position).append(tmp);var item=$('#gritter-item-'+this._item_count);item.fadeIn(this.fade_in_speed,function(){Gritter['_after_open_'+number]($(this));});if(!sticky){this._setFadeTimer(item,number);} +$(item).bind('mouseenter mouseleave',function(event){if(event.type=='mouseenter'){if(!sticky){Gritter._restoreItemIfFading($(this),number);}} +else{if(!sticky){Gritter._setFadeTimer($(this),number);}} +Gritter._hoverState($(this),event.type);});return number;},_countRemoveWrapper:function(unique_id,e,manual_close){e.remove();this['_after_close_'+unique_id](e,manual_close);if($('.gritter-item-wrapper').length==0){$('#gritter-notice-wrapper').remove();}},_fade:function(e,unique_id,params,unbind_events){var params=params||{},fade=(typeof(params.fade)!='undefined')?params.fade:true;fade_out_speed=params.speed||this.fade_out_speed,manual_close=unbind_events;this['_before_close_'+unique_id](e,manual_close);if(unbind_events){e.unbind('mouseenter mouseleave');} +if(fade){e.animate({opacity:0},fade_out_speed,function(){e.animate({height:0},300,function(){Gritter._countRemoveWrapper(unique_id,e,manual_close);})})} +else{this._countRemoveWrapper(unique_id,e);}},_hoverState:function(e,type){if(type=='mouseenter'){e.addClass('hover');var find_img=e.find('img');(find_img.length)?find_img.before(this._tpl_close):e.find('span').before(this._tpl_close);e.find('.gritter-close').click(function(){var unique_id=e.attr('id').split('-')[2];Gritter.removeSpecific(unique_id,{},e,true);});} +else{e.removeClass('hover');e.find('.gritter-close').remove();}},removeSpecific:function(unique_id,params,e,unbind_events){if(!e){var e=$('#gritter-item-'+unique_id);} +this._fade(e,unique_id,params||{},unbind_events);},_restoreItemIfFading:function(e,unique_id){clearTimeout(this['_int_id_'+unique_id]);e.stop().css({opacity:''});},_runSetup:function(){for(opt in $.gritter.options){this[opt]=$.gritter.options[opt];} +this._is_setup=1;},_setFadeTimer:function(e,unique_id){var timer_str=(this._custom_timer)?this._custom_timer:this.time;this['_int_id_'+unique_id]=setTimeout(function(){Gritter._fade(e,unique_id);},timer_str);},stop:function(params){var before_close=($.isFunction(params.before_close))?params.before_close:function(){};var after_close=($.isFunction(params.after_close))?params.after_close:function(){};var wrap=$('#gritter-notice-wrapper');before_close(wrap);wrap.fadeOut(function(){$(this).remove();after_close();});},_str_replace:function(search,replace,subject,count){var i=0,j=0,temp='',repl='',sl=0,fl=0,f=[].concat(search),r=[].concat(replace),s=subject,ra=r instanceof Array,sa=s instanceof Array;s=[].concat(s);if(count){this.window[count]=0;} +for(i=0,sl=s.length;i'); + form.attr('accept-charset', options.formAcceptCharset); + addParamChar = /\?/.test(options.url) ? '&' : '?'; + // XDomainRequest only supports GET and POST: + if (options.type === 'DELETE') { + options.url = options.url + addParamChar + '_method=DELETE'; + options.type = 'POST'; + } else if (options.type === 'PUT') { + options.url = options.url + addParamChar + '_method=PUT'; + options.type = 'POST'; + } else if (options.type === 'PATCH') { + options.url = options.url + addParamChar + '_method=PATCH'; + options.type = 'POST'; + } + // javascript:false as initial iframe src + // prevents warning popups on HTTPS in IE6. + // IE versions below IE8 cannot set the name property of + // elements that have already been added to the DOM, + // so we set the name along with the iframe HTML markup: + counter += 1; + iframe = $( + '' + ).bind('load', function () { + var fileInputClones, + paramNames = $.isArray(options.paramName) ? + options.paramName : [options.paramName]; + iframe + .unbind('load') + .bind('load', function () { + var response; + // Wrap in a try/catch block to catch exceptions thrown + // when trying to access cross-domain iframe contents: + try { + response = iframe.contents(); + // Google Chrome and Firefox do not throw an + // exception when calling iframe.contents() on + // cross-domain requests, so we unify the response: + if (!response.length || !response[0].firstChild) { + throw new Error(); + } + } catch (e) { + response = undefined; + } + // The complete callback returns the + // iframe content document as response object: + completeCallback( + 200, + 'success', + {'iframe': response} + ); + // Fix for IE endless progress bar activity bug + // (happens on form submits to iframe targets): + $('') + .appendTo(form); + window.setTimeout(function () { + // Removing the form in a setTimeout call + // allows Chrome's developer tools to display + // the response result + form.remove(); + }, 0); + }); + form + .prop('target', iframe.prop('name')) + .prop('action', options.url) + .prop('method', options.type); + if (options.formData) { + $.each(options.formData, function (index, field) { + $('') + .prop('name', field.name) + .val(field.value) + .appendTo(form); + }); + } + if (options.fileInput && options.fileInput.length && + options.type === 'POST') { + fileInputClones = options.fileInput.clone(); + // Insert a clone for each file input field: + options.fileInput.after(function (index) { + return fileInputClones[index]; + }); + if (options.paramName) { + options.fileInput.each(function (index) { + $(this).prop( + 'name', + paramNames[index] || options.paramName + ); + }); + } + // Appending the file input fields to the hidden form + // removes them from their original location: + form + .append(options.fileInput) + .prop('enctype', 'multipart/form-data') + // enctype must be set as encoding for IE: + .prop('encoding', 'multipart/form-data'); + } + form.submit(); + // Insert the file input fields at their original location + // by replacing the clones with the originals: + if (fileInputClones && fileInputClones.length) { + options.fileInput.each(function (index, input) { + var clone = $(fileInputClones[index]); + $(input).prop('name', clone.prop('name')); + clone.replaceWith(input); + }); + } + }); + form.append(iframe).appendTo(document.body); + }, + abort: function () { + if (iframe) { + // javascript:false as iframe src aborts the request + // and prevents warning popups on HTTPS in IE6. + // concat is used to avoid the "Script URL" JSLint error: + iframe + .unbind('load') + .prop('src', 'javascript'.concat(':false;')); + } + if (form) { + form.remove(); + } + } + }; + } + }); + + // The iframe transport returns the iframe content document as response. + // The following adds converters from iframe to text, json, html, xml + // and script. + // Please note that the Content-Type for JSON responses has to be text/plain + // or text/html, if the browser doesn't include application/json in the + // Accept header, else IE will show a download dialog. + // The Content-Type for XML responses on the other hand has to be always + // application/xml or text/xml, so IE properly parses the XML response. + // See also + // https://github.com/blueimp/jQuery-File-Upload/wiki/Setup#content-type-negotiation + $.ajaxSetup({ + converters: { + 'iframe text': function (iframe) { + return iframe && $(iframe[0].body).text(); + }, + 'iframe json': function (iframe) { + return iframe && $.parseJSON($(iframe[0].body).text()); + }, + 'iframe html': function (iframe) { + return iframe && $(iframe[0].body).html(); + }, + 'iframe xml': function (iframe) { + var xmlDoc = iframe && iframe[0]; + return xmlDoc && $.isXMLDoc(xmlDoc) ? xmlDoc : + $.parseXML((xmlDoc.XMLDocument && xmlDoc.XMLDocument.xml) || + $(xmlDoc.body).html()); + }, + 'iframe script': function (iframe) { + return iframe && $.globalEval($(iframe[0].body).text()); + } + } + }); + +})); diff --git a/client/js/libs/jquery.scrollTo-min.js b/client/js/libs/jquery.scrollTo-min.js new file mode 100644 index 000000000..5e7877810 --- /dev/null +++ b/client/js/libs/jquery.scrollTo-min.js @@ -0,0 +1,11 @@ +/** + * jQuery.ScrollTo - Easy element scrolling using jQuery. + * Copyright (c) 2007-2009 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com + * Dual licensed under MIT and GPL. + * Date: 5/25/2009 + * @author Ariel Flesler + * @version 1.4.2 + * + * http://flesler.blogspot.com/2007/10/jqueryscrollto.html + */ +;(function(d){var k=d.scrollTo=function(a,i,e){d(window).scrollTo(a,i,e)};k.defaults={axis:'xy',duration:parseFloat(d.fn.jquery)>=1.3?0:1};k.window=function(a){return d(window)._scrollable()};d.fn._scrollable=function(){return this.map(function(){var a=this,i=!a.nodeName||d.inArray(a.nodeName.toLowerCase(),['iframe','#document','html','body'])!=-1;if(!i)return a;var e=(a.contentWindow||a).document||a.ownerDocument||a;return d.browser.safari||e.compatMode=='BackCompat'?e.body:e.documentElement})};d.fn.scrollTo=function(n,j,b){if(typeof j=='object'){b=j;j=0}if(typeof b=='function')b={onAfter:b};if(n=='max')n=9e9;b=d.extend({},k.defaults,b);j=j||b.speed||b.duration;b.queue=b.queue&&b.axis.length>1;if(b.queue)j/=2;b.offset=p(b.offset);b.over=p(b.over);return this._scrollable().each(function(){var q=this,r=d(q),f=n,s,g={},u=r.is('html,body');switch(typeof f){case'number':case'string':if(/^([+-]=)?\d+(\.\d+)?(px|%)?$/.test(f)){f=p(f);break}f=d(f,this);case'object':if(f.is||f.style)s=(f=d(f)).offset()}d.each(b.axis.split(''),function(a,i){var e=i=='x'?'Left':'Top',h=e.toLowerCase(),c='scroll'+e,l=q[c],m=k.max(q,i);if(s){g[c]=s[h]+(u?0:l-r.offset()[h]);if(b.margin){g[c]-=parseInt(f.css('margin'+e))||0;g[c]-=parseInt(f.css('border'+e+'Width'))||0}g[c]+=b.offset[h]||0;if(b.over[h])g[c]+=f[i=='x'?'width':'height']()*b.over[h]}else{var o=f[h];g[c]=o.slice&&o.slice(-1)=='%'?parseFloat(o)/100*m:o}if(/^\d+$/.test(g[c]))g[c]=g[c]<=0?0:Math.min(g[c],m);if(!a&&b.queue){if(l!=g[c])t(b.onAfterFirst);delete g[c]}});t(b.onAfter);function t(a){r.animate(g,j,b.easing,a&&function(){a.call(this,n,b)})}}).end()};k.max=function(a,i){var e=i=='x'?'Width':'Height',h='scroll'+e;if(!d(a).is('html,body'))return a[h]-d(a)[e.toLowerCase()]();var c='client'+e,l=a.ownerDocument.documentElement,m=a.ownerDocument.body;return Math.max(l[h],m[h])-Math.min(l[c],m[c])};function p(a){return typeof a=='object'?a:{top:a,left:a}}})(jQuery); \ No newline at end of file diff --git a/client/js/libs/jquery.timeago.js b/client/js/libs/jquery.timeago.js new file mode 100644 index 000000000..9b05e35da --- /dev/null +++ b/client/js/libs/jquery.timeago.js @@ -0,0 +1,214 @@ +/** + * Timeago is a jQuery plugin that makes it easy to support automatically + * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago"). + * + * @name timeago + * @version 1.4.1 + * @requires jQuery v1.2.3+ + * @author Ryan McGeary + * @license MIT License - http://www.opensource.org/licenses/mit-license.php + * + * For usage and examples, visit: + * http://timeago.yarp.com/ + * + * Copyright (c) 2008-2013, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org) + */ + +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else { + // Browser globals + factory(jQuery); + } +}(function ($) { + $.timeago = function(timestamp) { + if (timestamp instanceof Date) { + return inWords(timestamp); + } else if (typeof timestamp === "string") { + return inWords($.timeago.parse(timestamp)); + } else if (typeof timestamp === "number") { + return inWords(new Date(timestamp)); + } else { + return inWords($.timeago.datetime(timestamp)); + } + }; + var $t = $.timeago; + $.extend($.timeago, { + settings: { + refreshMillis: 60000, + allowPast: true, + allowFuture: false, + localeTitle: false, + cutoff: 0, + strings: { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "ago", + suffixFromNow: "from now", + inPast: 'any moment now', + seconds: "less than a minute", + minute: "about a minute", + minutes: "%d minutes", + hour: "about an hour", + hours: "about %d hours", + day: "a day", + days: "%d days", + month: "about a month", + months: "%d months", + year: "about a year", + years: "%d years", + wordSeparator: " ", + numbers: [] + } + }, + + inWords: function(distanceMillis) { + if(!this.settings.allowPast && ! this.settings.allowFuture) { + throw 'timeago allowPast and allowFuture settings can not both be set to false.'; + } + + var $l = this.settings.strings; + var prefix = $l.prefixAgo; + var suffix = $l.suffixAgo; + if (this.settings.allowFuture) { + if (distanceMillis < 0) { + prefix = $l.prefixFromNow; + suffix = $l.suffixFromNow; + } + } + + if(!this.settings.allowPast && distanceMillis >= 0) { + return this.settings.strings.inPast; + } + + var seconds = Math.abs(distanceMillis) / 1000; + var minutes = seconds / 60; + var hours = minutes / 60; + var days = hours / 24; + var years = days / 365; + + function substitute(stringOrFunction, number) { + var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction; + var value = ($l.numbers && $l.numbers[number]) || number; + return string.replace(/%d/i, value); + } + + var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) || + seconds < 90 && substitute($l.minute, 1) || + minutes < 45 && substitute($l.minutes, Math.round(minutes)) || + minutes < 90 && substitute($l.hour, 1) || + hours < 24 && substitute($l.hours, Math.round(hours)) || + hours < 42 && substitute($l.day, 1) || + days < 30 && substitute($l.days, Math.round(days)) || + days < 45 && substitute($l.month, 1) || + days < 365 && substitute($l.months, Math.round(days / 30)) || + years < 1.5 && substitute($l.year, 1) || + substitute($l.years, Math.round(years)); + + var separator = $l.wordSeparator || ""; + if ($l.wordSeparator === undefined) { separator = " "; } + return $.trim([prefix, words, suffix].join(separator)); + }, + + parse: function(iso8601) { + var s = $.trim(iso8601); + s += " " + SITE_TIMEZONE; + s = s.replace(/\.\d+/,""); // remove milliseconds + s = s.replace(/-/,"/").replace(/-/,"/"); + s = s.replace(/T/," ").replace(/Z/," UTC"); + s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400 + s = s.replace(/([\+\-]\d\d)$/," $100"); // +09 -> +0900 + return new Date(s); + }, + datetime: function(elem) { + var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title"); + return $t.parse(iso8601); + }, + isTime: function(elem) { + // jQuery's `is()` doesn't play well with HTML5 in IE + return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time"); + } + }); + + // functions that can be called via $(el).timeago('action') + // init is default when no action is given + // functions are called with context of a single element + var functions = { + init: function(){ + var refresh_el = $.proxy(refresh, this); + refresh_el(); + var $s = $t.settings; + if ($s.refreshMillis > 0) { + this._timeagoInterval = setInterval(refresh_el, $s.refreshMillis); + } + }, + update: function(time){ + var parsedTime = $t.parse(time); + $(this).data('timeago', { datetime: parsedTime }); + if($t.settings.localeTitle) $(this).attr("title", parsedTime.toLocaleString()); + refresh.apply(this); + }, + updateFromDOM: function(){ + $(this).data('timeago', { datetime: $t.parse( $t.isTime(this) ? $(this).attr("datetime") : $(this).attr("title") ) }); + refresh.apply(this); + }, + dispose: function () { + if (this._timeagoInterval) { + window.clearInterval(this._timeagoInterval); + this._timeagoInterval = null; + } + } + }; + + $.fn.timeago = function(action, options) { + var fn = action ? functions[action] : functions.init; + if(!fn){ + throw new Error("Unknown function name '"+ action +"' for timeago"); + } + // each over objects here and call the requested function + this.each(function(){ + fn.call(this, options); + }); + return this; + }; + + function refresh() { + var data = prepareData(this); + var $s = $t.settings; + + if (!isNaN(data.datetime)) { + if ( $s.cutoff == 0 || Math.abs(distance(data.datetime)) < $s.cutoff) { + $(this).text(inWords(data.datetime)); + } + } + return this; + } + + function prepareData(element) { + element = $(element); + if (!element.data("timeago")) { + element.data("timeago", { datetime: $t.datetime(element) }); + var text = $.trim(element.text()); + if ($t.settings.localeTitle) { + element.attr("title", element.data('timeago').datetime.toLocaleString()); + } else if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) { + element.attr("title", text); + } + } + return element.data("timeago"); + } + + function inWords(date) { + return $t.inWords(distance(date)); + } + + function distance(date) { + return (new Date().getTime() - date.getTime()); + } + + // fix for IE6 suckage + document.createElement("abbr"); + document.createElement("time"); +})); diff --git a/client/js/libs/jquery.ui.touch-punch.min.js b/client/js/libs/jquery.ui.touch-punch.min.js new file mode 100644 index 000000000..33d6f97e5 --- /dev/null +++ b/client/js/libs/jquery.ui.touch-punch.min.js @@ -0,0 +1,11 @@ +/* + * jQuery UI Touch Punch 0.2.2 + * + * Copyright 2011, Dave Furfero + * Dual licensed under the MIT or GPL Version 2 licenses. + * + * Depends: + * jquery.ui.widget.js + * jquery.ui.mouse.js + */ +(function(b){b.support.touch="ontouchend" in document;if(!b.support.touch){return;}var c=b.ui.mouse.prototype,e=c._mouseInit,a;function d(g,h){if(g.originalEvent.touches.length>1){return;}g.preventDefault();var i=g.originalEvent.changedTouches[0],f=document.createEvent("MouseEvents");f.initMouseEvent(h,true,true,window,1,i.screenX,i.screenY,i.clientX,i.clientY,false,false,false,false,0,null);g.target.dispatchEvent(f);}c._touchStart=function(g){var f=this;if(a||!f._mouseCapture(g.originalEvent.changedTouches[0])){return;}a=true;f._touchMoved=false;d(g,"mouseover");d(g,"mousemove");d(g,"mousedown");};c._touchMove=function(f){if(!a){return;}this._touchMoved=true;d(f,"mousemove");};c._touchEnd=function(f){if(!a){return;}d(f,"mouseup");d(f,"mouseout");if(!this._touchMoved){d(f,"click");}a=false;};c._mouseInit=function(){var f=this;f.element.bind("touchstart",b.proxy(f,"_touchStart")).bind("touchmove",b.proxy(f,"_touchMove")).bind("touchend",b.proxy(f,"_touchEnd"));e.call(f);};})(jQuery); \ No newline at end of file diff --git a/client/js/libs/jquery.ui.widget.js b/client/js/libs/jquery.ui.widget.js new file mode 100644 index 000000000..2d370893a --- /dev/null +++ b/client/js/libs/jquery.ui.widget.js @@ -0,0 +1,530 @@ +/* + * jQuery UI Widget 1.10.3+amd + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/jQuery.widget/ + */ + +(function (factory) { + if (typeof define === "function" && define.amd) { + // Register as an anonymous AMD module: + define(["jquery"], factory); + } else { + // Browser globals: + factory(jQuery); + } +}(function( $, undefined ) { + +var uuid = 0, + slice = Array.prototype.slice, + _cleanData = $.cleanData; +$.cleanData = function( elems ) { + for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { + try { + $( elem ).triggerHandler( "remove" ); + // http://bugs.jquery.com/ticket/8235 + } catch( e ) {} + } + _cleanData( elems ); +}; + +$.widget = function( name, base, prototype ) { + var fullName, existingConstructor, constructor, basePrototype, + // proxiedPrototype allows the provided prototype to remain unmodified + // so that it can be used as a mixin for multiple widgets (#8876) + proxiedPrototype = {}, + namespace = name.split( "." )[ 0 ]; + + name = name.split( "." )[ 1 ]; + fullName = namespace + "-" + name; + + if ( !prototype ) { + prototype = base; + base = $.Widget; + } + + // create selector for plugin + $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { + return !!$.data( elem, fullName ); + }; + + $[ namespace ] = $[ namespace ] || {}; + existingConstructor = $[ namespace ][ name ]; + constructor = $[ namespace ][ name ] = function( options, element ) { + // allow instantiation without "new" keyword + if ( !this._createWidget ) { + return new constructor( options, element ); + } + + // allow instantiation without initializing for simple inheritance + // must use "new" keyword (the code above always passes args) + if ( arguments.length ) { + this._createWidget( options, element ); + } + }; + // extend with the existing constructor to carry over any static properties + $.extend( constructor, existingConstructor, { + version: prototype.version, + // copy the object used to create the prototype in case we need to + // redefine the widget later + _proto: $.extend( {}, prototype ), + // track widgets that inherit from this widget in case this widget is + // redefined after a widget inherits from it + _childConstructors: [] + }); + + basePrototype = new base(); + // we need to make the options hash a property directly on the new instance + // otherwise we'll modify the options hash on the prototype that we're + // inheriting from + basePrototype.options = $.widget.extend( {}, basePrototype.options ); + $.each( prototype, function( prop, value ) { + if ( !$.isFunction( value ) ) { + proxiedPrototype[ prop ] = value; + return; + } + proxiedPrototype[ prop ] = (function() { + var _super = function() { + return base.prototype[ prop ].apply( this, arguments ); + }, + _superApply = function( args ) { + return base.prototype[ prop ].apply( this, args ); + }; + return function() { + var __super = this._super, + __superApply = this._superApply, + returnValue; + + this._super = _super; + this._superApply = _superApply; + + returnValue = value.apply( this, arguments ); + + this._super = __super; + this._superApply = __superApply; + + return returnValue; + }; + })(); + }); + constructor.prototype = $.widget.extend( basePrototype, { + // TODO: remove support for widgetEventPrefix + // always use the name + a colon as the prefix, e.g., draggable:start + // don't prefix for widgets that aren't DOM-based + widgetEventPrefix: existingConstructor ? basePrototype.widgetEventPrefix : name + }, proxiedPrototype, { + constructor: constructor, + namespace: namespace, + widgetName: name, + widgetFullName: fullName + }); + + // If this widget is being redefined then we need to find all widgets that + // are inheriting from it and redefine all of them so that they inherit from + // the new version of this widget. We're essentially trying to replace one + // level in the prototype chain. + if ( existingConstructor ) { + $.each( existingConstructor._childConstructors, function( i, child ) { + var childPrototype = child.prototype; + + // redefine the child widget using the same prototype that was + // originally used, but inherit from the new version of the base + $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto ); + }); + // remove the list of existing child constructors from the old constructor + // so the old child constructors can be garbage collected + delete existingConstructor._childConstructors; + } else { + base._childConstructors.push( constructor ); + } + + $.widget.bridge( name, constructor ); +}; + +$.widget.extend = function( target ) { + var input = slice.call( arguments, 1 ), + inputIndex = 0, + inputLength = input.length, + key, + value; + for ( ; inputIndex < inputLength; inputIndex++ ) { + for ( key in input[ inputIndex ] ) { + value = input[ inputIndex ][ key ]; + if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { + // Clone objects + if ( $.isPlainObject( value ) ) { + target[ key ] = $.isPlainObject( target[ key ] ) ? + $.widget.extend( {}, target[ key ], value ) : + // Don't extend strings, arrays, etc. with objects + $.widget.extend( {}, value ); + // Copy everything else by reference + } else { + target[ key ] = value; + } + } + } + } + return target; +}; + +$.widget.bridge = function( name, object ) { + var fullName = object.prototype.widgetFullName || name; + $.fn[ name ] = function( options ) { + var isMethodCall = typeof options === "string", + args = slice.call( arguments, 1 ), + returnValue = this; + + // allow multiple hashes to be passed on init + options = !isMethodCall && args.length ? + $.widget.extend.apply( null, [ options ].concat(args) ) : + options; + + if ( isMethodCall ) { + this.each(function() { + var methodValue, + instance = $.data( this, fullName ); + if ( !instance ) { + return $.error( "cannot call methods on " + name + " prior to initialization; " + + "attempted to call method '" + options + "'" ); + } + if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) { + return $.error( "no such method '" + options + "' for " + name + " widget instance" ); + } + methodValue = instance[ options ].apply( instance, args ); + if ( methodValue !== instance && methodValue !== undefined ) { + returnValue = methodValue && methodValue.jquery ? + returnValue.pushStack( methodValue.get() ) : + methodValue; + return false; + } + }); + } else { + this.each(function() { + var instance = $.data( this, fullName ); + if ( instance ) { + instance.option( options || {} )._init(); + } else { + $.data( this, fullName, new object( options, this ) ); + } + }); + } + + return returnValue; + }; +}; + +$.Widget = function( /* options, element */ ) {}; +$.Widget._childConstructors = []; + +$.Widget.prototype = { + widgetName: "widget", + widgetEventPrefix: "", + defaultElement: "
    ", + options: { + disabled: false, + + // callbacks + create: null + }, + _createWidget: function( options, element ) { + element = $( element || this.defaultElement || this )[ 0 ]; + this.element = $( element ); + this.uuid = uuid++; + this.eventNamespace = "." + this.widgetName + this.uuid; + this.options = $.widget.extend( {}, + this.options, + this._getCreateOptions(), + options ); + + this.bindings = $(); + this.hoverable = $(); + this.focusable = $(); + + if ( element !== this ) { + $.data( element, this.widgetFullName, this ); + this._on( true, this.element, { + remove: function( event ) { + if ( event.target === element ) { + this.destroy(); + } + } + }); + this.document = $( element.style ? + // element within the document + element.ownerDocument : + // element is window or document + element.document || element ); + this.window = $( this.document[0].defaultView || this.document[0].parentWindow ); + } + + this._create(); + this._trigger( "create", null, this._getCreateEventData() ); + this._init(); + }, + _getCreateOptions: $.noop, + _getCreateEventData: $.noop, + _create: $.noop, + _init: $.noop, + + destroy: function() { + this._destroy(); + // we can probably remove the unbind calls in 2.0 + // all event bindings should go through this._on() + this.element + .unbind( this.eventNamespace ) + // 1.9 BC for #7810 + // TODO remove dual storage + .removeData( this.widgetName ) + .removeData( this.widgetFullName ) + // support: jquery <1.6.3 + // http://bugs.jquery.com/ticket/9413 + .removeData( $.camelCase( this.widgetFullName ) ); + this.widget() + .unbind( this.eventNamespace ) + .removeAttr( "aria-disabled" ) + .removeClass( + this.widgetFullName + "-disabled " + + "ui-state-disabled" ); + + // clean up events and states + this.bindings.unbind( this.eventNamespace ); + this.hoverable.removeClass( "ui-state-hover" ); + this.focusable.removeClass( "ui-state-focus" ); + }, + _destroy: $.noop, + + widget: function() { + return this.element; + }, + + option: function( key, value ) { + var options = key, + parts, + curOption, + i; + + if ( arguments.length === 0 ) { + // don't return a reference to the internal hash + return $.widget.extend( {}, this.options ); + } + + if ( typeof key === "string" ) { + // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } + options = {}; + parts = key.split( "." ); + key = parts.shift(); + if ( parts.length ) { + curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); + for ( i = 0; i < parts.length - 1; i++ ) { + curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; + curOption = curOption[ parts[ i ] ]; + } + key = parts.pop(); + if ( value === undefined ) { + return curOption[ key ] === undefined ? null : curOption[ key ]; + } + curOption[ key ] = value; + } else { + if ( value === undefined ) { + return this.options[ key ] === undefined ? null : this.options[ key ]; + } + options[ key ] = value; + } + } + + this._setOptions( options ); + + return this; + }, + _setOptions: function( options ) { + var key; + + for ( key in options ) { + this._setOption( key, options[ key ] ); + } + + return this; + }, + _setOption: function( key, value ) { + this.options[ key ] = value; + + if ( key === "disabled" ) { + this.widget() + .toggleClass( this.widgetFullName + "-disabled ui-state-disabled", !!value ) + .attr( "aria-disabled", value ); + this.hoverable.removeClass( "ui-state-hover" ); + this.focusable.removeClass( "ui-state-focus" ); + } + + return this; + }, + + enable: function() { + return this._setOption( "disabled", false ); + }, + disable: function() { + return this._setOption( "disabled", true ); + }, + + _on: function( suppressDisabledCheck, element, handlers ) { + var delegateElement, + instance = this; + + // no suppressDisabledCheck flag, shuffle arguments + if ( typeof suppressDisabledCheck !== "boolean" ) { + handlers = element; + element = suppressDisabledCheck; + suppressDisabledCheck = false; + } + + // no element argument, shuffle and use this.element + if ( !handlers ) { + handlers = element; + element = this.element; + delegateElement = this.widget(); + } else { + // accept selectors, DOM elements + element = delegateElement = $( element ); + this.bindings = this.bindings.add( element ); + } + + $.each( handlers, function( event, handler ) { + function handlerProxy() { + // allow widgets to customize the disabled handling + // - disabled as an array instead of boolean + // - disabled class as method for disabling individual parts + if ( !suppressDisabledCheck && + ( instance.options.disabled === true || + $( this ).hasClass( "ui-state-disabled" ) ) ) { + return; + } + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + + // copy the guid so direct unbinding works + if ( typeof handler !== "string" ) { + handlerProxy.guid = handler.guid = + handler.guid || handlerProxy.guid || $.guid++; + } + + var match = event.match( /^(\w+)\s*(.*)$/ ), + eventName = match[1] + instance.eventNamespace, + selector = match[2]; + if ( selector ) { + delegateElement.delegate( selector, eventName, handlerProxy ); + } else { + element.bind( eventName, handlerProxy ); + } + }); + }, + + _off: function( element, eventName ) { + eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + this.eventNamespace; + element.unbind( eventName ).undelegate( eventName ); + }, + + _delay: function( handler, delay ) { + function handlerProxy() { + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + var instance = this; + return setTimeout( handlerProxy, delay || 0 ); + }, + + _hoverable: function( element ) { + this.hoverable = this.hoverable.add( element ); + this._on( element, { + mouseenter: function( event ) { + $( event.currentTarget ).addClass( "ui-state-hover" ); + }, + mouseleave: function( event ) { + $( event.currentTarget ).removeClass( "ui-state-hover" ); + } + }); + }, + + _focusable: function( element ) { + this.focusable = this.focusable.add( element ); + this._on( element, { + focusin: function( event ) { + $( event.currentTarget ).addClass( "ui-state-focus" ); + }, + focusout: function( event ) { + $( event.currentTarget ).removeClass( "ui-state-focus" ); + } + }); + }, + + _trigger: function( type, event, data ) { + var prop, orig, + callback = this.options[ type ]; + + data = data || {}; + event = $.Event( event ); + event.type = ( type === this.widgetEventPrefix ? + type : + this.widgetEventPrefix + type ).toLowerCase(); + // the original event may come from any element + // so we need to reset the target on the new event + event.target = this.element[ 0 ]; + + // copy original event properties over to the new event + orig = event.originalEvent; + if ( orig ) { + for ( prop in orig ) { + if ( !( prop in event ) ) { + event[ prop ] = orig[ prop ]; + } + } + } + + this.element.trigger( event, data ); + return !( $.isFunction( callback ) && + callback.apply( this.element[0], [ event ].concat( data ) ) === false || + event.isDefaultPrevented() ); + } +}; + +$.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { + $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { + if ( typeof options === "string" ) { + options = { effect: options }; + } + var hasOptions, + effectName = !options ? + method : + options === true || typeof options === "number" ? + defaultEffect : + options.effect || defaultEffect; + options = options || {}; + if ( typeof options === "number" ) { + options = { duration: options }; + } + hasOptions = !$.isEmptyObject( options ); + options.complete = callback; + if ( options.delay ) { + element.delay( options.delay ); + } + if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) { + element[ method ]( options ); + } else if ( effectName !== method && element[ effectName ] ) { + element[ effectName ]( options.duration, options.easing, callback ); + } else { + element.queue(function( next ) { + $( this )[ method ](); + if ( callback ) { + callback.call( element[ 0 ] ); + } + next(); + }); + } + }; +}); + +})); diff --git a/client/js/libs/less-1.3.0.min.js b/client/js/libs/less-1.3.0.min.js new file mode 100644 index 000000000..309bf550d --- /dev/null +++ b/client/js/libs/less-1.3.0.min.js @@ -0,0 +1,9 @@ +// +// LESS - Leaner CSS v1.3.0 +// http://lesscss.org +// +// Copyright (c) 2009-2011, Alexis Sellier +// Licensed under the Apache 2.0 License. +// +(function(a,b){function c(b){return a.less[b.split("/")[1]]}function l(){var a=document.getElementsByTagName("style");for(var b=0;b0?d.firstChild.nodeValue!==a.nodeValue&&d.replaceChild(a,d.firstChild):d.appendChild(a)})(document.createTextNode(a));c&&g&&(t("saving "+e+" to cache."),g.setItem(e,a),g.setItem(e+":timestamp",c))}function q(a,b,c,e){function i(b,c,d){b.status>=200&&b.status<300?c(b.responseText,b.getResponseHeader("Last-Modified")):typeof d=="function"&&d(b.status,a)}var g=r(),h=f?!1:d.async;typeof g.overrideMimeType=="function"&&g.overrideMimeType("text/css"),g.open("GET",a,h),g.setRequestHeader("Accept",b||"text/x-less, text/css; q=0.9, */*; q=0.5"),g.send(null),f?g.status===0||g.status>=200&&g.status<300?c(g.responseText):e(g.status,a):h?g.onreadystatechange=function(){g.readyState==4&&i(g,c,e)}:i(g,c,e)}function r(){if(a.XMLHttpRequest)return new XMLHttpRequest;try{return new ActiveXObject("MSXML2.XMLHTTP.3.0")}catch(b){return t("browser doesn't support AJAX."),null}}function s(a){return a&&a.parentNode.removeChild(a)}function t(a){d.env=="development"&&typeof console!="undefined"&&console.log("less: "+a)}function u(a,b){var c="less-error-message:"+o(b),e='
  • {content}
  • ',f=document.createElement("div"),g,h,i=[],j=a.filename||b;f.id=c,f.className="less-error-message",h="

    "+(a.message||"There is an error in your .less file")+"

    "+'

    in '+j+" ";var k=function(a,b,c){a.extract[b]&&i.push(e.replace(/\{line\}/,parseInt(a.line)+(b-1)).replace(/\{class\}/,c).replace(/\{content\}/,a.extract[b]))};a.stack?h+="
    "+a.stack.split("\n").slice(1).join("
    "):a.extract&&(k(a,0,""),k(a,1,"line"),k(a,2,""),h+="on line "+a.line+", column "+(a.column+1)+":

    "+"
      "+i.join("")+"
    "),f.innerHTML=h,p([".less-error-message ul, .less-error-message li {","list-style-type: none;","margin-right: 15px;","padding: 4px 0;","margin: 0;","}",".less-error-message label {","font-size: 12px;","margin-right: 15px;","padding: 4px 0;","color: #cc7777;","}",".less-error-message pre {","color: #dd6666;","padding: 4px 0;","margin: 0;","display: inline-block;","}",".less-error-message pre.line {","color: #ff0000;","}",".less-error-message h3 {","font-size: 20px;","font-weight: bold;","padding: 15px 0 5px 0;","margin: 0;","}",".less-error-message a {","color: #10a","}",".less-error-message .error {","color: red;","font-weight: bold;","padding-bottom: 2px;","border-bottom: 1px dashed red;","}"].join("\n"),{title:"error-message"}),f.style.cssText=["font-family: Arial, sans-serif","border: 1px solid #e00","background-color: #eee","border-radius: 5px","-webkit-border-radius: 5px","-moz-border-radius: 5px","color: #e00","padding: 15px","margin-bottom: 15px"].join(";"),d.env=="development"&&(g=setInterval(function(){document.body&&(document.getElementById(c)?document.body.replaceChild(f,document.getElementById(c)):document.body.insertBefore(f,document.body.firstChild),clearInterval(g))},10))}typeof define=="function"&&define.amd&&define("less",[],function(){return d}),Array.isArray||(Array.isArray=function(a){return Object.prototype.toString.call(a)==="[object Array]"||a instanceof Array}),Array.prototype.forEach||(Array.prototype.forEach=function(a,b){var c=this.length>>>0;for(var d=0;d>>0,c=new Array(b),d=arguments[1];for(var e=0;e>>0,c=0;if(b===0&&arguments.length===1)throw new TypeError;if(arguments.length>=2)var d=arguments[1];else do{if(c in this){d=this[c++];break}if(++c>=b)throw new TypeError}while(!0);for(;c=b)return-1;c<0&&(c+=b);for(;cl&&(k[g]=k[g].slice(f-l),l=f)}function t(a){var c,d,e,h,i,j,n,o;if(a instanceof Function)return a.call(m.parsers);if(typeof a=="string")c=b.charAt(f)===a?a:null,e=1,s();else{s();if(c=a.exec(k[g]))e=c[0].length;else return null}if(c){o=f+=e,j=f+k[g].length-e;while(f=0&&b.charAt(c)!=="\n";c--)d++;return{line:typeof a=="number"?(b.slice(0,a).match(/\n/g)||"").length:null,column:d}}function A(a,b){var c=y(a,b),d=z(a.index,c),e=d.line,f=d.column,g=c.split("\n");this.type=a.type||"Syntax",this.message=a.message,this.filename=a.filename||b.filename,this.index=a.index,this.line=typeof e=="number"?e+1:null,this.callLine=a.call&&z(a.call,c).line+1,this.callExtract=g[z(a.call,c).line],this.stack=a.stack,this.column=f,this.extract=[g[e-1],g[e],g[e+1]]}var b,f,g,h,i,j,k,l,m,n=this,o=function(){},p=this.imports={paths:a&&a.paths||[],queue:[],files:{},contents:{},mime:a&&a.mime,error:null,push:function(b,c){var e=this;this.queue.push(b),d.Parser.importer(b,this.paths,function(a,d,f){e.queue.splice(e.queue.indexOf(b),1),e.files[b]=d,e.contents[b]=f,a&&!e.error&&(e.error=a),c(a,d),e.queue.length===0&&o()},a)}};return this.env=a=a||{},this.optimization="optimization"in this.env?this.env.optimization:1,this.env.filename=this.env.filename||null,m={imports:p,parse:function(h,i){var n,p,q,r,s,u,v=[],w,x=null;f=g=l=j=0,b=h.replace(/\r\n/g,"\n"),k=function(c){var d=0,e=/[^"'`\{\}\/\(\)\\]+/g,f=/\/\*(?:[^*]|\*+[^\/*])*\*+\/|\/\/.*/g,g=/"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'|`((?:[^`\\\r\n]|\\.)*)`/g,h=0,i,j=c[0],k;for(var l=0,m,n;l0&&(x=new A({index:l,type:"Parse",message:"missing closing `}`",filename:a.filename},a)),c.map(function(a){return a.join("")})}([[]]);if(x)return i(x);try{n=new e.Ruleset([],t(this.parsers.primary)),n.root=!0}catch(y){return i(new A(y,a))}n.toCSS=function(b){var f,g,h;return function(f,g){var h=[],i;f=f||{},typeof g=="object"&&!Array.isArray(g)&&(g=Object.keys(g).map(function(a){var b=g[a];return b instanceof e.Value||(b instanceof e.Expression||(b=new e.Expression([b])),b=new e.Value([b])),new e.Rule("@"+a,b,!1,0)}),h=[new e.Ruleset(null,g)]);try{var j=b.call(this,{frames:h}).toCSS([],{compress:f.compress||!1})}catch(k){throw new A(k,a)}if(i=m.imports.error)throw i instanceof A?i:new A(i,a);return f.yuicompress&&d.mode==="node"?c("./cssmin").compressor.cssmin(j):f.compress?j.replace(/(\s)+/g,"$1"):j}}(n.eval);if(f=0&&b.charAt(z)!=="\n";z--)B++;x={type:"Parse",message:"Syntax Error on line "+s,index:f,filename:a.filename,line:s,column:B,extract:[u[s-2],u[s-1],u[s]]}}this.imports.queue.length>0?o=function(){i(x,n)}:i(x,n)},parsers:{primary:function(){var a,b=[];while((a=t(this.mixin.definition)||t(this.rule)||t(this.ruleset)||t(this.mixin.call)||t(this.comment)||t(this.directive))||t(/^[\s\n]+/))a&&b.push(a);return b},comment:function(){var a;if(b.charAt(f)!=="/")return;if(b.charAt(f+1)==="/")return new e.Comment(t(/^\/\/.*/),!0);if(a=t(/^\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/))return new e.Comment(a)},entities:{quoted:function(){var a,c=f,d;b.charAt(c)==="~"&&(c++,d=!0);if(b.charAt(c)!=='"'&&b.charAt(c)!=="'")return;d&&t("~");if(a=t(/^"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'/))return new e.Quoted(a[0],a[1]||a[2],d)},keyword:function(){var a;if(a=t(/^[_A-Za-z-][_A-Za-z0-9-]*/))return e.colors.hasOwnProperty(a)?new e.Color(e.colors[a].slice(1)):new e.Keyword(a)},call:function(){var b,c,d=f;if(!(b=/^([\w-]+|%|progid:[\w\.]+)\(/.exec(k[g])))return;b=b[1].toLowerCase();if(b==="url")return null;f+=b.length;if(b==="alpha")return t(this.alpha);t("("),c=t(this.entities.arguments);if(!t(")"))return;if(b)return new e.Call(b,c,d,a.filename)},arguments:function(){var a=[],b;while(b=t(this.entities.assignment)||t(this.expression)){a.push(b);if(!t(","))break}return a},literal:function(){return t(this.entities.dimension)||t(this.entities.color)||t(this.entities.quoted)},assignment:function(){var a,b;if((a=t(/^\w+(?=\s?=)/i))&&t("=")&&(b=t(this.entity)))return new e.Assignment(a,b)},url:function(){var a;if(b.charAt(f)!=="u"||!t(/^url\(/))return;return a=t(this.entities.quoted)||t(this.entities.variable)||t(this.entities.dataURI)||t(/^[-\w%@$\/.&=:;#+?~]+/)||"",u(")"),new e.URL(a.value||a.data||a instanceof e.Variable?a:new e.Anonymous(a),p.paths)},dataURI:function(){var a;if(t(/^data:/)){a={},a.mime=t(/^[^\/]+\/[^,;)]+/)||"",a.charset=t(/^;\s*charset=[^,;)]+/)||"",a.base64=t(/^;\s*base64/)||"",a.data=t(/^,\s*[^)]+/);if(a.data)return a}},variable:function(){var c,d=f;if(b.charAt(f)==="@"&&(c=t(/^@@?[\w-]+/)))return new e.Variable(c,d,a.filename)},color:function(){var a;if(b.charAt(f)==="#"&&(a=t(/^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})/)))return new e.Color(a[1])},dimension:function(){var a,c=b.charCodeAt(f);if(c>57||c<45||c===47)return;if(a=t(/^(-?\d*\.?\d+)(px|%|em|rem|pc|ex|in|deg|s|ms|pt|cm|mm|rad|grad|turn)?/))return new e.Dimension(a[1],a[2])},javascript:function(){var a,c=f,d;b.charAt(c)==="~"&&(c++,d=!0);if(b.charAt(c)!=="`")return;d&&t("~");if(a=t(/^`([^`]*)`/))return new e.JavaScript(a[1],f,d)}},variable:function(){var a;if(b.charAt(f)==="@"&&(a=t(/^(@[\w-]+)\s*:/)))return a[1]},shorthand:function(){var a,b;if(!w(/^[@\w.%-]+\/[@\w.-]+/))return;if((a=t(this.entity))&&t("/")&&(b=t(this.entity)))return new e.Shorthand(a,b)},mixin:{call:function(){var c=[],d,g,h,i=f,j=b.charAt(f),k=!1;if(j!=="."&&j!=="#")return;while(d=t(/^[#.](?:[\w-]|\\(?:[a-fA-F0-9]{1,6} ?|[^a-fA-F0-9]))+/))c.push(new e.Element(g,d,f)),g=t(">");t("(")&&(h=t(this.entities.arguments))&&t(")"),t(this.important)&&(k=!0);if(c.length>0&&(t(";")||w("}")))return new e.mixin.Call(c,h||[],i,a.filename,k)},definition:function(){var a,c=[],d,g,h,i,j,k=!1;if(b.charAt(f)!=="."&&b.charAt(f)!=="#"||w(/^[^{]*(;|})/))return;q();if(d=t(/^([#.](?:[\w-]|\\(?:[a-fA-F0-9]{1,6} ?|[^a-fA-F0-9]))+)\s*\(/)){a=d[1];do{if(b.charAt(f)==="."&&t(/^\.{3}/)){k=!0;break}if(!(h=t(this.entities.variable)||t(this.entities.literal)||t(this.entities.keyword)))break;if(h instanceof e.Variable)if(t(":"))i=u(this.expression,"expected expression"),c.push({name:h.name,value:i});else{if(t(/^\.{3}/)){c.push({name:h.name,variadic:!0}),k=!0;break}c.push({name:h.name})}else c.push({value:h})}while(t(","));u(")"),t(/^when/)&&(j=u(this.conditions,"expected condition")),g=t(this.block);if(g)return new e.mixin.Definition(a,c,g,j,k);r()}}},entity:function(){return t(this.entities.literal)||t(this.entities.variable)||t(this.entities.url)||t(this.entities.call)||t(this.entities.keyword)||t(this.entities.javascript)||t(this.comment)},end:function(){return t(";")||w("}")},alpha:function(){var a;if(!t(/^\(opacity=/i))return;if(a=t(/^\d+/)||t(this.entities.variable))return u(")"),new e.Alpha(a)},element:function(){var a,b,c,d;c=t(this.combinator),a=t(/^(?:\d+\.\d+|\d+)%/)||t(/^(?:[.#]?|:*)(?:[\w-]|\\(?:[a-fA-F0-9]{1,6} ?|[^a-fA-F0-9]))+/)||t("*")||t(this.attribute)||t(/^\([^)@]+\)/),a||t("(")&&(d=t(this.entities.variable))&&t(")")&&(a=new e.Paren(d));if(a)return new e.Element(c,a,f);if(c.value&&c.value.charAt(0)==="&")return new e.Element(c,null,f)},combinator:function(){var a,c=b.charAt(f);if(c===">"||c==="+"||c==="~"){f++;while(b.charAt(f)===" ")f++;return new e.Combinator(c)}if(c==="&"){a="&",f++,b.charAt(f)===" "&&(a="& ");while(b.charAt(f)===" ")f++;return new e.Combinator(a)}return b.charAt(f-1)===" "?new e.Combinator(" "):new e.Combinator(null)},selector:function(){var a,c,d=[],g,h;if(t("("))return a=t(this.entity),u(")"),new e.Selector([new e.Element("",a,f)]);while(c=t(this.element)){g=b.charAt(f),d.push(c);if(g==="{"||g==="}"||g===";"||g===",")break}if(d.length>0)return new e.Selector(d)},tag:function(){return t(/^[a-zA-Z][a-zA-Z-]*[0-9]?/)||t("*")},attribute:function(){var a="",b,c,d;if(!t("["))return;if(b=t(/^[a-zA-Z-]+/)||t(this.entities.quoted))(d=t(/^[|~*$^]?=/))&&(c=t(this.entities.quoted)||t(/^[\w-]+/))?a=[b,d,c.toCSS?c.toCSS():c].join(""):a=b;if(!t("]"))return;if(a)return"["+a+"]"},block:function(){var a;if(t("{")&&(a=t(this.primary))&&t("}"))return a},ruleset:function(){var b=[],c,d,g;q();while(c=t(this.selector)){b.push(c),t(this.comment);if(!t(","))break;t(this.comment)}if(b.length>0&&(d=t(this.block)))return new e.Ruleset(b,d,a.strictImports);j=f,r()},rule:function(){var a,c,d=b.charAt(f),h,l;q();if(d==="."||d==="#"||d==="&")return;if(a=t(this.variable)||t(this.property)){a.charAt(0)!="@"&&(l=/^([^@+\/'"*`(;{}-]*);/.exec(k[g]))?(f+=l[0].length-1,c=new e.Anonymous(l[1])):a==="font"?c=t(this.font):c=t(this.value),h=t(this.important);if(c&&t(this.end))return new e.Rule(a,c,h,i);j=f,r()}},"import":function(){var a,b,c=f;if(t(/^@import\s+/)&&(a=t(this.entities.quoted)||t(this.entities.url))){b=t(this.mediaFeatures);if(t(";"))return new e.Import(a,p,b,c)}},mediaFeature:function(){var a,b,c=[];do if(a=t(this.entities.keyword))c.push(a);else if(t("(")){b=t(this.property),a=t(this.entity);if(!t(")"))return null;if(b&&a)c.push(new e.Paren(new e.Rule(b,a,null,f,!0)));else if(a)c.push(new e.Paren(a));else return null}while(a);if(c.length>0)return new e.Expression(c)},mediaFeatures:function(){var a,b=[];do if(a=t(this.mediaFeature)){b.push(a);if(!t(","))break}else if(a=t(this.entities.variable)){b.push(a);if(!t(","))break}while(a);return b.length>0?b:null},media:function(){var a,b;if(t(/^@media/)){a=t(this.mediaFeatures);if(b=t(this.block))return new e.Media(b,a)}},directive:function(){var a,c,d,g,h,i;if(b.charAt(f)!=="@")return;if(c=t(this["import"])||t(this.media))return c;if(a=t(/^@page|@keyframes/)||t(/^@(?:-webkit-|-moz-|-o-|-ms-)[a-z0-9-]+/)){g=(t(/^[^{]+/)||"").trim();if(d=t(this.block))return new e.Directive(a+" "+g,d)}else if(a=t(/^@[-a-z]+/))if(a==="@font-face"){if(d=t(this.block))return new e.Directive(a,d)}else if((c=t(this.entity))&&t(";"))return new e.Directive(a,c)},font:function(){var a=[],b=[],c,d,f,g;while(g=t(this.shorthand)||t(this.entity))b.push(g);a.push(new e.Expression(b));if(t(","))while(g=t(this.expression)){a.push(g);if(!t(","))break}return new e.Value(a)},value:function(){var a,b=[],c;while(a=t(this.expression)){b.push(a);if(!t(","))break}if(b.length>0)return new e.Value(b)},important:function(){if(b.charAt(f)==="!")return t(/^! *important/)},sub:function(){var a;if(t("(")&&(a=t(this.expression))&&t(")"))return a},multiplication:function(){var a,b,c,d;if(a=t(this.operand)){while(!w(/^\/\*/)&&(c=t("/")||t("*"))&&(b=t(this.operand)))d=new e.Operation(c,[d||a,b]);return d||a}},addition:function(){var a,c,d,g;if(a=t(this.multiplication)){while((d=t(/^[-+]\s+/)||b.charAt(f-1)!=" "&&(t("+")||t("-")))&&(c=t(this.multiplication)))g=new e.Operation(d,[g||a,c]);return g||a}},conditions:function(){var a,b,c=f,d;if(a=t(this.condition)){while(t(",")&&(b=t(this.condition)))d=new e.Condition("or",d||a,b,c);return d||a}},condition:function(){var a,b,c,d,g=f,h=!1;t(/^not/)&&(h=!0),u("(");if(a=t(this.addition)||t(this.entities.keyword)||t(this.entities.quoted))return(d=t(/^(?:>=|=<|[<=>])/))?(b=t(this.addition)||t(this.entities.keyword)||t(this.entities.quoted))?c=new e.Condition(d,a,b,g,h):v("expected expression"):c=new e.Condition("=",a,new e.Keyword("true"),g,h),u(")"),t(/^and/)?new e.Condition("and",c,t(this.condition)):c},operand:function(){var a,c=b.charAt(f+1);b.charAt(f)==="-"&&(c==="@"||c==="(")&&(a=t("-"));var d=t(this.sub)||t(this.entities.dimension)||t(this.entities.color)||t(this.entities.variable)||t(this.entities.call);return a?new e.Operation("*",[new e.Dimension(-1),d]):d},expression:function(){var a,b,c=[],d;while(a=t(this.addition)||t(this.entity))c.push(a);if(c.length>0)return new e.Expression(c)},property:function(){var a;if(a=t(/^(\*?-?[-a-z_0-9]+)\s*:/))return a[1]}}}};if(d.mode==="browser"||d.mode==="rhino")d.Parser.importer=function(a,b,c,d){!/^([a-z]+:)?\//.test(a)&&b.length>0&&(a=b[0]+a),n({href:a,title:a,type:d.mime},function(e){e&&typeof d.errback=="function"?d.errback.call(null,a,b,c,d):c.apply(null,arguments)},!0)};(function(a){function b(b){return a.functions.hsla(b.h,b.s,b.l,b.a)}function c(b){if(b instanceof a.Dimension)return parseFloat(b.unit=="%"?b.value/100:b.value);if(typeof b=="number")return b;throw{error:"RuntimeError",message:"color functions take numbers as parameters"}}function d(a){return Math.min(1,Math.max(0,a))}a.functions={rgb:function(a,b,c){return this.rgba(a,b,c,1)},rgba:function(b,d,e,f){var g=[b,d,e].map(function(a){return c(a)}),f=c(f);return new a.Color(g,f)},hsl:function(a,b,c){return this.hsla(a,b,c,1)},hsla:function(a,b,d,e){function h(a){return a=a<0?a+1:a>1?a-1:a,a*6<1?g+(f-g)*a*6:a*2<1?f:a*3<2?g+(f-g)*(2/3-a)*6:g}a=c(a)%360/360,b=c(b),d=c(d),e=c(e);var f=d<=.5?d*(b+1):d+b-d*b,g=d*2-f;return this.rgba(h(a+1/3)*255,h(a)*255,h(a-1/3)*255,e)},hue:function(b){return new a.Dimension(Math.round(b.toHSL().h))},saturation:function(b){return new a.Dimension(Math.round(b.toHSL().s*100),"%")},lightness:function(b){return new a.Dimension(Math.round(b.toHSL().l*100),"%")},alpha:function(b){return new a.Dimension(b.toHSL().a)},saturate:function(a,c){var e=a.toHSL();return e.s+=c.value/100,e.s=d(e.s),b(e)},desaturate:function(a,c){var e=a.toHSL();return e.s-=c.value/100,e.s=d(e.s),b(e)},lighten:function(a,c){var e=a.toHSL();return e.l+=c.value/100,e.l=d(e.l),b(e)},darken:function(a,c){var e=a.toHSL();return e.l-=c.value/100,e.l=d(e.l),b(e)},fadein:function(a,c){var e=a.toHSL();return e.a+=c.value/100,e.a=d(e.a),b(e)},fadeout:function(a,c){var e=a.toHSL();return e.a-=c.value/100,e.a=d(e.a),b(e)},fade:function(a,c){var e=a.toHSL();return e.a=c.value/100,e.a=d(e.a),b(e)},spin:function(a,c){var d=a.toHSL(),e=(d.h+c.value)%360;return d.h=e<0?360+e:e,b(d)},mix:function(b,c,d){var e=d.value/100,f=e*2-1,g=b.toHSL().a-c.toHSL().a,h=((f*g==-1?f:(f+g)/(1+f*g))+1)/2,i=1-h,j=[b.rgb[0]*h+c.rgb[0]*i,b.rgb[1]*h+c.rgb[1]*i,b.rgb[2]*h+c.rgb[2]*i],k=b.alpha*e+c.alpha*(1-e);return new a.Color(j,k)},greyscale:function(b){return this.desaturate(b,new a.Dimension(100))},e:function(b){return new a.Anonymous(b instanceof a.JavaScript?b.evaluated:b)},escape:function(b){return new a.Anonymous(encodeURI(b.value).replace(/=/g,"%3D").replace(/:/g,"%3A").replace(/#/g,"%23").replace(/;/g,"%3B").replace(/\(/g,"%28").replace(/\)/g,"%29"))},"%":function(b){var c=Array.prototype.slice.call(arguments,1),d=b.value;for(var e=0;e255?255:a<0?0:a).toString(16),a.length===1?"0"+a:a}).join("")},operate:function(b,c){var d=[];c instanceof a.Color||(c=c.toColor());for(var e=0;e<3;e++)d[e]=a.operate(b,this.rgb[e],c.rgb[e]);return new a.Color(d,this.alpha+c.alpha)},toHSL:function(){var a=this.rgb[0]/255,b=this.rgb[1]/255,c=this.rgb[2]/255,d=this.alpha,e=Math.max(a,b,c),f=Math.min(a,b,c),g,h,i=(e+f)/2,j=e-f;if(e===f)g=h=0;else{h=i>.5?j/(2-e-f):j/(e+f);switch(e){case a:g=(b-c)/j+(b255?255:a<0?0:a).toString(16),a.length===1?"0"+a:a}).join("")}}}(c("../tree")),function(a){a.Comment=function(a,b){this.value=a,this.silent=!!b},a.Comment.prototype={toCSS:function(a){return a.compress?"":this.value},eval:function(){return this}}}(c("../tree")),function(a){a.Condition=function(a,b,c,d,e){this.op=a.trim(),this.lvalue=b,this.rvalue=c,this.index=d,this.negate=e},a.Condition.prototype.eval=function(a){var b=this.lvalue.eval(a),c=this.rvalue.eval(a),d=this.index,e,e=function(a){switch(a){case"and":return b&&c;case"or":return b||c;default:if(b.compare)e=b.compare(c);else if(c.compare)e=c.compare(b);else throw{type:"Type",message:"Unable to perform comparison",index:d};switch(e){case-1:return a==="<"||a==="=<";case 0:return a==="="||a===">="||a==="=<";case 1:return a===">"||a===">="}}}(this.op);return this.negate?!e:e}}(c("../tree")),function(a){a.Dimension=function(a,b){this.value=parseFloat(a),this.unit=b||null},a.Dimension.prototype={eval:function(){return this},toColor:function(){return new a.Color([this.value,this.value,this.value])},toCSS:function(){var a=this.value+this.unit;return a},operate:function(b,c){return new a.Dimension(a.operate(b,this.value,c.value),this.unit||c.unit)},compare:function(b){return b instanceof a.Dimension?b.value>this.value?-1:b.value":a.compress?">":" > "}[this.value]}}(c("../tree")),function(a){a.Expression=function(a){this.value=a},a.Expression.prototype={eval:function(b){return this.value.length>1?new a.Expression(this.value.map(function(a){return a.eval(b)})):this.value.length===1?this.value[0].eval(b):this},toCSS:function(a){return this.value.map(function(b){return b.toCSS?b.toCSS(a):""}).join(" ")}}}(c("../tree")),function(a){a.Import=function(b,c,d,e){var f=this;this.index=e,this._path=b,this.features=d&&new a.Value(d),b instanceof a.Quoted?this.path=/\.(le?|c)ss(\?.*)?$/.test(b.value)?b.value:b.value+".less":this.path=b.value.value||b.value,this.css=/css(\?.*)?$/.test(this.path),this.css||c.push(this.path,function(b,c){b&&(b.index=e),f.root=c||new a.Ruleset([],[])})},a.Import.prototype={toCSS:function(a){var b=this.features?" "+this.features.toCSS(a):"";return this.css?"@import "+this._path.toCSS()+b+";\n":""},eval:function(b){var c,d=this.features&&this.features.eval(b);if(this.css)return this;c=new a.Ruleset([],this.root.rules.slice(0));for(var e=0;e1){var d=new a.Element("&",null,0),e=[new a.Selector([d])];c=new a.Ruleset(e,b.mediaBlocks),c.multiMedia=!0}return delete b.mediaBlocks,delete b.mediaPath,c},evalNested:function(b){var c,d,e=b.mediaPath.concat([this]);for(c=0;c0;c--)b.splice(c,0,new a.Anonymous("and"));return new a.Expression(b)})),new a.Ruleset([],[])},permute:function(a){if(a.length===0)return[];if(a.length===1)return a[0];var b=[],c=this.permute(a.slice(1));for(var d=0;d0){c=this.arguments&&this.arguments.map(function(b){return b.eval(a)});for(var g=0;gthis.params.length)return!1;if(this.required>0&&c>this.params.length)return!1}if(this.condition&&!this.condition.eval({frames:[this.evalParams(b,a)].concat(b.frames)}))return!1;d=Math.min(c,this.arity);for(var f=0;fe.selectors[g].elements.length?Array.prototype.push.apply(d,e.find(new a.Selector(b.elements.slice(1)),c)):d.push(e);break}}),this._lookups[g]=d)},toCSS:function(b,c){var d=[],e=[],f=[],g=[],h,i;this.root||(b.length===0?g=this.selectors.map(function(a){return[a]}):this.joinSelectors(g,b,this.selectors));for(var j=0;j0&&(h=g.map(function(a){return a.map(function(a){return a.toCSS(c)}).join("").trim()}).join(c.compress?",":",\n"),d.push(h,(c.compress?"{":" {\n ")+e.join(c.compress?"":"\n ")+(c.compress?"}":"\n}\n"))),d.push(f),d.join("")+(c.compress?"\n":"")},joinSelectors:function(a,b,c){for(var d=0;d0&&e.push(new a.Selector(g)),h.length>0&&f.push(new a.Selector(h));for(var l=0;l0&&(b.value=c[0]+(b.value.charAt(0)==="/"?b.value.slice(1):b.value)),this.value=b,this.paths=c)},b.URL.prototype={toCSS:function(){return"url("+(this.attrs?"data:"+this.attrs.mime+this.attrs.charset+this.attrs.base64+this.attrs.data:this.value.toCSS())+")"},eval:function(a){return this.attrs?this:new b.URL(this.value.eval(a),this.paths)}}}(c("../tree")),function(a){a.Value=function(a){this.value=a,this.is="value"},a.Value.prototype={eval:function(b){return this.value.length===1?this.value[0].eval(b):new a.Value(this.value.map(function(a){return a.eval(b)}))},toCSS:function(a){return this.value.map(function(b){return b.toCSS(a)}).join(a.compress?",":", ")}}}(c("../tree")),function(a){a.Variable=function(a,b,c){this.name=a,this.index=b,this.file=c},a.Variable.prototype={eval:function(b){var c,d,e=this.name;e.indexOf("@@")==0&&(e="@"+(new a.Variable(e.slice(1))).eval(b).value);if(c=a.find(b.frames,function(a){if(d=a.variable(e))return d.value.eval(b)}))return c;throw{type:"Name",message:"variable "+e+" is undefined",filename:this.file,index:this.index}}}}(c("../tree")),function(a){a.find=function(a,b){for(var c=0,d;c1?"["+a.value.map(function(a){return a.toCSS(!1)}).join(", ")+"]":a.toCSS(!1)}}(c("./tree"));var f=location.protocol==="file:"||location.protocol==="chrome:"||location.protocol==="chrome-extension:"||location.protocol==="resource:";d.env=d.env||(location.hostname=="127.0.0.1"||location.hostname=="0.0.0.0"||location.hostname=="localhost"||location.port.length>0||f?"development":"production"),d.async=!1,d.poll=d.poll||(f?1e3:1500),d.watch=function(){return this.watchMode=!0},d.unwatch=function(){return this.watchMode=!1},d.env==="development"?(d.optimization=0,/!watch/.test(location.hash)&&d.watch(),d.watchTimer=setInterval(function(){d.watchMode&&m(function(a,b,c,d,e){b&&p(b.toCSS(),d,e.lastModified)})},d.poll)):d.optimization=3;var g;try{g=typeof a.localStorage=="undefined"?null:a.localStorage}catch(h){g=null}var i=document.getElementsByTagName("link"),j=/^text\/(x-)?less$/;d.sheets=[];for(var k=0;k1&&(q=Math.ceil(q*a),r=Math.ceil(r*a))},t=function(){var a=Math.min((d||q)/q,(e||r)/r);1>a&&(q=Math.ceil(q*a),r=Math.ceil(r*a))};return n&&(c=b.getTransformedOptions(c),j=c.left||0,k=c.top||0,c.sourceWidth?(h=c.sourceWidth,void 0!==c.right&&void 0===c.left&&(j=o-h-c.right)):h=o-j-(c.right||0),c.sourceHeight?(i=c.sourceHeight,void 0!==c.bottom&&void 0===c.top&&(k=p-i-c.bottom)):i=p-k-(c.bottom||0),q=h,r=i),d=c.maxWidth,e=c.maxHeight,f=c.minWidth,g=c.minHeight,n&&d&&e&&c.crop?(q=d,r=e,l=h/i-d/e,0>l?(i=e*h/d,void 0===c.top&&void 0===c.bottom&&(k=(p-i)/2)):l>0&&(h=d*i/e,void 0===c.left&&void 0===c.right&&(j=(o-h)/2))):((c.contain||c.cover)&&(f=d=d||f,g=e=e||g),c.cover?(t(),s()):(s(),t())),n?(m.width=q,m.height=r,b.transformCoordinates(m,c),b.renderImageToCanvas(m,a,j,k,h,i,0,0,q,r)):(a.width=q,a.height=r,a)},b.createObjectURL=function(a){return c?c.createObjectURL(a):!1},b.revokeObjectURL=function(a){return c?c.revokeObjectURL(a):!1},b.readFile=function(a,b,c){if(window.FileReader){var d=new FileReader;if(d.onload=d.onerror=b,c=c||"readAsDataURL",d[c])return d[c](a),d}return!1},"function"==typeof define&&define.amd?define(function(){return b}):a.loadImage=b}(this),function(a){"use strict";"function"==typeof define&&define.amd?define(["load-image"],a):a(window.loadImage)}(function(a){"use strict";if(window.navigator&&window.navigator.platform&&/iP(hone|od|ad)/.test(window.navigator.platform)){var b=a.renderImageToCanvas;a.detectSubsampling=function(a){var b,c;return a.width*a.height>1048576?(b=document.createElement("canvas"),b.width=b.height=1,c=b.getContext("2d"),c.drawImage(a,-a.width+1,0),0===c.getImageData(0,0,1,1).data[3]):!1},a.detectVerticalSquash=function(a,b){var c,d,e,f,g,h=a.naturalHeight||a.height,i=document.createElement("canvas"),j=i.getContext("2d");for(b&&(h/=2),i.width=1,i.height=h,j.drawImage(a,0,0),c=j.getImageData(0,0,1,h).data,d=0,e=h,f=h;f>d;)g=c[4*(f-1)+3],0===g?e=f:d=f,f=e+d>>1;return f/h||1},a.renderImageToCanvas=function(c,d,e,f,g,h,i,j,k,l){if("image/jpeg"===d._type){var m,n,o,p,q=c.getContext("2d"),r=document.createElement("canvas"),s=1024,t=r.getContext("2d");if(r.width=s,r.height=s,q.save(),m=a.detectSubsampling(d),m&&(e/=2,f/=2,g/=2,h/=2),n=a.detectVerticalSquash(d,m),m||1!==n){for(f*=n,k=Math.ceil(s*k/g),l=Math.ceil(s*l/h/n),j=0,p=0;h>p;){for(i=0,o=0;g>o;)t.clearRect(0,0,s,s),t.drawImage(d,e,f,g,h,-o,-p,g,h),q.drawImage(r,0,0,s,s,i,j,k,l),o+=s,i+=k;p+=s,j+=l}return q.restore(),c}}return b(c,d,e,f,g,h,i,j,k,l)}}}),function(a){"use strict";"function"==typeof define&&define.amd?define(["load-image"],a):a(window.loadImage)}(function(a){"use strict";var b=a.hasCanvasOption;a.hasCanvasOption=function(a){return b(a)||a.orientation},a.transformCoordinates=function(a,b){var c=a.getContext("2d"),d=a.width,e=a.height,f=b.orientation;if(f)switch(f>4&&(a.width=e,a.height=d),f){case 2:c.translate(d,0),c.scale(-1,1);break;case 3:c.translate(d,e),c.rotate(Math.PI);break;case 4:c.translate(0,e),c.scale(1,-1);break;case 5:c.rotate(.5*Math.PI),c.scale(1,-1);break;case 6:c.rotate(.5*Math.PI),c.translate(0,-e);break;case 7:c.rotate(.5*Math.PI),c.translate(d,-e),c.scale(-1,1);break;case 8:c.rotate(-.5*Math.PI),c.translate(-d,0)}},a.getTransformedOptions=function(a){if(!a.orientation||1===a.orientation)return a;var b,c={};for(b in a)a.hasOwnProperty(b)&&(c[b]=a[b]);switch(a.orientation){case 2:c.left=a.right,c.right=a.left;break;case 3:c.left=a.right,c.top=a.bottom,c.right=a.left,c.bottom=a.top;break;case 4:c.top=a.bottom,c.bottom=a.top;break;case 5:c.left=a.top,c.top=a.left,c.right=a.bottom,c.bottom=a.right;break;case 6:c.left=a.top,c.top=a.right,c.right=a.bottom,c.bottom=a.left;break;case 7:c.left=a.bottom,c.top=a.right,c.right=a.top,c.bottom=a.left;break;case 8:c.left=a.bottom,c.top=a.left,c.right=a.top,c.bottom=a.right}return a.orientation>4&&(c.maxWidth=a.maxHeight,c.maxHeight=a.maxWidth,c.minWidth=a.minHeight,c.minHeight=a.minWidth,c.sourceWidth=a.sourceHeight,c.sourceHeight=a.sourceWidth),c}}),function(a){"use strict";"function"==typeof define&&define.amd?define(["load-image"],a):a(window.loadImage)}(function(a){"use strict";var b=window.Blob&&(Blob.prototype.slice||Blob.prototype.webkitSlice||Blob.prototype.mozSlice);a.blobSlice=b&&function(){var a=this.slice||this.webkitSlice||this.mozSlice;return a.apply(this,arguments)},a.metaDataParsers={jpeg:{65505:[]}},a.parseMetaData=function(b,c,d){d=d||{};var e=this,f=d.maxMetaDataSize||262144,g={},h=!(window.DataView&&b&&b.size>=12&&"image/jpeg"===b.type&&a.blobSlice);(h||!a.readFile(a.blobSlice.call(b,0,f),function(b){var f,h,i,j,k=b.target.result,l=new DataView(k),m=2,n=l.byteLength-4,o=m;if(65496===l.getUint16(0)){for(;n>m&&(f=l.getUint16(m),f>=65504&&65519>=f||65534===f);){if(h=l.getUint16(m+2)+2,m+h>l.byteLength){console.log("Invalid meta data: Invalid segment size.");break}if(i=a.metaDataParsers.jpeg[f])for(j=0;j6&&(g.imageHead=k.slice?k.slice(0,o):new Uint8Array(k).subarray(0,o))}else console.log("Invalid JPEG file: Missing JPEG marker.");c(g)},"readAsArrayBuffer"))&&c(g)}}),function(a){"use strict";"function"==typeof define&&define.amd?define(["load-image","load-image-meta"],a):a(window.loadImage)}(function(a){"use strict";a.ExifMap=function(){return this},a.ExifMap.prototype.map={Orientation:274},a.ExifMap.prototype.get=function(a){return this[a]||this[this.map[a]]},a.getExifThumbnail=function(a,b,c){var d,e,f;if(!c||b+c>a.byteLength)return console.log("Invalid Exif data: Invalid thumbnail data."),void 0;for(d=[],e=0;c>e;e+=1)f=a.getUint8(b+e),d.push((16>f?"0":"")+f.toString(16));return"data:image/jpeg,%"+d.join("%")},a.exifTagTypes={1:{getValue:function(a,b){return a.getUint8(b)},size:1},2:{getValue:function(a,b){return String.fromCharCode(a.getUint8(b))},size:1,ascii:!0},3:{getValue:function(a,b,c){return a.getUint16(b,c)},size:2},4:{getValue:function(a,b,c){return a.getUint32(b,c)},size:4},5:{getValue:function(a,b,c){return a.getUint32(b,c)/a.getUint32(b+4,c)},size:8},9:{getValue:function(a,b,c){return a.getInt32(b,c)},size:4},10:{getValue:function(a,b,c){return a.getInt32(b,c)/a.getInt32(b+4,c)},size:8}},a.exifTagTypes[7]=a.exifTagTypes[1],a.getExifValue=function(b,c,d,e,f,g){var h,i,j,k,l,m,n=a.exifTagTypes[e];if(!n)return console.log("Invalid Exif data: Invalid tag type."),void 0;if(h=n.size*f,i=h>4?c+b.getUint32(d+8,g):d+8,i+h>b.byteLength)return console.log("Invalid Exif data: Invalid data offset."),void 0;if(1===f)return n.getValue(b,i,g);for(j=[],k=0;f>k;k+=1)j[k]=n.getValue(b,i+k*n.size,g);if(n.ascii){for(l="",k=0;ka.byteLength)return console.log("Invalid Exif data: Invalid directory offset."),void 0;if(f=a.getUint16(c,d),g=c+2+12*f,g+4>a.byteLength)return console.log("Invalid Exif data: Invalid directory size."),void 0;for(h=0;f>h;h+=1)this.parseExifTag(a,b,c+2+12*h,d,e);return a.getUint32(g,d)},a.parseExifData=function(b,c,d,e,f){if(!f.disableExif){var g,h,i,j=c+10;if(1165519206===b.getUint32(c+4)){if(j+8>b.byteLength)return console.log("Invalid Exif data: Invalid segment size."),void 0;if(0!==b.getUint16(c+8))return console.log("Invalid Exif data: Missing byte alignment offset."),void 0;switch(b.getUint16(j)){case 18761:g=!0;break;case 19789:g=!1;break;default:return console.log("Invalid Exif data: Invalid byte alignment marker."),void 0}if(42!==b.getUint16(j+2,g))return console.log("Invalid Exif data: Missing TIFF marker."),void 0;h=b.getUint32(j+4,g),e.exif=new a.ExifMap,h=a.parseExifTags(b,j,j+h,g,e),h&&!f.disableExifThumbnail&&(i={exif:{}},h=a.parseExifTags(b,j,j+h,g,i),i.exif[513]&&(e.exif.Thumbnail=a.getExifThumbnail(b,j+i.exif[513],i.exif[514]))),e.exif[34665]&&!f.disableExifSub&&a.parseExifTags(b,j,j+e.exif[34665],g,e),e.exif[34853]&&!f.disableExifGps&&a.parseExifTags(b,j,j+e.exif[34853],g,e)}}},a.metaDataParsers.jpeg[65505].push(a.parseExifData)}),function(a){"use strict";"function"==typeof define&&define.amd?define(["load-image","load-image-exif"],a):a(window.loadImage)}(function(a){"use strict";a.ExifMap.prototype.tags={256:"ImageWidth",257:"ImageHeight",34665:"ExifIFDPointer",34853:"GPSInfoIFDPointer",40965:"InteroperabilityIFDPointer",258:"BitsPerSample",259:"Compression",262:"PhotometricInterpretation",274:"Orientation",277:"SamplesPerPixel",284:"PlanarConfiguration",530:"YCbCrSubSampling",531:"YCbCrPositioning",282:"XResolution",283:"YResolution",296:"ResolutionUnit",273:"StripOffsets",278:"RowsPerStrip",279:"StripByteCounts",513:"JPEGInterchangeFormat",514:"JPEGInterchangeFormatLength",301:"TransferFunction",318:"WhitePoint",319:"PrimaryChromaticities",529:"YCbCrCoefficients",532:"ReferenceBlackWhite",306:"DateTime",270:"ImageDescription",271:"Make",272:"Model",305:"Software",315:"Artist",33432:"Copyright",36864:"ExifVersion",40960:"FlashpixVersion",40961:"ColorSpace",40962:"PixelXDimension",40963:"PixelYDimension",42240:"Gamma",37121:"ComponentsConfiguration",37122:"CompressedBitsPerPixel",37500:"MakerNote",37510:"UserComment",40964:"RelatedSoundFile",36867:"DateTimeOriginal",36868:"DateTimeDigitized",37520:"SubSecTime",37521:"SubSecTimeOriginal",37522:"SubSecTimeDigitized",33434:"ExposureTime",33437:"FNumber",34850:"ExposureProgram",34852:"SpectralSensitivity",34855:"PhotographicSensitivity",34856:"OECF",34864:"SensitivityType",34865:"StandardOutputSensitivity",34866:"RecommendedExposureIndex",34867:"ISOSpeed",34868:"ISOSpeedLatitudeyyy",34869:"ISOSpeedLatitudezzz",37377:"ShutterSpeedValue",37378:"ApertureValue",37379:"BrightnessValue",37380:"ExposureBias",37381:"MaxApertureValue",37382:"SubjectDistance",37383:"MeteringMode",37384:"LightSource",37385:"Flash",37396:"SubjectArea",37386:"FocalLength",41483:"FlashEnergy",41484:"SpatialFrequencyResponse",41486:"FocalPlaneXResolution",41487:"FocalPlaneYResolution",41488:"FocalPlaneResolutionUnit",41492:"SubjectLocation",41493:"ExposureIndex",41495:"SensingMethod",41728:"FileSource",41729:"SceneType",41730:"CFAPattern",41985:"CustomRendered",41986:"ExposureMode",41987:"WhiteBalance",41988:"DigitalZoomRatio",41989:"FocalLengthIn35mmFilm",41990:"SceneCaptureType",41991:"GainControl",41992:"Contrast",41993:"Saturation",41994:"Sharpness",41995:"DeviceSettingDescription",41996:"SubjectDistanceRange",42016:"ImageUniqueID",42032:"CameraOwnerName",42033:"BodySerialNumber",42034:"LensSpecification",42035:"LensMake",42036:"LensModel",42037:"LensSerialNumber",0:"GPSVersionID",1:"GPSLatitudeRef",2:"GPSLatitude",3:"GPSLongitudeRef",4:"GPSLongitude",5:"GPSAltitudeRef",6:"GPSAltitude",7:"GPSTimeStamp",8:"GPSSatellites",9:"GPSStatus",10:"GPSMeasureMode",11:"GPSDOP",12:"GPSSpeedRef",13:"GPSSpeed",14:"GPSTrackRef",15:"GPSTrack",16:"GPSImgDirectionRef",17:"GPSImgDirection",18:"GPSMapDatum",19:"GPSDestLatitudeRef",20:"GPSDestLatitude",21:"GPSDestLongitudeRef",22:"GPSDestLongitude",23:"GPSDestBearingRef",24:"GPSDestBearing",25:"GPSDestDistanceRef",26:"GPSDestDistance",27:"GPSProcessingMethod",28:"GPSAreaInformation",29:"GPSDateStamp",30:"GPSDifferential",31:"GPSHPositioningError"},a.ExifMap.prototype.stringValues={ExposureProgram:{0:"Undefined",1:"Manual",2:"Normal program",3:"Aperture priority",4:"Shutter priority",5:"Creative program",6:"Action program",7:"Portrait mode",8:"Landscape mode"},MeteringMode:{0:"Unknown",1:"Average",2:"CenterWeightedAverage",3:"Spot",4:"MultiSpot",5:"Pattern",6:"Partial",255:"Other"},LightSource:{0:"Unknown",1:"Daylight",2:"Fluorescent",3:"Tungsten (incandescent light)",4:"Flash",9:"Fine weather",10:"Cloudy weather",11:"Shade",12:"Daylight fluorescent (D 5700 - 7100K)",13:"Day white fluorescent (N 4600 - 5400K)",14:"Cool white fluorescent (W 3900 - 4500K)",15:"White fluorescent (WW 3200 - 3700K)",17:"Standard light A",18:"Standard light B",19:"Standard light C",20:"D55",21:"D65",22:"D75",23:"D50",24:"ISO studio tungsten",255:"Other"},Flash:{0:"Flash did not fire",1:"Flash fired",5:"Strobe return light not detected",7:"Strobe return light detected",9:"Flash fired, compulsory flash mode",13:"Flash fired, compulsory flash mode, return light not detected",15:"Flash fired, compulsory flash mode, return light detected",16:"Flash did not fire, compulsory flash mode",24:"Flash did not fire, auto mode",25:"Flash fired, auto mode",29:"Flash fired, auto mode, return light not detected",31:"Flash fired, auto mode, return light detected",32:"No flash function",65:"Flash fired, red-eye reduction mode",69:"Flash fired, red-eye reduction mode, return light not detected",71:"Flash fired, red-eye reduction mode, return light detected",73:"Flash fired, compulsory flash mode, red-eye reduction mode",77:"Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected",79:"Flash fired, compulsory flash mode, red-eye reduction mode, return light detected",89:"Flash fired, auto mode, red-eye reduction mode",93:"Flash fired, auto mode, return light not detected, red-eye reduction mode",95:"Flash fired, auto mode, return light detected, red-eye reduction mode"},SensingMethod:{1:"Undefined",2:"One-chip color area sensor",3:"Two-chip color area sensor",4:"Three-chip color area sensor",5:"Color sequential area sensor",7:"Trilinear sensor",8:"Color sequential linear sensor"},SceneCaptureType:{0:"Standard",1:"Landscape",2:"Portrait",3:"Night scene"},SceneType:{1:"Directly photographed"},CustomRendered:{0:"Normal process",1:"Custom process"},WhiteBalance:{0:"Auto white balance",1:"Manual white balance"},GainControl:{0:"None",1:"Low gain up",2:"High gain up",3:"Low gain down",4:"High gain down"},Contrast:{0:"Normal",1:"Soft",2:"Hard"},Saturation:{0:"Normal",1:"Low saturation",2:"High saturation"},Sharpness:{0:"Normal",1:"Soft",2:"Hard"},SubjectDistanceRange:{0:"Unknown",1:"Macro",2:"Close view",3:"Distant view"},FileSource:{3:"DSC"},ComponentsConfiguration:{0:"",1:"Y",2:"Cb",3:"Cr",4:"R",5:"G",6:"B"},Orientation:{1:"top-left",2:"top-right",3:"bottom-right",4:"bottom-left",5:"left-top",6:"right-top",7:"right-bottom",8:"left-bottom"}},a.ExifMap.prototype.getText=function(a){var b=this.get(a);switch(a){case"LightSource":case"Flash":case"MeteringMode":case"ExposureProgram":case"SensingMethod":case"SceneCaptureType":case"SceneType":case"CustomRendered":case"WhiteBalance":case"GainControl":case"Contrast":case"Saturation":case"Sharpness":case"SubjectDistanceRange":case"FileSource":case"Orientation":return this.stringValues[a][b];case"ExifVersion":case"FlashpixVersion":return String.fromCharCode(b[0],b[1],b[2],b[3]);case"ComponentsConfiguration":return this.stringValues[a][b[0]]+this.stringValues[a][b[1]]+this.stringValues[a][b[2]]+this.stringValues[a][b[3]];case"GPSVersionID":return b[0]+"."+b[1]+"."+b[2]+"."+b[3]}return String(b)},function(a){var b,c=a.tags,d=a.map;for(b in c)c.hasOwnProperty(b)&&(d[c[b]]=b)}(a.ExifMap.prototype),a.ExifMap.prototype.getAll=function(){var a,b,c={};for(a in this)this.hasOwnProperty(a)&&(b=this.tags[a],b&&(c[b]=this.getText(b)));return c}}); \ No newline at end of file diff --git a/client/js/libs/locale.js b/client/js/libs/locale.js new file mode 100644 index 000000000..ea64b0a87 --- /dev/null +++ b/client/js/libs/locale.js @@ -0,0 +1,29 @@ +/* + * jQuery File Upload Plugin Localization Example 6.5.1 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2012, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/*global window */ + +window.locale = { + "fileupload": { + "errors": { + "maxFileSize": "File is too big", + "minFileSize": "File is too small", + "acceptFileTypes": "Filetype not allowed", + "maxNumberOfFiles": "Max number of files exceeded", + "uploadedBytes": "Uploaded bytes exceed file size", + "emptyResult": "Empty file upload result" + }, + "error": "Error", + "start": "Start", + "cancel": "Cancel", + "destroy": "Delete" + } +}; diff --git a/client/js/libs/md5.js b/client/js/libs/md5.js new file mode 100644 index 000000000..f62e63c90 --- /dev/null +++ b/client/js/libs/md5.js @@ -0,0 +1,174 @@ +/* + * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message + * Digest Algorithm, as defined in RFC 1321. + * Copyright (C) Paul Johnston 1999 - 2000. + * Updated by Greg Holt 2000 - 2001. + * See http://pajhome.org.uk/site/legal.html for details. + */ + +/* + * Convert a 32-bit number to a hex string with ls-byte first + */ +var hex_chr = "0123456789abcdef"; +function rhex(num) +{ + str = ""; + for(j = 0; j <= 3; j++) + str += hex_chr.charAt((num >> (j * 8 + 4)) & 0x0F) + + hex_chr.charAt((num >> (j * 8)) & 0x0F); + return str; +} + +/* + * Convert a string to a sequence of 16-word blocks, stored as an array. + * Append padding bits and the length, as described in the MD5 standard. + */ +function str2blks_MD5(str) +{ + nblk = ((str.length + 8) >> 6) + 1; + blks = new Array(nblk * 16); + for(i = 0; i < nblk * 16; i++) blks[i] = 0; + for(i = 0; i < str.length; i++) + blks[i >> 2] |= str.charCodeAt(i) << ((i % 4) * 8); + blks[i >> 2] |= 0x80 << ((i % 4) * 8); + blks[nblk * 16 - 2] = str.length * 8; + return blks; +} + +/* + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ +function add(x, y) +{ + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); +} + +/* + * Bitwise rotate a 32-bit number to the left + */ +function rol(num, cnt) +{ + return (num << cnt) | (num >>> (32 - cnt)); +} + +/* + * These functions implement the basic operation for each round of the + * algorithm. + */ +function cmn(q, a, b, x, s, t) +{ + return add(rol(add(add(a, q), add(x, t)), s), b); +} +function ff(a, b, c, d, x, s, t) +{ + return cmn((b & c) | ((~b) & d), a, b, x, s, t); +} +function gg(a, b, c, d, x, s, t) +{ + return cmn((b & d) | (c & (~d)), a, b, x, s, t); +} +function hh(a, b, c, d, x, s, t) +{ + return cmn(b ^ c ^ d, a, b, x, s, t); +} +function ii(a, b, c, d, x, s, t) +{ + return cmn(c ^ (b | (~d)), a, b, x, s, t); +} + +/* + * Take a string and return the hex representation of its MD5. + */ +function calcMD5(str) +{ + x = str2blks_MD5(str); + a = 1732584193; + b = -271733879; + c = -1732584194; + d = 271733878; + + for(i = 0; i < x.length; i += 16) + { + olda = a; + oldb = b; + oldc = c; + oldd = d; + + a = ff(a, b, c, d, x[i+ 0], 7 , -680876936); + d = ff(d, a, b, c, x[i+ 1], 12, -389564586); + c = ff(c, d, a, b, x[i+ 2], 17, 606105819); + b = ff(b, c, d, a, x[i+ 3], 22, -1044525330); + a = ff(a, b, c, d, x[i+ 4], 7 , -176418897); + d = ff(d, a, b, c, x[i+ 5], 12, 1200080426); + c = ff(c, d, a, b, x[i+ 6], 17, -1473231341); + b = ff(b, c, d, a, x[i+ 7], 22, -45705983); + a = ff(a, b, c, d, x[i+ 8], 7 , 1770035416); + d = ff(d, a, b, c, x[i+ 9], 12, -1958414417); + c = ff(c, d, a, b, x[i+10], 17, -42063); + b = ff(b, c, d, a, x[i+11], 22, -1990404162); + a = ff(a, b, c, d, x[i+12], 7 , 1804603682); + d = ff(d, a, b, c, x[i+13], 12, -40341101); + c = ff(c, d, a, b, x[i+14], 17, -1502002290); + b = ff(b, c, d, a, x[i+15], 22, 1236535329); + + a = gg(a, b, c, d, x[i+ 1], 5 , -165796510); + d = gg(d, a, b, c, x[i+ 6], 9 , -1069501632); + c = gg(c, d, a, b, x[i+11], 14, 643717713); + b = gg(b, c, d, a, x[i+ 0], 20, -373897302); + a = gg(a, b, c, d, x[i+ 5], 5 , -701558691); + d = gg(d, a, b, c, x[i+10], 9 , 38016083); + c = gg(c, d, a, b, x[i+15], 14, -660478335); + b = gg(b, c, d, a, x[i+ 4], 20, -405537848); + a = gg(a, b, c, d, x[i+ 9], 5 , 568446438); + d = gg(d, a, b, c, x[i+14], 9 , -1019803690); + c = gg(c, d, a, b, x[i+ 3], 14, -187363961); + b = gg(b, c, d, a, x[i+ 8], 20, 1163531501); + a = gg(a, b, c, d, x[i+13], 5 , -1444681467); + d = gg(d, a, b, c, x[i+ 2], 9 , -51403784); + c = gg(c, d, a, b, x[i+ 7], 14, 1735328473); + b = gg(b, c, d, a, x[i+12], 20, -1926607734); + + a = hh(a, b, c, d, x[i+ 5], 4 , -378558); + d = hh(d, a, b, c, x[i+ 8], 11, -2022574463); + c = hh(c, d, a, b, x[i+11], 16, 1839030562); + b = hh(b, c, d, a, x[i+14], 23, -35309556); + a = hh(a, b, c, d, x[i+ 1], 4 , -1530992060); + d = hh(d, a, b, c, x[i+ 4], 11, 1272893353); + c = hh(c, d, a, b, x[i+ 7], 16, -155497632); + b = hh(b, c, d, a, x[i+10], 23, -1094730640); + a = hh(a, b, c, d, x[i+13], 4 , 681279174); + d = hh(d, a, b, c, x[i+ 0], 11, -358537222); + c = hh(c, d, a, b, x[i+ 3], 16, -722521979); + b = hh(b, c, d, a, x[i+ 6], 23, 76029189); + a = hh(a, b, c, d, x[i+ 9], 4 , -640364487); + d = hh(d, a, b, c, x[i+12], 11, -421815835); + c = hh(c, d, a, b, x[i+15], 16, 530742520); + b = hh(b, c, d, a, x[i+ 2], 23, -995338651); + + a = ii(a, b, c, d, x[i+ 0], 6 , -198630844); + d = ii(d, a, b, c, x[i+ 7], 10, 1126891415); + c = ii(c, d, a, b, x[i+14], 15, -1416354905); + b = ii(b, c, d, a, x[i+ 5], 21, -57434055); + a = ii(a, b, c, d, x[i+12], 6 , 1700485571); + d = ii(d, a, b, c, x[i+ 3], 10, -1894986606); + c = ii(c, d, a, b, x[i+10], 15, -1051523); + b = ii(b, c, d, a, x[i+ 1], 21, -2054922799); + a = ii(a, b, c, d, x[i+ 8], 6 , 1873313359); + d = ii(d, a, b, c, x[i+15], 10, -30611744); + c = ii(c, d, a, b, x[i+ 6], 15, -1560198380); + b = ii(b, c, d, a, x[i+13], 21, 1309151649); + a = ii(a, b, c, d, x[i+ 4], 6 , -145523070); + d = ii(d, a, b, c, x[i+11], 10, -1120210379); + c = ii(c, d, a, b, x[i+ 2], 15, 718787259); + b = ii(b, c, d, a, x[i+ 9], 21, -343485551); + + a = add(a, olda); + b = add(b, oldb); + c = add(c, oldc); + d = add(d, oldd); + } + return rhex(a) + rhex(b) + rhex(c) + rhex(d); +} \ No newline at end of file diff --git a/client/js/libs/musical.js b/client/js/libs/musical.js new file mode 100644 index 000000000..63e931b79 --- /dev/null +++ b/client/js/libs/musical.js @@ -0,0 +1,1700 @@ +(function(global, module, define) { + +// A variable that can be used to interrupt things. +var interrupted = false; + +// Tests for the presence of HTML5 Web Audio (or webkit's version). +function isAudioPresent() { + return !!(global.AudioContext || global.webkitAudioContext); +} + +// All our audio funnels through the same AudioContext with a +// DynamicsCompressorNode used as the main output, to compress the +// dynamic range of all audio. getAudioTop sets this up. +function getAudioTop() { + if (getAudioTop.audioTop) { return getAudioTop.audioTop; } + if (!isAudioPresent()) { + return null; + } + var ac = new (global.AudioContext || global.webkitAudioContext); + getAudioTop.audioTop = { + ac: ac, + wavetable: makeWavetable(ac), + out: null, + currentStart: null + }; + resetAudio(); + return getAudioTop.audioTop; +} + +// When audio needs to be interrupted globally (e.g., when you press the +// stop button in the IDE), resetAudio does the job. +function resetAudio() { + if (getAudioTop.audioTop) { + var atop = getAudioTop.audioTop; + // Disconnect the top-level node and make a new one. + if (atop.out) { + atop.out.disconnect(); + atop.out = null; + atop.currentStart = null; + } + var dcn = atop.ac.createDynamicsCompressor(); + dcn.ratio = 16; + dcn.attack = 0.0005; + dcn.connect(atop.ac.destination); + atop.out = dcn; + } +} + +// For precise scheduling of future notes, the AudioContext currentTime is +// cached and is held constant until the script releases to the event loop. +function audioCurrentStartTime() { + var atop = getAudioTop(); + if (atop.currentStart != null) { + return atop.currentStart; + } + // A delay could be added below to introduce a universal delay in + // all beginning sounds (without skewing durations for scheduled + // sequences). + atop.currentStart = Math.max(0.25, atop.ac.currentTime /* + 0.0 delay */); + setTimeout(function() { atop.currentStart = null; }, 0); + return atop.currentStart; +} + +// All further details of audio handling are encapsulated in the Instrument +// class, which knows how to synthesize a basic timbre; how to play and +// schedule a tone; and how to parse and sequence a song written in ABC +// notation. +var Instrument = (function() { + // The constructor accepts a timbre string or object, specifying + // its default sound. The main mechanisms in Instrument are for handling + // sequencing of a (potentially large) set of notes over a (potentially + // long) period of time. The overall strategy: + // + // Events: 'noteon' 'noteoff' + // | | + // tone()-(quick tones)->| _startSet -->| _finishSet -->| _cleanupSet -->| + // \ | / | Playing tones | Done tones | + // \---- _queue ------|-/ | + // of future tones |3 secs ahead sent to WebAudio, removed when done + // + // The reason for this queuing is to reduce the complexity of the + // node graph sent to WebAudio: at any time, WebAudio is only + // responsible for about 2 seconds of music. If a graph with too + // too many nodes is sent to WebAudio at once, output distorts badly. + function Instrument(options) { + this._atop = getAudioTop(); // Audio context. + this._timbre = makeTimbre(options, this._atop); // The instrument's timbre. + this._queue = []; // A queue of future tones to play. + this._minQueueTime = Infinity; // The earliest time in _queue. + this._maxScheduledTime = 0; // The latest time in _queue. + this._unsortedQueue = false; // True if _queue is unsorted. + this._startSet = []; // Unstarted tones already sent to WebAudio. + this._finishSet = {}; // Started tones playing in WebAudio. + this._cleanupSet = []; // Tones waiting for cleanup. + this._callbackSet = []; // A set of scheduled callbacks. + this._handlers = {}; // 'noteon' and 'noteoff' handlers. + this._now = null; // A cached current-time value. + if (isAudioPresent()) { + this.silence(); // Initializes top-level audio node. + } + } + + Instrument.dequeueTime = 0.5; // Seconds before an event to reexamine queue. + Instrument.bufferSecs = 2; // Seconds ahead to put notes in WebAudio. + Instrument.toneLength = 1; // Default duration of a tone. + Instrument.cleanupDelay = 0.1; // Silent time before disconnecting nodes. + + // Sets the default timbre for the instrument. See defaultTimbre. + Instrument.prototype.setTimbre = function(t) { + this._timbre = makeTimbre(t, this._atop); // Saves a copy. + }; + + // Returns the default timbre for the instrument as an object. + Instrument.prototype.getTimbre = function(t) { + return makeTimbre(this._timbre, this._atop); // Makes a copy. + }; + + // Sets the overall volume for the instrument immediately. + Instrument.prototype.setVolume = function(v) { + // Without an audio system, volume cannot be set. + if (!this._out) { return; } + if (!isNaN(v)) { + this._out.gain.value = v; + } + }; + + // Sets the overall volume for the instrument. + Instrument.prototype.getVolume = function(v) { + // Without an audio system, volume is stuck at zero. + if (!this._out) { return 0.0; } + return this._out.gain.value; + }; + + // Silences the instrument immediately by reinitializing the audio + // graph for this instrument and emptying or flushing all queues in the + // scheduler. Carefully notifies all notes that have started but not + // yet finished, and sequences that are awaiting scheduled callbacks. + // Does not notify notes that have not yet started. + Instrument.prototype.silence = function() { + var j, finished, callbacks, initvolume = 1; + + // Clear future notes. + this._queue.length = 0; + this._minQueueTime = Infinity; + this._maxScheduledTime = 0; + + // Don't notify notes that haven't started yet. + this._startSet.length = 0; + + // Flush finish callbacks that are promised. + finished = this._finishSet; + this._finishSet = {}; + + // Flush one-time callacks that are promised. + callbacks = this._callbackSet; + this._callbackSet = []; + + // Disconnect the audio graph for this instrument. + if (this._out) { + this._out.disconnect(); + initvolume = this._out.gain.value; + } + + // Reinitialize the audio graph: all audio for the instrument + // multiplexes through a single gain node with a master volume. + this._atop = getAudioTop(); + this._out = this._atop.ac.createGain(); + this._out.gain.value = initvolume; + this._out.connect(this._atop.out); + + // As a last step, call all promised notifications. + for (j in finished) { this._trigger('noteoff', finished[j]); } + for (j = 0; j < callbacks.length; ++j) { callbacks[j].callback(); } + }; + + // Future notes are scheduled relative to now(), which provides + // access to audioCurrentStartTime(), a time that holds steady + // until the script releases to the event loop. When _now is + // non-null, it indicates that scheduling is already in progress. + // The timer-driven _doPoll function clears the cached _now. + Instrument.prototype.now = function() { + if (this._now != null) { + return this._now; + } + this._startPollTimer(true); // passing (true) sets this._now. + return this._now; + }; + + // Register an event handler. Done without jQuery to reduce dependencies. + Instrument.prototype.on = function(eventname, cb) { + if (!this._handlers.hasOwnProperty(eventname)) { + this._handlers[eventname] = []; + } + this._handlers[eventname].push(cb); + }; + + // Unregister an event handler. Done without jQuery to reduce dependencies. + Instrument.prototype.off = function(eventname, cb) { + if (this._handlers.hasOwnProperty(eventname)) { + if (!cb) { + this._handlers[eventname] = []; + } else { + var j, hunt = this._handlers[eventname]; + for (j = 0; j < hunt.length; ++j) { + if (hunt[j] === cb) { + hunt.splice(j, 1); + j -= 1; + } + } + } + } + }; + + // Trigger an event, notifying any registered handlers. + Instrument.prototype._trigger = function(eventname, record) { + var cb = this._handlers[eventname], j; + if (!cb) { return; } + if (cb.length == 1) { + // Special, common case of one handler: no copy needed. + cb[0](record); + return; + } + // Copy the array of callbacks before iterating, because the + // main this._handlers copy could be changed by a handler. + // You get notified if-and-only-if you are registered + // at the starting moment of _trigger. + cb = cb.slice(); + for (j = 0; j < cb.length; ++j) { + cb[j](record); + } + }; + + // Tells the WebAudio API to play a tone (now or soon). The passed + // record specifies a start time and release time, an ADSR envelope, + // and other timbre parameters. This function sets up a WebAudio + // node graph for the tone generators and filters for the tone. + Instrument.prototype._makeSound = function(record) { + var timbre = record.timbre || this._timbre, + starttime = record.time, + releasetime = starttime + record.duration, + attacktime = Math.min(releasetime, starttime + timbre.attack), + decaytime = timbre.decay * + Math.pow(440 / record.frequency, timbre.decayfollow), + decaystarttime = attacktime, + stoptime = releasetime + timbre.release, + doubled = timbre.detune && timbre.detune != 1.0, + amp = timbre.gain * record.velocity * (doubled ? 0.5 : 1.0), + ac = this._atop.ac, + g, f, o, o2, pwave, k, wf, bwf; + // Only hook up tone generators if it is an audible sound. + if (record.duration > 0 && record.velocity > 0) { + g = ac.createGain(); + g.gain.setValueAtTime(0, starttime); + g.gain.linearRampToValueAtTime(amp, attacktime); + // For the beginning of the decay, use linearRampToValue instead + // of setTargetAtTime, because it avoids http://crbug.com/254942. + while (decaystarttime < attacktime + 1/32 && + decaystarttime + 1/256 < releasetime) { + // Just trace out the curve in increments of 1/256 sec + // for up to 1/32 seconds. + decaystarttime += 1/256; + g.gain.linearRampToValueAtTime( + amp * (timbre.sustain + (1 - timbre.sustain) * + Math.exp((attacktime - decaystarttime) / decaytime)), + decaystarttime); + } + // For the rest of the decay, use setTargetAtTime. + g.gain.setTargetAtTime(amp * timbre.sustain, + decaystarttime, decaytime); + // Then at release time, mark the value and ramp to zero. + g.gain.setValueAtTime(amp * (timbre.sustain + (1 - timbre.sustain) * + Math.exp((attacktime - releasetime) / decaytime)), releasetime); + g.gain.linearRampToValueAtTime(0, stoptime); + g.connect(this._out); + // Hook up a low-pass filter if cutoff is specified. + if ((!timbre.cutoff && !timbre.cutfollow) || timbre.cutoff == Infinity) { + f = g; + } else { + // Apply the cutoff frequency adjusted using cutfollow. + f = ac.createBiquadFilter(); + f.frequency.value = + timbre.cutoff + record.frequency * timbre.cutfollow; + f.Q.value = timbre.resonance; + f.connect(g); + } + // Hook up the main oscillator. + o = makeOscillator(this._atop, timbre.wave, record.frequency); + o.connect(f); + o.start(starttime); + o.stop(stoptime); + // Hook up a detuned oscillator. + if (doubled) { + o2 = makeOscillator( + this._atop, timbre.wave, record.frequency * timbre.detune); + o2.connect(f); + o2.start(starttime); + o2.stop(stoptime); + } + // Store nodes in the record so that they can be modified + // in case the tone is truncated later. + record.gainNode = g; + record.oscillators = [o]; + if (doubled) { record.oscillators.push(o2); } + record.cleanuptime = stoptime; + } else { + // Inaudible sounds are scheduled: their purpose is to truncate + // audible tones at the same pitch. But duration is set to zero + // so that they are cleaned up quickly. + record.duration = 0; + } + this._startSet.push(record); + }; + // Truncates a sound previously scheduled by _makeSound by using + // cancelScheduledValues and directly ramping down to zero. + // Can only be used to shorten a sound. + Instrument.prototype._truncateSound = function(record, releasetime) { + if (releasetime < record.time + record.duration) { + record.duration = Math.max(0, releasetime - record.time); + if (record.gainNode) { + var timbre = record.timbre || this._timbre, + starttime = record.time, + attacktime = Math.min(releasetime, starttime + timbre.attack), + decaytime = timbre.decay * + Math.pow(440 / record.frequency, timbre.decayfollow), + stoptime = releasetime + timbre.release, + cleanuptime = stoptime + Instrument.cleanupDelay, + doubled = timbre.detune && timbre.detune != 1.0, + amp = timbre.gain * record.velocity * (doubled ? 0.5 : 1.0), + j, g = record.gainNode; + // Cancel any envelope points after the new releasetime. + g.gain.cancelScheduledValues(releasetime); + if (releasetime <= starttime) { + // Release before start? Totally silence the note. + g.gain.setValueAtTime(0, releasetime); + } else if (releasetime <= attacktime) { + // Release before attack is done? Interrupt ramp up. + g.gain.linearRampToValueAtTime( + amp * (releasetime - starttime) / (attacktime - starttime)); + } else { + // Release during decay? Interrupt decay down. + g.gain.setValueAtTime(amp * (timbre.sustain + (1 - timbre.sustain) * + Math.exp((attacktime - releasetime) / decaytime)), releasetime); + } + // Then ramp down to zero according to record.release. + g.gain.linearRampToValueAtTime(0, stoptime); + // After stoptime, stop the oscillators. This is necessary to + // eliminate extra work for WebAudio for no-longer-audible notes. + if (record.oscillators) { + for (j = 0; j < record.oscillators.length; ++j) { + record.oscillators[j].stop(stoptime); + } + } + // Schedule disconnect. + record.cleanuptime = cleanuptime; + } + } + }; + // The core scheduling loop is managed by Instrument._doPoll. It reads + // the audiocontext's current time and pushes tone records from one + // stage to the next. + // + // 1. The first stage is the _queue, which has tones that have not + // yet been given to WebAudio. This loop scans _queue to find + // notes that need to begin in the next few seconds; then it + // sends those to WebAduio and moves them to _startSet. Because + // scheduled songs can be long, _queue can be large. + // + // 2. Second is _startSet, which has tones that have been given to + // WebAudio, but whose start times have not yet elapsed. When + // the time advances past the start time of a record, a 'noteon' + // notification is fired for the tone, and it is moved to + // _finishSet. + // + // 3. _finishSet represents the notes that are currently sounding. + // The programming model for Instrument is that only one tone of + // a specific frequency may be played at once within a Instrument, + // so only one tone of a given frequency may exist in _finishSet + // at once. When there is a conflict, the sooner-to-end-note + // is truncated. + // + // 4. After a note is released, it may have a litle release time + // (depending on timbre.release), after which the nodes can + // be totally disconnected and cleaned up. _cleanupSet holds + // notes for which we are awaiting cleanup. + Instrument.prototype._doPoll = function() { + this._pollTimer = null; + this._now = null; + if (interrupted) { + this.silence(); + return; + } + // The shortest time we can delay is 1 / 1000 secs, so if an event + // is within the next 0.5 ms, now is the closest moment, and we go + // ahead and process it. + var instant = this._atop.ac.currentTime + (1 / 2000), + callbacks = [], + j, work, when, freq, record, conflict, save, cb; + // Schedule a batch of notes + if (this._minQueueTime - instant <= Instrument.bufferSecs) { + if (this._unsortedQueue) { + this._queue.sort(function(a, b) { + if (a.time != b.time) { return a.time - b.time; } + if (a.duration != b.duration) { return a.duration - b.duration; } + return a.frequency - b.frequency; + }); + this._unsortedQueue = false; + } + for (j = 0; j < this._queue.length; ++j) { + if (this._queue[j].time - instant > Instrument.bufferSecs) { break; } + } + if (j > 0) { + work = this._queue.splice(0, j); + for (j = 0; j < work.length; ++j) { + this._makeSound(work[j]); + } + this._minQueueTime = + (this._queue.length > 0) ? this._queue[0].time : Infinity; + } + } + // Disconnect notes from the cleanup set. + for (j = 0; j < this._cleanupSet.length; ++j) { + record = this._cleanupSet[j]; + if (record.cleanuptime < instant) { + if (record.gainNode) { + // This explicit disconnect is needed or else Chrome's WebAudio + // starts getting overloaded after a couple thousand notes. + record.gainNode.disconnect(); + record.gainNode = null; + } + this._cleanupSet.splice(j, 1); + j -= 1; + } + } + // Notify about any notes finishing. + for (freq in this._finishSet) { + record = this._finishSet[freq]; + when = record.time + record.duration; + if (when <= instant) { + callbacks.push({ + order: [when, 0], + f: this._trigger, t: this, a: ['noteoff', record]}); + if (record.cleanuptime != Infinity) { + this._cleanupSet.push(record); + } + delete this._finishSet[freq]; + } + } + // Call any specific one-time callbacks that were registered. + for (j = 0; j < this._callbackSet.length; ++j) { + cb = this._callbackSet[j]; + when = cb.time; + if (when <= instant) { + callbacks.push({ + order: [when, 1], + f: cb.callback, t: null, a: []}); + this._callbackSet.splice(j, 1); + j -= 1; + } + } + // Notify about any notes starting. + for (j = 0; j < this._startSet.length; ++j) { + if (this._startSet[j].time <= instant) { + save = record = this._startSet[j]; + freq = record.frequency; + conflict = null; + if (this._finishSet.hasOwnProperty(freq)) { + // If there is already a note at the same frequency playing, + // then release the one that starts first, immediately. + conflict = this._finishSet[freq]; + if (conflict.time < record.time || (conflict.time == record.time && + conflict.duration < record.duration)) { + // Our new sound conflicts with an old one: end the old one + // and notify immediately of its noteoff event. + this._truncateSound(conflict, record.time); + callbacks.push({ + order: [record.time, 0], + f: this._trigger, t: this, a: ['noteoff', conflict]}); + delete this._finishSet[freq]; + } else { + // A conflict from the future has already scheduled, + // so our own note shouldn't sound. Truncate ourselves + // immediately, and suppress our own noteon and noteoff. + this._truncateSound(record, conflict.time); + conflict = record; + } + } + this._startSet.splice(j, 1); + j -= 1; + if (record.duration > 0 && record.velocity > 0 && + conflict !== record) { + this._finishSet[freq] = record; + callbacks.push({ + order: [record.time, 2], + f: this._trigger, t: this, a: ['noteon', record]}); + } + } + } + // Schedule the next _doPoll. + this._startPollTimer(); + + // Sort callbacks according to the "order" tuple, so earlier events + // are notified first. + callbacks.sort(function(a, b) { + if (a.order[0] != b.order[0]) { return a.order[0] - b.order[0]; } + // tiebreak by notifying 'noteoff' first and 'noteon' last. + return a.order[1] - b.order[1]; + }); + // At the end, call all the callbacks without depending on "this" state. + for (j = 0; j < callbacks.length; ++j) { + cb = callbacks[j]; + cb.f.apply(cb.t, cb.a); + } + }; + // Schedules the next _doPoll call by examining times in the various + // sets and determining the soonest event that needs _doPoll processing. + Instrument.prototype._startPollTimer = function(setnow) { + // If we have already done a "setnow", then pollTimer is zero-timeout + // and cannot be faster. + if (this._pollTimer && this._now != null) { + return; + } + var self = this, + poll = function() { self._doPoll(); }, + earliest = Infinity, j, delay; + if (this._pollTimer) { + // Clear any old timer + clearTimeout(this._pollTimer); + this._pollTimer = null; + } + if (setnow) { + // When scheduling tones, cache _now and keep a zero-timeout poll. + // _now will be cleared the next time we execute _doPoll. + this._now = audioCurrentStartTime(); + this._pollTimer = setTimeout(poll, 0); + return; + } + // Timer due to notes starting: wake up for 'noteon' notification. + for (j = 0; j < this._startSet.length; ++j) { + earliest = Math.min(earliest, this._startSet[j].time); + } + // Timer due to notes finishing: wake up for 'noteoff' notification. + for (j in this._finishSet) { + earliest = Math.min( + earliest, this._finishSet[j].time + this._finishSet[j].duration); + } + // Timer due to scheduled callback. + for (j = 0; j < this._callbackSet.length; ++j) { + earliest = Math.min(earliest, this._callbackSet[j].time); + } + // Timer due to cleanup: add a second to give some time to batch up. + if (this._cleanupSet.length > 0) { + earliest = Math.min(earliest, this._cleanupSet[0].cleanuptime + 1); + } + // Timer due to sequencer events: subtract a little time to stay ahead. + earliest = Math.min( + earliest, this._minQueueTime - Instrument.dequeueTime); + + delay = Math.max(0.001, earliest - this._atop.ac.currentTime); + + // If there are no future events, then we do not need a timer. + if (isNaN(delay) || delay == Infinity) { return; } + + // Use the Javascript timer to wake up at the right moment. + this._pollTimer = setTimeout(poll, Math.round(delay * 1000)); + }; + + // The low-level tone function. + Instrument.prototype.tone = + function(pitch, duration, velocity, delay, timbre, origin) { + // If audio is not present, this is a no-op. + if (!this._atop) { return; } + + // Called with an object instead of listed args. + if (typeof(pitch) == 'object') { + if (velocity == null) velocity = pitch.velocity; + if (duration == null) duration = pitch.duration; + if (delay == null) delay = pitch.delay; + if (timbre == null) timbre = pitch.timbre; + if (origin == null) origin = pitch.origin; + pitch = pitch.pitch; + } + + // Convert pitch from various formats to Hz frequency and a midi num. + var midi, frequency; + if (!pitch) { pitch = 'C'; } + if (isNaN(pitch)) { + midi = pitchToMidi(pitch); + frequency = midiToFrequency(midi); + } else { + frequency = Number(pitch); + if (frequency < 0) { + midi = -frequency; + frequency = midiToFrequency(midi); + } else { + midi = frequencyToMidi(frequency); + } + } + + if (!timbre) { + timbre = this._timbre; + } + // If there is a custom timbre, validate and copy it. + if (timbre !== this._timbre) { + var given = timbre, key; + timbre = {} + for (key in defaultTimbre) { + if (key in given) { + timbre[key] = given[key]; + } else { + timbre[key] = defaulTimbre[key]; + } + } + } + + // Create the record for a tone. + var ac = this._atop.ac, + now = this.now(), + time = now + (delay || 0), + record = { + time: time, + on: false, + frequency: frequency, + midi: midi, + velocity: (velocity == null ? 1 : velocity), + duration: (duration == null ? Instrument.toneLength : duration), + timbre: timbre, + instrument: this, + gainNode: null, + oscillators: null, + cleanuptime: Infinity, + origin: origin // save the origin of the tone for visible feedback + }; + + if (time < now + Instrument.bufferSecs) { + // The tone starts soon! Give it directly to WebAudio. + this._makeSound(record); + } else { + // The tone is later: queue it. + if (!this._unsortedQueue && this._queue.length && + time < this._queue[this._queue.length -1].time) { + this._unsortedQueue = true; + } + this._queue.push(record); + this._minQueueTime = Math.min(this._minQueueTime, record.time); + } + }; + // The low-level callback scheduling method. + Instrument.prototype.schedule = function(delay, callback) { + this._callbackSet.push({ time: this.now() + delay, callback: callback }); + }; + // The high-level sequencing method. + Instrument.prototype.play = function(abcstring) { + var args = Array.prototype.slice.call(arguments), + done = null, + opts = {}, subfile, + abcfile, argindex, tempo, timbre, k, delay, maxdelay = 0, attenuate, + voicename, stems, ni, vn, j, stem, note, beatsecs, secs, v, files = []; + // Look for continuation as last argument. + if (args.length && 'function' == typeof(args[args.length - 1])) { + done = args.pop(); + } + if (!this._atop) { + if (done) { done(); } + return; + } + // Look for options as first object. + argindex = 0; + if ('object' == typeof(args[0])) { + // Copy own properties into an options object. + for (k in args[0]) if (args[0].hasOwnProperty(k)) { + opts[k] = args[0][k]; + } + argindex = 1; + // If a song is supplied by options object, process it. + if (opts.song) { + args.push(opts.song); + } + } + // Parse any number of ABC files as input. + for (; argindex < args.length; ++argindex) { + // Handle splitting of ABC subfiles at X: lines. + subfile = args[argindex].split(/\n(?=X:)/); + for (k = 0; k < subfile.length; ++k) { + abcfile = parseABCFile(subfile[k]); + if (!abcfile) continue; + // Take tempo markings from the first file, and share them. + if (!opts.tempo && abcfile.tempo) { + opts.tempo = abcfile.tempo; + if (abcfile.unitbeat) { + opts.tempo *= abcfile.unitbeat / (abcfile.unitnote || 1); + } + } + // Ignore files without songs. + if (!abcfile.voice) continue; + files.push(abcfile); + } + } + // Default tempo to 120 if nothing else is specified. + if (!opts.tempo) { opts.tempo = 120; } + // Default volume to 1 if nothing is specified. + if (opts.volume == null) { opts.volume = 1; } + beatsecs = 60.0 / opts.tempo; + // Schedule all notes from all the files. + for (k = 0; k < files.length; ++k) { + abcfile = files[k]; + // Each file can have multiple voices (e.g., left and right hands) + for (vn in abcfile.voice) { + // Each voice could have a separate timbre. + timbre = makeTimbre(opts.timbre || abcfile.voice[vn].timbre || + abcfile.timbre || this._timbre, this._atop); + // Each voice has a series of stems (notes or chords). + stems = abcfile.voice[vn].stems; + if (!stems) continue; + // Starting at delay zero (now), schedule all tones. + delay = 0; + for (ni = 0; ni < stems.length; ++ni) { + stem = stems[ni]; + // Attenuate chords to reduce clipping. + attenuate = 1 / Math.sqrt(stem.notes.length); + // Schedule every note inside a stem. + for (j = 0; j < stem.notes.length; ++j) { + note = stem.notes[j]; + if (note.holdover) { + // Skip holdover notes from ties. + continue; + } + secs = (note.time || stem.time) * beatsecs; + if (stem.staccato) { + // Shorten staccato notes. + secs = Math.min(Math.min(secs, beatsecs / 16), + timbre.attack + timbre.decay); + } else if (!note.slurred && secs >= 1/8) { + // Separate unslurred notes by about a 30th of a second. + secs -= 1/32; + } + v = (note.velocity || 1) * attenuate * opts.volume; + // This is innsermost part of the inner loop! + this.tone( // Play the tone: + note.pitch, // at the given pitch + secs, // for the given duration + v, // with the given volume + delay, // starting at the proper time + timbre, // with the selected timbre + note // the origin object for visual feedback + ); + } + delay += stem.time * beatsecs; // Advance the sequenced time. + } + maxdelay = Math.max(delay, maxdelay); + } + } + this._maxScheduledTime = + Math.max(this._maxScheduledTime, this.now() + maxdelay); + if (done) { + // Schedule a "done" callback after all sequencing is complete. + this.schedule(maxdelay, done); + } + }; + + // Parses an ABC file to an object with the following structure: + // { + // X: value from the X: lines in header (\n separated for multiple values) + // V: value from the V:myname lines that appear before K: + // (etc): for all the one-letter header-names. + // K: value from the K: lines in header. + // tempo: Q: line parsed as beatsecs + // timbre: ... I:timbre line as parsed by makeTimbre + // voice: { + // myname: { // voice with id "myname" + // V: value from the V:myname lines (from the body) + // stems: [...] as parsed by parseABCstems + // } + // } + // } + // ABC files are idiosyncratic to parse: the written specifications + // do not necessarily reflect the defacto standard implemented by + // ABC content on the web. This implementation is designed to be + // practical, working on content as it appears on the web, and only + // using the written standard as a guideline. + var ABCheader = /^([A-Za-z]):\s*(.*)$/; + function parseABCFile(str) { + var lines = str.split('\n'), + result = { + voice: {} + }, + context = result, timbre, + j, k, header, stems, key = {}, accent = {}, voiceid, out; + // Shifts context to a voice with the given id given. If no id + // given, then just sticks with the current voice. If the current + // voice is unnamed and empty, renames the current voice. + function startVoiceContext(id) { + id = id || ''; + if (!id && context !== result) { + return; + } + if (result.voice.hasOwnProperty(id)) { + // Resume a named voice. + context = result.voice[id]; + accent = context.accent; + } else { + // Start a new voice. + context = { id: id, accent: { slurred: 0 } }; + result.voice[id] = context; + accent = context.accent; + } + } + // For picking a default voice, looks for the first voice name. + function firstVoiceName() { + if (result.V) { + return result.V.split(/\s+/)[0]; + } else { + return ''; + } + } + // ABC files are parsed one line at a time. + for (j = 0; j < lines.length; ++j) { + // First, check to see if the line is a header line. + header = ABCheader.exec(lines[j]); + if (header) { + // The following headers are recognized and processed. + switch(header[1]) { + case 'V': + // A V: header switches voices if in the body. + // If in the header, then it is just advisory. + if (context !== result) { + startVoiceContext(header[2].split(' ')[0]); + } + break; + case 'M': + parseMeter(header[2], context); + break; + case 'L': + parseUnitNote(header[2], context); + break; + case 'Q': + parseTempo(header[2], context); + break; + } + // All headers (including unrecognized ones) are + // just accumulated as properties. Repeated header + // lines are accumulated as multiline properties. + if (context.hasOwnProperty(header[1])) { + context[header[1]] += '\n' + header[2]; + } else { + context[header[1]] = header[2]; + } + // The K header is special: it should be the last one + // before the voices and notes begin. + if (header[1] == 'K' && context === result) { + key = keysig(header[2]); + startVoiceContext(firstVoiceName()); + } + } else if (/^\s*(?:%.*)?$/.test(lines[j])) { + // Skip blank and comment lines. + continue; + } else { + // A non-blank non-header line should have notes. + voiceid = peekABCVoice(lines[j]); + if (voiceid) { + // If it declares a voice id, respect it. + startVoiceContext(voiceid); + } else { + // Otherwise, start a default voice. + if (context === result) { + startVoiceContext(firstVoiceName()); + } + } + // Parse the notes. + stems = parseABCNotes(lines[j], key, accent); + if (stems && stems.length) { + // Push the line of stems into the voice. + if (!('stems' in context)) { context.stems = []; } + context.stems.push.apply(context.stems, stems); + } + } + } + var infer = ['unitnote', 'unitbeat', 'tempo']; + if (result.voice) { + out = []; + for (j in result.voice) { + if (result.voice[j].stems && result.voice[j].stems.length) { + // Calculate times for all the tied notes. This happens at the end + // because in principle, the first note of a song could be tied all + // the way through to the last note. + processTies(result.voice[j].stems); + // Bring up inferred tempo values from voices if not specified + // in the header. + for (k = 0; k < infer.length; ++k) { + if (!(infer[k] in result) && (infer[k] in result.voice[j])) { + result[infer[k]] = result.voice[j][infer[k]]; + } + } + } else { + out.push(j); + } + } + // Delete any voices that had no stems. + for (j = 0; j < out.length; ++j) { + delete result.voice[out[j]]; + } + } + return result; + } + // Parse M: lines. "3/4" is 3/4 time and "C" is 4/4 (common) time. + function parseMeter(mline, beatinfo) { + var d = /^C/.test(mline) ? 4/4 : durationToTime(mline); + if (!d) { return; } + if (!beatinfo.unitnote) { + if (d < 0.75) { + beatinfo.unitnote = 1/16; + } else { + beatinfo.unitnote = 1/8; + } + } + } + // Parse L: lines, e.g., "1/8". + function parseUnitNote(lline, beatinfo) { + var d = durationToTime(lline); + if (!d) { return; } + beatinfo.unitnote = d; + } + // Parse Q: line, e.g., "1/4=66". + function parseTempo(qline, beatinfo) { + var parts = qline.split(/\s+|=/), j, unit = null, tempo = null; + for (j = 0; j < parts.length; ++j) { + // It could be reversed, like "66=1/4", or just "120", so + // determine what is going on by looking for a slash etc. + if (parts[j].indexOf('/') >= 0 || /^[1-4]$/.test(parts[j])) { + // The note-unit (e.g., 1/4). + unit = unit || durationToTime(parts[j]); + } else { + // The tempo-number (e.g., 120) + tempo = tempo || Number(parts[j]); + } + } + if (unit) { + beatinfo.unitbeat = unit; + } + if (tempo) { + beatinfo.tempo = tempo; + } + } + // Run through all the notes, adding up time for tied notes, + // and marking notes that were held over with holdover = true. + function processTies(stems) { + var tied = {}, nextTied, j, k, note, firstNote; + for (j = 0; j < stems.length; ++j) { + nextTied = {}; + for (k = 0; k < stems[j].notes.length; ++k) { + firstNote = note = stems[j].notes[k]; + if (tied.hasOwnProperty(note.pitch)) { // Pitch was tied from before. + firstNote = tied[note.pitch]; // Get the earliest note in the tie. + firstNote.time += note.time; // Extend its time. + note.holdover = true; // Silence this note as a holdover. + } + if (note.tie) { // This note is tied with the next. + nextTied[note.pitch] = firstNote; // Save it away. + } + } + tied = nextTied; + } + } + // Returns a map of A-G -> accidentals, according to the key signature. + // When n is zero, there are no accidentals (e.g., C major or A minor). + // When n is positive, there are n sharps (e.g., for G major, n = 1). + // When n is negative, there are -n flats (e.g., for F major, n = -1). + function accidentals(n) { + var sharps = 'FCGDAEB', + result = {}, j; + if (!n) { + return result; + } + if (n > 0) { // Handle sharps. + for (j = 0; j < n && j < 7; ++j) { + result[sharps.charAt(j)] = '^'; + } + } else { // Flats are in the opposite order. + for (j = 0; j > n && j > -7; --j) { + result[sharps.charAt(6 + j)] = '_'; + } + } + return result; + } + // Decodes the key signature line (e.g., K: C#m) at the front of an ABC tune. + // Supports the whole range of scale systems listed in the ABC spec. + function keysig(keyname) { + if (!keyname) { return {}; } + var key, sigcodes = { + // Major + 'c#':7, 'f#':6, 'b':5, 'e':4, 'a':3, 'd':2, 'g':1, 'c':0, + 'f':-1, 'bb':-2, 'eb':-3, 'ab':-4, 'db':-5, 'gb':-6, 'cb':-7, + // Minor + 'a#m':7, 'd#m':6, 'g#m':5, 'c#m':4, 'f#m':3, 'bm':2, 'em':1, 'am':0, + 'dm':-1, 'gm':-2, 'cm':-3, 'fm':-4, 'bbm':-5, 'ebm':-6, 'abm':-7, + // Mixolydian + 'g#mix':7, 'c#mix':6, 'f#mix':5, 'bmix':4, 'emix':3, + 'amix':2, 'dmix':1, 'gmix':0, 'cmix':-1, 'fmix':-2, + 'bbmix':-3, 'ebmix':-4, 'abmix':-5, 'dbmix':-6, 'gbmix':-7, + // Dorian + 'd#dor':7, 'g#dor':6, 'c#dor':5, 'f#dor':4, 'bdor':3, + 'edor':2, 'ador':1, 'ddor':0, 'gdor':-1, 'cdor':-2, + 'fdor':-3, 'bbdor':-4, 'ebdor':-5, 'abdor':-6, 'dbdor':-7, + // Phrygian + 'e#phr':7, 'a#phr':6, 'd#phr':5, 'g#phr':4, 'c#phr':3, + 'f#phr':2, 'bphr':1, 'ephr':0, 'aphr':-1, 'dphr':-2, + 'gphr':-3, 'cphr':-4, 'fphr':-5, 'bbphr':-6, 'ebphr':-7, + // Lydian + 'f#lyd':7, 'blyd':6, 'elyd':5, 'alyd':4, 'dlyd':3, + 'glyd':2, 'clyd':1, 'flyd':0, 'bblyd':-1, 'eblyd':-2, + 'ablyd':-3, 'dblyd':-4, 'gblyd':-5, 'cblyd':-6, 'fblyd':-7, + // Locrian + 'b#loc':7, 'e#loc':6, 'a#loc':5, 'd#loc':4, 'g#loc':3, + 'c#loc':2, 'f#loc':1, 'bloc':0, 'eloc':-1, 'aloc':-2, + 'dloc':-3, 'gloc':-4, 'cloc':-5, 'floc':-6, 'bbloc':-7 + }; + var k = keyname.replace(/\s+/g, '').toLowerCase().substr(0, 5); + var scale = k.match(/maj|min|mix|dor|phr|lyd|loc|m/); + if (scale) { + if (scale == 'maj') { + key = k.substr(0, scale.index); + } else if (scale == 'min') { + key = k.substr(0, scale.index + 1); + } else { + key = k.substr(0, scale.index + scale[0].length); + } + } else { + key = /^[a-g][#b]?/.exec(k) || ''; + } + var result = accidentals(sigcodes[key]); + var extras = keyname.substr(key.length).match(/(_+|=|\^+)[a-g]/ig); + if (extras) { + for (j = 0; j < extras.length; ++j) { + var note = extras[j].charAt(extras[j].length - 1).toUpperCase(); + if (extras[j].charAt(0) == '=') { + delete result[note]; + } else { + result[note] = extras[j].substr(0, extras[j].length - 1); + } + } + } + return result; + } + // Peeks and looks for a prefix of the form [V:voiceid]. + function peekABCVoice(line) { + var match = /^\[V:([^\]\s]*)\]/.exec(line); + if (!match) return null; + return match[1]; + } + // Parses a single line of ABC notes (i.e., not a header line). + // + // We process an ABC song stream by dividing it into tokens, each of + // which is a pitch, duration, or special decoration symbol; then + // we process each decoration individually, and we process each + // stem as a group using parseStem. + // The structure of a single ABC note is something like this: + // + // NOTE -> STACCATO? PITCH DURATION? TIE? + // + // I.e., it always has a pitch, and it is prefixed by some optional + // decorations such as a (.) staccato marking, and it is suffixed by + // an optional duration and an optional tie (-) marking. + // + // A stem is either a note or a bracketed series of notes, followed + // by duration and tie. + // + // STEM -> NOTE OR '[' NOTE * ']' DURAITON? TIE? + // + // Then a song is just a sequence of stems interleaved with other + // decorations such as dynamics markings and measure delimiters. + var ABCtoken = /(?:^\[V:[^\]\s]*\])|\s+|%[^\n]*|![^\s!:|\[\]]*!|\+[^+|!]*\+|[_<>@^]?"[^"]*"|\[|\]|>+|<+|(?:(?:\^+|_+|=|)[A-Ga-g](?:,+|'+|))|\(\d+(?::\d+){0,2}|\d*\/\d+|\d+\/?|\/+|[xzXZ]|\[?\|\]?|:?\|:?|::|./g; + function parseABCNotes(str, key, accent) { + var tokens = str.match(ABCtoken), result = [], parsed = null, + index = 0, dotted = 0, beatlet = null, t; + if (!tokens) { + return null; + } + while (index < tokens.length) { + // Ignore %comments and !markings! + if (/^[\s%]/.test(tokens[index])) { index++; continue; } + if (/^\[V:\S*\]$/.test(tokens[index])) { + // Voice id from [V:id] is handled in peekABCVoice. + index++; + continue; + } + // Handled dotted notation abbreviations. + if (//.test(tokens[index])) { + dotted = tokens[index++].length; + continue; + } + if (/^\(\d+(?::\d+)*/.test(tokens[index])) { + beatlet = parseBeatlet(tokens[index++]); + continue; + } + if (/^[!+].*[!+]$/.test(tokens[index])) { + parseDecoration(tokens[index++], accent); + continue; + } + if (/^.?".*"$/.test(tokens[index])) { + // Ignore double-quoted tokens (chords and general text annotations). + index++; + continue; + } + if (/^[()]$/.test(tokens[index])) { + if (tokens[index++] == '(') { + accent.slurred += 1; + } else { + accent.slurred -= 1; + if (accent.slurred <= 0) { + accent.slurred = 0; + if (result.length >= 1) { + // The last notes in a slur are not slurred. + slurStem(result[result.length - 1], false); + } + } + } + continue; + } + // Handle measure markings by clearing accidentals. + if (/\|/.test(tokens[index])) { + for (t in accent) { + if (t.length == 1) { + // Single-letter accent properties are note accidentals. + delete accent[t]; + } + } + index++; + continue; + } + parsed = parseStem(tokens, index, key, accent); + // Skip unparsable bits + if (parsed === null) { + index++; + continue; + } + // Process a parsed stem. + if (beatlet) { + scaleStem(parsed.stem, beatlet.time); + beatlet.count -= 1; + if (!beatlet.count) { + beatlet = null; + } + } + // If syncopated with > or < notation, shift part of a beat + // between this stem and the previous one. + if (dotted && result.length) { + if (dotted > 0) { + t = (1 - Math.pow(0.5, dotted)) * parsed.stem.time; + } else { + t = (Math.pow(0.5, -dotted) - 1) * result[result.length - 1].time; + } + syncopateStem(result[result.length - 1], t); + syncopateStem(parsed.stem, -t); + } + dotted = 0; + // Slur all the notes contained within a strem. + if (accent.slurred) { + slurStem(parsed.stem, true); + } + // Add the stem to the sequence of stems for this voice. + result.push(parsed.stem); + // Advance the parsing index since a stem is multiple tokens. + index = parsed.index; + } + return result; + } + // Additively adjusts the beats for a stem and the contained notes. + function syncopateStem(stem, t) { + var j, note, stemtime = stem.time, newtime = stemtime + t; + stem.time = newtime; + syncopateStem + for (j = 0; j < stem.notes.length; ++j) { + note = stem.notes[j]; + // Only adjust a note's duration if it matched the stem's duration. + if (note.time == stemtime) { note.time = newtime; } + } + } + // Marks everything in the stem with the slur attribute (or deletes it). + function slurStem(stem, addSlur) { + var j, note; + for (j = 0; j < stem.notes.length; ++j) { + note = stem.notes[j]; + if (addSlur) { + note.slurred = true; + } else if (note.slurred) { + delete note.slurred; + } + } + } + // Scales the beats for a stem and the contained notes. + function scaleStem(stem, s) { + var j; + stem.time *= s; + for (j = 0; j < stem.notes.length; ++j) { + stem.notes[j].time *= s;; + } + } + // Parses notation of the form (3 or (5:2:10, which means to do + // the following 3 notes in the space of 2 notes, or to do the following + // 10 notes at the rate of 5 notes per 2 beats. + function parseBeatlet(token) { + var m = /^\((\d+)(?::(\d+)(?::(\d+))?)?$/.exec(token); + if (!m) { return null; } + var count = Number(m[1]), + beats = Number(m[2]) || 2, + duration = Number(m[3]) || count; + return { + time: beats / count, + count: duration + }; + } + // Parse !ppp! markings. + function parseDecoration(token, accent) { + if (token.length < 2) { return; } + token = token.substring(1, token.length - 1); + switch (token) { + case 'pppp': case 'ppp': + accent.dynamics = 0.2; break; + case 'pp': + accent.dynamics = 0.4; break; + case 'p': + accent.dynamics = 0.6; break; + case 'mp': + accent.dynamics = 0.8; break; + case 'mf': + accent.dynamics = 1.0; break; + case 'f': + accent.dynamics = 1.2; break; + case 'ff': + accent.dynamics = 1.4; break; + case 'fff': case 'ffff': + accent.dynamics = 1.5; break; + } + } + // Parses a stem, which may be a single note, or which may be + // a chorded note. + function parseStem(tokens, index, key, accent) { + var notes = [], + duration = '', staccato = false, + noteDuration, noteTime, velocity, + lastNote = null, minStemTime = Infinity, j; + // A single staccato marking applies to the entire stem. + if (index < tokens.length && '.' == tokens[index]) { + staccato = true; + index++; + } + if (index < tokens.length && tokens[index] == '[') { + // Deal with [CEG] chorded notation. + index++; + // Scan notes within the chord. + while (index < tokens.length) { + // Ignore and space and %comments. + if (/^[\s%]/.test(tokens[index])) { + index++; + continue; + } + if (/[A-Ga-g]/.test(tokens[index])) { + // Grab a pitch. + lastNote = { + pitch: applyAccent(tokens[index++], key, accent), + tie: false + } + lastNote.frequency = pitchToFrequency(lastNote.pitch); + notes.push(lastNote); + } else if (/[xzXZ]/.test(tokens[index])) { + // Grab a rest. + lastNote = null; + index++; + } else if ('.' == tokens[index]) { + // A staccato mark applies to the entire stem. + staccato = true; + index++; + continue; + } else { + // Stop parsing the stem if something is unrecognized. + break; + } + // After a pitch or rest, look for a duration. + if (index < tokens.length && + /^(?![\s%!]).*[\d\/]/.test(tokens[index])) { + noteDuration = tokens[index++]; + noteTime = durationToTime(noteDuration); + } else { + noteDuration = ''; + noteTime = 1; + } + // If it's a note (not a rest), store the duration + if (lastNote) { + lastNote.duration = noteDuration; + lastNote.time = noteTime; + } + // When a stem has more than one duration, use the shortest + // one for timing. The standard says to pick the first one, + // but in practice, transcribed music online seems to + // follow the rule that the stem's duration is determined + // by the shortest contained duration. + if (noteTime && noteTime < minStemTime) { + duration = noteDuration; + minStemTime = noteTime; + } + // After a duration, look for a tie mark. Individual notes + // within a stem can be tied. + if (index < tokens.length && '-' == tokens[index]) { + if (lastNote) { + notes[notes.length - 1].tie = true; + } + index++; + } + } + // The last thing in a chord should be a ]. If it isn't, then + // this doesn't look like a stem after all, and return null. + if (tokens[index] != ']') { + return null; + } + index++; + } else if (index < tokens.length && /[A-Ga-g]/.test(tokens[index])) { + // Grab a single note. + lastNote = { + pitch: applyAccent(tokens[index++], key, accent), + tie: false, + duration: '', + time: 1 + } + lastNote.frequency = pitchToFrequency(lastNote.pitch); + notes.push(lastNote); + } else if (index < tokens.length && /^[xzXZ]$/.test(tokens[index])) { + // Grab a rest - no pitch. + index++; + } else { + // Something we don't recognize - not a stem. + return null; + } + // Right after a [chord], note, or rest, look for a duration marking. + if (index < tokens.length && /^(?![\s%!]).*[\d\/]/.test(tokens[index])) { + duration = tokens[index++]; + noteTime = durationToTime(duration); + // Apply the duration to all the ntoes in the stem. + // NOTE: spec suggests multiplying this duration, but that + // idiom is not seen (so far) in practice. + for (j = 0; j < notes.length; ++j) { + notes[j].duration = duration; + notes[j].time = noteTime; + } + } + // Then look for a trailing tie marking. Will tie every note in a chord. + if (index < tokens.length && '-' == tokens[index]) { + index++; + for (j = 0; j < notes.length; ++j) { + notes[j].tie = true; + } + } + if (accent.dynamics) { + velocity = accent.dynamics; + for (j = 0; j < notes.length; ++j) { + notes[j].velocity = velocity; + } + } + return { + index: index, + stem: { + notes: notes, + duration: duration, + staccato: staccato, + time: durationToTime(duration) + } + }; + } + // Normalizes pitch markings by stripping leading = if present. + function stripNatural(pitch) { + if (pitch.length > 0 && pitch.charAt(0) == '=') { + return pitch.substr(1); + } + return pitch; + } + // Processes an accented pitch, automatically applying accidentals + // that have accumulated within the measure, and also saving + // explicit accidentals to continue to apply in the measure. + function applyAccent(pitch, key, accent) { + var m = /^(\^+|_+|=|)([A-Ga-g])(.*)$/.exec(pitch), letter; + if (!m) { return pitch; } + // Note that an accidental in one octave applies in other octaves. + letter = m[2].toUpperCase(); + if (m[1].length > 0) { + // When there is an explicit accidental, then remember it for + // the rest of the measure. + accent[letter] = m[1]; + return stripNatural(pitch); + } + if (accent.hasOwnProperty(letter)) { + // Accidentals from this measure apply to unaccented notes. + return stripNatural(accent[letter] + m[2] + m[3]); + } + if (key.hasOwnProperty(letter)) { + // Key signatures apply by default. + return stripNatural(key[letter] + m[2] + m[3]); + } + return stripNatural(pitch); + } + // Converts a midi note number to a frequency in Hz. + function midiToFrequency(midi) { + return 440 * Math.pow(2, (midi - 69) / 12); + } + // Some constants. + var noteNum = + {C:0,D:2,E:4,F:5,G:7,A:9,B:11,c:12,d:14,e:16,f:17,g:19,a:21,b:23}; + var accSym = + { '^':1, '': 0, '=':0, '_':-1 }; + var noteName = + ['C', '^C', 'D', '_E', 'E', 'F', '^F', 'G', '_A', 'A', '_B', 'B', + 'c', '^c', 'd', '_e', 'e', 'f', '^f', 'g', '_a', 'a', '_b', 'b']; + // Converts a frequency in Hz to the closest midi number. + function frequencyToMidi(freq) { + return Math.round(69 + Math.log(freq / 440) * 12 / Math.LN2); + } + // Converts an ABC pitch (such as "^G,,") to a midi note number. + function pitchToMidi(pitch) { + var m = /^(\^+|_+|=|)([A-Ga-g])([,']*)$/.exec(pitch); + if (!m) { return null; } + var octave = m[3].replace(/,/g, '').length - m[3].replace(/'/g, '').length; + var semitone = + noteNum[m[2]] + accSym[m[1].charAt(0)] * m[1].length + 12 * octave; + return semitone + 60; // 60 = midi code middle "C". + } + // Converts a midi number to an ABC notation pitch. + function midiToPitch(midi) { + var index = ((midi - 72) % 12); + if (midi > 60 || index != 0) { index += 12; } + var octaves = Math.round((midi - index - 60) / 12), + result = noteName[index]; + while (octaves != 0) { + result += octaves > 0 ? "'" : ","; + octaves += octaves > 0 ? -1 : 1; + } + return result; + } + // Converts an ABC pitch to a frequency in Hz. + function pitchToFrequency(pitch) { + return midiToFrequency(pitchToMidi(pitch)); + } + // Converts an ABC duration to a number (e.g., "/3"->0.333 or "11/2"->1.5). + function durationToTime(duration) { + var m = /^(\d*)(?:\/(\d*))?$|^(\/+)$/.exec(duration), n, d, i = 0, ilen; + if (!m) return; + if (m[3]) return Math.pow(0.5, m[3].length); + d = (m[2] ? parseFloat(m[2]) : /\//.test(duration) ? 2 : 1); + // Handle mixed frations: + ilen = 0; + n = (m[1] ? parseFloat(m[1]) : 1); + if (m[2]) { + while (ilen + 1 < m[1].length && n > d) { + ilen += 1 + i = parseFloat(m[1].substring(0, ilen)) + n = parseFloat(m[1].substring(ilen)) + } + } + return i + (n / d); + } + + // The default sound is a square wave with a pretty quick decay to zero. + var defaultTimbre = Instrument.defaultTimbre = { + wave: 'square', // Oscillator type. + gain: 0.1, // Overall gain at maximum attack. + attack: 0.002, // Attack time at the beginning of a tone. + decay: 0.4, // Rate of exponential decay after attack. + decayfollow: 0, // Amount of decay shortening for higher notes. + sustain: 0, // Portion of gain to sustain indefinitely. + release: 0.1, // Release time after a tone is done. + cutoff: 0, // Low-pass filter cutoff frequency. + cutfollow: 0, // Cutoff adjustment, a multiple of oscillator freq. + resonance: 0, // Low-pass filter resonance. + detune: 0 // Detune factor for a second oscillator. + }; + + // Norrmalizes a timbre object by making a copy that has exactly + // the right set of timbre fields, defaulting when needed. + // A timbre can specify any of the fields of defaultTimbre; any + // unspecified fields are treated as they are set in defaultTimbre. + function makeTimbre(options, atop) { + if (!options) { + options = {}; + } + if (typeof(options) == 'string') { + // Abbreviation: name a wave to get a default timbre for that wave. + options = { wave: options }; + } + var result = {}, key, + wt = atop && atop.wavetable && atop.wavetable[options.wave]; + for (key in defaultTimbre) { + if (options.hasOwnProperty(key)) { + result[key] = options[key]; + } else if (wt && wt.defs && wt.defs.hasOwnProperty(key)) { + result[key] = wt.defs[key]; + } else{ + result[key] = defaultTimbre[key]; + } + } + return result; + } + + var whiteNoiseBuf = null; + function getWhiteNoiseBuf() { + if (whiteNoiseBuf == null) { + var ac = getAudioTop().ac, + bufferSize = 2 * ac.sampleRate, + whiteNoiseBuf = ac.createBuffer(1, bufferSize, ac.sampleRate), + output = whiteNoiseBuf.getChannelData(0); + for (var i = 0; i < bufferSize; i++) { + output[i] = Math.random() * 2 - 1; + } + } + return whiteNoiseBuf; + } + + // This utility function creates an oscillator at the given frequency + // and the given wavename. It supports lookups in a static wavetable, + // defined right below. + function makeOscillator(atop, wavename, freq) { + if (wavename == 'noise') { + var whiteNoise = atop.ac.createBufferSource(); + whiteNoise.buffer = getWhiteNoiseBuf(); + whiteNoise.loop = true; + return whiteNoise; + } + var wavetable = atop.wavetable, o = atop.ac.createOscillator(), + k, pwave, bwf, wf; + try { + if (wavetable.hasOwnProperty(wavename)) { + // Use a customized wavetable. + pwave = wavetable[wavename].wave; + if (wavetable[wavename].freq) { + bwf = 0; + // Look for a higher-frequency variant. + for (k in wavetable[wavename].freq) { + wf = Number(k); + if (freq > wf && wf > bwf) { + bwf = wf; + pwave = wavetable[wavename].freq[bwf]; + } + } + } + if (!o.setPeriodicWave && o.setWaveTable) { + // The old API name: Safari 7 still uses this. + o.setWaveTable(pwave); + } else { + // The new API name. + o.setPeriodicWave(pwave); + } + } else { + o.type = wavename; + } + } catch(e) { + if (window.console) { window.console.log(e); } + // If unrecognized, just use square. + // TODO: support "noise" or other wave shapes. + o.type = 'square'; + } + o.frequency.value = freq; + return o; + } + + // Accepts either an ABC pitch or a midi number and converts to midi. + Instrument.pitchToMidi = function(n) { + if (typeof(n) == 'string') { return pitchToMidi(n); } + return n; + } + + // Accepts either an ABC pitch or a midi number and converts to ABC pitch. + Instrument.midiToPitch = function(n) { + if (typeof(n) == 'number') { return midiToPitch(n); } + return n; + } + + return Instrument; +})(); + +// wavetable is a table of names for nonstandard waveforms. +// The table maps names to objects that have wave: and freq: +// properties. The wave: property is a PeriodicWave to use +// for the oscillator. The freq: property, if present, +// is a map from higher frequencies to more PeriodicWave +// objects; when a frequency higher than the given threshold +// is requested, the alternate PeriodicWave is used. +function makeWavetable(ac) { + return (function(wavedata) { + function makePeriodicWave(data) { + var n = data.real.length, + real = new Float32Array(n), + imag = new Float32Array(n), + j; + for (j = 0; j < n; ++j) { + real[j] = data.real[j]; + imag[j] = data.imag[j]; + } + try { + // Latest API naming. + return ac.createPeriodicWave(real, imag); + } catch (e) { } + try { + // Earlier API naming. + return ac.createWaveTable(real, imag); + } catch (e) { } + return null; + } + function makeMultiple(data, mult, amt) { + var result = { real: [], imag: [] }, j, n = data.real.length, m; + for (j = 0; j < n; ++j) { + m = Math.log(mult[Math.min(j, mult.length - 1)]); + result.real.push(data.real[j] * Math.exp(amt * m)); + result.imag.push(data.imag[j] * Math.exp(amt * m)); + } + return result; + } + var result = {}, k, d, n, j, ff, record, wave, pw; + for (k in wavedata) { + d = wavedata[k]; + wave = makePeriodicWave(d); + if (!wave) { continue; } + record = { wave: wave }; + // A strategy for computing higher frequency waveforms: apply + // multipliers to each harmonic according to d.mult. These + // multipliers can be interpolated and applied at any number + // of transition frequencies. + if (d.mult) { + ff = wavedata[k].freq; + record.freq = {}; + for (j = 0; j < ff.length; ++j) { + wave = + makePeriodicWave(makeMultiple(d, d.mult, (j + 1) / ff.length)); + if (wave) { record.freq[ff[j]] = wave; } + } + } + // This wave has some default filter settings. + if (d.defs) { + record.defs = d.defs; + } + result[k] = record; + } + return result; + })({ + // Currently the only nonstandard waveform is "piano". + // It is based on the first 32 harmonics from the example: + // https://github.com/GoogleChrome/web-audio-samples + // /blob/gh-pages/samples/audio/wave-tables/Piano + // That is a terrific sound for the lowest piano tones. + // For higher tones, interpolate to a customzed wave + // shape created by hand, and apply a lowpass filter. + piano: { + real: [0, 0, -0.203569, 0.5, -0.401676, 0.137128, -0.104117, 0.115965, + -0.004413, 0.067884, -0.00888, 0.0793, -0.038756, 0.011882, + -0.030883, 0.027608, -0.013429, 0.00393, -0.014029, 0.00972, + -0.007653, 0.007866, -0.032029, 0.046127, -0.024155, 0.023095, + -0.005522, 0.004511, -0.003593, 0.011248, -0.004919, 0.008505], + imag: [0, 0.147621, 0, 0.000007, -0.00001, 0.000005, -0.000006, 0.000009, + 0, 0.000008, -0.000001, 0.000014, -0.000008, 0.000003, + -0.000009, 0.000009, -0.000005, 0.000002, -0.000007, 0.000005, + -0.000005, 0.000005, -0.000023, 0.000037, -0.000021, 0.000022, + -0.000006, 0.000005, -0.000004, 0.000014, -0.000007, 0.000012], + // How to adjust the harmonics for the higest notes. + mult: [1, 1, 0.18, 0.016, 0.01, 0.01, 0.01, 0.004, + 0.014, 0.02, 0.014, 0.004, 0.002, 0.00001], + // The frequencies at which to interpolate the harmonics. + freq: [65, 80, 100, 135, 180, 240, 620, 1360], + // The default filter settings to use for the piano wave. + // TODO: this approach attenuates low notes too much - + // this should be fixed. + defs: { wave: 'piano', gain: 0.5, + attack: 0.002, decay: 0.4, sustain: 0.005, release: 0.1, + decayfollow: 0.7, + cutoff: 800, cutfollow: 0.1, resonance: 1, detune: 1.001 } + } + }); +} + + +// The package implementation. Right now, just one class. +var impl = { + Instrument: Instrument +}; + +if (module && module.exports) { + // Nodejs usage: export the impl object as the package. + module.exports = impl; +} else if (define && define.amd) { + // Requirejs usage: define the impl object as the package. + define(function() { return impl; }); +} else { + // Plain script tag usage: stick Instrument on the window object. + for (var exp in impl) { + global[exp] = impl[exp]; + } +} + +})( + this, // global (window) object + (typeof module) == 'object' && module, // present in node.js + (typeof define) == 'function' && define // present with an AMD loader +); diff --git a/client/js/libs/select2.js b/client/js/libs/select2.js new file mode 100644 index 000000000..98800a587 --- /dev/null +++ b/client/js/libs/select2.js @@ -0,0 +1,3485 @@ +/* +Copyright 2012 Igor Vaynberg + +Version: 3.5.0 Timestamp: Mon Jun 16 19:29:44 EDT 2014 + +This software is licensed under the Apache License, Version 2.0 (the "Apache License") or the GNU +General Public License version 2 (the "GPL License"). You may choose either license to govern your +use of this software only upon the condition that you accept all of the terms of either the Apache +License or the GPL License. + +You may obtain a copy of the Apache License and the GPL License at: + + http://www.apache.org/licenses/LICENSE-2.0 + http://www.gnu.org/licenses/gpl-2.0.html + +Unless required by applicable law or agreed to in writing, software distributed under the +Apache License or the GPL License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the Apache License and the GPL License for +the specific language governing permissions and limitations under the Apache License and the GPL License. +*/ +(function ($) { + if(typeof $.fn.each2 == "undefined") { + $.extend($.fn, { + /* + * 4-10 times faster .each replacement + * use it carefully, as it overrides jQuery context of element on each iteration + */ + each2 : function (c) { + var j = $([0]), i = -1, l = this.length; + while ( + ++i < l + && (j.context = j[0] = this[i]) + && c.call(j[0], i, j) !== false //"this"=DOM, i=index, j=jQuery object + ); + return this; + } + }); + } +})(jQuery); + +(function ($, undefined) { + "use strict"; + /*global document, window, jQuery, console */ + + if (window.Select2 !== undefined) { + return; + } + + var KEY, AbstractSelect2, SingleSelect2, MultiSelect2, nextUid, sizer, + lastMousePosition={x:0,y:0}, $document, scrollBarDimensions, + + KEY = { + TAB: 9, + ENTER: 13, + ESC: 27, + SPACE: 32, + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + SHIFT: 16, + CTRL: 17, + ALT: 18, + PAGE_UP: 33, + PAGE_DOWN: 34, + HOME: 36, + END: 35, + BACKSPACE: 8, + DELETE: 46, + isArrow: function (k) { + k = k.which ? k.which : k; + switch (k) { + case KEY.LEFT: + case KEY.RIGHT: + case KEY.UP: + case KEY.DOWN: + return true; + } + return false; + }, + isControl: function (e) { + var k = e.which; + switch (k) { + case KEY.SHIFT: + case KEY.CTRL: + case KEY.ALT: + return true; + } + + if (e.metaKey) return true; + + return false; + }, + isFunctionKey: function (k) { + k = k.which ? k.which : k; + return k >= 112 && k <= 123; + } + }, + MEASURE_SCROLLBAR_TEMPLATE = "
    ", + + DIACRITICS = {"\u24B6":"A","\uFF21":"A","\u00C0":"A","\u00C1":"A","\u00C2":"A","\u1EA6":"A","\u1EA4":"A","\u1EAA":"A","\u1EA8":"A","\u00C3":"A","\u0100":"A","\u0102":"A","\u1EB0":"A","\u1EAE":"A","\u1EB4":"A","\u1EB2":"A","\u0226":"A","\u01E0":"A","\u00C4":"A","\u01DE":"A","\u1EA2":"A","\u00C5":"A","\u01FA":"A","\u01CD":"A","\u0200":"A","\u0202":"A","\u1EA0":"A","\u1EAC":"A","\u1EB6":"A","\u1E00":"A","\u0104":"A","\u023A":"A","\u2C6F":"A","\uA732":"AA","\u00C6":"AE","\u01FC":"AE","\u01E2":"AE","\uA734":"AO","\uA736":"AU","\uA738":"AV","\uA73A":"AV","\uA73C":"AY","\u24B7":"B","\uFF22":"B","\u1E02":"B","\u1E04":"B","\u1E06":"B","\u0243":"B","\u0182":"B","\u0181":"B","\u24B8":"C","\uFF23":"C","\u0106":"C","\u0108":"C","\u010A":"C","\u010C":"C","\u00C7":"C","\u1E08":"C","\u0187":"C","\u023B":"C","\uA73E":"C","\u24B9":"D","\uFF24":"D","\u1E0A":"D","\u010E":"D","\u1E0C":"D","\u1E10":"D","\u1E12":"D","\u1E0E":"D","\u0110":"D","\u018B":"D","\u018A":"D","\u0189":"D","\uA779":"D","\u01F1":"DZ","\u01C4":"DZ","\u01F2":"Dz","\u01C5":"Dz","\u24BA":"E","\uFF25":"E","\u00C8":"E","\u00C9":"E","\u00CA":"E","\u1EC0":"E","\u1EBE":"E","\u1EC4":"E","\u1EC2":"E","\u1EBC":"E","\u0112":"E","\u1E14":"E","\u1E16":"E","\u0114":"E","\u0116":"E","\u00CB":"E","\u1EBA":"E","\u011A":"E","\u0204":"E","\u0206":"E","\u1EB8":"E","\u1EC6":"E","\u0228":"E","\u1E1C":"E","\u0118":"E","\u1E18":"E","\u1E1A":"E","\u0190":"E","\u018E":"E","\u24BB":"F","\uFF26":"F","\u1E1E":"F","\u0191":"F","\uA77B":"F","\u24BC":"G","\uFF27":"G","\u01F4":"G","\u011C":"G","\u1E20":"G","\u011E":"G","\u0120":"G","\u01E6":"G","\u0122":"G","\u01E4":"G","\u0193":"G","\uA7A0":"G","\uA77D":"G","\uA77E":"G","\u24BD":"H","\uFF28":"H","\u0124":"H","\u1E22":"H","\u1E26":"H","\u021E":"H","\u1E24":"H","\u1E28":"H","\u1E2A":"H","\u0126":"H","\u2C67":"H","\u2C75":"H","\uA78D":"H","\u24BE":"I","\uFF29":"I","\u00CC":"I","\u00CD":"I","\u00CE":"I","\u0128":"I","\u012A":"I","\u012C":"I","\u0130":"I","\u00CF":"I","\u1E2E":"I","\u1EC8":"I","\u01CF":"I","\u0208":"I","\u020A":"I","\u1ECA":"I","\u012E":"I","\u1E2C":"I","\u0197":"I","\u24BF":"J","\uFF2A":"J","\u0134":"J","\u0248":"J","\u24C0":"K","\uFF2B":"K","\u1E30":"K","\u01E8":"K","\u1E32":"K","\u0136":"K","\u1E34":"K","\u0198":"K","\u2C69":"K","\uA740":"K","\uA742":"K","\uA744":"K","\uA7A2":"K","\u24C1":"L","\uFF2C":"L","\u013F":"L","\u0139":"L","\u013D":"L","\u1E36":"L","\u1E38":"L","\u013B":"L","\u1E3C":"L","\u1E3A":"L","\u0141":"L","\u023D":"L","\u2C62":"L","\u2C60":"L","\uA748":"L","\uA746":"L","\uA780":"L","\u01C7":"LJ","\u01C8":"Lj","\u24C2":"M","\uFF2D":"M","\u1E3E":"M","\u1E40":"M","\u1E42":"M","\u2C6E":"M","\u019C":"M","\u24C3":"N","\uFF2E":"N","\u01F8":"N","\u0143":"N","\u00D1":"N","\u1E44":"N","\u0147":"N","\u1E46":"N","\u0145":"N","\u1E4A":"N","\u1E48":"N","\u0220":"N","\u019D":"N","\uA790":"N","\uA7A4":"N","\u01CA":"NJ","\u01CB":"Nj","\u24C4":"O","\uFF2F":"O","\u00D2":"O","\u00D3":"O","\u00D4":"O","\u1ED2":"O","\u1ED0":"O","\u1ED6":"O","\u1ED4":"O","\u00D5":"O","\u1E4C":"O","\u022C":"O","\u1E4E":"O","\u014C":"O","\u1E50":"O","\u1E52":"O","\u014E":"O","\u022E":"O","\u0230":"O","\u00D6":"O","\u022A":"O","\u1ECE":"O","\u0150":"O","\u01D1":"O","\u020C":"O","\u020E":"O","\u01A0":"O","\u1EDC":"O","\u1EDA":"O","\u1EE0":"O","\u1EDE":"O","\u1EE2":"O","\u1ECC":"O","\u1ED8":"O","\u01EA":"O","\u01EC":"O","\u00D8":"O","\u01FE":"O","\u0186":"O","\u019F":"O","\uA74A":"O","\uA74C":"O","\u01A2":"OI","\uA74E":"OO","\u0222":"OU","\u24C5":"P","\uFF30":"P","\u1E54":"P","\u1E56":"P","\u01A4":"P","\u2C63":"P","\uA750":"P","\uA752":"P","\uA754":"P","\u24C6":"Q","\uFF31":"Q","\uA756":"Q","\uA758":"Q","\u024A":"Q","\u24C7":"R","\uFF32":"R","\u0154":"R","\u1E58":"R","\u0158":"R","\u0210":"R","\u0212":"R","\u1E5A":"R","\u1E5C":"R","\u0156":"R","\u1E5E":"R","\u024C":"R","\u2C64":"R","\uA75A":"R","\uA7A6":"R","\uA782":"R","\u24C8":"S","\uFF33":"S","\u1E9E":"S","\u015A":"S","\u1E64":"S","\u015C":"S","\u1E60":"S","\u0160":"S","\u1E66":"S","\u1E62":"S","\u1E68":"S","\u0218":"S","\u015E":"S","\u2C7E":"S","\uA7A8":"S","\uA784":"S","\u24C9":"T","\uFF34":"T","\u1E6A":"T","\u0164":"T","\u1E6C":"T","\u021A":"T","\u0162":"T","\u1E70":"T","\u1E6E":"T","\u0166":"T","\u01AC":"T","\u01AE":"T","\u023E":"T","\uA786":"T","\uA728":"TZ","\u24CA":"U","\uFF35":"U","\u00D9":"U","\u00DA":"U","\u00DB":"U","\u0168":"U","\u1E78":"U","\u016A":"U","\u1E7A":"U","\u016C":"U","\u00DC":"U","\u01DB":"U","\u01D7":"U","\u01D5":"U","\u01D9":"U","\u1EE6":"U","\u016E":"U","\u0170":"U","\u01D3":"U","\u0214":"U","\u0216":"U","\u01AF":"U","\u1EEA":"U","\u1EE8":"U","\u1EEE":"U","\u1EEC":"U","\u1EF0":"U","\u1EE4":"U","\u1E72":"U","\u0172":"U","\u1E76":"U","\u1E74":"U","\u0244":"U","\u24CB":"V","\uFF36":"V","\u1E7C":"V","\u1E7E":"V","\u01B2":"V","\uA75E":"V","\u0245":"V","\uA760":"VY","\u24CC":"W","\uFF37":"W","\u1E80":"W","\u1E82":"W","\u0174":"W","\u1E86":"W","\u1E84":"W","\u1E88":"W","\u2C72":"W","\u24CD":"X","\uFF38":"X","\u1E8A":"X","\u1E8C":"X","\u24CE":"Y","\uFF39":"Y","\u1EF2":"Y","\u00DD":"Y","\u0176":"Y","\u1EF8":"Y","\u0232":"Y","\u1E8E":"Y","\u0178":"Y","\u1EF6":"Y","\u1EF4":"Y","\u01B3":"Y","\u024E":"Y","\u1EFE":"Y","\u24CF":"Z","\uFF3A":"Z","\u0179":"Z","\u1E90":"Z","\u017B":"Z","\u017D":"Z","\u1E92":"Z","\u1E94":"Z","\u01B5":"Z","\u0224":"Z","\u2C7F":"Z","\u2C6B":"Z","\uA762":"Z","\u24D0":"a","\uFF41":"a","\u1E9A":"a","\u00E0":"a","\u00E1":"a","\u00E2":"a","\u1EA7":"a","\u1EA5":"a","\u1EAB":"a","\u1EA9":"a","\u00E3":"a","\u0101":"a","\u0103":"a","\u1EB1":"a","\u1EAF":"a","\u1EB5":"a","\u1EB3":"a","\u0227":"a","\u01E1":"a","\u00E4":"a","\u01DF":"a","\u1EA3":"a","\u00E5":"a","\u01FB":"a","\u01CE":"a","\u0201":"a","\u0203":"a","\u1EA1":"a","\u1EAD":"a","\u1EB7":"a","\u1E01":"a","\u0105":"a","\u2C65":"a","\u0250":"a","\uA733":"aa","\u00E6":"ae","\u01FD":"ae","\u01E3":"ae","\uA735":"ao","\uA737":"au","\uA739":"av","\uA73B":"av","\uA73D":"ay","\u24D1":"b","\uFF42":"b","\u1E03":"b","\u1E05":"b","\u1E07":"b","\u0180":"b","\u0183":"b","\u0253":"b","\u24D2":"c","\uFF43":"c","\u0107":"c","\u0109":"c","\u010B":"c","\u010D":"c","\u00E7":"c","\u1E09":"c","\u0188":"c","\u023C":"c","\uA73F":"c","\u2184":"c","\u24D3":"d","\uFF44":"d","\u1E0B":"d","\u010F":"d","\u1E0D":"d","\u1E11":"d","\u1E13":"d","\u1E0F":"d","\u0111":"d","\u018C":"d","\u0256":"d","\u0257":"d","\uA77A":"d","\u01F3":"dz","\u01C6":"dz","\u24D4":"e","\uFF45":"e","\u00E8":"e","\u00E9":"e","\u00EA":"e","\u1EC1":"e","\u1EBF":"e","\u1EC5":"e","\u1EC3":"e","\u1EBD":"e","\u0113":"e","\u1E15":"e","\u1E17":"e","\u0115":"e","\u0117":"e","\u00EB":"e","\u1EBB":"e","\u011B":"e","\u0205":"e","\u0207":"e","\u1EB9":"e","\u1EC7":"e","\u0229":"e","\u1E1D":"e","\u0119":"e","\u1E19":"e","\u1E1B":"e","\u0247":"e","\u025B":"e","\u01DD":"e","\u24D5":"f","\uFF46":"f","\u1E1F":"f","\u0192":"f","\uA77C":"f","\u24D6":"g","\uFF47":"g","\u01F5":"g","\u011D":"g","\u1E21":"g","\u011F":"g","\u0121":"g","\u01E7":"g","\u0123":"g","\u01E5":"g","\u0260":"g","\uA7A1":"g","\u1D79":"g","\uA77F":"g","\u24D7":"h","\uFF48":"h","\u0125":"h","\u1E23":"h","\u1E27":"h","\u021F":"h","\u1E25":"h","\u1E29":"h","\u1E2B":"h","\u1E96":"h","\u0127":"h","\u2C68":"h","\u2C76":"h","\u0265":"h","\u0195":"hv","\u24D8":"i","\uFF49":"i","\u00EC":"i","\u00ED":"i","\u00EE":"i","\u0129":"i","\u012B":"i","\u012D":"i","\u00EF":"i","\u1E2F":"i","\u1EC9":"i","\u01D0":"i","\u0209":"i","\u020B":"i","\u1ECB":"i","\u012F":"i","\u1E2D":"i","\u0268":"i","\u0131":"i","\u24D9":"j","\uFF4A":"j","\u0135":"j","\u01F0":"j","\u0249":"j","\u24DA":"k","\uFF4B":"k","\u1E31":"k","\u01E9":"k","\u1E33":"k","\u0137":"k","\u1E35":"k","\u0199":"k","\u2C6A":"k","\uA741":"k","\uA743":"k","\uA745":"k","\uA7A3":"k","\u24DB":"l","\uFF4C":"l","\u0140":"l","\u013A":"l","\u013E":"l","\u1E37":"l","\u1E39":"l","\u013C":"l","\u1E3D":"l","\u1E3B":"l","\u017F":"l","\u0142":"l","\u019A":"l","\u026B":"l","\u2C61":"l","\uA749":"l","\uA781":"l","\uA747":"l","\u01C9":"lj","\u24DC":"m","\uFF4D":"m","\u1E3F":"m","\u1E41":"m","\u1E43":"m","\u0271":"m","\u026F":"m","\u24DD":"n","\uFF4E":"n","\u01F9":"n","\u0144":"n","\u00F1":"n","\u1E45":"n","\u0148":"n","\u1E47":"n","\u0146":"n","\u1E4B":"n","\u1E49":"n","\u019E":"n","\u0272":"n","\u0149":"n","\uA791":"n","\uA7A5":"n","\u01CC":"nj","\u24DE":"o","\uFF4F":"o","\u00F2":"o","\u00F3":"o","\u00F4":"o","\u1ED3":"o","\u1ED1":"o","\u1ED7":"o","\u1ED5":"o","\u00F5":"o","\u1E4D":"o","\u022D":"o","\u1E4F":"o","\u014D":"o","\u1E51":"o","\u1E53":"o","\u014F":"o","\u022F":"o","\u0231":"o","\u00F6":"o","\u022B":"o","\u1ECF":"o","\u0151":"o","\u01D2":"o","\u020D":"o","\u020F":"o","\u01A1":"o","\u1EDD":"o","\u1EDB":"o","\u1EE1":"o","\u1EDF":"o","\u1EE3":"o","\u1ECD":"o","\u1ED9":"o","\u01EB":"o","\u01ED":"o","\u00F8":"o","\u01FF":"o","\u0254":"o","\uA74B":"o","\uA74D":"o","\u0275":"o","\u01A3":"oi","\u0223":"ou","\uA74F":"oo","\u24DF":"p","\uFF50":"p","\u1E55":"p","\u1E57":"p","\u01A5":"p","\u1D7D":"p","\uA751":"p","\uA753":"p","\uA755":"p","\u24E0":"q","\uFF51":"q","\u024B":"q","\uA757":"q","\uA759":"q","\u24E1":"r","\uFF52":"r","\u0155":"r","\u1E59":"r","\u0159":"r","\u0211":"r","\u0213":"r","\u1E5B":"r","\u1E5D":"r","\u0157":"r","\u1E5F":"r","\u024D":"r","\u027D":"r","\uA75B":"r","\uA7A7":"r","\uA783":"r","\u24E2":"s","\uFF53":"s","\u00DF":"s","\u015B":"s","\u1E65":"s","\u015D":"s","\u1E61":"s","\u0161":"s","\u1E67":"s","\u1E63":"s","\u1E69":"s","\u0219":"s","\u015F":"s","\u023F":"s","\uA7A9":"s","\uA785":"s","\u1E9B":"s","\u24E3":"t","\uFF54":"t","\u1E6B":"t","\u1E97":"t","\u0165":"t","\u1E6D":"t","\u021B":"t","\u0163":"t","\u1E71":"t","\u1E6F":"t","\u0167":"t","\u01AD":"t","\u0288":"t","\u2C66":"t","\uA787":"t","\uA729":"tz","\u24E4":"u","\uFF55":"u","\u00F9":"u","\u00FA":"u","\u00FB":"u","\u0169":"u","\u1E79":"u","\u016B":"u","\u1E7B":"u","\u016D":"u","\u00FC":"u","\u01DC":"u","\u01D8":"u","\u01D6":"u","\u01DA":"u","\u1EE7":"u","\u016F":"u","\u0171":"u","\u01D4":"u","\u0215":"u","\u0217":"u","\u01B0":"u","\u1EEB":"u","\u1EE9":"u","\u1EEF":"u","\u1EED":"u","\u1EF1":"u","\u1EE5":"u","\u1E73":"u","\u0173":"u","\u1E77":"u","\u1E75":"u","\u0289":"u","\u24E5":"v","\uFF56":"v","\u1E7D":"v","\u1E7F":"v","\u028B":"v","\uA75F":"v","\u028C":"v","\uA761":"vy","\u24E6":"w","\uFF57":"w","\u1E81":"w","\u1E83":"w","\u0175":"w","\u1E87":"w","\u1E85":"w","\u1E98":"w","\u1E89":"w","\u2C73":"w","\u24E7":"x","\uFF58":"x","\u1E8B":"x","\u1E8D":"x","\u24E8":"y","\uFF59":"y","\u1EF3":"y","\u00FD":"y","\u0177":"y","\u1EF9":"y","\u0233":"y","\u1E8F":"y","\u00FF":"y","\u1EF7":"y","\u1E99":"y","\u1EF5":"y","\u01B4":"y","\u024F":"y","\u1EFF":"y","\u24E9":"z","\uFF5A":"z","\u017A":"z","\u1E91":"z","\u017C":"z","\u017E":"z","\u1E93":"z","\u1E95":"z","\u01B6":"z","\u0225":"z","\u0240":"z","\u2C6C":"z","\uA763":"z","\u0386":"\u0391","\u0388":"\u0395","\u0389":"\u0397","\u038A":"\u0399","\u03AA":"\u0399","\u038C":"\u039F","\u038E":"\u03A5","\u03AB":"\u03A5","\u038F":"\u03A9","\u03AC":"\u03B1","\u03AD":"\u03B5","\u03AE":"\u03B7","\u03AF":"\u03B9","\u03CA":"\u03B9","\u0390":"\u03B9","\u03CC":"\u03BF","\u03CD":"\u03C5","\u03CB":"\u03C5","\u03B0":"\u03C5","\u03C9":"\u03C9","\u03C2":"\u03C3"}; + + $document = $(document); + + nextUid=(function() { var counter=1; return function() { return counter++; }; }()); + + + function reinsertElement(element) { + var placeholder = $(document.createTextNode('')); + + element.before(placeholder); + placeholder.before(element); + placeholder.remove(); + } + + function stripDiacritics(str) { + // Used 'uni range + named function' from http://jsperf.com/diacritics/18 + function match(a) { + return DIACRITICS[a] || a; + } + + return str.replace(/[^\u0000-\u007E]/g, match); + } + + function indexOf(value, array) { + var i = 0, l = array.length; + for (; i < l; i = i + 1) { + if (equal(value, array[i])) return i; + } + return -1; + } + + function measureScrollbar () { + var $template = $( MEASURE_SCROLLBAR_TEMPLATE ); + $template.appendTo('body'); + + var dim = { + width: $template.width() - $template[0].clientWidth, + height: $template.height() - $template[0].clientHeight + }; + $template.remove(); + + return dim; + } + + /** + * Compares equality of a and b + * @param a + * @param b + */ + function equal(a, b) { + if (a === b) return true; + if (a === undefined || b === undefined) return false; + if (a === null || b === null) return false; + // Check whether 'a' or 'b' is a string (primitive or object). + // The concatenation of an empty string (+'') converts its argument to a string's primitive. + if (a.constructor === String) return a+'' === b+''; // a+'' - in case 'a' is a String object + if (b.constructor === String) return b+'' === a+''; // b+'' - in case 'b' is a String object + return false; + } + + /** + * Splits the string into an array of values, trimming each value. An empty array is returned for nulls or empty + * strings + * @param string + * @param separator + */ + function splitVal(string, separator) { + var val, i, l; + if (string === null || string.length < 1) return []; + val = string.split(separator); + for (i = 0, l = val.length; i < l; i = i + 1) val[i] = $.trim(val[i]); + return val; + } + + function getSideBorderPadding(element) { + return element.outerWidth(false) - element.width(); + } + + function installKeyUpChangeEvent(element) { + var key="keyup-change-value"; + element.on("keydown", function () { + if ($.data(element, key) === undefined) { + $.data(element, key, element.val()); + } + }); + element.on("keyup", function () { + var val= $.data(element, key); + if (val !== undefined && element.val() !== val) { + $.removeData(element, key); + element.trigger("keyup-change"); + } + }); + } + + + /** + * filters mouse events so an event is fired only if the mouse moved. + * + * filters out mouse events that occur when mouse is stationary but + * the elements under the pointer are scrolled. + */ + function installFilteredMouseMove(element) { + element.on("mousemove", function (e) { + var lastpos = lastMousePosition; + if (lastpos === undefined || lastpos.x !== e.pageX || lastpos.y !== e.pageY) { + $(e.target).trigger("mousemove-filtered", e); + } + }); + } + + /** + * Debounces a function. Returns a function that calls the original fn function only if no invocations have been made + * within the last quietMillis milliseconds. + * + * @param quietMillis number of milliseconds to wait before invoking fn + * @param fn function to be debounced + * @param ctx object to be used as this reference within fn + * @return debounced version of fn + */ + function debounce(quietMillis, fn, ctx) { + ctx = ctx || undefined; + var timeout; + return function () { + var args = arguments; + window.clearTimeout(timeout); + timeout = window.setTimeout(function() { + fn.apply(ctx, args); + }, quietMillis); + }; + } + + function installDebouncedScroll(threshold, element) { + var notify = debounce(threshold, function (e) { element.trigger("scroll-debounced", e);}); + element.on("scroll", function (e) { + if (indexOf(e.target, element.get()) >= 0) notify(e); + }); + } + + function focus($el) { + if ($el[0] === document.activeElement) return; + + /* set the focus in a 0 timeout - that way the focus is set after the processing + of the current event has finished - which seems like the only reliable way + to set focus */ + window.setTimeout(function() { + var el=$el[0], pos=$el.val().length, range; + + $el.focus(); + + /* make sure el received focus so we do not error out when trying to manipulate the caret. + sometimes modals or others listeners may steal it after its set */ + var isVisible = (el.offsetWidth > 0 || el.offsetHeight > 0); + if (isVisible && el === document.activeElement) { + + /* after the focus is set move the caret to the end, necessary when we val() + just before setting focus */ + if(el.setSelectionRange) + { + el.setSelectionRange(pos, pos); + } + else if (el.createTextRange) { + range = el.createTextRange(); + range.collapse(false); + range.select(); + } + } + }, 0); + } + + function getCursorInfo(el) { + el = $(el)[0]; + var offset = 0; + var length = 0; + if ('selectionStart' in el) { + offset = el.selectionStart; + length = el.selectionEnd - offset; + } else if ('selection' in document) { + el.focus(); + var sel = document.selection.createRange(); + length = document.selection.createRange().text.length; + sel.moveStart('character', -el.value.length); + offset = sel.text.length - length; + } + return { offset: offset, length: length }; + } + + function killEvent(event) { + event.preventDefault(); + event.stopPropagation(); + } + function killEventImmediately(event) { + event.preventDefault(); + event.stopImmediatePropagation(); + } + + function measureTextWidth(e) { + if (!sizer){ + var style = e[0].currentStyle || window.getComputedStyle(e[0], null); + sizer = $(document.createElement("div")).css({ + position: "absolute", + left: "-10000px", + top: "-10000px", + display: "none", + fontSize: style.fontSize, + fontFamily: style.fontFamily, + fontStyle: style.fontStyle, + fontWeight: style.fontWeight, + letterSpacing: style.letterSpacing, + textTransform: style.textTransform, + whiteSpace: "nowrap" + }); + sizer.attr("class","select2-sizer"); + $("body").append(sizer); + } + sizer.text(e.val()); + return sizer.width(); + } + + function syncCssClasses(dest, src, adapter) { + var classes, replacements = [], adapted; + + classes = $.trim(dest.attr("class")); + + if (classes) { + classes = '' + classes; // for IE which returns object + + $(classes.split(/\s+/)).each2(function() { + if (this.indexOf("select2-") === 0) { + replacements.push(this); + } + }); + } + + classes = $.trim(src.attr("class")); + + if (classes) { + classes = '' + classes; // for IE which returns object + + $(classes.split(/\s+/)).each2(function() { + if (this.indexOf("select2-") !== 0) { + adapted = adapter(this); + + if (adapted) { + replacements.push(adapted); + } + } + }); + } + + dest.attr("class", replacements.join(" ")); + } + + + function markMatch(text, term, markup, escapeMarkup) { + var match=stripDiacritics(text.toUpperCase()).indexOf(stripDiacritics(term.toUpperCase())), + tl=term.length; + + if (match<0) { + markup.push(escapeMarkup(text)); + return; + } + + markup.push(escapeMarkup(text.substring(0, match))); + markup.push(""); + markup.push(escapeMarkup(text.substring(match, match + tl))); + markup.push(""); + markup.push(escapeMarkup(text.substring(match + tl, text.length))); + } + + function defaultEscapeMarkup(markup) { + var replace_map = { + '\\': '\', + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + "/": '/' + }; + + return String(markup).replace(/[&<>"'\/\\]/g, function (match) { + return replace_map[match]; + }); + } + + /** + * Produces an ajax-based query function + * + * @param options object containing configuration parameters + * @param options.params parameter map for the transport ajax call, can contain such options as cache, jsonpCallback, etc. see $.ajax + * @param options.transport function that will be used to execute the ajax request. must be compatible with parameters supported by $.ajax + * @param options.url url for the data + * @param options.data a function(searchTerm, pageNumber, context) that should return an object containing query string parameters for the above url. + * @param options.dataType request data type: ajax, jsonp, other datatypes supported by jQuery's $.ajax function or the transport function if specified + * @param options.quietMillis (optional) milliseconds to wait before making the ajaxRequest, helps debounce the ajax function if invoked too often + * @param options.results a function(remoteData, pageNumber, query) that converts data returned form the remote request to the format expected by Select2. + * The expected format is an object containing the following keys: + * results array of objects that will be used as choices + * more (optional) boolean indicating whether there are more results available + * Example: {results:[{id:1, text:'Red'},{id:2, text:'Blue'}], more:true} + */ + function ajax(options) { + var timeout, // current scheduled but not yet executed request + handler = null, + quietMillis = options.quietMillis || 100, + ajaxUrl = options.url, + self = this; + + return function (query) { + window.clearTimeout(timeout); + timeout = window.setTimeout(function () { + var data = options.data, // ajax data function + url = ajaxUrl, // ajax url string or function + transport = options.transport || $.fn.select2.ajaxDefaults.transport, + // deprecated - to be removed in 4.0 - use params instead + deprecated = { + type: options.type || 'GET', // set type of request (GET or POST) + cache: options.cache || false, + jsonpCallback: options.jsonpCallback||undefined, + dataType: options.dataType||"json" + }, + params = $.extend({}, $.fn.select2.ajaxDefaults.params, deprecated); + + data = data ? data.call(self, query.term, query.page, query.context) : null; + url = (typeof url === 'function') ? url.call(self, query.term, query.page, query.context) : url; + + if (handler && typeof handler.abort === "function") { handler.abort(); } + + if (options.params) { + if ($.isFunction(options.params)) { + $.extend(params, options.params.call(self)); + } else { + $.extend(params, options.params); + } + } + + $.extend(params, { + url: url, + dataType: options.dataType, + data: data, + success: function (data) { + // TODO - replace query.page with query so users have access to term, page, etc. + // added query as third paramter to keep backwards compatibility + var results = options.results(data, query.page, query); + query.callback(results); + } + }); + handler = transport.call(self, params); + }, quietMillis); + }; + } + + /** + * Produces a query function that works with a local array + * + * @param options object containing configuration parameters. The options parameter can either be an array or an + * object. + * + * If the array form is used it is assumed that it contains objects with 'id' and 'text' keys. + * + * If the object form is used it is assumed that it contains 'data' and 'text' keys. The 'data' key should contain + * an array of objects that will be used as choices. These objects must contain at least an 'id' key. The 'text' + * key can either be a String in which case it is expected that each element in the 'data' array has a key with the + * value of 'text' which will be used to match choices. Alternatively, text can be a function(item) that can extract + * the text. + */ + function local(options) { + var data = options, // data elements + dataText, + tmp, + text = function (item) { return ""+item.text; }; // function used to retrieve the text portion of a data item that is matched against the search + + if ($.isArray(data)) { + tmp = data; + data = { results: tmp }; + } + + if ($.isFunction(data) === false) { + tmp = data; + data = function() { return tmp; }; + } + + var dataItem = data(); + if (dataItem.text) { + text = dataItem.text; + // if text is not a function we assume it to be a key name + if (!$.isFunction(text)) { + dataText = dataItem.text; // we need to store this in a separate variable because in the next step data gets reset and data.text is no longer available + text = function (item) { return item[dataText]; }; + } + } + + return function (query) { + var t = query.term, filtered = { results: [] }, process; + if (t === "") { + query.callback(data()); + return; + } + + process = function(datum, collection) { + var group, attr; + datum = datum[0]; + if (datum.children) { + group = {}; + for (attr in datum) { + if (datum.hasOwnProperty(attr)) group[attr]=datum[attr]; + } + group.children=[]; + $(datum.children).each2(function(i, childDatum) { process(childDatum, group.children); }); + if (group.children.length || query.matcher(t, text(group), datum)) { + collection.push(group); + } + } else { + if (query.matcher(t, text(datum), datum)) { + collection.push(datum); + } + } + }; + + $(data().results).each2(function(i, datum) { process(datum, filtered.results); }); + query.callback(filtered); + }; + } + + // TODO javadoc + function tags(data) { + var isFunc = $.isFunction(data); + return function (query) { + var t = query.term, filtered = {results: []}; + var result = isFunc ? data(query) : data; + if ($.isArray(result)) { + $(result).each(function () { + var isObject = this.text !== undefined, + text = isObject ? this.text : this; + if (t === "" || query.matcher(t, text)) { + filtered.results.push(isObject ? this : {id: this, text: this}); + } + }); + query.callback(filtered); + } + }; + } + + /** + * Checks if the formatter function should be used. + * + * Throws an error if it is not a function. Returns true if it should be used, + * false if no formatting should be performed. + * + * @param formatter + */ + function checkFormatter(formatter, formatterName) { + if ($.isFunction(formatter)) return true; + if (!formatter) return false; + if (typeof(formatter) === 'string') return true; + throw new Error(formatterName +" must be a string, function, or falsy value"); + } + + /** + * Returns a given value + * If given a function, returns its output + * + * @param val string|function + * @param context value of "this" to be passed to function + * @returns {*} + */ + function evaluate(val, context) { + if ($.isFunction(val)) { + var args = Array.prototype.slice.call(arguments, 2); + return val.apply(context, args); + } + return val; + } + + function countResults(results) { + var count = 0; + $.each(results, function(i, item) { + if (item.children) { + count += countResults(item.children); + } else { + count++; + } + }); + return count; + } + + /** + * Default tokenizer. This function uses breaks the input on substring match of any string from the + * opts.tokenSeparators array and uses opts.createSearchChoice to create the choice object. Both of those + * two options have to be defined in order for the tokenizer to work. + * + * @param input text user has typed so far or pasted into the search field + * @param selection currently selected choices + * @param selectCallback function(choice) callback tho add the choice to selection + * @param opts select2's opts + * @return undefined/null to leave the current input unchanged, or a string to change the input to the returned value + */ + function defaultTokenizer(input, selection, selectCallback, opts) { + var original = input, // store the original so we can compare and know if we need to tell the search to update its text + dupe = false, // check for whether a token we extracted represents a duplicate selected choice + token, // token + index, // position at which the separator was found + i, l, // looping variables + separator; // the matched separator + + if (!opts.createSearchChoice || !opts.tokenSeparators || opts.tokenSeparators.length < 1) return undefined; + + while (true) { + index = -1; + + for (i = 0, l = opts.tokenSeparators.length; i < l; i++) { + separator = opts.tokenSeparators[i]; + index = input.indexOf(separator); + if (index >= 0) break; + } + + if (index < 0) break; // did not find any token separator in the input string, bail + + token = input.substring(0, index); + input = input.substring(index + separator.length); + + if (token.length > 0) { + token = opts.createSearchChoice.call(this, token, selection); + if (token !== undefined && token !== null && opts.id(token) !== undefined && opts.id(token) !== null) { + dupe = false; + for (i = 0, l = selection.length; i < l; i++) { + if (equal(opts.id(token), opts.id(selection[i]))) { + dupe = true; break; + } + } + + if (!dupe) selectCallback(token); + } + } + } + + if (original!==input) return input; + } + + function cleanupJQueryElements() { + var self = this; + + $.each(arguments, function (i, element) { + self[element].remove(); + self[element] = null; + }); + } + + /** + * Creates a new class + * + * @param superClass + * @param methods + */ + function clazz(SuperClass, methods) { + var constructor = function () {}; + constructor.prototype = new SuperClass; + constructor.prototype.constructor = constructor; + constructor.prototype.parent = SuperClass.prototype; + constructor.prototype = $.extend(constructor.prototype, methods); + return constructor; + } + + AbstractSelect2 = clazz(Object, { + + // abstract + bind: function (func) { + var self = this; + return function () { + func.apply(self, arguments); + }; + }, + + // abstract + init: function (opts) { + var results, search, resultsSelector = ".select2-results"; + + // prepare options + this.opts = opts = this.prepareOpts(opts); + + this.id=opts.id; + + // destroy if called on an existing component + if (opts.element.data("select2") !== undefined && + opts.element.data("select2") !== null) { + opts.element.data("select2").destroy(); + } + + this.container = this.createContainer(); + + this.liveRegion = $("", { + role: "status", + "aria-live": "polite" + }) + .addClass("select2-hidden-accessible") + .appendTo(document.body); + + this.containerId="s2id_"+(opts.element.attr("id") || "autogen"+nextUid()); + this.containerEventName= this.containerId + .replace(/([.])/g, '_') + .replace(/([;&,\-\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '\\$1'); + this.container.attr("id", this.containerId); + + this.container.attr("title", opts.element.attr("title")); + + this.body = $("body"); + + syncCssClasses(this.container, this.opts.element, this.opts.adaptContainerCssClass); + + this.container.attr("style", opts.element.attr("style")); + this.container.css(evaluate(opts.containerCss, this.opts.element)); + this.container.addClass(evaluate(opts.containerCssClass, this.opts.element)); + + this.elementTabIndex = this.opts.element.attr("tabindex"); + + // swap container for the element + this.opts.element + .data("select2", this) + .attr("tabindex", "-1") + .before(this.container) + .on("click.select2", killEvent); // do not leak click events + + this.container.data("select2", this); + + this.dropdown = this.container.find(".select2-drop"); + + syncCssClasses(this.dropdown, this.opts.element, this.opts.adaptDropdownCssClass); + + this.dropdown.addClass(evaluate(opts.dropdownCssClass, this.opts.element)); + this.dropdown.data("select2", this); + this.dropdown.on("click", killEvent); + + this.results = results = this.container.find(resultsSelector); + this.search = search = this.container.find("input.select2-input"); + + this.queryCount = 0; + this.resultsPage = 0; + this.context = null; + + // initialize the container + this.initContainer(); + + this.container.on("click", killEvent); + + installFilteredMouseMove(this.results); + + this.dropdown.on("mousemove-filtered", resultsSelector, this.bind(this.highlightUnderEvent)); + this.dropdown.on("touchstart touchmove touchend", resultsSelector, this.bind(function (event) { + this._touchEvent = true; + this.highlightUnderEvent(event); + })); + this.dropdown.on("touchmove", resultsSelector, this.bind(this.touchMoved)); + this.dropdown.on("touchstart touchend", resultsSelector, this.bind(this.clearTouchMoved)); + + // Waiting for a click event on touch devices to select option and hide dropdown + // otherwise click will be triggered on an underlying element + this.dropdown.on('click', this.bind(function (event) { + if (this._touchEvent) { + this._touchEvent = false; + this.selectHighlighted(); + } + })); + + installDebouncedScroll(80, this.results); + this.dropdown.on("scroll-debounced", resultsSelector, this.bind(this.loadMoreIfNeeded)); + + // do not propagate change event from the search field out of the component + $(this.container).on("change", ".select2-input", function(e) {e.stopPropagation();}); + $(this.dropdown).on("change", ".select2-input", function(e) {e.stopPropagation();}); + + // if jquery.mousewheel plugin is installed we can prevent out-of-bounds scrolling of results via mousewheel + if ($.fn.mousewheel) { + results.mousewheel(function (e, delta, deltaX, deltaY) { + var top = results.scrollTop(); + if (deltaY > 0 && top - deltaY <= 0) { + results.scrollTop(0); + killEvent(e); + } else if (deltaY < 0 && results.get(0).scrollHeight - results.scrollTop() + deltaY <= results.height()) { + results.scrollTop(results.get(0).scrollHeight - results.height()); + killEvent(e); + } + }); + } + + installKeyUpChangeEvent(search); + search.on("keyup-change input paste", this.bind(this.updateResults)); + search.on("focus", function () { search.addClass("select2-focused"); }); + search.on("blur", function () { search.removeClass("select2-focused");}); + + this.dropdown.on("mouseup", resultsSelector, this.bind(function (e) { + if ($(e.target).closest(".select2-result-selectable").length > 0) { + this.highlightUnderEvent(e); + this.selectHighlighted(e); + } + })); + + // trap all mouse events from leaving the dropdown. sometimes there may be a modal that is listening + // for mouse events outside of itself so it can close itself. since the dropdown is now outside the select2's + // dom it will trigger the popup close, which is not what we want + // focusin can cause focus wars between modals and select2 since the dropdown is outside the modal. + this.dropdown.on("click mouseup mousedown touchstart touchend focusin", function (e) { e.stopPropagation(); }); + + this.nextSearchTerm = undefined; + + if ($.isFunction(this.opts.initSelection)) { + // initialize selection based on the current value of the source element + this.initSelection(); + + // if the user has provided a function that can set selection based on the value of the source element + // we monitor the change event on the element and trigger it, allowing for two way synchronization + this.monitorSource(); + } + + if (opts.maximumInputLength !== null) { + this.search.attr("maxlength", opts.maximumInputLength); + } + + var disabled = opts.element.prop("disabled"); + if (disabled === undefined) disabled = false; + this.enable(!disabled); + + var readonly = opts.element.prop("readonly"); + if (readonly === undefined) readonly = false; + this.readonly(readonly); + + // Calculate size of scrollbar + scrollBarDimensions = scrollBarDimensions || measureScrollbar(); + + this.autofocus = opts.element.prop("autofocus"); + opts.element.prop("autofocus", false); + if (this.autofocus) this.focus(); + + this.search.attr("placeholder", opts.searchInputPlaceholder); + }, + + // abstract + destroy: function () { + var element=this.opts.element, select2 = element.data("select2"); + + this.close(); + + if (element.length && element[0].detachEvent) { + element.each(function () { + this.detachEvent("onpropertychange", this._sync); + }); + } + if (this.propertyObserver) { + this.propertyObserver.disconnect(); + this.propertyObserver = null; + } + this._sync = null; + + if (select2 !== undefined) { + select2.container.remove(); + select2.liveRegion.remove(); + select2.dropdown.remove(); + element + .removeClass("select2-offscreen") + .removeData("select2") + .off(".select2") + .prop("autofocus", this.autofocus || false); + if (this.elementTabIndex) { + element.attr({tabindex: this.elementTabIndex}); + } else { + element.removeAttr("tabindex"); + } + element.show(); + } + + cleanupJQueryElements.call(this, + "container", + "liveRegion", + "dropdown", + "results", + "search" + ); + }, + + // abstract + optionToData: function(element) { + if (element.is("option")) { + return { + id:element.prop("value"), + text:element.text(), + element: element.get(), + css: element.attr("class"), + disabled: element.prop("disabled"), + locked: equal(element.attr("locked"), "locked") || equal(element.data("locked"), true) + }; + } else if (element.is("optgroup")) { + return { + text:element.attr("label"), + children:[], + element: element.get(), + css: element.attr("class") + }; + } + }, + + // abstract + prepareOpts: function (opts) { + var element, select, idKey, ajaxUrl, self = this; + + element = opts.element; + + if (element.get(0).tagName.toLowerCase() === "select") { + this.select = select = opts.element; + } + + if (select) { + // these options are not allowed when attached to a select because they are picked up off the element itself + $.each(["id", "multiple", "ajax", "query", "createSearchChoice", "initSelection", "data", "tags"], function () { + if (this in opts) { + throw new Error("Option '" + this + "' is not allowed for Select2 when attached to a ", + "
    ", + " ", + "
      ", + "
    ", + "
    "].join("")); + return container; + }, + + // single + enableInterface: function() { + if (this.parent.enableInterface.apply(this, arguments)) { + this.focusser.prop("disabled", !this.isInterfaceEnabled()); + } + }, + + // single + opening: function () { + var el, range, len; + + if (this.opts.minimumResultsForSearch >= 0) { + this.showSearch(true); + } + + this.parent.opening.apply(this, arguments); + + if (this.showSearchInput !== false) { + // IE appends focusser.val() at the end of field :/ so we manually insert it at the beginning using a range + // all other browsers handle this just fine + + this.search.val(this.focusser.val()); + } + if (this.opts.shouldFocusInput(this)) { + this.search.focus(); + // move the cursor to the end after focussing, otherwise it will be at the beginning and + // new text will appear *before* focusser.val() + el = this.search.get(0); + if (el.createTextRange) { + range = el.createTextRange(); + range.collapse(false); + range.select(); + } else if (el.setSelectionRange) { + len = this.search.val().length; + el.setSelectionRange(len, len); + } + } + + // initializes search's value with nextSearchTerm (if defined by user) + // ignore nextSearchTerm if the dropdown is opened by the user pressing a letter + if(this.search.val() === "") { + if(this.nextSearchTerm != undefined){ + this.search.val(this.nextSearchTerm); + this.search.select(); + } + } + + this.focusser.prop("disabled", true).val(""); + this.updateResults(true); + this.opts.element.trigger($.Event("select2-open")); + }, + + // single + close: function () { + if (!this.opened()) return; + this.parent.close.apply(this, arguments); + + this.focusser.prop("disabled", false); + + if (this.opts.shouldFocusInput(this)) { + this.focusser.focus(); + } + }, + + // single + focus: function () { + if (this.opened()) { + this.close(); + } else { + this.focusser.prop("disabled", false); + if (this.opts.shouldFocusInput(this)) { + this.focusser.focus(); + } + } + }, + + // single + isFocused: function () { + return this.container.hasClass("select2-container-active"); + }, + + // single + cancel: function () { + this.parent.cancel.apply(this, arguments); + this.focusser.prop("disabled", false); + + if (this.opts.shouldFocusInput(this)) { + this.focusser.focus(); + } + }, + + // single + destroy: function() { + $("label[for='" + this.focusser.attr('id') + "']") + .attr('for', this.opts.element.attr("id")); + this.parent.destroy.apply(this, arguments); + + cleanupJQueryElements.call(this, + "selection", + "focusser" + ); + }, + + // single + initContainer: function () { + + var selection, + container = this.container, + dropdown = this.dropdown, + idSuffix = nextUid(), + elementLabel; + + if (this.opts.minimumResultsForSearch < 0) { + this.showSearch(false); + } else { + this.showSearch(true); + } + + this.selection = selection = container.find(".select2-choice"); + + this.focusser = container.find(".select2-focusser"); + + // add aria associations + selection.find(".select2-chosen").attr("id", "select2-chosen-"+idSuffix); + this.focusser.attr("aria-labelledby", "select2-chosen-"+idSuffix); + this.results.attr("id", "select2-results-"+idSuffix); + this.search.attr("aria-owns", "select2-results-"+idSuffix); + + // rewrite labels from original element to focusser + this.focusser.attr("id", "s2id_autogen"+idSuffix); + + elementLabel = $("label[for='" + this.opts.element.attr("id") + "']"); + + this.focusser.prev() + .text(elementLabel.text()) + .attr('for', this.focusser.attr('id')); + + // Ensure the original element retains an accessible name + var originalTitle = this.opts.element.attr("title"); + this.opts.element.attr("title", (originalTitle || elementLabel.text())); + + this.focusser.attr("tabindex", this.elementTabIndex); + + // write label for search field using the label from the focusser element + this.search.attr("id", this.focusser.attr('id') + '_search'); + + this.search.prev() + .text($("label[for='" + this.focusser.attr('id') + "']").text()) + .attr('for', this.search.attr('id')); + + this.search.on("keydown", this.bind(function (e) { + if (!this.isInterfaceEnabled()) return; + + if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) { + // prevent the page from scrolling + killEvent(e); + return; + } + + switch (e.which) { + case KEY.UP: + case KEY.DOWN: + this.moveHighlight((e.which === KEY.UP) ? -1 : 1); + killEvent(e); + return; + case KEY.ENTER: + this.selectHighlighted(); + killEvent(e); + return; + case KEY.TAB: + this.selectHighlighted({noFocus: true}); + return; + case KEY.ESC: + this.cancel(e); + killEvent(e); + return; + } + })); + + this.search.on("blur", this.bind(function(e) { + // a workaround for chrome to keep the search field focussed when the scroll bar is used to scroll the dropdown. + // without this the search field loses focus which is annoying + if (document.activeElement === this.body.get(0)) { + window.setTimeout(this.bind(function() { + if (this.opened()) { + this.search.focus(); + } + }), 0); + } + })); + + this.focusser.on("keydown", this.bind(function (e) { + if (!this.isInterfaceEnabled()) return; + + if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) { + return; + } + + if (this.opts.openOnEnter === false && e.which === KEY.ENTER) { + killEvent(e); + return; + } + + if (e.which == KEY.DOWN || e.which == KEY.UP + || (e.which == KEY.ENTER && this.opts.openOnEnter)) { + + if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) return; + + this.open(); + killEvent(e); + return; + } + + if (e.which == KEY.DELETE || e.which == KEY.BACKSPACE) { + if (this.opts.allowClear) { + this.clear(); + } + killEvent(e); + return; + } + })); + + + installKeyUpChangeEvent(this.focusser); + this.focusser.on("keyup-change input", this.bind(function(e) { + if (this.opts.minimumResultsForSearch >= 0) { + e.stopPropagation(); + if (this.opened()) return; + this.open(); + } + })); + + selection.on("mousedown touchstart", "abbr", this.bind(function (e) { + if (!this.isInterfaceEnabled()) return; + this.clear(); + killEventImmediately(e); + this.close(); + this.selection.focus(); + })); + + selection.on("mousedown touchstart", this.bind(function (e) { + // Prevent IE from generating a click event on the body + reinsertElement(selection); + + if (!this.container.hasClass("select2-container-active")) { + this.opts.element.trigger($.Event("select2-focus")); + } + + if (this.opened()) { + this.close(); + } else if (this.isInterfaceEnabled()) { + this.open(); + } + + killEvent(e); + })); + + dropdown.on("mousedown touchstart", this.bind(function() { + if (this.opts.shouldFocusInput(this)) { + this.search.focus(); + } + })); + + selection.on("focus", this.bind(function(e) { + killEvent(e); + })); + + this.focusser.on("focus", this.bind(function(){ + if (!this.container.hasClass("select2-container-active")) { + this.opts.element.trigger($.Event("select2-focus")); + } + this.container.addClass("select2-container-active"); + })).on("blur", this.bind(function() { + if (!this.opened()) { + this.container.removeClass("select2-container-active"); + this.opts.element.trigger($.Event("select2-blur")); + } + })); + this.search.on("focus", this.bind(function(){ + if (!this.container.hasClass("select2-container-active")) { + this.opts.element.trigger($.Event("select2-focus")); + } + this.container.addClass("select2-container-active"); + })); + + this.initContainerWidth(); + this.opts.element.addClass("select2-offscreen"); + this.setPlaceholder(); + + }, + + // single + clear: function(triggerChange) { + var data=this.selection.data("select2-data"); + if (data) { // guard against queued quick consecutive clicks + var evt = $.Event("select2-clearing"); + this.opts.element.trigger(evt); + if (evt.isDefaultPrevented()) { + return; + } + var placeholderOption = this.getPlaceholderOption(); + this.opts.element.val(placeholderOption ? placeholderOption.val() : ""); + this.selection.find(".select2-chosen").empty(); + this.selection.removeData("select2-data"); + this.setPlaceholder(); + + if (triggerChange !== false){ + this.opts.element.trigger({ type: "select2-removed", val: this.id(data), choice: data }); + this.triggerChange({removed:data}); + } + } + }, + + /** + * Sets selection based on source element's value + */ + // single + initSelection: function () { + var selected; + if (this.isPlaceholderOptionSelected()) { + this.updateSelection(null); + this.close(); + this.setPlaceholder(); + } else { + var self = this; + this.opts.initSelection.call(null, this.opts.element, function(selected){ + if (selected !== undefined && selected !== null) { + self.updateSelection(selected); + self.close(); + self.setPlaceholder(); + self.nextSearchTerm = self.opts.nextSearchTerm(selected, self.search.val()); + } + }); + } + }, + + isPlaceholderOptionSelected: function() { + var placeholderOption; + if (this.getPlaceholder() === undefined) return false; // no placeholder specified so no option should be considered + return ((placeholderOption = this.getPlaceholderOption()) !== undefined && placeholderOption.prop("selected")) + || (this.opts.element.val() === "") + || (this.opts.element.val() === undefined) + || (this.opts.element.val() === null); + }, + + // single + prepareOpts: function () { + var opts = this.parent.prepareOpts.apply(this, arguments), + self=this; + + if (opts.element.get(0).tagName.toLowerCase() === "select") { + // install the selection initializer + opts.initSelection = function (element, callback) { + var selected = element.find("option").filter(function() { return this.selected && !this.disabled }); + // a single select box always has a value, no need to null check 'selected' + callback(self.optionToData(selected)); + }; + } else if ("data" in opts) { + // install default initSelection when applied to hidden input and data is local + opts.initSelection = opts.initSelection || function (element, callback) { + var id = element.val(); + //search in data by id, storing the actual matching item + var match = null; + opts.query({ + matcher: function(term, text, el){ + var is_match = equal(id, opts.id(el)); + if (is_match) { + match = el; + } + return is_match; + }, + callback: !$.isFunction(callback) ? $.noop : function() { + callback(match); + } + }); + }; + } + + return opts; + }, + + // single + getPlaceholder: function() { + // if a placeholder is specified on a single select without a valid placeholder option ignore it + if (this.select) { + if (this.getPlaceholderOption() === undefined) { + return undefined; + } + } + + return this.parent.getPlaceholder.apply(this, arguments); + }, + + // single + setPlaceholder: function () { + var placeholder = this.getPlaceholder(); + + if (this.isPlaceholderOptionSelected() && placeholder !== undefined) { + + // check for a placeholder option if attached to a select + if (this.select && this.getPlaceholderOption() === undefined) return; + + this.selection.find(".select2-chosen").html(this.opts.escapeMarkup(placeholder)); + + this.selection.addClass("select2-default"); + + this.container.removeClass("select2-allowclear"); + } + }, + + // single + postprocessResults: function (data, initial, noHighlightUpdate) { + var selected = 0, self = this, showSearchInput = true; + + // find the selected element in the result list + + this.findHighlightableChoices().each2(function (i, elm) { + if (equal(self.id(elm.data("select2-data")), self.opts.element.val())) { + selected = i; + return false; + } + }); + + // and highlight it + if (noHighlightUpdate !== false) { + if (initial === true && selected >= 0) { + this.highlight(selected); + } else { + this.highlight(0); + } + } + + // hide the search box if this is the first we got the results and there are enough of them for search + + if (initial === true) { + var min = this.opts.minimumResultsForSearch; + if (min >= 0) { + this.showSearch(countResults(data.results) >= min); + } + } + }, + + // single + showSearch: function(showSearchInput) { + if (this.showSearchInput === showSearchInput) return; + + this.showSearchInput = showSearchInput; + + this.dropdown.find(".select2-search").toggleClass("select2-search-hidden", !showSearchInput); + this.dropdown.find(".select2-search").toggleClass("select2-offscreen", !showSearchInput); + //add "select2-with-searchbox" to the container if search box is shown + $(this.dropdown, this.container).toggleClass("select2-with-searchbox", showSearchInput); + }, + + // single + onSelect: function (data, options) { + + if (!this.triggerSelect(data)) { return; } + + var old = this.opts.element.val(), + oldData = this.data(); + + this.opts.element.val(this.id(data)); + this.updateSelection(data); + + this.opts.element.trigger({ type: "select2-selected", val: this.id(data), choice: data }); + + this.nextSearchTerm = this.opts.nextSearchTerm(data, this.search.val()); + this.close(); + + if ((!options || !options.noFocus) && this.opts.shouldFocusInput(this)) { + this.focusser.focus(); + } + + if (!equal(old, this.id(data))) { + this.triggerChange({ added: data, removed: oldData }); + } + }, + + // single + updateSelection: function (data) { + + var container=this.selection.find(".select2-chosen"), formatted, cssClass; + + this.selection.data("select2-data", data); + + container.empty(); + if (data !== null) { + formatted=this.opts.formatSelection(data, container, this.opts.escapeMarkup); + } + if (formatted !== undefined) { + container.append(formatted); + } + cssClass=this.opts.formatSelectionCssClass(data, container); + if (cssClass !== undefined) { + container.addClass(cssClass); + } + + this.selection.removeClass("select2-default"); + + if (this.opts.allowClear && this.getPlaceholder() !== undefined) { + this.container.addClass("select2-allowclear"); + } + }, + + // single + val: function () { + var val, + triggerChange = false, + data = null, + self = this, + oldData = this.data(); + + if (arguments.length === 0) { + return this.opts.element.val(); + } + + val = arguments[0]; + + if (arguments.length > 1) { + triggerChange = arguments[1]; + } + + if (this.select) { + this.select + .val(val) + .find("option").filter(function() { return this.selected }).each2(function (i, elm) { + data = self.optionToData(elm); + return false; + }); + this.updateSelection(data); + this.setPlaceholder(); + if (triggerChange) { + this.triggerChange({added: data, removed:oldData}); + } + } else { + // val is an id. !val is true for [undefined,null,'',0] - 0 is legal + if (!val && val !== 0) { + this.clear(triggerChange); + return; + } + if (this.opts.initSelection === undefined) { + throw new Error("cannot call val() if initSelection() is not defined"); + } + this.opts.element.val(val); + this.opts.initSelection(this.opts.element, function(data){ + self.opts.element.val(!data ? "" : self.id(data)); + self.updateSelection(data); + self.setPlaceholder(); + if (triggerChange) { + self.triggerChange({added: data, removed:oldData}); + } + }); + } + }, + + // single + clearSearch: function () { + this.search.val(""); + this.focusser.val(""); + }, + + // single + data: function(value) { + var data, + triggerChange = false; + + if (arguments.length === 0) { + data = this.selection.data("select2-data"); + if (data == undefined) data = null; + return data; + } else { + if (arguments.length > 1) { + triggerChange = arguments[1]; + } + if (!value) { + this.clear(triggerChange); + } else { + data = this.data(); + this.opts.element.val(!value ? "" : this.id(value)); + this.updateSelection(value); + if (triggerChange) { + this.triggerChange({added: value, removed:data}); + } + } + } + } + }); + + MultiSelect2 = clazz(AbstractSelect2, { + + // multi + createContainer: function () { + var container = $(document.createElement("div")).attr({ + "class": "select2-container select2-container-multi" + }).html([ + "
      ", + "
    • ", + " ", + " ", + "
    • ", + "
    ", + "
    ", + "
      ", + "
    ", + "
    "].join("")); + return container; + }, + + // multi + prepareOpts: function () { + var opts = this.parent.prepareOpts.apply(this, arguments), + self=this; + + // TODO validate placeholder is a string if specified + + if (opts.element.get(0).tagName.toLowerCase() === "select") { + // install the selection initializer + opts.initSelection = function (element, callback) { + + var data = []; + + element.find("option").filter(function() { return this.selected && !this.disabled }).each2(function (i, elm) { + data.push(self.optionToData(elm)); + }); + callback(data); + }; + } else if ("data" in opts) { + // install default initSelection when applied to hidden input and data is local + opts.initSelection = opts.initSelection || function (element, callback) { + var ids = splitVal(element.val(), opts.separator); + //search in data by array of ids, storing matching items in a list + var matches = []; + opts.query({ + matcher: function(term, text, el){ + var is_match = $.grep(ids, function(id) { + return equal(id, opts.id(el)); + }).length; + if (is_match) { + matches.push(el); + } + return is_match; + }, + callback: !$.isFunction(callback) ? $.noop : function() { + // reorder matches based on the order they appear in the ids array because right now + // they are in the order in which they appear in data array + var ordered = []; + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + for (var j = 0; j < matches.length; j++) { + var match = matches[j]; + if (equal(id, opts.id(match))) { + ordered.push(match); + matches.splice(j, 1); + break; + } + } + } + callback(ordered); + } + }); + }; + } + + return opts; + }, + + // multi + selectChoice: function (choice) { + + var selected = this.container.find(".select2-search-choice-focus"); + if (selected.length && choice && choice[0] == selected[0]) { + + } else { + if (selected.length) { + this.opts.element.trigger("choice-deselected", selected); + } + selected.removeClass("select2-search-choice-focus"); + if (choice && choice.length) { + this.close(); + choice.addClass("select2-search-choice-focus"); + this.opts.element.trigger("choice-selected", choice); + } + } + }, + + // multi + destroy: function() { + $("label[for='" + this.search.attr('id') + "']") + .attr('for', this.opts.element.attr("id")); + this.parent.destroy.apply(this, arguments); + + cleanupJQueryElements.call(this, + "searchContainer", + "selection" + ); + }, + + // multi + initContainer: function () { + + var selector = ".select2-choices", selection; + + this.searchContainer = this.container.find(".select2-search-field"); + this.selection = selection = this.container.find(selector); + + var _this = this; + this.selection.on("click", ".select2-search-choice:not(.select2-locked)", function (e) { + //killEvent(e); + _this.search[0].focus(); + _this.selectChoice($(this)); + }); + + // rewrite labels from original element to focusser + this.search.attr("id", "s2id_autogen"+nextUid()); + + this.search.prev() + .text($("label[for='" + this.opts.element.attr("id") + "']").text()) + .attr('for', this.search.attr('id')); + + this.search.on("input paste", this.bind(function() { + if (this.search.attr('placeholder') && this.search.val().length == 0) return; + if (!this.isInterfaceEnabled()) return; + if (!this.opened()) { + this.open(); + } + })); + + this.search.attr("tabindex", this.elementTabIndex); + + this.keydowns = 0; + this.search.on("keydown", this.bind(function (e) { + if (!this.isInterfaceEnabled()) return; + + ++this.keydowns; + var selected = selection.find(".select2-search-choice-focus"); + var prev = selected.prev(".select2-search-choice:not(.select2-locked)"); + var next = selected.next(".select2-search-choice:not(.select2-locked)"); + var pos = getCursorInfo(this.search); + + if (selected.length && + (e.which == KEY.LEFT || e.which == KEY.RIGHT || e.which == KEY.BACKSPACE || e.which == KEY.DELETE || e.which == KEY.ENTER)) { + var selectedChoice = selected; + if (e.which == KEY.LEFT && prev.length) { + selectedChoice = prev; + } + else if (e.which == KEY.RIGHT) { + selectedChoice = next.length ? next : null; + } + else if (e.which === KEY.BACKSPACE) { + if (this.unselect(selected.first())) { + this.search.width(10); + selectedChoice = prev.length ? prev : next; + } + } else if (e.which == KEY.DELETE) { + if (this.unselect(selected.first())) { + this.search.width(10); + selectedChoice = next.length ? next : null; + } + } else if (e.which == KEY.ENTER) { + selectedChoice = null; + } + + this.selectChoice(selectedChoice); + killEvent(e); + if (!selectedChoice || !selectedChoice.length) { + this.open(); + } + return; + } else if (((e.which === KEY.BACKSPACE && this.keydowns == 1) + || e.which == KEY.LEFT) && (pos.offset == 0 && !pos.length)) { + + this.selectChoice(selection.find(".select2-search-choice:not(.select2-locked)").last()); + killEvent(e); + return; + } else { + this.selectChoice(null); + } + + if (this.opened()) { + switch (e.which) { + case KEY.UP: + case KEY.DOWN: + this.moveHighlight((e.which === KEY.UP) ? -1 : 1); + killEvent(e); + return; + case KEY.ENTER: + this.selectHighlighted(); + killEvent(e); + return; + case KEY.TAB: + this.selectHighlighted({noFocus:true}); + this.close(); + return; + case KEY.ESC: + this.cancel(e); + killEvent(e); + return; + } + } + + if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) + || e.which === KEY.BACKSPACE || e.which === KEY.ESC) { + return; + } + + if (e.which === KEY.ENTER) { + if (this.opts.openOnEnter === false) { + return; + } else if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) { + return; + } + } + + this.open(); + + if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) { + // prevent the page from scrolling + killEvent(e); + } + + if (e.which === KEY.ENTER) { + // prevent form from being submitted + killEvent(e); + } + + })); + + this.search.on("keyup", this.bind(function (e) { + this.keydowns = 0; + this.resizeSearch(); + }) + ); + + this.search.on("blur", this.bind(function(e) { + this.container.removeClass("select2-container-active"); + this.search.removeClass("select2-focused"); + this.selectChoice(null); + if (!this.opened()) this.clearSearch(); + e.stopImmediatePropagation(); + this.opts.element.trigger($.Event("select2-blur")); + })); + + this.container.on("click", selector, this.bind(function (e) { + if (!this.isInterfaceEnabled()) return; + if ($(e.target).closest(".select2-search-choice").length > 0) { + // clicked inside a select2 search choice, do not open + return; + } + this.selectChoice(null); + this.clearPlaceholder(); + if (!this.container.hasClass("select2-container-active")) { + this.opts.element.trigger($.Event("select2-focus")); + } + this.open(); + this.focusSearch(); + e.preventDefault(); + })); + + this.container.on("focus", selector, this.bind(function () { + if (!this.isInterfaceEnabled()) return; + if (!this.container.hasClass("select2-container-active")) { + this.opts.element.trigger($.Event("select2-focus")); + } + this.container.addClass("select2-container-active"); + this.dropdown.addClass("select2-drop-active"); + this.clearPlaceholder(); + })); + + this.initContainerWidth(); + this.opts.element.addClass("select2-offscreen"); + + // set the placeholder if necessary + this.clearSearch(); + }, + + // multi + enableInterface: function() { + if (this.parent.enableInterface.apply(this, arguments)) { + this.search.prop("disabled", !this.isInterfaceEnabled()); + } + }, + + // multi + initSelection: function () { + var data; + if (this.opts.element.val() === "" && this.opts.element.text() === "") { + this.updateSelection([]); + this.close(); + // set the placeholder if necessary + this.clearSearch(); + } + if (this.select || this.opts.element.val() !== "") { + var self = this; + this.opts.initSelection.call(null, this.opts.element, function(data){ + if (data !== undefined && data !== null) { + self.updateSelection(data); + self.close(); + // set the placeholder if necessary + self.clearSearch(); + } + }); + } + }, + + // multi + clearSearch: function () { + var placeholder = this.getPlaceholder(), + maxWidth = this.getMaxSearchWidth(); + + if (placeholder !== undefined && this.getVal().length === 0 && this.search.hasClass("select2-focused") === false) { + this.search.val(placeholder).addClass("select2-default"); + // stretch the search box to full width of the container so as much of the placeholder is visible as possible + // we could call this.resizeSearch(), but we do not because that requires a sizer and we do not want to create one so early because of a firefox bug, see #944 + this.search.width(maxWidth > 0 ? maxWidth : this.container.css("width")); + } else { + this.search.val("").width(10); + } + }, + + // multi + clearPlaceholder: function () { + if (this.search.hasClass("select2-default")) { + this.search.val("").removeClass("select2-default"); + } + }, + + // multi + opening: function () { + this.clearPlaceholder(); // should be done before super so placeholder is not used to search + this.resizeSearch(); + + this.parent.opening.apply(this, arguments); + + this.focusSearch(); + + // initializes search's value with nextSearchTerm (if defined by user) + // ignore nextSearchTerm if the dropdown is opened by the user pressing a letter + if(this.search.val() === "") { + if(this.nextSearchTerm != undefined){ + this.search.val(this.nextSearchTerm); + this.search.select(); + } + } + + this.updateResults(true); + if (this.opts.shouldFocusInput(this)) { + this.search.focus(); + } + this.opts.element.trigger($.Event("select2-open")); + }, + + // multi + close: function () { + if (!this.opened()) return; + this.parent.close.apply(this, arguments); + }, + + // multi + focus: function () { + this.close(); + this.search.focus(); + }, + + // multi + isFocused: function () { + return this.search.hasClass("select2-focused"); + }, + + // multi + updateSelection: function (data) { + var ids = [], filtered = [], self = this; + + // filter out duplicates + $(data).each(function () { + if (indexOf(self.id(this), ids) < 0) { + ids.push(self.id(this)); + filtered.push(this); + } + }); + data = filtered; + + this.selection.find(".select2-search-choice").remove(); + $(data).each(function () { + self.addSelectedChoice(this); + }); + self.postprocessResults(); + }, + + // multi + tokenize: function() { + var input = this.search.val(); + input = this.opts.tokenizer.call(this, input, this.data(), this.bind(this.onSelect), this.opts); + if (input != null && input != undefined) { + this.search.val(input); + if (input.length > 0) { + this.open(); + } + } + + }, + + // multi + onSelect: function (data, options) { + + if (!this.triggerSelect(data)) { return; } + + this.addSelectedChoice(data); + + this.opts.element.trigger({ type: "selected", val: this.id(data), choice: data }); + + // keep track of the search's value before it gets cleared + this.nextSearchTerm = this.opts.nextSearchTerm(data, this.search.val()); + + this.clearSearch(); + this.updateResults(); + + if (this.select || !this.opts.closeOnSelect) this.postprocessResults(data, false, this.opts.closeOnSelect===true); + + if (this.opts.closeOnSelect) { + this.close(); + this.search.width(10); + } else { + if (this.countSelectableResults()>0) { + this.search.width(10); + this.resizeSearch(); + if (this.getMaximumSelectionSize() > 0 && this.val().length >= this.getMaximumSelectionSize()) { + // if we reached max selection size repaint the results so choices + // are replaced with the max selection reached message + this.updateResults(true); + } else { + // initializes search's value with nextSearchTerm and update search result + if(this.nextSearchTerm != undefined){ + this.search.val(this.nextSearchTerm); + this.updateResults(); + this.search.select(); + } + } + this.positionDropdown(); + } else { + // if nothing left to select close + this.close(); + this.search.width(10); + } + } + + // since its not possible to select an element that has already been + // added we do not need to check if this is a new element before firing change + this.triggerChange({ added: data }); + + if (!options || !options.noFocus) + this.focusSearch(); + }, + + // multi + cancel: function () { + this.close(); + this.focusSearch(); + }, + getLabelcolor: function(string){ + return calcMD5(string).slice(0, 6); + }, + addSelectedChoice: function (data) { + var enableChoice = !data.locked, + enabledItem = $( + "
  • " + + "
    " + + " " + + "
  • "), + disabledItem = $( + "
  • " + + "
    " + + "
  • "); + var choice = enableChoice ? enabledItem : disabledItem, + id = this.id(data), + val = this.getVal(), + formatted, + cssClass; + + formatted=this.opts.formatSelection(data, choice.find("div"), this.opts.escapeMarkup); + if (formatted != undefined) { + choice.find("div").replaceWith("
    "+formatted+"
    "); + choice.attr("style","background:#"+this.getLabelcolor(formatted).substring(0, 6)+";color:#fff;"); + } + cssClass=this.opts.formatSelectionCssClass(data, choice.find("div")); + if (cssClass != undefined) { + choice.addClass(cssClass); + } + + if(enableChoice){ + choice.find(".select2-search-choice-close") + .on("mousedown", killEvent) + .on("click dblclick", this.bind(function (e) { + if (!this.isInterfaceEnabled()) return; + + this.unselect($(e.target)); + this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus"); + killEvent(e); + this.close(); + this.focusSearch(); + })).on("focus", this.bind(function () { + if (!this.isInterfaceEnabled()) return; + this.container.addClass("select2-container-active"); + this.dropdown.addClass("select2-drop-active"); + })); + } + + choice.data("select2-data", data); + choice.insertBefore(this.searchContainer); + + val.push(id); + this.setVal(val); + }, + + // multi + unselect: function (selected) { + var val = this.getVal(), + data, + index; + selected = selected.closest(".select2-search-choice"); + + if (selected.length === 0) { + throw "Invalid argument: " + selected + ". Must be .select2-search-choice"; + } + + data = selected.data("select2-data"); + + if (!data) { + // prevent a race condition when the 'x' is clicked really fast repeatedly the event can be queued + // and invoked on an element already removed + return; + } + + var evt = $.Event("select2-removing"); + evt.val = this.id(data); + evt.choice = data; + this.opts.element.trigger(evt); + + if (evt.isDefaultPrevented()) { + return false; + } + + while((index = indexOf(this.id(data), val)) >= 0) { + val.splice(index, 1); + this.setVal(val); + if (this.select) this.postprocessResults(); + } + + selected.remove(); + + this.opts.element.trigger({ type: "select2-removed", val: this.id(data), choice: data }); + this.triggerChange({ removed: data }); + + return true; + }, + + // multi + postprocessResults: function (data, initial, noHighlightUpdate) { + var val = this.getVal(), + choices = this.results.find(".select2-result"), + compound = this.results.find(".select2-result-with-children"), + self = this; + + choices.each2(function (i, choice) { + var id = self.id(choice.data("select2-data")); + if (indexOf(id, val) >= 0) { + choice.addClass("select2-selected"); + // mark all children of the selected parent as selected + choice.find(".select2-result-selectable").addClass("select2-selected"); + } + }); + + compound.each2(function(i, choice) { + // hide an optgroup if it doesn't have any selectable children + if (!choice.is('.select2-result-selectable') + && choice.find(".select2-result-selectable:not(.select2-selected)").length === 0) { + choice.addClass("select2-selected"); + } + }); + + if (this.highlight() == -1 && noHighlightUpdate !== false){ + self.highlight(0); + } + + //If all results are chosen render formatNoMatches + if(!this.opts.createSearchChoice && !choices.filter('.select2-result:not(.select2-selected)').length > 0){ + if(!data || data && !data.more && this.results.find(".select2-no-results").length === 0) { + if (checkFormatter(self.opts.formatNoMatches, "formatNoMatches")) { + this.results.append("
  • " + evaluate(self.opts.formatNoMatches, self.opts.element, self.search.val()) + "
  • "); + } + } + } + + }, + + // multi + getMaxSearchWidth: function() { + return this.selection.width() - getSideBorderPadding(this.search); + }, + + // multi + resizeSearch: function () { + var minimumWidth, left, maxWidth, containerLeft, searchWidth, + sideBorderPadding = getSideBorderPadding(this.search); + + minimumWidth = measureTextWidth(this.search) + 10; + + left = this.search.offset().left; + + maxWidth = this.selection.width(); + containerLeft = this.selection.offset().left; + + searchWidth = maxWidth - (left - containerLeft) - sideBorderPadding; + + if (searchWidth < minimumWidth) { + searchWidth = maxWidth - sideBorderPadding; + } + + if (searchWidth < 40) { + searchWidth = maxWidth - sideBorderPadding; + } + + if (searchWidth <= 0) { + searchWidth = minimumWidth; + } + + this.search.width(Math.floor(searchWidth)); + }, + + // multi + getVal: function () { + var val; + if (this.select) { + val = this.select.val(); + return val === null ? [] : val; + } else { + val = this.opts.element.val(); + return splitVal(val, this.opts.separator); + } + }, + + // multi + setVal: function (val) { + var unique; + if (this.select) { + this.select.val(val); + } else { + unique = []; + // filter out duplicates + $(val).each(function () { + if (indexOf(this, unique) < 0) unique.push(this); + }); + this.opts.element.val(unique.length === 0 ? "" : unique.join(this.opts.separator)); + } + }, + + // multi + buildChangeDetails: function (old, current) { + var current = current.slice(0), + old = old.slice(0); + + // remove intersection from each array + for (var i = 0; i < current.length; i++) { + for (var j = 0; j < old.length; j++) { + if (equal(this.opts.id(current[i]), this.opts.id(old[j]))) { + current.splice(i, 1); + if(i>0){ + i--; + } + old.splice(j, 1); + j--; + } + } + } + + return {added: current, removed: old}; + }, + + + // multi + val: function (val, triggerChange) { + var oldData, self=this; + + if (arguments.length === 0) { + return this.getVal(); + } + + oldData=this.data(); + if (!oldData.length) oldData=[]; + + // val is an id. !val is true for [undefined,null,'',0] - 0 is legal + if (!val && val !== 0) { + this.opts.element.val(""); + this.updateSelection([]); + this.clearSearch(); + if (triggerChange) { + this.triggerChange({added: this.data(), removed: oldData}); + } + return; + } + + // val is a list of ids + this.setVal(val); + + if (this.select) { + this.opts.initSelection(this.select, this.bind(this.updateSelection)); + if (triggerChange) { + this.triggerChange(this.buildChangeDetails(oldData, this.data())); + } + } else { + if (this.opts.initSelection === undefined) { + throw new Error("val() cannot be called if initSelection() is not defined"); + } + + this.opts.initSelection(this.opts.element, function(data){ + var ids=$.map(data, self.id); + self.setVal(ids); + self.updateSelection(data); + self.clearSearch(); + if (triggerChange) { + self.triggerChange(self.buildChangeDetails(oldData, self.data())); + } + }); + } + this.clearSearch(); + }, + + // multi + onSortStart: function() { + if (this.select) { + throw new Error("Sorting of elements is not supported when attached to instead."); + } + + // collapse search field into 0 width so its container can be collapsed as well + this.search.width(0); + // hide the container + this.searchContainer.hide(); + }, + + // multi + onSortEnd:function() { + + var val=[], self=this; + + // show search and move it to the end of the list + this.searchContainer.show(); + // make sure the search container is the last item in the list + this.searchContainer.appendTo(this.searchContainer.parent()); + // since we collapsed the width in dragStarted, we resize it here + this.resizeSearch(); + + // update selection + this.selection.find(".select2-search-choice").each(function() { + val.push(self.opts.id($(this).data("select2-data"))); + }); + this.setVal(val); + this.triggerChange(); + }, + + // multi + data: function(values, triggerChange) { + var self=this, ids, old; + if (arguments.length === 0) { + return this.selection + .children(".select2-search-choice") + .map(function() { return $(this).data("select2-data"); }) + .get(); + } else { + old = this.data(); + if (!values) { values = []; } + ids = $.map(values, function(e) { return self.opts.id(e); }); + this.setVal(ids); + this.updateSelection(values); + this.clearSearch(); + if (triggerChange) { + this.triggerChange(this.buildChangeDetails(old, this.data())); + } + } + } + }); + + $.fn.select2 = function () { + + var args = Array.prototype.slice.call(arguments, 0), + opts, + select2, + method, value, multiple, + allowedMethods = ["val", "destroy", "opened", "open", "close", "focus", "isFocused", "container", "dropdown", "onSortStart", "onSortEnd", "enable", "disable", "readonly", "positionDropdown", "data", "search"], + valueMethods = ["opened", "isFocused", "container", "dropdown"], + propertyMethods = ["val", "data"], + methodsMap = { search: "externalSearch" }; + + this.each(function () { + if (args.length === 0 || typeof(args[0]) === "object") { + opts = args.length === 0 ? {} : $.extend({}, args[0]); + opts.element = $(this); + + if (opts.element.get(0).tagName.toLowerCase() === "select") { + multiple = opts.element.prop("multiple"); + } else { + multiple = opts.multiple || false; + if ("tags" in opts) {opts.multiple = multiple = true;} + } + + select2 = multiple ? new window.Select2["class"].multi() : new window.Select2["class"].single(); + select2.init(opts); + } else if (typeof(args[0]) === "string") { + + if (indexOf(args[0], allowedMethods) < 0) { + throw "Unknown method: " + args[0]; + } + + value = undefined; + select2 = $(this).data("select2"); + if (select2 === undefined) return; + + method=args[0]; + + if (method === "container") { + value = select2.container; + } else if (method === "dropdown") { + value = select2.dropdown; + } else { + if (methodsMap[method]) method = methodsMap[method]; + + value = select2[method].apply(select2, args.slice(1)); + } + if (indexOf(args[0], valueMethods) >= 0 + || (indexOf(args[0], propertyMethods) >= 0 && args.length == 1)) { + return false; // abort the iteration, ready to return first matched value + } + } else { + throw "Invalid arguments to select2 plugin: " + args; + } + }); + return (value === undefined) ? this : value; + }; + + // plugin defaults, accessible to users + $.fn.select2.defaults = { + width: "copy", + loadMorePadding: 0, + closeOnSelect: true, + openOnEnter: true, + containerCss: {}, + dropdownCss: {}, + containerCssClass: "", + dropdownCssClass: "", + formatResult: function(result, container, query, escapeMarkup) { + var markup=[]; + markMatch(result.text, query.term, markup, escapeMarkup); + return markup.join(""); + }, + formatSelection: function (data, container, escapeMarkup) { + return data ? escapeMarkup(data.text) : undefined; + }, + sortResults: function (results, container, query) { + return results; + }, + formatResultCssClass: function(data) {return data.css;}, + formatSelectionCssClass: function(data, container) {return undefined;}, + formatMatches: function (matches) { if (matches === 1) { return "One result is available, press enter to select it."; } return matches + " results are available, use up and down arrow keys to navigate."; }, + formatNoMatches: function () { return "No matches found"; }, + formatInputTooShort: function (input, min) { var n = min - input.length; return "Please enter " + n + " or more character" + (n == 1? "" : "s"); }, + formatInputTooLong: function (input, max) { var n = input.length - max; return "Please delete " + n + " character" + (n == 1? "" : "s"); }, + formatSelectionTooBig: function (limit) { return "You can only select " + limit + " item" + (limit == 1 ? "" : "s"); }, + formatLoadMore: function (pageNumber) { return "Loading more results…"; }, + formatSearching: function () { return "Searching…"; }, + minimumResultsForSearch: 0, + minimumInputLength: 0, + maximumInputLength: null, + maximumSelectionSize: 0, + id: function (e) { return e == undefined ? null : e.id; }, + matcher: function(term, text) { + return stripDiacritics(''+text).toUpperCase().indexOf(stripDiacritics(''+term).toUpperCase()) >= 0; + }, + separator: ",", + tokenSeparators: [], + tokenizer: defaultTokenizer, + escapeMarkup: defaultEscapeMarkup, + blurOnChange: false, + selectOnBlur: false, + adaptContainerCssClass: function(c) { return c; }, + adaptDropdownCssClass: function(c) { return null; }, + nextSearchTerm: function(selectedObject, currentSearchTerm) { return undefined; }, + searchInputPlaceholder: '', + createSearchChoicePosition: 'top', + shouldFocusInput: function (instance) { + // Attempt to detect touch devices + var supportsTouchEvents = (('ontouchstart' in window) || + (navigator.msMaxTouchPoints > 0)); + + // Only devices which support touch events should be special cased + if (!supportsTouchEvents) { + return true; + } + + // Never focus the input if search is disabled + if (instance.opts.minimumResultsForSearch < 0) { + return false; + } + + return true; + } + }; + + $.fn.select2.ajaxDefaults = { + transport: $.ajax, + params: { + type: "GET", + cache: false, + dataType: "json" + } + }; + + // exports + window.Select2 = { + query: { + ajax: ajax, + local: local, + tags: tags + }, util: { + debounce: debounce, + markMatch: markMatch, + escapeMarkup: defaultEscapeMarkup, + stripDiacritics: stripDiacritics + }, "class": { + "abstract": AbstractSelect2, + "single": SingleSelect2, + "multi": MultiSelect2 + } + }; + +}(jQuery)); diff --git a/client/js/libs/select2.min.js b/client/js/libs/select2.min.js new file mode 100644 index 000000000..277f12965 --- /dev/null +++ b/client/js/libs/select2.min.js @@ -0,0 +1,23 @@ +/* +Copyright 2014 Igor Vaynberg + +Version: 3.5.0 Timestamp: Mon Jun 16 19:29:44 EDT 2014 + +This software is licensed under the Apache License, Version 2.0 (the "Apache License") or the GNU +General Public License version 2 (the "GPL License"). You may choose either license to govern your +use of this software only upon the condition that you accept all of the terms of either the Apache +License or the GPL License. + +You may obtain a copy of the Apache License and the GPL License at: + +http://www.apache.org/licenses/LICENSE-2.0 +http://www.gnu.org/licenses/gpl-2.0.html + +Unless required by applicable law or agreed to in writing, software distributed under the Apache License +or the GPL Licesnse is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +either express or implied. See the Apache License and the GPL License for the specific language governing +permissions and limitations under the Apache License and the GPL License. +*/ +!function(a){"undefined"==typeof a.fn.each2&&a.extend(a.fn,{each2:function(b){for(var c=a([0]),d=-1,e=this.length;++dc;c+=1)if(r(a,b[c]))return c;return-1}function q(){var b=a(l);b.appendTo("body");var c={width:b.width()-b[0].clientWidth,height:b.height()-b[0].clientHeight};return b.remove(),c}function r(a,c){return a===c?!0:a===b||c===b?!1:null===a||null===c?!1:a.constructor===String?a+""==c+"":c.constructor===String?c+""==a+"":!1}function s(b,c){var d,e,f;if(null===b||b.length<1)return[];for(d=b.split(c),e=0,f=d.length;f>e;e+=1)d[e]=a.trim(d[e]);return d}function t(a){return a.outerWidth(!1)-a.width()}function u(c){var d="keyup-change-value";c.on("keydown",function(){a.data(c,d)===b&&a.data(c,d,c.val())}),c.on("keyup",function(){var e=a.data(c,d);e!==b&&c.val()!==e&&(a.removeData(c,d),c.trigger("keyup-change"))})}function v(c){c.on("mousemove",function(c){var d=i;(d===b||d.x!==c.pageX||d.y!==c.pageY)&&a(c.target).trigger("mousemove-filtered",c)})}function w(a,c,d){d=d||b;var e;return function(){var b=arguments;window.clearTimeout(e),e=window.setTimeout(function(){c.apply(d,b)},a)}}function x(a,b){var c=w(a,function(a){b.trigger("scroll-debounced",a)});b.on("scroll",function(a){p(a.target,b.get())>=0&&c(a)})}function y(a){a[0]!==document.activeElement&&window.setTimeout(function(){var d,b=a[0],c=a.val().length;a.focus();var e=b.offsetWidth>0||b.offsetHeight>0;e&&b===document.activeElement&&(b.setSelectionRange?b.setSelectionRange(c,c):b.createTextRange&&(d=b.createTextRange(),d.collapse(!1),d.select()))},0)}function z(b){b=a(b)[0];var c=0,d=0;if("selectionStart"in b)c=b.selectionStart,d=b.selectionEnd-c;else if("selection"in document){b.focus();var e=document.selection.createRange();d=document.selection.createRange().text.length,e.moveStart("character",-b.value.length),c=e.text.length-d}return{offset:c,length:d}}function A(a){a.preventDefault(),a.stopPropagation()}function B(a){a.preventDefault(),a.stopImmediatePropagation()}function C(b){if(!h){var c=b[0].currentStyle||window.getComputedStyle(b[0],null);h=a(document.createElement("div")).css({position:"absolute",left:"-10000px",top:"-10000px",display:"none",fontSize:c.fontSize,fontFamily:c.fontFamily,fontStyle:c.fontStyle,fontWeight:c.fontWeight,letterSpacing:c.letterSpacing,textTransform:c.textTransform,whiteSpace:"nowrap"}),h.attr("class","select2-sizer"),a("body").append(h)}return h.text(b.val()),h.width()}function D(b,c,d){var e,g,f=[];e=a.trim(b.attr("class")),e&&(e=""+e,a(e.split(/\s+/)).each2(function(){0===this.indexOf("select2-")&&f.push(this)})),e=a.trim(c.attr("class")),e&&(e=""+e,a(e.split(/\s+/)).each2(function(){0!==this.indexOf("select2-")&&(g=d(this),g&&f.push(g))})),b.attr("class",f.join(" "))}function E(a,b,c,d){var e=o(a.toUpperCase()).indexOf(o(b.toUpperCase())),f=b.length;return 0>e?(c.push(d(a)),void 0):(c.push(d(a.substring(0,e))),c.push(""),c.push(d(a.substring(e,e+f))),c.push(""),c.push(d(a.substring(e+f,a.length))),void 0)}function F(a){var b={"\\":"\","&":"&","<":"<",">":">",'"':""","'":"'","/":"/"};return String(a).replace(/[&<>"'\/\\]/g,function(a){return b[a]})}function G(c){var d,e=null,f=c.quietMillis||100,g=c.url,h=this;return function(i){window.clearTimeout(d),d=window.setTimeout(function(){var d=c.data,f=g,j=c.transport||a.fn.select2.ajaxDefaults.transport,k={type:c.type||"GET",cache:c.cache||!1,jsonpCallback:c.jsonpCallback||b,dataType:c.dataType||"json"},l=a.extend({},a.fn.select2.ajaxDefaults.params,k);d=d?d.call(h,i.term,i.page,i.context):null,f="function"==typeof f?f.call(h,i.term,i.page,i.context):f,e&&"function"==typeof e.abort&&e.abort(),c.params&&(a.isFunction(c.params)?a.extend(l,c.params.call(h)):a.extend(l,c.params)),a.extend(l,{url:f,dataType:c.dataType,data:d,success:function(a){var b=c.results(a,i.page,i);i.callback(b)}}),e=j.call(h,l)},f)}}function H(b){var d,e,c=b,f=function(a){return""+a.text};a.isArray(c)&&(e=c,c={results:e}),a.isFunction(c)===!1&&(e=c,c=function(){return e});var g=c();return g.text&&(f=g.text,a.isFunction(f)||(d=g.text,f=function(a){return a[d]})),function(b){var g,d=b.term,e={results:[]};return""===d?(b.callback(c()),void 0):(g=function(c,e){var h,i;if(c=c[0],c.children){h={};for(i in c)c.hasOwnProperty(i)&&(h[i]=c[i]);h.children=[],a(c.children).each2(function(a,b){g(b,h.children)}),(h.children.length||b.matcher(d,f(h),c))&&e.push(h)}else b.matcher(d,f(c),c)&&e.push(c)},a(c().results).each2(function(a,b){g(b,e.results)}),b.callback(e),void 0)}}function I(c){var d=a.isFunction(c);return function(e){var f=e.term,g={results:[]},h=d?c(e):c;a.isArray(h)&&(a(h).each(function(){var a=this.text!==b,c=a?this.text:this;(""===f||e.matcher(f,c))&&g.results.push(a?this:{id:this,text:this})}),e.callback(g))}}function J(b,c){if(a.isFunction(b))return!0;if(!b)return!1;if("string"==typeof b)return!0;throw new Error(c+" must be a string, function, or falsy value")}function K(b,c){if(a.isFunction(b)){var d=Array.prototype.slice.call(arguments,2);return b.apply(c,d)}return b}function L(b){var c=0;return a.each(b,function(a,b){b.children?c+=L(b.children):c++}),c}function M(a,c,d,e){var h,i,j,k,l,f=a,g=!1;if(!e.createSearchChoice||!e.tokenSeparators||e.tokenSeparators.length<1)return b;for(;;){for(i=-1,j=0,k=e.tokenSeparators.length;k>j&&(l=e.tokenSeparators[j],i=a.indexOf(l),!(i>=0));j++);if(0>i)break;if(h=a.substring(0,i),a=a.substring(i+l.length),h.length>0&&(h=e.createSearchChoice.call(this,h,c),h!==b&&null!==h&&e.id(h)!==b&&null!==e.id(h))){for(g=!1,j=0,k=c.length;k>j;j++)if(r(e.id(h),e.id(c[j]))){g=!0;break}g||d(h)}}return f!==a?a:void 0}function N(){var b=this;a.each(arguments,function(a,c){b[c].remove(),b[c]=null})}function O(b,c){var d=function(){};return d.prototype=new b,d.prototype.constructor=d,d.prototype.parent=b.prototype,d.prototype=a.extend(d.prototype,c),d}if(window.Select2===b){var c,d,e,f,g,h,j,k,i={x:0,y:0},c={TAB:9,ENTER:13,ESC:27,SPACE:32,LEFT:37,UP:38,RIGHT:39,DOWN:40,SHIFT:16,CTRL:17,ALT:18,PAGE_UP:33,PAGE_DOWN:34,HOME:36,END:35,BACKSPACE:8,DELETE:46,isArrow:function(a){switch(a=a.which?a.which:a){case c.LEFT:case c.RIGHT:case c.UP:case c.DOWN:return!0}return!1},isControl:function(a){var b=a.which;switch(b){case c.SHIFT:case c.CTRL:case c.ALT:return!0}return a.metaKey?!0:!1},isFunctionKey:function(a){return a=a.which?a.which:a,a>=112&&123>=a}},l="
    ",m={"\u24b6":"A","\uff21":"A","\xc0":"A","\xc1":"A","\xc2":"A","\u1ea6":"A","\u1ea4":"A","\u1eaa":"A","\u1ea8":"A","\xc3":"A","\u0100":"A","\u0102":"A","\u1eb0":"A","\u1eae":"A","\u1eb4":"A","\u1eb2":"A","\u0226":"A","\u01e0":"A","\xc4":"A","\u01de":"A","\u1ea2":"A","\xc5":"A","\u01fa":"A","\u01cd":"A","\u0200":"A","\u0202":"A","\u1ea0":"A","\u1eac":"A","\u1eb6":"A","\u1e00":"A","\u0104":"A","\u023a":"A","\u2c6f":"A","\ua732":"AA","\xc6":"AE","\u01fc":"AE","\u01e2":"AE","\ua734":"AO","\ua736":"AU","\ua738":"AV","\ua73a":"AV","\ua73c":"AY","\u24b7":"B","\uff22":"B","\u1e02":"B","\u1e04":"B","\u1e06":"B","\u0243":"B","\u0182":"B","\u0181":"B","\u24b8":"C","\uff23":"C","\u0106":"C","\u0108":"C","\u010a":"C","\u010c":"C","\xc7":"C","\u1e08":"C","\u0187":"C","\u023b":"C","\ua73e":"C","\u24b9":"D","\uff24":"D","\u1e0a":"D","\u010e":"D","\u1e0c":"D","\u1e10":"D","\u1e12":"D","\u1e0e":"D","\u0110":"D","\u018b":"D","\u018a":"D","\u0189":"D","\ua779":"D","\u01f1":"DZ","\u01c4":"DZ","\u01f2":"Dz","\u01c5":"Dz","\u24ba":"E","\uff25":"E","\xc8":"E","\xc9":"E","\xca":"E","\u1ec0":"E","\u1ebe":"E","\u1ec4":"E","\u1ec2":"E","\u1ebc":"E","\u0112":"E","\u1e14":"E","\u1e16":"E","\u0114":"E","\u0116":"E","\xcb":"E","\u1eba":"E","\u011a":"E","\u0204":"E","\u0206":"E","\u1eb8":"E","\u1ec6":"E","\u0228":"E","\u1e1c":"E","\u0118":"E","\u1e18":"E","\u1e1a":"E","\u0190":"E","\u018e":"E","\u24bb":"F","\uff26":"F","\u1e1e":"F","\u0191":"F","\ua77b":"F","\u24bc":"G","\uff27":"G","\u01f4":"G","\u011c":"G","\u1e20":"G","\u011e":"G","\u0120":"G","\u01e6":"G","\u0122":"G","\u01e4":"G","\u0193":"G","\ua7a0":"G","\ua77d":"G","\ua77e":"G","\u24bd":"H","\uff28":"H","\u0124":"H","\u1e22":"H","\u1e26":"H","\u021e":"H","\u1e24":"H","\u1e28":"H","\u1e2a":"H","\u0126":"H","\u2c67":"H","\u2c75":"H","\ua78d":"H","\u24be":"I","\uff29":"I","\xcc":"I","\xcd":"I","\xce":"I","\u0128":"I","\u012a":"I","\u012c":"I","\u0130":"I","\xcf":"I","\u1e2e":"I","\u1ec8":"I","\u01cf":"I","\u0208":"I","\u020a":"I","\u1eca":"I","\u012e":"I","\u1e2c":"I","\u0197":"I","\u24bf":"J","\uff2a":"J","\u0134":"J","\u0248":"J","\u24c0":"K","\uff2b":"K","\u1e30":"K","\u01e8":"K","\u1e32":"K","\u0136":"K","\u1e34":"K","\u0198":"K","\u2c69":"K","\ua740":"K","\ua742":"K","\ua744":"K","\ua7a2":"K","\u24c1":"L","\uff2c":"L","\u013f":"L","\u0139":"L","\u013d":"L","\u1e36":"L","\u1e38":"L","\u013b":"L","\u1e3c":"L","\u1e3a":"L","\u0141":"L","\u023d":"L","\u2c62":"L","\u2c60":"L","\ua748":"L","\ua746":"L","\ua780":"L","\u01c7":"LJ","\u01c8":"Lj","\u24c2":"M","\uff2d":"M","\u1e3e":"M","\u1e40":"M","\u1e42":"M","\u2c6e":"M","\u019c":"M","\u24c3":"N","\uff2e":"N","\u01f8":"N","\u0143":"N","\xd1":"N","\u1e44":"N","\u0147":"N","\u1e46":"N","\u0145":"N","\u1e4a":"N","\u1e48":"N","\u0220":"N","\u019d":"N","\ua790":"N","\ua7a4":"N","\u01ca":"NJ","\u01cb":"Nj","\u24c4":"O","\uff2f":"O","\xd2":"O","\xd3":"O","\xd4":"O","\u1ed2":"O","\u1ed0":"O","\u1ed6":"O","\u1ed4":"O","\xd5":"O","\u1e4c":"O","\u022c":"O","\u1e4e":"O","\u014c":"O","\u1e50":"O","\u1e52":"O","\u014e":"O","\u022e":"O","\u0230":"O","\xd6":"O","\u022a":"O","\u1ece":"O","\u0150":"O","\u01d1":"O","\u020c":"O","\u020e":"O","\u01a0":"O","\u1edc":"O","\u1eda":"O","\u1ee0":"O","\u1ede":"O","\u1ee2":"O","\u1ecc":"O","\u1ed8":"O","\u01ea":"O","\u01ec":"O","\xd8":"O","\u01fe":"O","\u0186":"O","\u019f":"O","\ua74a":"O","\ua74c":"O","\u01a2":"OI","\ua74e":"OO","\u0222":"OU","\u24c5":"P","\uff30":"P","\u1e54":"P","\u1e56":"P","\u01a4":"P","\u2c63":"P","\ua750":"P","\ua752":"P","\ua754":"P","\u24c6":"Q","\uff31":"Q","\ua756":"Q","\ua758":"Q","\u024a":"Q","\u24c7":"R","\uff32":"R","\u0154":"R","\u1e58":"R","\u0158":"R","\u0210":"R","\u0212":"R","\u1e5a":"R","\u1e5c":"R","\u0156":"R","\u1e5e":"R","\u024c":"R","\u2c64":"R","\ua75a":"R","\ua7a6":"R","\ua782":"R","\u24c8":"S","\uff33":"S","\u1e9e":"S","\u015a":"S","\u1e64":"S","\u015c":"S","\u1e60":"S","\u0160":"S","\u1e66":"S","\u1e62":"S","\u1e68":"S","\u0218":"S","\u015e":"S","\u2c7e":"S","\ua7a8":"S","\ua784":"S","\u24c9":"T","\uff34":"T","\u1e6a":"T","\u0164":"T","\u1e6c":"T","\u021a":"T","\u0162":"T","\u1e70":"T","\u1e6e":"T","\u0166":"T","\u01ac":"T","\u01ae":"T","\u023e":"T","\ua786":"T","\ua728":"TZ","\u24ca":"U","\uff35":"U","\xd9":"U","\xda":"U","\xdb":"U","\u0168":"U","\u1e78":"U","\u016a":"U","\u1e7a":"U","\u016c":"U","\xdc":"U","\u01db":"U","\u01d7":"U","\u01d5":"U","\u01d9":"U","\u1ee6":"U","\u016e":"U","\u0170":"U","\u01d3":"U","\u0214":"U","\u0216":"U","\u01af":"U","\u1eea":"U","\u1ee8":"U","\u1eee":"U","\u1eec":"U","\u1ef0":"U","\u1ee4":"U","\u1e72":"U","\u0172":"U","\u1e76":"U","\u1e74":"U","\u0244":"U","\u24cb":"V","\uff36":"V","\u1e7c":"V","\u1e7e":"V","\u01b2":"V","\ua75e":"V","\u0245":"V","\ua760":"VY","\u24cc":"W","\uff37":"W","\u1e80":"W","\u1e82":"W","\u0174":"W","\u1e86":"W","\u1e84":"W","\u1e88":"W","\u2c72":"W","\u24cd":"X","\uff38":"X","\u1e8a":"X","\u1e8c":"X","\u24ce":"Y","\uff39":"Y","\u1ef2":"Y","\xdd":"Y","\u0176":"Y","\u1ef8":"Y","\u0232":"Y","\u1e8e":"Y","\u0178":"Y","\u1ef6":"Y","\u1ef4":"Y","\u01b3":"Y","\u024e":"Y","\u1efe":"Y","\u24cf":"Z","\uff3a":"Z","\u0179":"Z","\u1e90":"Z","\u017b":"Z","\u017d":"Z","\u1e92":"Z","\u1e94":"Z","\u01b5":"Z","\u0224":"Z","\u2c7f":"Z","\u2c6b":"Z","\ua762":"Z","\u24d0":"a","\uff41":"a","\u1e9a":"a","\xe0":"a","\xe1":"a","\xe2":"a","\u1ea7":"a","\u1ea5":"a","\u1eab":"a","\u1ea9":"a","\xe3":"a","\u0101":"a","\u0103":"a","\u1eb1":"a","\u1eaf":"a","\u1eb5":"a","\u1eb3":"a","\u0227":"a","\u01e1":"a","\xe4":"a","\u01df":"a","\u1ea3":"a","\xe5":"a","\u01fb":"a","\u01ce":"a","\u0201":"a","\u0203":"a","\u1ea1":"a","\u1ead":"a","\u1eb7":"a","\u1e01":"a","\u0105":"a","\u2c65":"a","\u0250":"a","\ua733":"aa","\xe6":"ae","\u01fd":"ae","\u01e3":"ae","\ua735":"ao","\ua737":"au","\ua739":"av","\ua73b":"av","\ua73d":"ay","\u24d1":"b","\uff42":"b","\u1e03":"b","\u1e05":"b","\u1e07":"b","\u0180":"b","\u0183":"b","\u0253":"b","\u24d2":"c","\uff43":"c","\u0107":"c","\u0109":"c","\u010b":"c","\u010d":"c","\xe7":"c","\u1e09":"c","\u0188":"c","\u023c":"c","\ua73f":"c","\u2184":"c","\u24d3":"d","\uff44":"d","\u1e0b":"d","\u010f":"d","\u1e0d":"d","\u1e11":"d","\u1e13":"d","\u1e0f":"d","\u0111":"d","\u018c":"d","\u0256":"d","\u0257":"d","\ua77a":"d","\u01f3":"dz","\u01c6":"dz","\u24d4":"e","\uff45":"e","\xe8":"e","\xe9":"e","\xea":"e","\u1ec1":"e","\u1ebf":"e","\u1ec5":"e","\u1ec3":"e","\u1ebd":"e","\u0113":"e","\u1e15":"e","\u1e17":"e","\u0115":"e","\u0117":"e","\xeb":"e","\u1ebb":"e","\u011b":"e","\u0205":"e","\u0207":"e","\u1eb9":"e","\u1ec7":"e","\u0229":"e","\u1e1d":"e","\u0119":"e","\u1e19":"e","\u1e1b":"e","\u0247":"e","\u025b":"e","\u01dd":"e","\u24d5":"f","\uff46":"f","\u1e1f":"f","\u0192":"f","\ua77c":"f","\u24d6":"g","\uff47":"g","\u01f5":"g","\u011d":"g","\u1e21":"g","\u011f":"g","\u0121":"g","\u01e7":"g","\u0123":"g","\u01e5":"g","\u0260":"g","\ua7a1":"g","\u1d79":"g","\ua77f":"g","\u24d7":"h","\uff48":"h","\u0125":"h","\u1e23":"h","\u1e27":"h","\u021f":"h","\u1e25":"h","\u1e29":"h","\u1e2b":"h","\u1e96":"h","\u0127":"h","\u2c68":"h","\u2c76":"h","\u0265":"h","\u0195":"hv","\u24d8":"i","\uff49":"i","\xec":"i","\xed":"i","\xee":"i","\u0129":"i","\u012b":"i","\u012d":"i","\xef":"i","\u1e2f":"i","\u1ec9":"i","\u01d0":"i","\u0209":"i","\u020b":"i","\u1ecb":"i","\u012f":"i","\u1e2d":"i","\u0268":"i","\u0131":"i","\u24d9":"j","\uff4a":"j","\u0135":"j","\u01f0":"j","\u0249":"j","\u24da":"k","\uff4b":"k","\u1e31":"k","\u01e9":"k","\u1e33":"k","\u0137":"k","\u1e35":"k","\u0199":"k","\u2c6a":"k","\ua741":"k","\ua743":"k","\ua745":"k","\ua7a3":"k","\u24db":"l","\uff4c":"l","\u0140":"l","\u013a":"l","\u013e":"l","\u1e37":"l","\u1e39":"l","\u013c":"l","\u1e3d":"l","\u1e3b":"l","\u017f":"l","\u0142":"l","\u019a":"l","\u026b":"l","\u2c61":"l","\ua749":"l","\ua781":"l","\ua747":"l","\u01c9":"lj","\u24dc":"m","\uff4d":"m","\u1e3f":"m","\u1e41":"m","\u1e43":"m","\u0271":"m","\u026f":"m","\u24dd":"n","\uff4e":"n","\u01f9":"n","\u0144":"n","\xf1":"n","\u1e45":"n","\u0148":"n","\u1e47":"n","\u0146":"n","\u1e4b":"n","\u1e49":"n","\u019e":"n","\u0272":"n","\u0149":"n","\ua791":"n","\ua7a5":"n","\u01cc":"nj","\u24de":"o","\uff4f":"o","\xf2":"o","\xf3":"o","\xf4":"o","\u1ed3":"o","\u1ed1":"o","\u1ed7":"o","\u1ed5":"o","\xf5":"o","\u1e4d":"o","\u022d":"o","\u1e4f":"o","\u014d":"o","\u1e51":"o","\u1e53":"o","\u014f":"o","\u022f":"o","\u0231":"o","\xf6":"o","\u022b":"o","\u1ecf":"o","\u0151":"o","\u01d2":"o","\u020d":"o","\u020f":"o","\u01a1":"o","\u1edd":"o","\u1edb":"o","\u1ee1":"o","\u1edf":"o","\u1ee3":"o","\u1ecd":"o","\u1ed9":"o","\u01eb":"o","\u01ed":"o","\xf8":"o","\u01ff":"o","\u0254":"o","\ua74b":"o","\ua74d":"o","\u0275":"o","\u01a3":"oi","\u0223":"ou","\ua74f":"oo","\u24df":"p","\uff50":"p","\u1e55":"p","\u1e57":"p","\u01a5":"p","\u1d7d":"p","\ua751":"p","\ua753":"p","\ua755":"p","\u24e0":"q","\uff51":"q","\u024b":"q","\ua757":"q","\ua759":"q","\u24e1":"r","\uff52":"r","\u0155":"r","\u1e59":"r","\u0159":"r","\u0211":"r","\u0213":"r","\u1e5b":"r","\u1e5d":"r","\u0157":"r","\u1e5f":"r","\u024d":"r","\u027d":"r","\ua75b":"r","\ua7a7":"r","\ua783":"r","\u24e2":"s","\uff53":"s","\xdf":"s","\u015b":"s","\u1e65":"s","\u015d":"s","\u1e61":"s","\u0161":"s","\u1e67":"s","\u1e63":"s","\u1e69":"s","\u0219":"s","\u015f":"s","\u023f":"s","\ua7a9":"s","\ua785":"s","\u1e9b":"s","\u24e3":"t","\uff54":"t","\u1e6b":"t","\u1e97":"t","\u0165":"t","\u1e6d":"t","\u021b":"t","\u0163":"t","\u1e71":"t","\u1e6f":"t","\u0167":"t","\u01ad":"t","\u0288":"t","\u2c66":"t","\ua787":"t","\ua729":"tz","\u24e4":"u","\uff55":"u","\xf9":"u","\xfa":"u","\xfb":"u","\u0169":"u","\u1e79":"u","\u016b":"u","\u1e7b":"u","\u016d":"u","\xfc":"u","\u01dc":"u","\u01d8":"u","\u01d6":"u","\u01da":"u","\u1ee7":"u","\u016f":"u","\u0171":"u","\u01d4":"u","\u0215":"u","\u0217":"u","\u01b0":"u","\u1eeb":"u","\u1ee9":"u","\u1eef":"u","\u1eed":"u","\u1ef1":"u","\u1ee5":"u","\u1e73":"u","\u0173":"u","\u1e77":"u","\u1e75":"u","\u0289":"u","\u24e5":"v","\uff56":"v","\u1e7d":"v","\u1e7f":"v","\u028b":"v","\ua75f":"v","\u028c":"v","\ua761":"vy","\u24e6":"w","\uff57":"w","\u1e81":"w","\u1e83":"w","\u0175":"w","\u1e87":"w","\u1e85":"w","\u1e98":"w","\u1e89":"w","\u2c73":"w","\u24e7":"x","\uff58":"x","\u1e8b":"x","\u1e8d":"x","\u24e8":"y","\uff59":"y","\u1ef3":"y","\xfd":"y","\u0177":"y","\u1ef9":"y","\u0233":"y","\u1e8f":"y","\xff":"y","\u1ef7":"y","\u1e99":"y","\u1ef5":"y","\u01b4":"y","\u024f":"y","\u1eff":"y","\u24e9":"z","\uff5a":"z","\u017a":"z","\u1e91":"z","\u017c":"z","\u017e":"z","\u1e93":"z","\u1e95":"z","\u01b6":"z","\u0225":"z","\u0240":"z","\u2c6c":"z","\ua763":"z","\u0386":"\u0391","\u0388":"\u0395","\u0389":"\u0397","\u038a":"\u0399","\u03aa":"\u0399","\u038c":"\u039f","\u038e":"\u03a5","\u03ab":"\u03a5","\u038f":"\u03a9","\u03ac":"\u03b1","\u03ad":"\u03b5","\u03ae":"\u03b7","\u03af":"\u03b9","\u03ca":"\u03b9","\u0390":"\u03b9","\u03cc":"\u03bf","\u03cd":"\u03c5","\u03cb":"\u03c5","\u03b0":"\u03c5","\u03c9":"\u03c9","\u03c2":"\u03c3"};j=a(document),g=function(){var a=1;return function(){return a++}}(),d=O(Object,{bind:function(a){var b=this;return function(){a.apply(b,arguments)}},init:function(c){var d,e,f=".select2-results";this.opts=c=this.prepareOpts(c),this.id=c.id,c.element.data("select2")!==b&&null!==c.element.data("select2")&&c.element.data("select2").destroy(),this.container=this.createContainer(),this.liveRegion=a("",{role:"status","aria-live":"polite"}).addClass("select2-hidden-accessible").appendTo(document.body),this.containerId="s2id_"+(c.element.attr("id")||"autogen"+g()),this.containerEventName=this.containerId.replace(/([.])/g,"_").replace(/([;&,\-\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g,"\\$1"),this.container.attr("id",this.containerId),this.container.attr("title",c.element.attr("title")),this.body=a("body"),D(this.container,this.opts.element,this.opts.adaptContainerCssClass),this.container.attr("style",c.element.attr("style")),this.container.css(K(c.containerCss,this.opts.element)),this.container.addClass(K(c.containerCssClass,this.opts.element)),this.elementTabIndex=this.opts.element.attr("tabindex"),this.opts.element.data("select2",this).attr("tabindex","-1").before(this.container).on("click.select2",A),this.container.data("select2",this),this.dropdown=this.container.find(".select2-drop"),D(this.dropdown,this.opts.element,this.opts.adaptDropdownCssClass),this.dropdown.addClass(K(c.dropdownCssClass,this.opts.element)),this.dropdown.data("select2",this),this.dropdown.on("click",A),this.results=d=this.container.find(f),this.search=e=this.container.find("input.select2-input"),this.queryCount=0,this.resultsPage=0,this.context=null,this.initContainer(),this.container.on("click",A),v(this.results),this.dropdown.on("mousemove-filtered",f,this.bind(this.highlightUnderEvent)),this.dropdown.on("touchstart touchmove touchend",f,this.bind(function(a){this._touchEvent=!0,this.highlightUnderEvent(a)})),this.dropdown.on("touchmove",f,this.bind(this.touchMoved)),this.dropdown.on("touchstart touchend",f,this.bind(this.clearTouchMoved)),this.dropdown.on("click",this.bind(function(){this._touchEvent&&(this._touchEvent=!1,this.selectHighlighted())})),x(80,this.results),this.dropdown.on("scroll-debounced",f,this.bind(this.loadMoreIfNeeded)),a(this.container).on("change",".select2-input",function(a){a.stopPropagation()}),a(this.dropdown).on("change",".select2-input",function(a){a.stopPropagation()}),a.fn.mousewheel&&d.mousewheel(function(a,b,c,e){var f=d.scrollTop();e>0&&0>=f-e?(d.scrollTop(0),A(a)):0>e&&d.get(0).scrollHeight-d.scrollTop()+e<=d.height()&&(d.scrollTop(d.get(0).scrollHeight-d.height()),A(a))}),u(e),e.on("keyup-change input paste",this.bind(this.updateResults)),e.on("focus",function(){e.addClass("select2-focused")}),e.on("blur",function(){e.removeClass("select2-focused")}),this.dropdown.on("mouseup",f,this.bind(function(b){a(b.target).closest(".select2-result-selectable").length>0&&(this.highlightUnderEvent(b),this.selectHighlighted(b))})),this.dropdown.on("click mouseup mousedown touchstart touchend focusin",function(a){a.stopPropagation()}),this.nextSearchTerm=b,a.isFunction(this.opts.initSelection)&&(this.initSelection(),this.monitorSource()),null!==c.maximumInputLength&&this.search.attr("maxlength",c.maximumInputLength);var h=c.element.prop("disabled");h===b&&(h=!1),this.enable(!h);var i=c.element.prop("readonly");i===b&&(i=!1),this.readonly(i),k=k||q(),this.autofocus=c.element.prop("autofocus"),c.element.prop("autofocus",!1),this.autofocus&&this.focus(),this.search.attr("placeholder",c.searchInputPlaceholder)},destroy:function(){var a=this.opts.element,c=a.data("select2");this.close(),a.length&&a[0].detachEvent&&a.each(function(){this.detachEvent("onpropertychange",this._sync)}),this.propertyObserver&&(this.propertyObserver.disconnect(),this.propertyObserver=null),this._sync=null,c!==b&&(c.container.remove(),c.liveRegion.remove(),c.dropdown.remove(),a.removeClass("select2-offscreen").removeData("select2").off(".select2").prop("autofocus",this.autofocus||!1),this.elementTabIndex?a.attr({tabindex:this.elementTabIndex}):a.removeAttr("tabindex"),a.show()),N.call(this,"container","liveRegion","dropdown","results","search")},optionToData:function(a){return a.is("option")?{id:a.prop("value"),text:a.text(),element:a.get(),css:a.attr("class"),disabled:a.prop("disabled"),locked:r(a.attr("locked"),"locked")||r(a.data("locked"),!0)}:a.is("optgroup")?{text:a.attr("label"),children:[],element:a.get(),css:a.attr("class")}:void 0},prepareOpts:function(c){var d,e,f,h,i=this;if(d=c.element,"select"===d.get(0).tagName.toLowerCase()&&(this.select=e=c.element),e&&a.each(["id","multiple","ajax","query","createSearchChoice","initSelection","data","tags"],function(){if(this in c)throw new Error("Option '"+this+"' is not allowed for Select2 when attached to a ","
    "," ","
      ","
    ","
    "].join(""));return b},enableInterface:function(){this.parent.enableInterface.apply(this,arguments)&&this.focusser.prop("disabled",!this.isInterfaceEnabled())},opening:function(){var c,d,e;this.opts.minimumResultsForSearch>=0&&this.showSearch(!0),this.parent.opening.apply(this,arguments),this.showSearchInput!==!1&&this.search.val(this.focusser.val()),this.opts.shouldFocusInput(this)&&(this.search.focus(),c=this.search.get(0),c.createTextRange?(d=c.createTextRange(),d.collapse(!1),d.select()):c.setSelectionRange&&(e=this.search.val().length,c.setSelectionRange(e,e))),""===this.search.val()&&this.nextSearchTerm!=b&&(this.search.val(this.nextSearchTerm),this.search.select()),this.focusser.prop("disabled",!0).val(""),this.updateResults(!0),this.opts.element.trigger(a.Event("select2-open"))},close:function(){this.opened()&&(this.parent.close.apply(this,arguments),this.focusser.prop("disabled",!1),this.opts.shouldFocusInput(this)&&this.focusser.focus())},focus:function(){this.opened()?this.close():(this.focusser.prop("disabled",!1),this.opts.shouldFocusInput(this)&&this.focusser.focus())},isFocused:function(){return this.container.hasClass("select2-container-active")},cancel:function(){this.parent.cancel.apply(this,arguments),this.focusser.prop("disabled",!1),this.opts.shouldFocusInput(this)&&this.focusser.focus()},destroy:function(){a("label[for='"+this.focusser.attr("id")+"']").attr("for",this.opts.element.attr("id")),this.parent.destroy.apply(this,arguments),N.call(this,"selection","focusser")},initContainer:function(){var b,h,d=this.container,e=this.dropdown,f=g();this.opts.minimumResultsForSearch<0?this.showSearch(!1):this.showSearch(!0),this.selection=b=d.find(".select2-choice"),this.focusser=d.find(".select2-focusser"),b.find(".select2-chosen").attr("id","select2-chosen-"+f),this.focusser.attr("aria-labelledby","select2-chosen-"+f),this.results.attr("id","select2-results-"+f),this.search.attr("aria-owns","select2-results-"+f),this.focusser.attr("id","s2id_autogen"+f),h=a("label[for='"+this.opts.element.attr("id")+"']"),this.focusser.prev().text(h.text()).attr("for",this.focusser.attr("id"));var i=this.opts.element.attr("title");this.opts.element.attr("title",i||h.text()),this.focusser.attr("tabindex",this.elementTabIndex),this.search.attr("id",this.focusser.attr("id")+"_search"),this.search.prev().text(a("label[for='"+this.focusser.attr("id")+"']").text()).attr("for",this.search.attr("id")),this.search.on("keydown",this.bind(function(a){if(this.isInterfaceEnabled()){if(a.which===c.PAGE_UP||a.which===c.PAGE_DOWN)return A(a),void 0;switch(a.which){case c.UP:case c.DOWN:return this.moveHighlight(a.which===c.UP?-1:1),A(a),void 0;case c.ENTER:return this.selectHighlighted(),A(a),void 0;case c.TAB:return this.selectHighlighted({noFocus:!0}),void 0;case c.ESC:return this.cancel(a),A(a),void 0}}})),this.search.on("blur",this.bind(function(){document.activeElement===this.body.get(0)&&window.setTimeout(this.bind(function(){this.opened()&&this.search.focus()}),0)})),this.focusser.on("keydown",this.bind(function(a){if(this.isInterfaceEnabled()&&a.which!==c.TAB&&!c.isControl(a)&&!c.isFunctionKey(a)&&a.which!==c.ESC){if(this.opts.openOnEnter===!1&&a.which===c.ENTER)return A(a),void 0;if(a.which==c.DOWN||a.which==c.UP||a.which==c.ENTER&&this.opts.openOnEnter){if(a.altKey||a.ctrlKey||a.shiftKey||a.metaKey)return;return this.open(),A(a),void 0}return a.which==c.DELETE||a.which==c.BACKSPACE?(this.opts.allowClear&&this.clear(),A(a),void 0):void 0}})),u(this.focusser),this.focusser.on("keyup-change input",this.bind(function(a){if(this.opts.minimumResultsForSearch>=0){if(a.stopPropagation(),this.opened())return;this.open()}})),b.on("mousedown touchstart","abbr",this.bind(function(a){this.isInterfaceEnabled()&&(this.clear(),B(a),this.close(),this.selection.focus())})),b.on("mousedown touchstart",this.bind(function(c){n(b),this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.opened()?this.close():this.isInterfaceEnabled()&&this.open(),A(c)})),e.on("mousedown touchstart",this.bind(function(){this.opts.shouldFocusInput(this)&&this.search.focus()})),b.on("focus",this.bind(function(a){A(a)})),this.focusser.on("focus",this.bind(function(){this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.container.addClass("select2-container-active")})).on("blur",this.bind(function(){this.opened()||(this.container.removeClass("select2-container-active"),this.opts.element.trigger(a.Event("select2-blur")))})),this.search.on("focus",this.bind(function(){this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.container.addClass("select2-container-active")})),this.initContainerWidth(),this.opts.element.addClass("select2-offscreen"),this.setPlaceholder()},clear:function(b){var c=this.selection.data("select2-data");if(c){var d=a.Event("select2-clearing");if(this.opts.element.trigger(d),d.isDefaultPrevented())return;var e=this.getPlaceholderOption();this.opts.element.val(e?e.val():""),this.selection.find(".select2-chosen").empty(),this.selection.removeData("select2-data"),this.setPlaceholder(),b!==!1&&(this.opts.element.trigger({type:"select2-removed",val:this.id(c),choice:c}),this.triggerChange({removed:c}))}},initSelection:function(){if(this.isPlaceholderOptionSelected())this.updateSelection(null),this.close(),this.setPlaceholder();else{var c=this;this.opts.initSelection.call(null,this.opts.element,function(a){a!==b&&null!==a&&(c.updateSelection(a),c.close(),c.setPlaceholder(),c.nextSearchTerm=c.opts.nextSearchTerm(a,c.search.val()))})}},isPlaceholderOptionSelected:function(){var a;return this.getPlaceholder()===b?!1:(a=this.getPlaceholderOption())!==b&&a.prop("selected")||""===this.opts.element.val()||this.opts.element.val()===b||null===this.opts.element.val()},prepareOpts:function(){var b=this.parent.prepareOpts.apply(this,arguments),c=this;return"select"===b.element.get(0).tagName.toLowerCase()?b.initSelection=function(a,b){var d=a.find("option").filter(function(){return this.selected&&!this.disabled});b(c.optionToData(d))}:"data"in b&&(b.initSelection=b.initSelection||function(c,d){var e=c.val(),f=null;b.query({matcher:function(a,c,d){var g=r(e,b.id(d));return g&&(f=d),g},callback:a.isFunction(d)?function(){d(f)}:a.noop})}),b},getPlaceholder:function(){return this.select&&this.getPlaceholderOption()===b?b:this.parent.getPlaceholder.apply(this,arguments)},setPlaceholder:function(){var a=this.getPlaceholder();if(this.isPlaceholderOptionSelected()&&a!==b){if(this.select&&this.getPlaceholderOption()===b)return;this.selection.find(".select2-chosen").html(this.opts.escapeMarkup(a)),this.selection.addClass("select2-default"),this.container.removeClass("select2-allowclear")}},postprocessResults:function(a,b,c){var d=0,e=this;if(this.findHighlightableChoices().each2(function(a,b){return r(e.id(b.data("select2-data")),e.opts.element.val())?(d=a,!1):void 0}),c!==!1&&(b===!0&&d>=0?this.highlight(d):this.highlight(0)),b===!0){var g=this.opts.minimumResultsForSearch;g>=0&&this.showSearch(L(a.results)>=g)}},showSearch:function(b){this.showSearchInput!==b&&(this.showSearchInput=b,this.dropdown.find(".select2-search").toggleClass("select2-search-hidden",!b),this.dropdown.find(".select2-search").toggleClass("select2-offscreen",!b),a(this.dropdown,this.container).toggleClass("select2-with-searchbox",b))},onSelect:function(a,b){if(this.triggerSelect(a)){var c=this.opts.element.val(),d=this.data();this.opts.element.val(this.id(a)),this.updateSelection(a),this.opts.element.trigger({type:"select2-selected",val:this.id(a),choice:a}),this.nextSearchTerm=this.opts.nextSearchTerm(a,this.search.val()),this.close(),b&&b.noFocus||!this.opts.shouldFocusInput(this)||this.focusser.focus(),r(c,this.id(a))||this.triggerChange({added:a,removed:d})}},updateSelection:function(a){var d,e,c=this.selection.find(".select2-chosen");this.selection.data("select2-data",a),c.empty(),null!==a&&(d=this.opts.formatSelection(a,c,this.opts.escapeMarkup)),d!==b&&c.append(d),e=this.opts.formatSelectionCssClass(a,c),e!==b&&c.addClass(e),this.selection.removeClass("select2-default"),this.opts.allowClear&&this.getPlaceholder()!==b&&this.container.addClass("select2-allowclear")},val:function(){var a,c=!1,d=null,e=this,f=this.data();if(0===arguments.length)return this.opts.element.val();if(a=arguments[0],arguments.length>1&&(c=arguments[1]),this.select)this.select.val(a).find("option").filter(function(){return this.selected}).each2(function(a,b){return d=e.optionToData(b),!1}),this.updateSelection(d),this.setPlaceholder(),c&&this.triggerChange({added:d,removed:f});else{if(!a&&0!==a)return this.clear(c),void 0;if(this.opts.initSelection===b)throw new Error("cannot call val() if initSelection() is not defined");this.opts.element.val(a),this.opts.initSelection(this.opts.element,function(a){e.opts.element.val(a?e.id(a):""),e.updateSelection(a),e.setPlaceholder(),c&&e.triggerChange({added:a,removed:f})})}},clearSearch:function(){this.search.val(""),this.focusser.val("")},data:function(a){var c,d=!1;return 0===arguments.length?(c=this.selection.data("select2-data"),c==b&&(c=null),c):(arguments.length>1&&(d=arguments[1]),a?(c=this.data(),this.opts.element.val(a?this.id(a):""),this.updateSelection(a),d&&this.triggerChange({added:a,removed:c})):this.clear(d),void 0)}}),f=O(d,{createContainer:function(){var b=a(document.createElement("div")).attr({"class":"select2-container select2-container-multi form-control"}).html(["
      ","
    • "," "," ","
    • ","
    ","
    ","
      ","
    ","
    "].join(""));return b},prepareOpts:function(){var b=this.parent.prepareOpts.apply(this,arguments),c=this;return"select"===b.element.get(0).tagName.toLowerCase()?b.initSelection=function(a,b){var d=[];a.find("option").filter(function(){return this.selected&&!this.disabled}).each2(function(a,b){d.push(c.optionToData(b))}),b(d)}:"data"in b&&(b.initSelection=b.initSelection||function(c,d){var e=s(c.val(),b.separator),f=[];b.query({matcher:function(c,d,g){var h=a.grep(e,function(a){return r(a,b.id(g))}).length;return h&&f.push(g),h},callback:a.isFunction(d)?function(){for(var a=[],c=0;c0||(this.selectChoice(null),this.clearPlaceholder(),this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.open(),this.focusSearch(),b.preventDefault()))})),this.container.on("focus",b,this.bind(function(){this.isInterfaceEnabled()&&(this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.container.addClass("select2-container-active"),this.dropdown.addClass("select2-drop-active"),this.clearPlaceholder())})),this.initContainerWidth(),this.opts.element.addClass("select2-offscreen"),this.clearSearch()},enableInterface:function(){this.parent.enableInterface.apply(this,arguments)&&this.search.prop("disabled",!this.isInterfaceEnabled())},initSelection:function(){if(""===this.opts.element.val()&&""===this.opts.element.text()&&(this.updateSelection([]),this.close(),this.clearSearch()),this.select||""!==this.opts.element.val()){var c=this;this.opts.initSelection.call(null,this.opts.element,function(a){a!==b&&null!==a&&(c.updateSelection(a),c.close(),c.clearSearch())})}},clearSearch:function(){var a=this.getPlaceholder(),c=this.getMaxSearchWidth();a!==b&&0===this.getVal().length&&this.search.hasClass("select2-focused")===!1?(this.search.val(a).addClass("select2-default"),this.search.width(c>0?c:this.container.css("width"))):this.search.val("").width(10)},clearPlaceholder:function(){this.search.hasClass("select2-default")&&this.search.val("").removeClass("select2-default")},opening:function(){this.clearPlaceholder(),this.resizeSearch(),this.parent.opening.apply(this,arguments),this.focusSearch(),""===this.search.val()&&this.nextSearchTerm!=b&&(this.search.val(this.nextSearchTerm),this.search.select()),this.updateResults(!0),this.opts.shouldFocusInput(this)&&this.search.focus(),this.opts.element.trigger(a.Event("select2-open"))},close:function(){this.opened()&&this.parent.close.apply(this,arguments)},focus:function(){this.close(),this.search.focus()},isFocused:function(){return this.search.hasClass("select2-focused")},updateSelection:function(b){var c=[],d=[],e=this;a(b).each(function(){p(e.id(this),c)<0&&(c.push(e.id(this)),d.push(this))}),b=d,this.selection.find(".select2-search-choice").remove(),a(b).each(function(){e.addSelectedChoice(this)}),e.postprocessResults()},tokenize:function(){var a=this.search.val();a=this.opts.tokenizer.call(this,a,this.data(),this.bind(this.onSelect),this.opts),null!=a&&a!=b&&(this.search.val(a),a.length>0&&this.open())},onSelect:function(a,c){this.triggerSelect(a)&&(this.addSelectedChoice(a),this.opts.element.trigger({type:"selected",val:this.id(a),choice:a}),this.nextSearchTerm=this.opts.nextSearchTerm(a,this.search.val()),this.clearSearch(),this.updateResults(),(this.select||!this.opts.closeOnSelect)&&this.postprocessResults(a,!1,this.opts.closeOnSelect===!0),this.opts.closeOnSelect?(this.close(),this.search.width(10)):this.countSelectableResults()>0?(this.search.width(10),this.resizeSearch(),this.getMaximumSelectionSize()>0&&this.val().length>=this.getMaximumSelectionSize()?this.updateResults(!0):this.nextSearchTerm!=b&&(this.search.val(this.nextSearchTerm),this.updateResults(),this.search.select()),this.positionDropdown()):(this.close(),this.search.width(10)),this.triggerChange({added:a}),c&&c.noFocus||this.focusSearch())},cancel:function(){this.close(),this.focusSearch()},addSelectedChoice:function(c){var j,k,d=!c.locked,e=a("
  • "),f=a("
  • "),g=d?e:f,h=this.id(c),i=this.getVal();j=this.opts.formatSelection(c,g.find("div"),this.opts.escapeMarkup),j!=b&&g.find("div").replaceWith("
    "+j+"
    "),k=this.opts.formatSelectionCssClass(c,g.find("div")),k!=b&&g.addClass(k),d&&g.find(".select2-search-choice-close").on("mousedown",A).on("click dblclick",this.bind(function(b){this.isInterfaceEnabled()&&(this.unselect(a(b.target)),this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus"),A(b),this.close(),this.focusSearch())})).on("focus",this.bind(function(){this.isInterfaceEnabled()&&(this.container.addClass("select2-container-active"),this.dropdown.addClass("select2-drop-active"))})),g.data("select2-data",c),g.insertBefore(this.searchContainer),i.push(h),this.setVal(i)},unselect:function(b){var d,e,c=this.getVal();if(b=b.closest(".select2-search-choice"),0===b.length)throw"Invalid argument: "+b+". Must be .select2-search-choice";if(d=b.data("select2-data")){var f=a.Event("select2-removing");if(f.val=this.id(d),f.choice=d,this.opts.element.trigger(f),f.isDefaultPrevented())return!1;for(;(e=p(this.id(d),c))>=0;)c.splice(e,1),this.setVal(c),this.select&&this.postprocessResults();return b.remove(),this.opts.element.trigger({type:"select2-removed",val:this.id(d),choice:d}),this.triggerChange({removed:d}),!0}},postprocessResults:function(a,b,c){var d=this.getVal(),e=this.results.find(".select2-result"),f=this.results.find(".select2-result-with-children"),g=this;e.each2(function(a,b){var c=g.id(b.data("select2-data"));p(c,d)>=0&&(b.addClass("select2-selected"),b.find(".select2-result-selectable").addClass("select2-selected"))}),f.each2(function(a,b){b.is(".select2-result-selectable")||0!==b.find(".select2-result-selectable:not(.select2-selected)").length||b.addClass("select2-selected")}),-1==this.highlight()&&c!==!1&&g.highlight(0),!this.opts.createSearchChoice&&!e.filter(".select2-result:not(.select2-selected)").length>0&&(!a||a&&!a.more&&0===this.results.find(".select2-no-results").length)&&J(g.opts.formatNoMatches,"formatNoMatches")&&this.results.append("
  • "+K(g.opts.formatNoMatches,g.opts.element,g.search.val())+"
  • ")},getMaxSearchWidth:function(){return this.selection.width()-t(this.search)},resizeSearch:function(){var a,b,c,d,e,f=t(this.search);a=C(this.search)+10,b=this.search.offset().left,c=this.selection.width(),d=this.selection.offset().left,e=c-(b-d)-f,a>e&&(e=c-f),40>e&&(e=c-f),0>=e&&(e=a),this.search.width(Math.floor(e))},getVal:function(){var a;return this.select?(a=this.select.val(),null===a?[]:a):(a=this.opts.element.val(),s(a,this.opts.separator))},setVal:function(b){var c;this.select?this.select.val(b):(c=[],a(b).each(function(){p(this,c)<0&&c.push(this)}),this.opts.element.val(0===c.length?"":c.join(this.opts.separator)))},buildChangeDetails:function(a,b){for(var b=b.slice(0),a=a.slice(0),c=0;c0&&c--,a.splice(d,1),d--);return{added:b,removed:a}},val:function(c,d){var e,f=this;if(0===arguments.length)return this.getVal();if(e=this.data(),e.length||(e=[]),!c&&0!==c)return this.opts.element.val(""),this.updateSelection([]),this.clearSearch(),d&&this.triggerChange({added:this.data(),removed:e}),void 0;if(this.setVal(c),this.select)this.opts.initSelection(this.select,this.bind(this.updateSelection)),d&&this.triggerChange(this.buildChangeDetails(e,this.data()));else{if(this.opts.initSelection===b)throw new Error("val() cannot be called if initSelection() is not defined");this.opts.initSelection(this.opts.element,function(b){var c=a.map(b,f.id);f.setVal(c),f.updateSelection(b),f.clearSearch(),d&&f.triggerChange(f.buildChangeDetails(e,f.data()))})}this.clearSearch()},onSortStart:function(){if(this.select)throw new Error("Sorting of elements is not supported when attached to instead.");this.search.width(0),this.searchContainer.hide()},onSortEnd:function(){var b=[],c=this;this.searchContainer.show(),this.searchContainer.appendTo(this.searchContainer.parent()),this.resizeSearch(),this.selection.find(".select2-search-choice").each(function(){b.push(c.opts.id(a(this).data("select2-data")))}),this.setVal(b),this.triggerChange()},data:function(b,c){var e,f,d=this;return 0===arguments.length?this.selection.children(".select2-search-choice").map(function(){return a(this).data("select2-data")}).get():(f=this.data(),b||(b=[]),e=a.map(b,function(a){return d.opts.id(a)}),this.setVal(e),this.updateSelection(b),this.clearSearch(),c&&this.triggerChange(this.buildChangeDetails(f,this.data())),void 0)}}),a.fn.select2=function(){var d,e,f,g,h,c=Array.prototype.slice.call(arguments,0),i=["val","destroy","opened","open","close","focus","isFocused","container","dropdown","onSortStart","onSortEnd","enable","disable","readonly","positionDropdown","data","search"],j=["opened","isFocused","container","dropdown"],k=["val","data"],l={search:"externalSearch"};return this.each(function(){if(0===c.length||"object"==typeof c[0])d=0===c.length?{}:a.extend({},c[0]),d.element=a(this),"select"===d.element.get(0).tagName.toLowerCase()?h=d.element.prop("multiple"):(h=d.multiple||!1,"tags"in d&&(d.multiple=h=!0)),e=h?new window.Select2["class"].multi:new window.Select2["class"].single,e.init(d);else{if("string"!=typeof c[0])throw"Invalid arguments to select2 plugin: "+c;if(p(c[0],i)<0)throw"Unknown method: "+c[0];if(g=b,e=a(this).data("select2"),e===b)return;if(f=c[0],"container"===f?g=e.container:"dropdown"===f?g=e.dropdown:(l[f]&&(f=l[f]),g=e[f].apply(e,c.slice(1))),p(c[0],j)>=0||p(c[0],k)>=0&&1==c.length)return!1}}),g===b?this:g},a.fn.select2.defaults={width:"copy",loadMorePadding:0,closeOnSelect:!0,openOnEnter:!0,containerCss:{},dropdownCss:{},containerCssClass:"",dropdownCssClass:"",formatResult:function(a,b,c,d){var e=[];return E(a.text,c.term,e,d),e.join("")},formatSelection:function(a,c,d){return a?d(a.text):b},sortResults:function(a){return a},formatResultCssClass:function(a){return a.css},formatSelectionCssClass:function(){return b},formatMatches:function(a){return 1===a?"One result is available, press enter to select it.":a+" results are available, use up and down arrow keys to navigate."},formatNoMatches:function(){return"No matches found"},formatInputTooShort:function(a,b){var c=b-a.length;return"Please enter "+c+" or more character"+(1==c?"":"s")},formatInputTooLong:function(a,b){var c=a.length-b;return"Please delete "+c+" character"+(1==c?"":"s")},formatSelectionTooBig:function(a){return"You can only select "+a+" item"+(1==a?"":"s")},formatLoadMore:function(){return"Loading more results\u2026"},formatSearching:function(){return"Searching\u2026"},minimumResultsForSearch:0,minimumInputLength:0,maximumInputLength:null,maximumSelectionSize:0,id:function(a){return a==b?null:a.id},matcher:function(a,b){return o(""+b).toUpperCase().indexOf(o(""+a).toUpperCase())>=0},separator:",",tokenSeparators:[],tokenizer:M,escapeMarkup:F,blurOnChange:!1,selectOnBlur:!1,adaptContainerCssClass:function(a){return a},adaptDropdownCssClass:function(){return null +},nextSearchTerm:function(){return b},searchInputPlaceholder:"",createSearchChoicePosition:"top",shouldFocusInput:function(a){var b="ontouchstart"in window||navigator.msMaxTouchPoints>0;return b?a.opts.minimumResultsForSearch<0?!1:!0:!0}},a.fn.select2.ajaxDefaults={transport:a.ajax,params:{type:"GET",cache:!1,dataType:"json"}},window.Select2={query:{ajax:G,local:H,tags:I},util:{debounce:w,markMatch:E,escapeMarkup:F,stripDiacritics:o},"class":{"abstract":d,single:e,multi:f}}}}(jQuery); \ No newline at end of file diff --git a/client/js/libs/showdown.js b/client/js/libs/showdown.js new file mode 100644 index 000000000..b950d4a5f --- /dev/null +++ b/client/js/libs/showdown.js @@ -0,0 +1,1457 @@ +// +// showdown.js -- A javascript port of Markdown. +// +// Copyright (c) 2007 John Fraser. +// +// Original Markdown Copyright (c) 2004-2005 John Gruber +// +// +// Redistributable under a BSD-style open source license. +// See license.txt for more information. +// +// The full source distribution is at: +// +// A A L +// T C A +// T K B +// +// +// + +// +// Wherever possible, Showdown is a straight, line-by-line port +// of the Perl version of Markdown. +// +// This is not a normal parser design; it's basically just a +// series of string substitutions. It's hard to read and +// maintain this way, but keeping Showdown close to the original +// design makes it easier to port new features. +// +// More importantly, Showdown behaves like markdown.pl in most +// edge cases. So web applications can do client-side preview +// in Javascript, and then build identical HTML on the server. +// +// This port needs the new RegExp functionality of ECMA 262, +// 3rd Edition (i.e. Javascript 1.5). Most modern web browsers +// should do fine. Even with the new regular expression features, +// We do a lot of work to emulate Perl's regex functionality. +// The tricky changes in this file mostly have the "attacklab:" +// label. Major or self-explanatory changes don't. +// +// Smart diff tools like Araxis Merge will be able to match up +// this file with markdown.pl in a useful way. A little tweaking +// helps: in a copy of markdown.pl, replace "#" with "//" and +// replace "$text" with "text". Be sure to ignore whitespace +// and line endings. +// + + +// +// Showdown usage: +// +// var text = "Markdown *rocks*."; +// +// var converter = new Showdown.converter(); +// var html = converter.makeHtml(text); +// +// alert(html); +// +// Note: move the sample code to the bottom of this +// file before uncommenting it. +// + + +// +// Showdown namespace +// +var Showdown = { extensions: {} }; + +// +// forEach +// +var forEach = Showdown.forEach = function(obj, callback) { + if (typeof obj.forEach === 'function') { + obj.forEach(callback); + } else { + var i, len = obj.length; + for (i = 0; i < len; i++) { + callback(obj[i], i, obj); + } + } +}; + +// +// Standard extension naming +// +var stdExtName = function(s) { + return s.replace(/[_-]||\s/g, '').toLowerCase(); +}; + +// +// converter +// +// Wraps all "globals" so that the only thing +// exposed is makeHtml(). +// +Showdown.converter = function(converter_options) { + +// +// Globals: +// + +// Global hashes, used by various utility routines +var g_urls; +var g_titles; +var g_html_blocks; + +// Used to track when we're inside an ordered or unordered list +// (see _ProcessListItems() for details): +var g_list_level = 0; + +// Global extensions +var g_lang_extensions = []; +var g_output_modifiers = []; + + +// +// Automatic Extension Loading (node only): +// + +if (typeof module !== 'undefind' && typeof exports !== 'undefined' && typeof require !== 'undefind') { + var fs = require('fs'); + + if (fs) { + // Search extensions folder + var extensions = fs.readdirSync((__dirname || '.')+'/extensions').filter(function(file){ + return ~file.indexOf('.js'); + }).map(function(file){ + return file.replace(/\.js$/, ''); + }); + // Load extensions into Showdown namespace + Showdown.forEach(extensions, function(ext){ + var name = stdExtName(ext); + Showdown.extensions[name] = require('./extensions/' + ext); + }); + } +} + +this.colorCode = function(string) { +return calcMD5(string).slice(0, 6); +}; + +this.makeHtml = function(text) { +// +// Main function. The order in which other subs are called here is +// essential. Link and image substitutions need to happen before +// _EscapeSpecialCharsWithinTagAttributes(), so that any *'s or _'s in the +// and tags get encoded. +// + + // Clear the global hashes. If we don't clear these, you get conflicts + // from other articles when generating a page which contains more than + // one article (e.g. an index page that shows the N most recent + // articles): + g_urls = {}; + g_titles = {}; + g_html_blocks = []; + + // attacklab: Replace ~ with ~T + // This lets us use tilde as an escape char to avoid md5 hashes + // The choice of character is arbitray; anything that isn't + // magic in Markdown will work. + text = text.replace(/~/g,"~T"); + + // attacklab: Replace $ with ~D + // RegExp interprets $ as a special character + // when it's in a replacement string + text = text.replace(/\$/g,"~D"); + + // Standardize line endings + text = text.replace(/\r\n/g,"\n"); // DOS to Unix + text = text.replace(/\r/g,"\n"); // Mac to Unix + // Make sure text begins and ends with a couple of newlines: + text = "\n\n" + text + "\n\n"; + + // Convert all tabs to spaces. + text = _Detab(text); + + // Strip any lines consisting only of spaces and tabs. + // This makes subsequent regexen easier to write, because we can + // match consecutive blank lines with /\n+/ instead of something + // contorted like /[ \t]*\n+/ . + text = text.replace(/^[ \t]+$/mg,""); + + // Run language extensions + Showdown.forEach(g_lang_extensions, function(x){ + text = _ExecuteExtension(x, text); + }); + + // Handle github codeblocks prior to running HashHTML so that + // HTML contained within the codeblock gets escaped propertly + text = _DoGithubCodeBlocks(text); + + // Turn block-level HTML blocks into hash entries + text = _HashHTMLBlocks(text); + + // Strip link definitions, store in hashes. + text = _StripLinkDefinitions(text); + + text = _RunBlockGamut(text); + + text = _UnescapeSpecialChars(text); + + // attacklab: Restore dollar signs + text = text.replace(/~D/g,"$$"); + + // attacklab: Restore tildes + text = text.replace(/~T/g,"~"); + + // Run output modifiers + Showdown.forEach(g_output_modifiers, function(x){ + text = _ExecuteExtension(x, text); + }); + + return text; +}; +// +// Options: +// + +// Parse extensions options into separate arrays +if (converter_options && converter_options.extensions) { + + var self = this; + + // Iterate over each plugin + Showdown.forEach(converter_options.extensions, function(plugin){ + + // Assume it's a bundled plugin if a string is given + if (typeof plugin === 'string') { + plugin = Showdown.extensions[stdExtName(plugin)]; + } + + if (typeof plugin === 'function') { + // Iterate over each extension within that plugin + Showdown.forEach(plugin(self), function(ext){ + // Sort extensions by type + if (ext.type) { + if (ext.type === 'language' || ext.type === 'lang') { + g_lang_extensions.push(ext); + } else if (ext.type === 'output' || ext.type === 'html') { + g_output_modifiers.push(ext); + } + } else { + // Assume language extension + g_output_modifiers.push(ext); + } + }); + } else { + throw "Extension '" + plugin + "' could not be loaded. It was either not found or is not a valid extension."; + } + }); +} + + +var _ExecuteExtension = function(ext, text) { + if (ext.regex) { + var re = new RegExp(ext.regex, 'g'); + return text.replace(re, ext.replace); + } else if (ext.filter) { + return ext.filter(text); + } +}; + +var _StripLinkDefinitions = function(text) { +// +// Strips link definitions from text, stores the URLs and titles in +// hash references. +// + + // Link defs are in the form: ^[id]: url "optional title" + + /* + var text = text.replace(/ + ^[ ]{0,3}\[(.+)\]: // id = $1 attacklab: g_tab_width - 1 + [ \t]* + \n? // maybe *one* newline + [ \t]* + ? // url = $2 + [ \t]* + \n? // maybe one newline + [ \t]* + (?: + (\n*) // any lines skipped = $3 attacklab: lookbehind removed + ["(] + (.+?) // title = $4 + [")] + [ \t]* + )? // title is optional + (?:\n+|$) + /gm, + function(){...}); + */ + + // attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug + text += "~0"; + + text = text.replace(/^[ ]{0,3}\[(.+)\]:[ \t]*\n?[ \t]*?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|(?=~0))/gm, + function (wholeMatch,m1,m2,m3,m4) { + m1 = m1.toLowerCase(); + g_urls[m1] = _EncodeAmpsAndAngles(m2); // Link IDs are case-insensitive + if (m3) { + // Oops, found blank lines, so it's not a title. + // Put back the parenthetical statement we stole. + return m3+m4; + } else if (m4) { + g_titles[m1] = m4.replace(/"/g,"""); + } + + // Completely remove the definition from the text + return ""; + } + ); + + // attacklab: strip sentinel + text = text.replace(/~0/,""); + + return text; +} + + +var _HashHTMLBlocks = function(text) { + // attacklab: Double up blank lines to reduce lookaround + text = text.replace(/\n/g,"\n\n"); + + // Hashify HTML blocks: + // We only want to do this for block-level HTML tags, such as headers, + // lists, and tables. That's because we still want to wrap

    s around + // "paragraphs" that are wrapped in non-block-level tags, such as anchors, + // phrase emphasis, and spans. The list of tags we're looking for is + // hard-coded: + var block_tags_a = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del|style|section|header|footer|nav|article|aside"; + var block_tags_b = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|style|section|header|footer|nav|article|aside"; + + // First, look for nested blocks, e.g.: + //

    + //
    + // tags for inner block must be indented. + //
    + //
    + // + // The outermost tags must start at the left margin for this to match, and + // the inner nested divs must be indented. + // We need to do this before the next, more liberal match, because the next + // match will start at the first `
    ` and stop at the first `
    `. + + // attacklab: This regex can be expensive when it fails. + /* + var text = text.replace(/ + ( // save in $1 + ^ // start of line (with /m) + <($block_tags_a) // start tag = $2 + \b // word break + // attacklab: hack around khtml/pcre bug... + [^\r]*?\n // any number of lines, minimally matching + // the matching end tag + [ \t]* // trailing spaces/tabs + (?=\n+) // followed by a newline + ) // attacklab: there are sentinel newlines at end of document + /gm,function(){...}}; + */ + text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del)\b[^\r]*?\n<\/\2>[ \t]*(?=\n+))/gm,hashElement); + + // + // Now match more liberally, simply from `\n` to `\n` + // + + /* + var text = text.replace(/ + ( // save in $1 + ^ // start of line (with /m) + <($block_tags_b) // start tag = $2 + \b // word break + // attacklab: hack around khtml/pcre bug... + [^\r]*? // any number of lines, minimally matching + // the matching end tag + [ \t]* // trailing spaces/tabs + (?=\n+) // followed by a newline + ) // attacklab: there are sentinel newlines at end of document + /gm,function(){...}}; + */ + text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|style|section|header|footer|nav|article|aside)\b[^\r]*?<\/\2>[ \t]*(?=\n+)\n)/gm,hashElement); + + // Special case just for
    . It was easier to make a special case than + // to make the other regex more complicated. + + /* + text = text.replace(/ + ( // save in $1 + \n\n // Starting after a blank line + [ ]{0,3} + (<(hr) // start tag = $2 + \b // word break + ([^<>])*? // + \/?>) // the matching end tag + [ \t]* + (?=\n{2,}) // followed by a blank line + ) + /g,hashElement); + */ + text = text.replace(/(\n[ ]{0,3}(<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g,hashElement); + + // Special case for standalone HTML comments: + + /* + text = text.replace(/ + ( // save in $1 + \n\n // Starting after a blank line + [ ]{0,3} // attacklab: g_tab_width - 1 + + [ \t]* + (?=\n{2,}) // followed by a blank line + ) + /g,hashElement); + */ + text = text.replace(/(\n\n[ ]{0,3}[ \t]*(?=\n{2,}))/g,hashElement); + + // PHP and ASP-style processor instructions ( and <%...%>) + + /* + text = text.replace(/ + (?: + \n\n // Starting after a blank line + ) + ( // save in $1 + [ ]{0,3} // attacklab: g_tab_width - 1 + (?: + <([?%]) // $2 + [^\r]*? + \2> + ) + [ \t]* + (?=\n{2,}) // followed by a blank line + ) + /g,hashElement); + */ + text = text.replace(/(?:\n\n)([ ]{0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g,hashElement); + + // attacklab: Undo double lines (see comment at top of this function) + text = text.replace(/\n\n/g,"\n"); + return text; +} + +var hashElement = function(wholeMatch,m1) { + var blockText = m1; + + // Undo double lines + blockText = blockText.replace(/\n\n/g,"\n"); + blockText = blockText.replace(/^\n/,""); + + // strip trailing blank lines + blockText = blockText.replace(/\n+$/g,""); + + // Replace the element text with a marker ("~KxK" where x is its key) + blockText = "\n\n~K" + (g_html_blocks.push(blockText)-1) + "K\n\n"; + + return blockText; +}; + +var _RunBlockGamut = function(text) { +// +// These are all the transformations that form block-level +// tags like paragraphs, headers, and list items. +// + text = _DoHeaders(text); + + // Do Horizontal Rules: + var key = hashBlock("
    "); + text = text.replace(/^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$/gm,key); + text = text.replace(/^[ ]{0,2}([ ]?\-[ ]?){3,}[ \t]*$/gm,key); + text = text.replace(/^[ ]{0,2}([ ]?\_[ ]?){3,}[ \t]*$/gm,key); + + text = _DoLists(text); + text = _DoCodeBlocks(text); + text = _DoBlockQuotes(text); + + // We already ran _HashHTMLBlocks() before, in Markdown(), but that + // was to escape raw HTML in the original Markdown source. This time, + // we're escaping the markup we've just created, so that we don't wrap + //

    tags around block-level tags. + text = _HashHTMLBlocks(text); + text = _FormParagraphs(text); + + return text; +}; + + +var _RunSpanGamut = function(text) { +// +// These are all the transformations that occur *within* block-level +// tags like paragraphs, headers, and list items. +// + + text = _DoCodeSpans(text); + text = _EscapeSpecialCharsWithinTagAttributes(text); + text = _EncodeBackslashEscapes(text); + + // Process anchor and image tags. Images must come first, + // because ![foo][f] looks like an anchor. + text = _DoImages(text); + text = _DoAnchors(text); + + // Make links out of things like `` + // Must come after _DoAnchors(), because you can use < and > + // delimiters in inline links like [this](). + text = _DoAutoLinks(text); + text = _EncodeAmpsAndAngles(text); + text = _DoItalicsAndBold(text); + + // Do hard breaks: + text = text.replace(/ +\n/g,"
    \n"); + + return text; +} + +var _EscapeSpecialCharsWithinTagAttributes = function(text) { +// +// Within tags -- meaning between < and > -- encode [\ ` * _] so they +// don't conflict with their use in Markdown for code, italics and strong. +// + + // Build a regex to find HTML tags and comments. See Friedl's + // "Mastering Regular Expressions", 2nd Ed., pp. 200-201. + var regex = /(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|)/gi; + + text = text.replace(regex, function(wholeMatch) { + var tag = wholeMatch.replace(/(.)<\/?code>(?=.)/g,"$1`"); + tag = escapeCharacters(tag,"\\`*_"); + return tag; + }); + + return text; +} + +var _DoAnchors = function(text) { +// +// Turn Markdown link shortcuts into XHTML
    tags. +// + // + // First, handle reference-style links: [link text] [id] + // + + /* + text = text.replace(/ + ( // wrap whole match in $1 + \[ + ( + (?: + \[[^\]]*\] // allow brackets nested one level + | + [^\[] // or anything else + )* + ) + \] + + [ ]? // one optional space + (?:\n[ ]*)? // one optional newline followed by spaces + + \[ + (.*?) // id = $3 + \] + )()()()() // pad remaining backreferences + /g,_DoAnchors_callback); + */ + text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g,writeAnchorTag); + + // + // Next, inline-style links: [link text](url "optional title") + // + + /* + text = text.replace(/ + ( // wrap whole match in $1 + \[ + ( + (?: + \[[^\]]*\] // allow brackets nested one level + | + [^\[\]] // or anything else + ) + ) + \] + \( // literal paren + [ \t]* + () // no id, so leave $3 empty + ? // href = $4 + [ \t]* + ( // $5 + (['"]) // quote char = $6 + (.*?) // Title = $7 + \6 // matching quote + [ \t]* // ignore any spaces/tabs between closing quote and ) + )? // title is optional + \) + ) + /g,writeAnchorTag); + */ + text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,writeAnchorTag); + + // + // Last, handle reference-style shortcuts: [link text] + // These must come last in case you've also got [link test][1] + // or [link test](/foo) + // + + /* + text = text.replace(/ + ( // wrap whole match in $1 + \[ + ([^\[\]]+) // link text = $2; can't contain '[' or ']' + \] + )()()()()() // pad rest of backreferences + /g, writeAnchorTag); + */ + text = text.replace(/(\[([^\[\]]+)\])()()()()()/g, writeAnchorTag); + + return text; +} + +var writeAnchorTag = function(wholeMatch,m1,m2,m3,m4,m5,m6,m7) { + if (m7 == undefined) m7 = ""; + var whole_match = m1; + var link_text = m2; + var link_id = m3.toLowerCase(); + var url = m4; + var title = m7; + + if (url == "") { + if (link_id == "") { + // lower-case and turn embedded newlines into spaces + link_id = link_text.toLowerCase().replace(/ ?\n/g," "); + } + url = "#"+link_id; + + if (g_urls[link_id] != undefined) { + url = g_urls[link_id]; + if (g_titles[link_id] != undefined) { + title = g_titles[link_id]; + } + } + else { + if (whole_match.search(/\(\s*\)$/m)>-1) { + // Special case for explicit empty url + url = ""; + } else { + return whole_match; + } + } + } + + url = escapeCharacters(url,"*_"); + var result = ""; + + return result; +} + + +var _DoImages = function(text) { +// +// Turn Markdown image shortcuts into tags. +// + + // + // First, handle reference-style labeled images: ![alt text][id] + // + + /* + text = text.replace(/ + ( // wrap whole match in $1 + !\[ + (.*?) // alt text = $2 + \] + + [ ]? // one optional space + (?:\n[ ]*)? // one optional newline followed by spaces + + \[ + (.*?) // id = $3 + \] + )()()()() // pad rest of backreferences + /g,writeImageTag); + */ + text = text.replace(/(!\[(.*?)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g,writeImageTag); + + // + // Next, handle inline images: ![alt text](url "optional title") + // Don't forget: encode * and _ + + /* + text = text.replace(/ + ( // wrap whole match in $1 + !\[ + (.*?) // alt text = $2 + \] + \s? // One optional whitespace character + \( // literal paren + [ \t]* + () // no id, so leave $3 empty + ? // src url = $4 + [ \t]* + ( // $5 + (['"]) // quote char = $6 + (.*?) // title = $7 + \6 // matching quote + [ \t]* + )? // title is optional + \) + ) + /g,writeImageTag); + */ + text = text.replace(/(!\[(.*?)\]\s?\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,writeImageTag); + + return text; +} + +var writeImageTag = function(wholeMatch,m1,m2,m3,m4,m5,m6,m7) { + var whole_match = m1; + var alt_text = m2; + var link_id = m3.toLowerCase(); + var url = m4; + var title = m7; + + if (!title) title = ""; + + if (url == "") { + if (link_id == "") { + // lower-case and turn embedded newlines into spaces + link_id = alt_text.toLowerCase().replace(/ ?\n/g," "); + } + url = "#"+link_id; + + if (g_urls[link_id] != undefined) { + url = g_urls[link_id]; + if (g_titles[link_id] != undefined) { + title = g_titles[link_id]; + } + } + else { + return whole_match; + } + } + + alt_text = alt_text.replace(/"/g,"""); + url = escapeCharacters(url,"*_"); + var result = "\""' + _RunSpanGamut(m1) + "");}); + + text = text.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm, + function(matchFound,m1){return hashBlock('

    ' + _RunSpanGamut(m1) + "

    ");}); + + // atx-style headers: + // # Header 1 + // ## Header 2 + // ## Header 2 with closing hashes ## + // ... + // ###### Header 6 + // + + /* + text = text.replace(/ + ^(\#{1,6}) // $1 = string of #'s + [ \t]* + (.+?) // $2 = Header text + [ \t]* + \#* // optional closing #'s (not counted) + \n+ + /gm, function() {...}); + */ + + text = text.replace(/^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+/gm, + function(wholeMatch,m1,m2) { + var h_level = m1.length; + return hashBlock("' + _RunSpanGamut(m2) + ""); + }); + + function headerId(m) { + return m.replace(/[^\w]/g, '').toLowerCase(); + } + return text; +} + +// This declaration keeps Dojo compressor from outputting garbage: +var _ProcessListItems; + +var _DoLists = function(text) { +// +// Form HTML ordered (numbered) and unordered (bulleted) lists. +// + + // attacklab: add sentinel to hack around khtml/safari bug: + // http://bugs.webkit.org/show_bug.cgi?id=11231 + text += "~0"; + + // Re-usable pattern to match any entirel ul or ol list: + + /* + var whole_list = / + ( // $1 = whole list + ( // $2 + [ ]{0,3} // attacklab: g_tab_width - 1 + ([*+-]|\d+[.]) // $3 = first list item marker + [ \t]+ + ) + [^\r]+? + ( // $4 + ~0 // sentinel for workaround; should be $ + | + \n{2,} + (?=\S) + (?! // Negative lookahead for another list item marker + [ \t]* + (?:[*+-]|\d+[.])[ \t]+ + ) + ) + )/g + */ + var whole_list = /^(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm; + + if (g_list_level) { + text = text.replace(whole_list,function(wholeMatch,m1,m2) { + var list = m1; + var list_type = (m2.search(/[*+-]/g)>-1) ? "ul" : "ol"; + + // Turn double returns into triple returns, so that we can make a + // paragraph for the last item in a list, if necessary: + list = list.replace(/\n{2,}/g,"\n\n\n");; + var result = _ProcessListItems(list); + + // Trim any trailing whitespace, to put the closing `` + // up on the preceding line, to get it past the current stupid + // HTML block parser. This is a hack to work around the terrible + // hack that is the HTML block parser. + result = result.replace(/\s+$/,""); + result = "<"+list_type+">" + result + "\n"; + return result; + }); + } else { + whole_list = /(\n\n|^\n?)(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/g; + text = text.replace(whole_list,function(wholeMatch,m1,m2,m3) { + var runup = m1; + var list = m2; + + var list_type = (m3.search(/[*+-]/g)>-1) ? "ul" : "ol"; + // Turn double returns into triple returns, so that we can make a + // paragraph for the last item in a list, if necessary: + var list = list.replace(/\n{2,}/g,"\n\n\n");; + var result = _ProcessListItems(list); + result = runup + "<"+list_type+">\n" + result + "\n"; + return result; + }); + } + + // attacklab: strip sentinel + text = text.replace(/~0/,""); + + return text; +} + +_ProcessListItems = function(list_str) { +// +// Process the contents of a single ordered or unordered list, splitting it +// into individual list items. +// + // The $g_list_level global keeps track of when we're inside a list. + // Each time we enter a list, we increment it; when we leave a list, + // we decrement. If it's zero, we're not in a list anymore. + // + // We do this because when we're not inside a list, we want to treat + // something like this: + // + // I recommend upgrading to version + // 8. Oops, now this line is treated + // as a sub-list. + // + // As a single paragraph, despite the fact that the second line starts + // with a digit-period-space sequence. + // + // Whereas when we're inside a list (or sub-list), that line will be + // treated as the start of a sub-list. What a kludge, huh? This is + // an aspect of Markdown's syntax that's hard to parse perfectly + // without resorting to mind-reading. Perhaps the solution is to + // change the syntax rules such that sub-lists must start with a + // starting cardinal number; e.g. "1." or "a.". + + g_list_level++; + + // trim trailing blank lines: + list_str = list_str.replace(/\n{2,}$/,"\n"); + + // attacklab: add sentinel to emulate \z + list_str += "~0"; + + /* + list_str = list_str.replace(/ + (\n)? // leading line = $1 + (^[ \t]*) // leading whitespace = $2 + ([*+-]|\d+[.]) [ \t]+ // list marker = $3 + ([^\r]+? // list item text = $4 + (\n{1,2})) + (?= \n* (~0 | \2 ([*+-]|\d+[.]) [ \t]+)) + /gm, function(){...}); + */ + list_str = list_str.replace(/(\n)?(^[ \t]*)([*+-]|\d+[.])[ \t]+([^\r]+?(\n{1,2}))(?=\n*(~0|\2([*+-]|\d+[.])[ \t]+))/gm, + function(wholeMatch,m1,m2,m3,m4){ + var item = m4; + var leading_line = m1; + var leading_space = m2; + + if (leading_line || (item.search(/\n{2,}/)>-1)) { + item = _RunBlockGamut(_Outdent(item)); + } + else { + // Recursion for sub-lists: + item = _DoLists(_Outdent(item)); + item = item.replace(/\n$/,""); // chomp(item) + item = _RunSpanGamut(item); + } + + return "
  • " + item + "
  • \n"; + } + ); + + // attacklab: strip sentinel + list_str = list_str.replace(/~0/g,""); + + g_list_level--; + return list_str; +} + + +var _DoCodeBlocks = function(text) { +// +// Process Markdown `
    ` blocks.
    +//
    +
    +	/*
    +		text = text.replace(text,
    +			/(?:\n\n|^)
    +			(								// $1 = the code block -- one or more lines, starting with a space/tab
    +				(?:
    +					(?:[ ]{4}|\t)			// Lines must start with a tab or a tab-width of spaces - attacklab: g_tab_width
    +					.*\n+
    +				)+
    +			)
    +			(\n*[ ]{0,3}[^ \t\n]|(?=~0))	// attacklab: g_tab_width
    +		/g,function(){...});
    +	*/
    +
    +	// attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug
    +	text += "~0";
    +
    +	text = text.replace(/(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g,
    +		function(wholeMatch,m1,m2) {
    +			var codeblock = m1;
    +			var nextChar = m2;
    +
    +			codeblock = _EncodeCode( _Outdent(codeblock));
    +			codeblock = _Detab(codeblock);
    +			codeblock = codeblock.replace(/^\n+/g,""); // trim leading newlines
    +			codeblock = codeblock.replace(/\n+$/g,""); // trim trailing whitespace
    +
    +			codeblock = "
    " + codeblock + "\n
    "; + + return hashBlock(codeblock) + nextChar; + } + ); + + // attacklab: strip sentinel + text = text.replace(/~0/,""); + + return text; +}; + +var _DoGithubCodeBlocks = function(text) { +// +// Process Github-style code blocks +// Example: +// ```ruby +// def hello_world(x) +// puts "Hello, #{x}" +// end +// ``` +// + + + // attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug + text += "~0"; + + text = text.replace(/(?:^|\n)```(.*)\n([\s\S]*?)\n```/g, + function(wholeMatch,m1,m2) { + var language = m1; + var codeblock = m2; + + codeblock = _EncodeCode(codeblock); + codeblock = _Detab(codeblock); + codeblock = codeblock.replace(/^\n+/g,""); // trim leading newlines + codeblock = codeblock.replace(/\n+$/g,""); // trim trailing whitespace + + codeblock = "
    " + codeblock + "\n
    "; + + return hashBlock(codeblock); + } + ); + + // attacklab: strip sentinel + text = text.replace(/~0/,""); + + return text; +} + +var hashBlock = function(text) { + text = text.replace(/(^\n+|\n+$)/g,""); + return "\n\n~K" + (g_html_blocks.push(text)-1) + "K\n\n"; +} + +var _DoCodeSpans = function(text) { +// +// * Backtick quotes are used for spans. +// +// * You can use multiple backticks as the delimiters if you want to +// include literal backticks in the code span. So, this input: +// +// Just type ``foo `bar` baz`` at the prompt. +// +// Will translate to: +// +//

    Just type foo `bar` baz at the prompt.

    +// +// There's no arbitrary limit to the number of backticks you +// can use as delimters. If you need three consecutive backticks +// in your code, use four for delimiters, etc. +// +// * You can use spaces to get literal backticks at the edges: +// +// ... type `` `bar` `` ... +// +// Turns to: +// +// ... type `bar` ... +// + + /* + text = text.replace(/ + (^|[^\\]) // Character before opening ` can't be a backslash + (`+) // $2 = Opening run of ` + ( // $3 = The code block + [^\r]*? + [^`] // attacklab: work around lack of lookbehind + ) + \2 // Matching closer + (?!`) + /gm, function(){...}); + */ + + text = text.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm, + function(wholeMatch,m1,m2,m3,m4) { + var c = m3; + c = c.replace(/^([ \t]*)/g,""); // leading whitespace + c = c.replace(/[ \t]*$/g,""); // trailing whitespace + c = _EncodeCode(c); + return m1+""+c+""; + }); + + return text; +} + +var _EncodeCode = function(text) { +// +// Encode/escape certain characters inside Markdown code runs. +// The point is that in code, these characters are literals, +// and lose their special Markdown meanings. +// + // Encode all ampersands; HTML entities are not + // entities within a Markdown code span. + text = text.replace(/&/g,"&"); + + // Do the angle bracket song and dance: + text = text.replace(//g,">"); + + // Now, escape characters that are magic in Markdown: + text = escapeCharacters(text,"\*_{}[]\\",false); + +// jj the line above breaks this: +//--- + +//* Item + +// 1. Subitem + +// special char: * +//--- + + return text; +} + + +var _DoItalicsAndBold = function(text) { + + // must go first: + text = text.replace(/(\*\*|__)(?=\S)([^\r]*?\S[*_]*)\1/g, + "$2"); + + text = text.replace(/(\*|_)(?=\S)([^\r]*?\S)\1/g, + "$2"); + + return text; +} + + +var _DoBlockQuotes = function(text) { + + /* + text = text.replace(/ + ( // Wrap whole match in $1 + ( + ^[ \t]*>[ \t]? // '>' at the start of a line + .+\n // rest of the first line + (.+\n)* // subsequent consecutive lines + \n* // blanks + )+ + ) + /gm, function(){...}); + */ + + text = text.replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm, + function(wholeMatch,m1) { + var bq = m1; + + // attacklab: hack around Konqueror 3.5.4 bug: + // "----------bug".replace(/^-/g,"") == "bug" + + bq = bq.replace(/^[ \t]*>[ \t]?/gm,"~0"); // trim one level of quoting + + // attacklab: clean up hack + bq = bq.replace(/~0/g,""); + + bq = bq.replace(/^[ \t]+$/gm,""); // trim whitespace-only lines + bq = _RunBlockGamut(bq); // recurse + + bq = bq.replace(/(^|\n)/g,"$1 "); + // These leading spaces screw with
     content, so we need to fix that:
    +			bq = bq.replace(
    +					/(\s*
    [^\r]+?<\/pre>)/gm,
    +				function(wholeMatch,m1) {
    +					var pre = m1;
    +					// attacklab: hack around Konqueror 3.5.4 bug:
    +					pre = pre.replace(/^  /mg,"~0");
    +					pre = pre.replace(/~0/g,"");
    +					return pre;
    +				});
    +
    +			return hashBlock("
    \n" + bq + "\n
    "); + }); + return text; +} + + +var _FormParagraphs = function(text) { +// +// Params: +// $text - string to process with html

    tags +// + + // Strip leading and trailing lines: + text = text.replace(/^\n+/g,""); + text = text.replace(/\n+$/g,""); + + var grafs = text.split(/\n{2,}/g); + var grafsOut = []; + + // + // Wrap

    tags. + // + var end = grafs.length; + for (var i=0; i= 0) { + grafsOut.push(str); + } + else if (str.search(/\S/) >= 0) { + str = _RunSpanGamut(str); + str = str.replace(/^([ \t]*)/g,"

    "); + str += "

    " + grafsOut.push(str); + } + + } + + // + // Unhashify HTML blocks + // + end = grafsOut.length; + for (var i=0; i= 0) { + var blockText = g_html_blocks[RegExp.$1]; + blockText = blockText.replace(/\$/g,"$$$$"); // Escape any dollar signs + grafsOut[i] = grafsOut[i].replace(/~K\d+K/,blockText); + } + } + + return grafsOut.join("\n\n"); +} + + +var _EncodeAmpsAndAngles = function(text) { +// Smart processing for ampersands and angle brackets that need to be encoded. + + // Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin: + // http://bumppo.net/projects/amputator/ + text = text.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g,"&"); + + // Encode naked <'s + text = text.replace(/<(?![a-z\/?\$!])/gi,"<"); + + return text; +} + + +var _EncodeBackslashEscapes = function(text) { +// +// Parameter: String. +// Returns: The string, with after processing the following backslash +// escape sequences. +// + + // attacklab: The polite way to do this is with the new + // escapeCharacters() function: + // + // text = escapeCharacters(text,"\\",true); + // text = escapeCharacters(text,"`*_{}[]()>#+-.!",true); + // + // ...but we're sidestepping its use of the (slow) RegExp constructor + // as an optimization for Firefox. This function gets called a LOT. + + text = text.replace(/\\(\\)/g,escapeCharacters_callback); + text = text.replace(/\\([`*_{}\[\]()>#+-.!])/g,escapeCharacters_callback); + return text; +} + + +var _DoAutoLinks = function(text) { + + text = text.replace(/<((https?|ftp|dict):[^'">\s]+)>/gi,"
    $1"); + + // Email addresses: + + /* + text = text.replace(/ + < + (?:mailto:)? + ( + [-.\w]+ + \@ + [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+ + ) + > + /gi, _DoAutoLinks_callback()); + */ + text = text.replace(/<(?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi, + function(wholeMatch,m1) { + return _EncodeEmailAddress( _UnescapeSpecialChars(m1) ); + } + ); + + return text; +} + + +var _EncodeEmailAddress = function(addr) { +// +// Input: an email address, e.g. "foo@example.com" +// +// Output: the email address as a mailto link, with each character +// of the address encoded as either a decimal or hex entity, in +// the hopes of foiling most address harvesting spam bots. E.g.: +// +// foo +// @example.com +// +// Based on a filter by Matthew Wickline, posted to the BBEdit-Talk +// mailing list: +// + + var encode = [ + function(ch){return "&#"+ch.charCodeAt(0)+";";}, + function(ch){return "&#x"+ch.charCodeAt(0).toString(16)+";";}, + function(ch){return ch;} + ]; + + addr = "mailto:" + addr; + + addr = addr.replace(/./g, function(ch) { + if (ch == "@") { + // this *must* be encoded. I insist. + ch = encode[Math.floor(Math.random()*2)](ch); + } else if (ch !=":") { + // leave ':' alone (to spot mailto: later) + var r = Math.random(); + // roughly 10% raw, 45% hex, 45% dec + ch = ( + r > .9 ? encode[2](ch) : + r > .45 ? encode[1](ch) : + encode[0](ch) + ); + } + return ch; + }); + + addr = "" + addr + ""; + addr = addr.replace(/">.+:/g,"\">"); // strip the mailto: from the visible part + + return addr; +} + + +var _UnescapeSpecialChars = function(text) { +// +// Swap back in all the special characters we've hidden. +// + text = text.replace(/~E(\d+)E/g, + function(wholeMatch,m1) { + var charCodeToReplace = parseInt(m1); + return String.fromCharCode(charCodeToReplace); + } + ); + return text; +} + + +var _Outdent = function(text) { +// +// Remove one level of line-leading tabs or spaces +// + + // attacklab: hack around Konqueror 3.5.4 bug: + // "----------bug".replace(/^-/g,"") == "bug" + + text = text.replace(/^(\t|[ ]{1,4})/gm,"~0"); // attacklab: g_tab_width + + // attacklab: clean up hack + text = text.replace(/~0/g,"") + + return text; +} + +var _Detab = function(text) { +// attacklab: Detab's completely rewritten for speed. +// In perl we could fix it by anchoring the regexp with \G. +// In javascript we're less fortunate. + + // expand first n-1 tabs + text = text.replace(/\t(?=\t)/g," "); // attacklab: g_tab_width + + // replace the nth with two sentinels + text = text.replace(/\t/g,"~A~B"); + + // use the sentinel to anchor our regex so it doesn't explode + text = text.replace(/~B(.+?)~A/g, + function(wholeMatch,m1,m2) { + var leadingText = m1; + var numSpaces = 4 - leadingText.length % 4; // attacklab: g_tab_width + + // there *must* be a better way to do this: + for (var i=0; i} a_fn + */ +, runPluginCallbacks = function (Instance, a_fn) { + if ($.isArray(a_fn)) + for (var i=0, c=a_fn.length; i').appendTo("body"); + var d = { width: $c.css("width") - $c[0].clientWidth, height: $c.height() - $c[0].clientHeight }; + $c.remove(); + window.scrollbarWidth = d.width; + window.scrollbarHeight = d.height; + return dim.match(/^(width|height)$/) ? d[dim] : d; + } + + + /** + * Returns hash container 'display' and 'visibility' + * + * @see $.swap() - swaps CSS, runs callback, resets CSS + * @param {!Object} $E jQuery element + * @param {boolean=} [force=false] Run even if display != none + * @return {!Object} Returns current style props, if applicable + */ +, showInvisibly: function ($E, force) { + if ($E && $E.length && (force || $E.css("display") === "none")) { // only if not *already hidden* + var s = $E[0].style + // save ONLY the 'style' props because that is what we must restore + , CSS = { display: s.display || '', visibility: s.visibility || '' }; + // show element 'invisibly' so can be measured + $E.css({ display: "block", visibility: "hidden" }); + return CSS; + } + return {}; + } + + /** + * Returns data for setting size of an element (container or a pane). + * + * @see _create(), onWindowResize() for container, plus others for pane + * @return JSON Returns a hash of all dimensions: top, bottom, left, right, outerWidth, innerHeight, etc + */ +, getElementDimensions: function ($E, inset) { + var + // dimensions hash - start with current data IF passed + d = { css: {}, inset: {} } + , x = d.css // CSS hash + , i = { bottom: 0 } // TEMP insets (bottom = complier hack) + , N = $.layout.cssNum + , off = $E.offset() + , b, p, ei // TEMP border, padding + ; + d.offsetLeft = off.left; + d.offsetTop = off.top; + + if (!inset) inset = {}; // simplify logic below + + $.each("Left,Right,Top,Bottom".split(","), function (idx, e) { // e = edge + b = x["border" + e] = $.layout.borderWidth($E, e); + p = x["padding"+ e] = $.layout.cssNum($E, "padding"+e); + ei = e.toLowerCase(); + d.inset[ei] = inset[ei] >= 0 ? inset[ei] : p; // any missing insetX value = paddingX + i[ei] = d.inset[ei] + b; // total offset of content from outer side + }); + + x.width = $E.width(); + x.height = $E.height(); + x.top = N($E,"top",true); + x.bottom = N($E,"bottom",true); + x.left = N($E,"left",true); + x.right = N($E,"right",true); + + d.outerWidth = $E.outerWidth(); + d.outerHeight = $E.outerHeight(); + // calc the TRUE inner-dimensions, even in quirks-mode! + d.innerWidth = max(0, d.outerWidth - i.left - i.right); + d.innerHeight = max(0, d.outerHeight - i.top - i.bottom); + // layoutWidth/Height is used in calcs for manual resizing + // layoutW/H only differs from innerW/H when in quirks-mode - then is like outerW/H + d.layoutWidth = $E.innerWidth(); + d.layoutHeight = $E.innerHeight(); + + //if ($E.prop('tagName') === 'BODY') { debugData( d, $E.prop('tagName') ); } // DEBUG + + //d.visible = $E.is(":visible");// && x.width > 0 && x.height > 0; + + return d; + } + +, getElementStyles: function ($E, list) { + var + CSS = {} + , style = $E[0].style + , props = list.split(",") + , sides = "Top,Bottom,Left,Right".split(",") + , attrs = "Color,Style,Width".split(",") + , p, s, a, i, j, k + ; + for (i=0; i < props.length; i++) { + p = props[i]; + if (p.match(/(border|padding|margin)$/)) + for (j=0; j < 4; j++) { + s = sides[j]; + if (p === "border") + for (k=0; k < 3; k++) { + a = attrs[k]; + CSS[p+s+a] = style[p+s+a]; + } + else + CSS[p+s] = style[p+s]; + } + else + CSS[p] = style[p]; + }; + return CSS + } + + /** + * Return the innerWidth for the current browser/doctype + * + * @see initPanes(), sizeMidPanes(), initHandles(), sizeHandles() + * @param {Array.} $E Must pass a jQuery object - first element is processed + * @param {number=} outerWidth (optional) Can pass a width, allowing calculations BEFORE element is resized + * @return {number} Returns the innerWidth of the elem by subtracting padding and borders + */ +, cssWidth: function ($E, outerWidth) { + // a 'calculated' outerHeight can be passed so borders and/or padding are removed if needed + if (outerWidth <= 0) return 0; + + var bs = !$.layout.browser.boxModel ? "border-box" : $.support.boxSizing ? $E.css("boxSizing") : "content-box" + , b = $.layout.borderWidth + , n = $.layout.cssNum + , W = outerWidth + ; + // strip border and/or padding from outerWidth to get CSS Width + if (bs !== "border-box") + W -= (b($E, "Left") + b($E, "Right")); + if (bs === "content-box") + W -= (n($E, "paddingLeft") + n($E, "paddingRight")); + return max(0,W); + } + + /** + * Return the innerHeight for the current browser/doctype + * + * @see initPanes(), sizeMidPanes(), initHandles(), sizeHandles() + * @param {Array.} $E Must pass a jQuery object - first element is processed + * @param {number=} outerHeight (optional) Can pass a width, allowing calculations BEFORE element is resized + * @return {number} Returns the innerHeight of the elem by subtracting padding and borders + */ +, cssHeight: function ($E, outerHeight) { + // a 'calculated' outerHeight can be passed so borders and/or padding are removed if needed + if (outerHeight <= 0) return 0; + + var bs = !$.layout.browser.boxModel ? "border-box" : $.support.boxSizing ? $E.css("boxSizing") : "content-box" + , b = $.layout.borderWidth + , n = $.layout.cssNum + , H = outerHeight + ; + // strip border and/or padding from outerHeight to get CSS Height + if (bs !== "border-box") + H -= (b($E, "Top") + b($E, "Bottom")); + if (bs === "content-box") + H -= (n($E, "paddingTop") + n($E, "paddingBottom")); + return max(0,H); + } + + /** + * Returns the 'current CSS numeric value' for a CSS property - 0 if property does not exist + * + * @see Called by many methods + * @param {Array.} $E Must pass a jQuery object - first element is processed + * @param {string} prop The name of the CSS property, eg: top, width, etc. + * @param {boolean=} [allowAuto=false] true = return 'auto' if that is value; false = return 0 + * @return {(string|number)} Usually used to get an integer value for position (top, left) or size (height, width) + */ +, cssNum: function ($E, prop, allowAuto) { + if (!$E.jquery) $E = $($E); + var CSS = $.layout.showInvisibly($E) + , p = $.css($E[0], prop, true) + , v = allowAuto && p=="auto" ? p : Math.round(parseFloat(p) || 0); + $E.css( CSS ); // RESET + return v; + } + +, borderWidth: function (el, side) { + if (el.jquery) el = el[0]; + var b = "border"+ side.substr(0,1).toUpperCase() + side.substr(1); // left => Left + return $.css(el, b+"Style", true) === "none" ? 0 : Math.round(parseFloat($.css(el, b+"Width", true)) || 0); + } + + /** + * Mouse-tracking utility - FUTURE REFERENCE + * + * init: if (!window.mouse) { + * window.mouse = { x: 0, y: 0 }; + * $(document).mousemove( $.layout.trackMouse ); + * } + * + * @param {Object} evt + * +, trackMouse: function (evt) { + window.mouse = { x: evt.clientX, y: evt.clientY }; + } + */ + + /** + * SUBROUTINE for preventPrematureSlideClose option + * + * @param {Object} evt + * @param {Object=} el + */ +, isMouseOverElem: function (evt, el) { + var + $E = $(el || this) + , d = $E.offset() + , T = d.top + , L = d.left + , R = L + $E.outerWidth() + , B = T + $E.outerHeight() + , x = evt.pageX // evt.clientX ? + , y = evt.pageY // evt.clientY ? + ; + // if X & Y are < 0, probably means is over an open SELECT + return ($.layout.browser.msie && x < 0 && y < 0) || ((x >= L && x <= R) && (y >= T && y <= B)); + } + + /** + * Message/Logging Utility + * + * @example $.layout.msg("My message"); // log text + * @example $.layout.msg("My message", true); // alert text + * @example $.layout.msg({ foo: "bar" }, "Title"); // log hash-data, with custom title + * @example $.layout.msg({ foo: "bar" }, true, "Title", { sort: false }); -OR- + * @example $.layout.msg({ foo: "bar" }, "Title", { sort: false, display: true }); // alert hash-data + * + * @param {(Object|string)} info String message OR Hash/Array + * @param {(Boolean|string|Object)=} [popup=false] True means alert-box - can be skipped + * @param {(Object|string)=} [debugTitle=""] Title for Hash data - can be skipped + * @param {Object=} [debugOpts] Extra options for debug output + */ +, msg: function (info, popup, debugTitle, debugOpts) { + if ($.isPlainObject(info) && window.debugData) { + if (typeof popup === "string") { + debugOpts = debugTitle; + debugTitle = popup; + } + else if (typeof debugTitle === "object") { + debugOpts = debugTitle; + debugTitle = null; + } + var t = debugTitle || "log( )" + , o = $.extend({ sort: false, returnHTML: false, display: false }, debugOpts); + if (popup === true || o.display) + debugData( info, t, o ); + else if (window.console) + console.log(debugData( info, t, o )); + } + else if (popup) + alert(info); + else if (window.console) + console.log(info); + else { + var id = "#layoutLogger" + , $l = $(id); + if (!$l.length) + $l = createLog(); + $l.children("ul").append('
  • '+ info.replace(/\/g,">") +'
  • '); + } + + function createLog () { + var pos = $.support.fixedPosition ? 'fixed' : 'absolute' + , $e = $('
    ' + + '
    ' + + 'XLayout console.log
    ' + + '
      ' + + '
      ' + ).appendTo("body"); + $e.css('left', $(window).width() - $e.outerWidth() - 5) + if ($.ui.draggable) $e.draggable({ handle: ':first-child' }); + return $e; + }; + } + +}; + + +/* + * $.layout.browser REPLACES removed $.browser, with extra data + * Parsing code here adapted from jQuery 1.8 $.browse + */ +var u = navigator.userAgent.toLowerCase() +, m = /(chrome)[ \/]([\w.]+)/.exec( u ) + || /(webkit)[ \/]([\w.]+)/.exec( u ) + || /(opera)(?:.*version|)[ \/]([\w.]+)/.exec( u ) + || /(msie) ([\w.]+)/.exec( u ) + || u.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec( u ) + || [] +, b = m[1] || "" +, v = m[2] || 0 +, ie = b === "msie" +; +$.layout.browser = { + version: v +, safari: b === "webkit" // webkit (NOT chrome) = safari +, webkit: b === "chrome" // chrome = webkit +, msie: ie +, isIE6: ie && v == 6 + // ONLY IE reverts to old box-model - update for older jQ onReady +, boxModel: !ie || $.support.boxModel !== false +}; +if (b) $.layout.browser[b] = true; // set CURRENT browser +/* OLD versions of jQuery only set $.support.boxModel after page is loaded + * so if this is IE, use support.boxModel to test for quirks-mode (ONLY IE changes boxModel) */ +if (ie) $(function(){ $.layout.browser.boxModel = $.support.boxModel; }); + + +// DEFAULT OPTIONS +$.layout.defaults = { +/* + * LAYOUT & LAYOUT-CONTAINER OPTIONS + * - none of these options are applicable to individual panes + */ + name: "" // Not required, but useful for buttons and used for the state-cookie +, containerClass: "ui-layout-container" // layout-container element +, inset: null // custom container-inset values (override padding) +, scrollToBookmarkOnLoad: true // after creating a layout, scroll to bookmark in URL (.../page.htm#myBookmark) +, resizeWithWindow: true // bind thisLayout.resizeAll() to the window.resize event +, resizeWithWindowDelay: 200 // delay calling resizeAll because makes window resizing very jerky +, resizeWithWindowMaxDelay: 0 // 0 = none - force resize every XX ms while window is being resized +, maskPanesEarly: false // true = create pane-masks on resizer.mouseDown instead of waiting for resizer.dragstart +, onresizeall_start: null // CALLBACK when resizeAll() STARTS - NOT pane-specific +, onresizeall_end: null // CALLBACK when resizeAll() ENDS - NOT pane-specific +, onload_start: null // CALLBACK when Layout inits - after options initialized, but before elements +, onload_end: null // CALLBACK when Layout inits - after EVERYTHING has been initialized +, onunload_start: null // CALLBACK when Layout is destroyed OR onWindowUnload +, onunload_end: null // CALLBACK when Layout is destroyed OR onWindowUnload +, initPanes: true // false = DO NOT initialize the panes onLoad - will init later +, showErrorMessages: true // enables fatal error messages to warn developers of common errors +, showDebugMessages: false // display console-and-alert debug msgs - IF this Layout version _has_ debugging code! +// Changing this zIndex value will cause other zIndex values to automatically change +, zIndex: null // the PANE zIndex - resizers and masks will be +1 +// DO NOT CHANGE the zIndex values below unless you clearly understand their relationships +, zIndexes: { // set _default_ z-index values here... + pane_normal: 0 // normal z-index for panes + , content_mask: 1 // applied to overlays used to mask content INSIDE panes during resizing + , resizer_normal: 2 // normal z-index for resizer-bars + , pane_sliding: 100 // applied to *BOTH* the pane and its resizer when a pane is 'slid open' + , pane_animate: 1000 // applied to the pane when being animated - not applied to the resizer + , resizer_drag: 10000 // applied to the CLONED resizer-bar when being 'dragged' + } +, errors: { + pane: "pane" // description of "layout pane element" - used only in error messages + , selector: "selector" // description of "jQuery-selector" - used only in error messages + , addButtonError: "Error Adding Button\nInvalid " + , containerMissing: "UI Layout Initialization Error\nThe specified layout-container does not exist." + , centerPaneMissing: "UI Layout Initialization Error\nThe center-pane element does not exist.\nThe center-pane is a required element." + , noContainerHeight: "UI Layout Initialization Warning\nThe layout-container \"CONTAINER\" has no height.\nTherefore the layout is 0-height and hence 'invisible'!" + , callbackError: "UI Layout Callback Error\nThe EVENT callback is not a valid function." + } +/* + * PANE DEFAULT SETTINGS + * - settings under the 'panes' key become the default settings for *all panes* + * - ALL pane-options can also be set specifically for each panes, which will override these 'default values' + */ +, panes: { // default options for 'all panes' - will be overridden by 'per-pane settings' + applyDemoStyles: false // NOTE: renamed from applyDefaultStyles for clarity + , closable: true // pane can open & close + , resizable: true // when open, pane can be resized + , slidable: true // when closed, pane can 'slide open' over other panes - closes on mouse-out + , initClosed: false // true = init pane as 'closed' + , initHidden: false // true = init pane as 'hidden' - no resizer-bar/spacing + // SELECTORS + //, paneSelector: "" // MUST be pane-specific - jQuery selector for pane + , contentSelector: ".ui-layout-content" // INNER div/element to auto-size so only it scrolls, not the entire pane! + , contentIgnoreSelector: ".ui-layout-ignore" // element(s) to 'ignore' when measuring 'content' + , findNestedContent: false // true = $P.find(contentSelector), false = $P.children(contentSelector) + // GENERIC ROOT-CLASSES - for auto-generated classNames + , paneClass: "ui-layout-pane" // Layout Pane + , resizerClass: "ui-layout-resizer" // Resizer Bar + , togglerClass: "ui-layout-toggler" // Toggler Button + , buttonClass: "ui-layout-button" // CUSTOM Buttons - eg: '[ui-layout-button]-toggle/-open/-close/-pin' + // ELEMENT SIZE & SPACING + //, size: 100 // MUST be pane-specific -initial size of pane + , minSize: 0 // when manually resizing a pane + , maxSize: 0 // ditto, 0 = no limit + , spacing_open: 6 // space between pane and adjacent panes - when pane is 'open' + , spacing_closed: 6 // ditto - when pane is 'closed' + , togglerLength_open: 50 // Length = WIDTH of toggler button on north/south sides - HEIGHT on east/west sides + , togglerLength_closed: 50 // 100% OR -1 means 'full height/width of resizer bar' - 0 means 'hidden' + , togglerAlign_open: "center" // top/left, bottom/right, center, OR... + , togglerAlign_closed: "center" // 1 => nn = offset from top/left, -1 => -nn == offset from bottom/right + , togglerContent_open: "" // text or HTML to put INSIDE the toggler + , togglerContent_closed: "" // ditto + // RESIZING OPTIONS + , resizerDblClickToggle: true // + , autoResize: true // IF size is 'auto' or a percentage, then recalc 'pixel size' whenever the layout resizes + , autoReopen: true // IF a pane was auto-closed due to noRoom, reopen it when there is room? False = leave it closed + , resizerDragOpacity: 1 // option for ui.draggable + //, resizerCursor: "" // MUST be pane-specific - cursor when over resizer-bar + , maskContents: false // true = add DIV-mask over-or-inside this pane so can 'drag' over IFRAMES + , maskObjects: false // true = add IFRAME-mask over-or-inside this pane to cover objects/applets - content-mask will overlay this mask + , maskZindex: null // will override zIndexes.content_mask if specified - not applicable to iframe-panes + , resizingGrid: false // grid size that the resizers will snap-to during resizing, eg: [20,20] + , livePaneResizing: false // true = LIVE Resizing as resizer is dragged + , liveContentResizing: false // true = re-measure header/footer heights as resizer is dragged + , liveResizingTolerance: 1 // how many px change before pane resizes, to control performance + // SLIDING OPTIONS + , sliderCursor: "pointer" // cursor when resizer-bar will trigger 'sliding' + , slideTrigger_open: "click" // click, dblclick, mouseenter + , slideTrigger_close: "mouseleave"// click, mouseleave + , slideDelay_open: 300 // applies only for mouseenter event - 0 = instant open + , slideDelay_close: 300 // applies only for mouseleave event (300ms is the minimum!) + , hideTogglerOnSlide: false // when pane is slid-open, should the toggler show? + , preventQuickSlideClose: $.layout.browser.webkit // Chrome triggers slideClosed as it is opening + , preventPrematureSlideClose: false // handle incorrect mouseleave trigger, like when over a SELECT-list in IE + // PANE-SPECIFIC TIPS & MESSAGES + , tips: { + Open: "Open" // eg: "Open Pane" + , Close: "Close" + , Resize: "Resize" + , Slide: "Slide Open" + , Pin: "Pin" + , Unpin: "Un-Pin" + , noRoomToOpen: "Not enough room to show this panel." // alert if user tries to open a pane that cannot + , minSizeWarning: "Panel has reached its minimum size" // displays in browser statusbar + , maxSizeWarning: "Panel has reached its maximum size" // ditto + } + // HOT-KEYS & MISC + , showOverflowOnHover: false // will bind allowOverflow() utility to pane.onMouseOver + , enableCursorHotkey: true // enabled 'cursor' hotkeys + //, customHotkey: "" // MUST be pane-specific - EITHER a charCode OR a character + , customHotkeyModifier: "SHIFT" // either 'SHIFT', 'CTRL' or 'CTRL+SHIFT' - NOT 'ALT' + // PANE ANIMATION + // NOTE: fxSss_open, fxSss_close & fxSss_size options (eg: fxName_open) are auto-generated if not passed + , fxName: "slide" // ('none' or blank), slide, drop, scale -- only relevant to 'open' & 'close', NOT 'size' + , fxSpeed: null // slow, normal, fast, 200, nnn - if passed, will OVERRIDE fxSettings.duration + , fxSettings: {} // can be passed, eg: { easing: "easeOutBounce", duration: 1500 } + , fxOpacityFix: true // tries to fix opacity in IE to restore anti-aliasing after animation + , animatePaneSizing: false // true = animate resizing after dragging resizer-bar OR sizePane() is called + /* NOTE: Action-specific FX options are auto-generated from the options above if not specifically set: + fxName_open: "slide" // 'Open' pane animation + fnName_close: "slide" // 'Close' pane animation + fxName_size: "slide" // 'Size' pane animation - when animatePaneSizing = true + fxSpeed_open: null + fxSpeed_close: null + fxSpeed_size: null + fxSettings_open: {} + fxSettings_close: {} + fxSettings_size: {} + */ + // CHILD/NESTED LAYOUTS + , children: null // Layout-options for nested/child layout - even {} is valid as options + , containerSelector: '' // if child is NOT 'directly nested', a selector to find it/them (can have more than one child layout!) + , initChildren: true // true = child layout will be created as soon as _this_ layout completes initialization + , destroyChildren: true // true = destroy child-layout if this pane is destroyed + , resizeChildren: true // true = trigger child-layout.resizeAll() when this pane is resized + // EVENT TRIGGERING + , triggerEventsOnLoad: false // true = trigger onopen OR onclose callbacks when layout initializes + , triggerEventsDuringLiveResize: true // true = trigger onresize callback REPEATEDLY if livePaneResizing==true + // PANE CALLBACKS + , onshow_start: null // CALLBACK when pane STARTS to Show - BEFORE onopen/onhide_start + , onshow_end: null // CALLBACK when pane ENDS being Shown - AFTER onopen/onhide_end + , onhide_start: null // CALLBACK when pane STARTS to Close - BEFORE onclose_start + , onhide_end: null // CALLBACK when pane ENDS being Closed - AFTER onclose_end + , onopen_start: null // CALLBACK when pane STARTS to Open + , onopen_end: null // CALLBACK when pane ENDS being Opened + , onclose_start: null // CALLBACK when pane STARTS to Close + , onclose_end: null // CALLBACK when pane ENDS being Closed + , onresize_start: null // CALLBACK when pane STARTS being Resized ***FOR ANY REASON*** + , onresize_end: null // CALLBACK when pane ENDS being Resized ***FOR ANY REASON*** + , onsizecontent_start: null // CALLBACK when sizing of content-element STARTS + , onsizecontent_end: null // CALLBACK when sizing of content-element ENDS + , onswap_start: null // CALLBACK when pane STARTS to Swap + , onswap_end: null // CALLBACK when pane ENDS being Swapped + , ondrag_start: null // CALLBACK when pane STARTS being ***MANUALLY*** Resized + , ondrag_end: null // CALLBACK when pane ENDS being ***MANUALLY*** Resized + } +/* + * PANE-SPECIFIC SETTINGS + * - options listed below MUST be specified per-pane - they CANNOT be set under 'panes' + * - all options under the 'panes' key can also be set specifically for any pane + * - most options under the 'panes' key apply only to 'border-panes' - NOT the the center-pane + */ +, north: { + paneSelector: ".ui-layout-north" + , size: "auto" // eg: "auto", "30%", .30, 200 + , resizerCursor: "n-resize" // custom = url(myCursor.cur) + , customHotkey: "" // EITHER a charCode (43) OR a character ("o") + } +, south: { + paneSelector: ".ui-layout-south" + , size: "auto" + , resizerCursor: "s-resize" + , customHotkey: "" + } +, east: { + paneSelector: ".ui-layout-east" + , size: 200 + , resizerCursor: "e-resize" + , customHotkey: "" + } +, west: { + paneSelector: ".ui-layout-west" + , size: 200 + , resizerCursor: "w-resize" + , customHotkey: "" + } +, center: { + paneSelector: ".ui-layout-center" + , minWidth: 0 + , minHeight: 0 + } +}; + +$.layout.optionsMap = { + // layout/global options - NOT pane-options + layout: ("name,instanceKey,stateManagement,effects,inset,zIndexes,errors," + + "zIndex,scrollToBookmarkOnLoad,showErrorMessages,maskPanesEarly," + + "outset,resizeWithWindow,resizeWithWindowDelay,resizeWithWindowMaxDelay," + + "onresizeall,onresizeall_start,onresizeall_end,onload,onload_start,onload_end,onunload,onunload_start,onunload_end").split(",") +// borderPanes: [ ALL options that are NOT specified as 'layout' ] + // default.panes options that apply to the center-pane (most options apply _only_ to border-panes) +, center: ("paneClass,contentSelector,contentIgnoreSelector,findNestedContent,applyDemoStyles,triggerEventsOnLoad," + + "showOverflowOnHover,maskContents,maskObjects,liveContentResizing," + + "containerSelector,children,initChildren,resizeChildren,destroyChildren," + + "onresize,onresize_start,onresize_end,onsizecontent,onsizecontent_start,onsizecontent_end").split(",") + // options that MUST be specifically set 'per-pane' - CANNOT set in the panes (defaults) key +, noDefault: ("paneSelector,resizerCursor,customHotkey").split(",") +}; + +/** + * Processes options passed in converts flat-format data into subkey (JSON) format + * In flat-format, subkeys are _currently_ separated with 2 underscores, like north__optName + * Plugins may also call this method so they can transform their own data + * + * @param {!Object} hash Data/options passed by user - may be a single level or nested levels + * @param {boolean=} [addKeys=false] Should the primary layout.options keys be added if they do not exist? + * @return {Object} Returns hash of minWidth & minHeight + */ +$.layout.transformData = function (hash, addKeys) { + var json = addKeys ? { panes: {}, center: {} } : {} // init return object + , branch, optKey, keys, key, val, i, c; + + if (typeof hash !== "object") return json; // no options passed + + // convert all 'flat-keys' to 'sub-key' format + for (optKey in hash) { + branch = json; + val = hash[ optKey ]; + keys = optKey.split("__"); // eg: west__size or north__fxSettings__duration + c = keys.length - 1; + // convert underscore-delimited to subkeys + for (i=0; i <= c; i++) { + key = keys[i]; + if (i === c) { // last key = value + if ($.isPlainObject( val )) + branch[key] = $.layout.transformData( val ); // RECURSE + else + branch[key] = val; + } + else { + if (!branch[key]) + branch[key] = {}; // create the subkey + // recurse to sub-key for next loop - if not done + branch = branch[key]; + } + } + } + return json; +}; + +// INTERNAL CONFIG DATA - DO NOT CHANGE THIS! +$.layout.backwardCompatibility = { + // data used by renameOldOptions() + map: { + // OLD Option Name: NEW Option Name + applyDefaultStyles: "applyDemoStyles" + // CHILD/NESTED LAYOUTS + , childOptions: "children" + , initChildLayout: "initChildren" + , destroyChildLayout: "destroyChildren" + , resizeChildLayout: "resizeChildren" + , resizeNestedLayout: "resizeChildren" + // MISC Options + , resizeWhileDragging: "livePaneResizing" + , resizeContentWhileDragging: "liveContentResizing" + , triggerEventsWhileDragging: "triggerEventsDuringLiveResize" + , maskIframesOnResize: "maskContents" + // STATE MANAGEMENT + , useStateCookie: "stateManagement.enabled" + , "cookie.autoLoad": "stateManagement.autoLoad" + , "cookie.autoSave": "stateManagement.autoSave" + , "cookie.keys": "stateManagement.stateKeys" + , "cookie.name": "stateManagement.cookie.name" + , "cookie.domain": "stateManagement.cookie.domain" + , "cookie.path": "stateManagement.cookie.path" + , "cookie.expires": "stateManagement.cookie.expires" + , "cookie.secure": "stateManagement.cookie.secure" + // OLD Language options + , noRoomToOpenTip: "tips.noRoomToOpen" + , togglerTip_open: "tips.Close" // open = Close + , togglerTip_closed: "tips.Open" // closed = Open + , resizerTip: "tips.Resize" + , sliderTip: "tips.Slide" + } + +/** +* @param {Object} opts +*/ +, renameOptions: function (opts) { + var map = $.layout.backwardCompatibility.map + , oldData, newData, value + ; + for (var itemPath in map) { + oldData = getBranch( itemPath ); + value = oldData.branch[ oldData.key ]; + if (value !== undefined) { + newData = getBranch( map[itemPath], true ); + newData.branch[ newData.key ] = value; + delete oldData.branch[ oldData.key ]; + } + } + + /** + * @param {string} path + * @param {boolean=} [create=false] Create path if does not exist + */ + function getBranch (path, create) { + var a = path.split(".") // split keys into array + , c = a.length - 1 + , D = { branch: opts, key: a[c] } // init branch at top & set key (last item) + , i = 0, k, undef; + for (; i 0) { + if (autoHide && $E.data('autoHidden') && $E.innerHeight() > 0) { + $E.show().data('autoHidden', false); + if (!browser.mozilla) // FireFox refreshes iframes - IE does not + // make hidden, then visible to 'refresh' display after animation + $E.css(_c.hidden).css(_c.visible); + } + } + else if (autoHide && !$E.data('autoHidden')) + $E.hide().data('autoHidden', true); + } + + /** + * @param {(string|!Object)} el + * @param {number=} outerHeight + * @param {boolean=} [autoHide=false] + */ +, setOuterHeight = function (el, outerHeight, autoHide) { + var $E = el, h; + if (isStr(el)) $E = $Ps[el]; // west + else if (!el.jquery) $E = $(el); + h = cssH($E, outerHeight); + $E.css({ height: h, visibility: "visible" }); // may have been 'hidden' by sizeContent + if (h > 0 && $E.innerWidth() > 0) { + if (autoHide && $E.data('autoHidden')) { + $E.show().data('autoHidden', false); + if (!browser.mozilla) // FireFox refreshes iframes - IE does not + $E.css(_c.hidden).css(_c.visible); + } + } + else if (autoHide && !$E.data('autoHidden')) + $E.hide().data('autoHidden', true); + } + + + /** + * Converts any 'size' params to a pixel/integer size, if not already + * If 'auto' or a decimal/percentage is passed as 'size', a pixel-size is calculated + * + /** + * @param {string} pane + * @param {(string|number)=} size + * @param {string=} [dir] + * @return {number} + */ +, _parseSize = function (pane, size, dir) { + if (!dir) dir = _c[pane].dir; + + if (isStr(size) && size.match(/%/)) + size = (size === '100%') ? -1 : parseInt(size, 10) / 100; // convert % to decimal + + if (size === 0) + return 0; + else if (size >= 1) + return parseInt(size, 10); + + var o = options, avail = 0; + if (dir=="horz") // north or south or center.minHeight + avail = sC.innerHeight - ($Ps.north ? o.north.spacing_open : 0) - ($Ps.south ? o.south.spacing_open : 0); + else if (dir=="vert") // east or west or center.minWidth + avail = sC.innerWidth - ($Ps.west ? o.west.spacing_open : 0) - ($Ps.east ? o.east.spacing_open : 0); + + if (size === -1) // -1 == 100% + return avail; + else if (size > 0) // percentage, eg: .25 + return round(avail * size); + else if (pane=="center") + return 0; + else { // size < 0 || size=='auto' || size==Missing || size==Invalid + // auto-size the pane + var dim = (dir === "horz" ? "height" : "width") + , $P = $Ps[pane] + , $C = dim === 'height' ? $Cs[pane] : false + , vis = $.layout.showInvisibly($P) // show pane invisibly if hidden + , szP = $P.css(dim) // SAVE current pane size + , szC = $C ? $C.css(dim) : 0 // SAVE current content size + ; + $P.css(dim, "auto"); + if ($C) $C.css(dim, "auto"); + size = (dim === "height") ? $P.outerHeight() : $P.outerWidth(); // MEASURE + $P.css(dim, szP).css(vis); // RESET size & visibility + if ($C) $C.css(dim, szC); + return size; + } + } + + /** + * Calculates current 'size' (outer-width or outer-height) of a border-pane - optionally with 'pane-spacing' added + * + * @param {(string|!Object)} pane + * @param {boolean=} [inclSpace=false] + * @return {number} Returns EITHER Width for east/west panes OR Height for north/south panes + */ +, getPaneSize = function (pane, inclSpace) { + var + $P = $Ps[pane] + , o = options[pane] + , s = state[pane] + , oSp = (inclSpace ? o.spacing_open : 0) + , cSp = (inclSpace ? o.spacing_closed : 0) + ; + if (!$P || s.isHidden) + return 0; + else if (s.isClosed || (s.isSliding && inclSpace)) + return cSp; + else if (_c[pane].dir === "horz") + return $P.outerHeight() + oSp; + else // dir === "vert" + return $P.outerWidth() + oSp; + } + + /** + * Calculate min/max pane dimensions and limits for resizing + * + * @param {string} pane + * @param {boolean=} [slide=false] + */ +, setSizeLimits = function (pane, slide) { + if (!isInitialized()) return; + var + o = options[pane] + , s = state[pane] + , c = _c[pane] + , dir = c.dir + , type = c.sizeType.toLowerCase() + , isSliding = (slide != undefined ? slide : s.isSliding) // only open() passes 'slide' param + , $P = $Ps[pane] + , paneSpacing = o.spacing_open + // measure the pane on the *opposite side* from this pane + , altPane = _c.oppositeEdge[pane] + , altS = state[altPane] + , $altP = $Ps[altPane] + , altPaneSize = (!$altP || altS.isVisible===false || altS.isSliding ? 0 : (dir=="horz" ? $altP.outerHeight() : $altP.outerWidth())) + , altPaneSpacing = ((!$altP || altS.isHidden ? 0 : options[altPane][ altS.isClosed !== false ? "spacing_closed" : "spacing_open" ]) || 0) + // limitSize prevents this pane from 'overlapping' opposite pane + , containerSize = (dir=="horz" ? sC.innerHeight : sC.innerWidth) + , minCenterDims = cssMinDims("center") + , minCenterSize = dir=="horz" ? max(options.center.minHeight, minCenterDims.minHeight) : max(options.center.minWidth, minCenterDims.minWidth) + // if pane is 'sliding', then ignore center and alt-pane sizes - because 'overlays' them + , limitSize = (containerSize - paneSpacing - (isSliding ? 0 : (_parseSize("center", minCenterSize, dir) + altPaneSize + altPaneSpacing))) + , minSize = s.minSize = max( _parseSize(pane, o.minSize), cssMinDims(pane).minSize ) + , maxSize = s.maxSize = min( (o.maxSize ? _parseSize(pane, o.maxSize) : 100000), limitSize ) + , r = s.resizerPosition = {} // used to set resizing limits + , top = sC.inset.top + , left = sC.inset.left + , W = sC.innerWidth + , H = sC.innerHeight + , rW = o.spacing_open // subtract resizer-width to get top/left position for south/east + ; + switch (pane) { + case "north": r.min = top + minSize; + r.max = top + maxSize; + break; + case "west": r.min = left + minSize; + r.max = left + maxSize; + break; + case "south": r.min = top + H - maxSize - rW; + r.max = top + H - minSize - rW; + break; + case "east": r.min = left + W - maxSize - rW; + r.max = left + W - minSize - rW; + break; + }; + } + + /** + * Returns data for setting the size/position of center pane. Also used to set Height for east/west panes + * + * @return JSON Returns a hash of all dimensions: top, bottom, left, right, (outer) width and (outer) height + */ +, calcNewCenterPaneDims = function () { + var d = { + top: getPaneSize("north", true) // true = include 'spacing' value for pane + , bottom: getPaneSize("south", true) + , left: getPaneSize("west", true) + , right: getPaneSize("east", true) + , width: 0 + , height: 0 + }; + + // NOTE: sC = state.container + // calc center-pane outer dimensions + d.width = sC.innerWidth - d.left - d.right; // outerWidth + d.height = sC.innerHeight - d.bottom - d.top; // outerHeight + // add the 'container border/padding' to get final positions relative to the container + d.top += sC.inset.top; + d.bottom += sC.inset.bottom; + d.left += sC.inset.left; + d.right += sC.inset.right; + + return d; + } + + + /** + * @param {!Object} el + * @param {boolean=} [allStates=false] + */ +, getHoverClasses = function (el, allStates) { + var + $El = $(el) + , type = $El.data("layoutRole") + , pane = $El.data("layoutEdge") + , o = options[pane] + , root = o[type +"Class"] + , _pane = "-"+ pane // eg: "-west" + , _open = "-open" + , _closed = "-closed" + , _slide = "-sliding" + , _hover = "-hover " // NOTE the trailing space + , _state = $El.hasClass(root+_closed) ? _closed : _open + , _alt = _state === _closed ? _open : _closed + , classes = (root+_hover) + (root+_pane+_hover) + (root+_state+_hover) + (root+_pane+_state+_hover) + ; + if (allStates) // when 'removing' classes, also remove alternate-state classes + classes += (root+_alt+_hover) + (root+_pane+_alt+_hover); + + if (type=="resizer" && $El.hasClass(root+_slide)) + classes += (root+_slide+_hover) + (root+_pane+_slide+_hover); + + return $.trim(classes); + } +, addHover = function (evt, el) { + var $E = $(el || this); + if (evt && $E.data("layoutRole") === "toggler") + evt.stopPropagation(); // prevent triggering 'slide' on Resizer-bar + $E.addClass( getHoverClasses($E) ); + } +, removeHover = function (evt, el) { + var $E = $(el || this); + $E.removeClass( getHoverClasses($E, true) ); + } + +, onResizerEnter = function (evt) { // ALSO called by toggler.mouseenter + var pane = $(this).data("layoutEdge") + , s = state[pane] + ; + // ignore closed-panes and mouse moving back & forth over resizer! + // also ignore if ANY pane is currently resizing + if ( s.isClosed || s.isResizing || state.paneResizing ) return; + + if ($.fn.disableSelection) + $("body").disableSelection(); + if (options.maskPanesEarly) + showMasks( pane, { resizing: true }); + } +, onResizerLeave = function (evt, el) { + var e = el || this // el is only passed when called by the timer + , pane = $(e).data("layoutEdge") + , name = pane +"ResizerLeave" + ; + timer.clear(pane+"_openSlider"); // cancel slideOpen timer, if set + timer.clear(name); // cancel enableSelection timer - may re/set below + // this method calls itself on a timer because it needs to allow + // enough time for dragging to kick-in and set the isResizing flag + // dragging has a 100ms delay set, so this delay must be >100 + if (!el) // 1st call - mouseleave event + timer.set(name, function(){ onResizerLeave(evt, e); }, 200); + // if user is resizing, then dragStop will enableSelection(), so can skip it here + else if ( !state.paneResizing ) { // 2nd call - by timer + if ($.fn.enableSelection) + $("body").enableSelection(); + if (options.maskPanesEarly) + hideMasks(); + } + } + +/* + * ########################### + * INITIALIZATION METHODS + * ########################### + */ + + /** + * Initialize the layout - called automatically whenever an instance of layout is created + * + * @see none - triggered onInit + * @return mixed true = fully initialized | false = panes not initialized (yet) | 'cancel' = abort + */ +, _create = function () { + // initialize config/options + initOptions(); + var o = options + , s = state; + + // TEMP state so isInitialized returns true during init process + s.creatingLayout = true; + + // init plugins for this layout, if there are any (eg: stateManagement) + runPluginCallbacks( Instance, $.layout.onCreate ); + + // options & state have been initialized, so now run beforeLoad callback + // onload will CANCEL layout creation if it returns false + if (false === _runCallbacks("onload_start")) + return 'cancel'; + + // initialize the container element + _initContainer(); + + // bind hotkey function - keyDown - if required + initHotkeys(); + + // bind window.onunload + $(window).bind("unload."+ sID, unload); + + // init plugins for this layout, if there are any (eg: customButtons) + runPluginCallbacks( Instance, $.layout.onLoad ); + + // if layout elements are hidden, then layout WILL NOT complete initialization! + // initLayoutElements will set initialized=true and run the onload callback IF successful + if (o.initPanes) _initLayoutElements(); + + delete s.creatingLayout; + + return state.initialized; + } + + /** + * Initialize the layout IF not already + * + * @see All methods in Instance run this test + * @return boolean true = layoutElements have been initialized | false = panes are not initialized (yet) + */ +, isInitialized = function () { + if (state.initialized || state.creatingLayout) return true; // already initialized + else return _initLayoutElements(); // try to init panes NOW + } + + /** + * Initialize the layout - called automatically whenever an instance of layout is created + * + * @see _create() & isInitialized + * @param {boolean=} [retry=false] // indicates this is a 2nd try + * @return An object pointer to the instance created + */ +, _initLayoutElements = function (retry) { + // initialize config/options + var o = options; + // CANNOT init panes inside a hidden container! + if (!$N.is(":visible")) { + // handle Chrome bug where popup window 'has no height' + // if layout is BODY element, try again in 50ms + // SEE: http://layout.jquery-dev.net/samples/test_popup_window.html + if ( !retry && browser.webkit && $N[0].tagName === "BODY" ) + setTimeout(function(){ _initLayoutElements(true); }, 50); + return false; + } + + // a center pane is required, so make sure it exists + if (!getPane("center").length) { + return _log( o.errors.centerPaneMissing ); + } + + // TEMP state so isInitialized returns true during init process + state.creatingLayout = true; + + // update Container dims + $.extend(sC, elDims( $N, o.inset )); // passing inset means DO NOT include insetX values + + // initialize all layout elements + initPanes(); // size & position panes - calls initHandles() - which calls initResizable() + + if (o.scrollToBookmarkOnLoad) { + var l = self.location; + if (l.hash) l.replace( l.hash ); // scrollTo Bookmark + } + + // check to see if this layout 'nested' inside a pane + if (Instance.hasParentLayout) + o.resizeWithWindow = false; + // bind resizeAll() for 'this layout instance' to window.resize event + else if (o.resizeWithWindow) + $(window).bind("resize."+ sID, windowResize); + + delete state.creatingLayout; + state.initialized = true; + + // init plugins for this layout, if there are any + runPluginCallbacks( Instance, $.layout.onReady ); + + // now run the onload callback, if exists + _runCallbacks("onload_end"); + + return true; // elements initialized successfully + } + + /** + * Initialize nested layouts for a specific pane - can optionally pass layout-options + * + * @param {(string|Object)} evt_or_pane The pane being opened, ie: north, south, east, or west + * @param {Object=} [opts] Layout-options - if passed, will OVERRRIDE options[pane].children + * @return An object pointer to the layout instance created - or null + */ +, createChildren = function (evt_or_pane, opts) { + var pane = evtPane.call(this, evt_or_pane) + , $P = $Ps[pane] + ; + if (!$P) return; + var $C = $Cs[pane] + , s = state[pane] + , o = options[pane] + , sm = options.stateManagement || {} + , cos = opts ? (o.children = opts) : o.children + ; + if ( $.isPlainObject( cos ) ) + cos = [ cos ]; // convert a hash to a 1-elem array + else if (!cos || !$.isArray( cos )) + return; + + $.each( cos, function (idx, co) { + if ( !$.isPlainObject( co ) ) return; + + // determine which element is supposed to be the 'child container' + // if pane has a 'containerSelector' OR a 'content-div', use those instead of the pane + var $containers = co.containerSelector ? $P.find( co.containerSelector ) : ($C || $P); + + $containers.each(function(){ + var $cont = $(this) + , child = $cont.data("layout") // see if a child-layout ALREADY exists on this element + ; + // if no layout exists, but children are set, try to create the layout now + if (!child) { + // TODO: see about moving this to the stateManagement plugin, as a method + // set a unique child-instance key for this layout, if not already set + setInstanceKey({ container: $cont, options: co }, s ); + // If THIS layout has a hash in stateManagement.autoLoad, + // then see if it also contains state-data for this child-layout + // If so, copy the stateData to child.options.stateManagement.autoLoad + if ( sm.includeChildren && state.stateData[pane] ) { + // THIS layout's state was cached when its state was loaded + var paneChildren = state.stateData[pane].children || {} + , childState = paneChildren[ co.instanceKey ] + , co_sm = co.stateManagement || (co.stateManagement = { autoLoad: true }) + ; + // COPY the stateData into the autoLoad key + if ( co_sm.autoLoad === true && childState ) { + co_sm.autoSave = false; // disable autoSave because saving handled by parent-layout + co_sm.includeChildren = true; // cascade option - FOR NOW + co_sm.autoLoad = $.extend(true, {}, childState); // COPY the state-hash + } + } + + // create the layout + child = $cont.layout( co ); + + // if successful, update data + if (child) { + // add the child and update all layout-pointers + // MAY have already been done by child-layout calling parent.refreshChildren() + refreshChildren( pane, child ); + } + } + }); + }); + } + +, setInstanceKey = function (child, parentPaneState) { + // create a named key for use in state and instance branches + var $c = child.container + , o = child.options + , sm = o.stateManagement + , key = o.instanceKey || $c.data("layoutInstanceKey") + ; + if (!key) key = (sm && sm.cookie ? sm.cookie.name : '') || o.name; // look for a name/key + if (!key) key = "layout"+ (++parentPaneState.childIdx); // if no name/key found, generate one + else key = key.replace(/[^\w-]/gi, '_').replace(/_{2,}/g, '_'); // ensure is valid as a hash key + o.instanceKey = key; + $c.data("layoutInstanceKey", key); // useful if layout is destroyed and then recreated + return key; + } + + /** + * @param {string} pane The pane being opened, ie: north, south, east, or west + * @param {Object=} newChild New child-layout Instance to add to this pane + */ +, refreshChildren = function (pane, newChild) { + var $P = $Ps[pane] + , pC = children[pane] + , s = state[pane] + , o + ; + // check for destroy()ed layouts and update the child pointers & arrays + if ($.isPlainObject( pC )) { + $.each( pC, function (key, child) { + if (child.destroyed) delete pC[key] + }); + // if no more children, remove the children hash + if ($.isEmptyObject( pC )) + pC = children[pane] = null; // clear children hash + } + + // see if there is a directly-nested layout inside this pane + // if there is, then there can be only ONE child-layout, so check that... + if (!newChild && !pC) { + newChild = $P.data("layout"); + } + + // if a newChild instance was passed, add it to children[pane] + if (newChild) { + // update child.state + newChild.hasParentLayout = true; // set parent-flag in child + // instanceKey is a key-name used in both state and children + o = newChild.options; + // set a unique child-instance key for this layout, if not already set + setInstanceKey( newChild, s ); + // add pointer to pane.children hash + if (!pC) pC = children[pane] = {}; // create an empty children hash + pC[ o.instanceKey ] = newChild.container.data("layout"); // add childLayout instance + } + + // ALWAYS refresh the pane.children alias, even if null + Instance[pane].children = children[pane]; + + // if newChild was NOT passed - see if there is a child layout NOW + if (!newChild) { + createChildren(pane); // MAY create a child and re-call this method + } + } + +, windowResize = function () { + var o = options + , delay = Number(o.resizeWithWindowDelay); + if (delay < 10) delay = 100; // MUST have a delay! + // resizing uses a delay-loop because the resize event fires repeatly - except in FF, but delay anyway + timer.clear("winResize"); // if already running + timer.set("winResize", function(){ + timer.clear("winResize"); + timer.clear("winResizeRepeater"); + var dims = elDims( $N, o.inset ); + // only trigger resizeAll() if container has changed size + if (dims.innerWidth !== sC.innerWidth || dims.innerHeight !== sC.innerHeight) + resizeAll(); + }, delay); + // ALSO set fixed-delay timer, if not already running + if (!timer.data["winResizeRepeater"]) setWindowResizeRepeater(); + } + +, setWindowResizeRepeater = function () { + var delay = Number(options.resizeWithWindowMaxDelay); + if (delay > 0) + timer.set("winResizeRepeater", function(){ setWindowResizeRepeater(); resizeAll(); }, delay); + } + +, unload = function () { + var o = options; + + _runCallbacks("onunload_start"); + + // trigger plugin callabacks for this layout (eg: stateManagement) + runPluginCallbacks( Instance, $.layout.onUnload ); + + _runCallbacks("onunload_end"); + } + + /** + * Validate and initialize container CSS and events + * + * @see _create() + */ +, _initContainer = function () { + var + N = $N[0] + , $H = $("html") + , tag = sC.tagName = N.tagName + , id = sC.id = N.id + , cls = sC.className = N.className + , o = options + , name = o.name + , props = "position,margin,padding,border" + , css = "layoutCSS" + , CSS = {} + , hid = "hidden" // used A LOT! + // see if this container is a 'pane' inside an outer-layout + , parent = $N.data("parentLayout") // parent-layout Instance + , pane = $N.data("layoutEdge") // pane-name in parent-layout + , isChild = parent && pane + , num = $.layout.cssNum + , $parent, n + ; + // sC = state.container + sC.selector = $N.selector.split(".slice")[0]; + sC.ref = (o.name ? o.name +' layout / ' : '') + tag + (id ? "#"+id : cls ? '.['+cls+']' : ''); // used in messages + sC.isBody = (tag === "BODY"); + + // try to find a parent-layout + if (!isChild && !sC.isBody) { + $parent = $N.closest("."+ $.layout.defaults.panes.paneClass); + parent = $parent.data("parentLayout"); + pane = $parent.data("layoutEdge"); + isChild = parent && pane; + } + + $N .data({ + layout: Instance + , layoutContainer: sID // FLAG to indicate this is a layout-container - contains unique internal ID + }) + .addClass(o.containerClass) + ; + var layoutMethods = { + destroy: '' + , initPanes: '' + , resizeAll: 'resizeAll' + , resize: 'resizeAll' + }; + // loop hash and bind all methods - include layoutID namespacing + for (name in layoutMethods) { + $N.bind("layout"+ name.toLowerCase() +"."+ sID, Instance[ layoutMethods[name] || name ]); + } + + // if this container is another layout's 'pane', then set child/parent pointers + if (isChild) { + // update parent flag + Instance.hasParentLayout = true; + // set pointers to THIS child-layout (Instance) in parent-layout + parent.refreshChildren( pane, Instance ); + } + + // SAVE original container CSS for use in destroy() + if (!$N.data(css)) { + // handle props like overflow different for BODY & HTML - has 'system default' values + if (sC.isBody) { + // SAVE CSS + $N.data(css, $.extend( styles($N, props), { + height: $N.css("height") + , overflow: $N.css("overflow") + , overflowX: $N.css("overflowX") + , overflowY: $N.css("overflowY") + })); + // ALSO SAVE CSS + $H.data(css, $.extend( styles($H, 'padding'), { + height: "auto" // FF would return a fixed px-size! + , overflow: $H.css("overflow") + , overflowX: $H.css("overflowX") + , overflowY: $H.css("overflowY") + })); + } + else // handle props normally for non-body elements + $N.data(css, styles($N, props+",top,bottom,left,right,width,height,overflow,overflowX,overflowY") ); + } + + try { + // common container CSS + CSS = { + overflow: hid + , overflowX: hid + , overflowY: hid + }; + $N.css( CSS ); + + if (o.inset && !$.isPlainObject(o.inset)) { + // can specify a single number for equal outset all-around + n = parseInt(o.inset, 10) || 0 + o.inset = { + top: n + , bottom: n + , left: n + , right: n + }; + } + + // format html & body if this is a full page layout + if (sC.isBody) { + // if HTML has padding, use this as an outer-spacing around BODY + if (!o.outset) { + // use padding from parent-elem (HTML) as outset + o.outset = { + top: num($H, "paddingTop") + , bottom: num($H, "paddingBottom") + , left: num($H, "paddingLeft") + , right: num($H, "paddingRight") + }; + } + else if (!$.isPlainObject(o.outset)) { + // can specify a single number for equal outset all-around + n = parseInt(o.outset, 10) || 0 + o.outset = { + top: n + , bottom: n + , left: n + , right: n + }; + } + // HTML + $H.css( CSS ).css({ + height: "100%" + , border: "none" // no border or padding allowed when using height = 100% + , padding: 0 // ditto + , margin: 0 + }); + // BODY + if (browser.isIE6) { + // IE6 CANNOT use the trick of setting absolute positioning on all 4 sides - must have 'height' + $N.css({ + width: "100%" + , height: "100%" + , border: "none" // no border or padding allowed when using height = 100% + , padding: 0 // ditto + , margin: 0 + , position: "relative" + }); + // convert body padding to an inset option - the border cannot be measured in IE6! + if (!o.inset) o.inset = elDims( $N ).inset; + } + else { // use absolute positioning for BODY to allow borders & padding without overflow + $N.css({ + width: "auto" + , height: "auto" + , margin: 0 + , position: "absolute" // allows for border and padding on BODY + }); + // apply edge-positioning created above + $N.css( o.outset ); + } + // set current layout-container dimensions + $.extend(sC, elDims( $N, o.inset )); // passing inset means DO NOT include insetX values + } + else { + // container MUST have 'position' + var p = $N.css("position"); + if (!p || !p.match(/(fixed|absolute|relative)/)) + $N.css("position","relative"); + + // set current layout-container dimensions + if ( $N.is(":visible") ) { + $.extend(sC, elDims( $N, o.inset )); // passing inset means DO NOT change insetX (padding) values + if (sC.innerHeight < 1) // container has no 'height' - warn developer + _log( o.errors.noContainerHeight.replace(/CONTAINER/, sC.ref) ); + } + } + + // if container has min-width/height, then enable scrollbar(s) + if ( num($N, "minWidth") ) $N.parent().css("overflowX","auto"); + if ( num($N, "minHeight") ) $N.parent().css("overflowY","auto"); + + } catch (ex) {} + } + + /** + * Bind layout hotkeys - if options enabled + * + * @see _create() and addPane() + * @param {string=} [panes=""] The edge(s) to process + */ +, initHotkeys = function (panes) { + panes = panes ? panes.split(",") : _c.borderPanes; + // bind keyDown to capture hotkeys, if option enabled for ANY pane + $.each(panes, function (i, pane) { + var o = options[pane]; + if (o.enableCursorHotkey || o.customHotkey) { + $(document).bind("keydown."+ sID, keyDown); // only need to bind this ONCE + return false; // BREAK - binding was done + } + }); + } + + /** + * Build final OPTIONS data + * + * @see _create() + */ +, initOptions = function () { + var data, d, pane, key, val, i, c, o; + + // reprocess user's layout-options to have correct options sub-key structure + opts = $.layout.transformData( opts, true ); // panes = default subkey + + // auto-rename old options for backward compatibility + opts = $.layout.backwardCompatibility.renameAllOptions( opts ); + + // if user-options has 'panes' key (pane-defaults), clean it... + if (!$.isEmptyObject(opts.panes)) { + // REMOVE any pane-defaults that MUST be set per-pane + data = $.layout.optionsMap.noDefault; + for (i=0, c=data.length; i 0) { + z.pane_normal = zo; + z.content_mask = max(zo+1, z.content_mask); // MIN = +1 + z.resizer_normal = max(zo+2, z.resizer_normal); // MIN = +2 + } + + // DELETE 'panes' key now that we are done - values were copied to EACH pane + delete options.panes; + + + function createFxOptions ( pane ) { + var o = options[pane] + , d = options.panes; + // ensure fxSettings key to avoid errors + if (!o.fxSettings) o.fxSettings = {}; + if (!d.fxSettings) d.fxSettings = {}; + + $.each(["_open","_close","_size"], function (i,n) { + var + sName = "fxName"+ n + , sSpeed = "fxSpeed"+ n + , sSettings = "fxSettings"+ n + // recalculate fxName according to specificity rules + , fxName = o[sName] = + o[sName] // options.west.fxName_open + || d[sName] // options.panes.fxName_open + || o.fxName // options.west.fxName + || d.fxName // options.panes.fxName + || "none" // MEANS $.layout.defaults.panes.fxName == "" || false || null || 0 + , fxExists = $.effects && ($.effects[fxName] || ($.effects.effect && $.effects.effect[fxName])) + ; + // validate fxName to ensure is valid effect - MUST have effect-config data in options.effects + if (fxName === "none" || !options.effects[fxName] || !fxExists) + fxName = o[sName] = "none"; // effect not loaded OR unrecognized fxName + + // set vars for effects subkeys to simplify logic + var fx = options.effects[fxName] || {} // effects.slide + , fx_all = fx.all || null // effects.slide.all + , fx_pane = fx[pane] || null // effects.slide.west + ; + // create fxSpeed[_open|_close|_size] + o[sSpeed] = + o[sSpeed] // options.west.fxSpeed_open + || d[sSpeed] // options.west.fxSpeed_open + || o.fxSpeed // options.west.fxSpeed + || d.fxSpeed // options.panes.fxSpeed + || null // DEFAULT - let fxSetting.duration control speed + ; + // create fxSettings[_open|_close|_size] + o[sSettings] = $.extend( + true + , {} + , fx_all // effects.slide.all + , fx_pane // effects.slide.west + , d.fxSettings // options.panes.fxSettings + , o.fxSettings // options.west.fxSettings + , d[sSettings] // options.panes.fxSettings_open + , o[sSettings] // options.west.fxSettings_open + ); + }); + + // DONE creating action-specific-settings for this pane, + // so DELETE generic options - are no longer meaningful + delete o.fxName; + delete o.fxSpeed; + delete o.fxSettings; + } + } + + /** + * Initialize module objects, styling, size and position for all panes + * + * @see _initElements() + * @param {string} pane The pane to process + */ +, getPane = function (pane) { + var sel = options[pane].paneSelector + if (sel.substr(0,1)==="#") // ID selector + // NOTE: elements selected 'by ID' DO NOT have to be 'children' + return $N.find(sel).eq(0); + else { // class or other selector + var $P = $N.children(sel).eq(0); + // look for the pane nested inside a 'form' element + return $P.length ? $P : $N.children("form:first").children(sel).eq(0); + } + } + + /** + * @param {Object=} evt + */ +, initPanes = function (evt) { + // stopPropagation if called by trigger("layoutinitpanes") - use evtPane utility + evtPane(evt); + + // NOTE: do north & south FIRST so we can measure their height - do center LAST + $.each(_c.allPanes, function (idx, pane) { + addPane( pane, true ); + }); + + // init the pane-handles NOW in case we have to hide or close the pane below + initHandles(); + + // now that all panes have been initialized and initially-sized, + // make sure there is really enough space available for each pane + $.each(_c.borderPanes, function (i, pane) { + if ($Ps[pane] && state[pane].isVisible) { // pane is OPEN + setSizeLimits(pane); + makePaneFit(pane); // pane may be Closed, Hidden or Resized by makePaneFit() + } + }); + // size center-pane AGAIN in case we 'closed' a border-pane in loop above + sizeMidPanes("center"); + + // Chrome/Webkit sometimes fires callbacks BEFORE it completes resizing! + // Before RC30.3, there was a 10ms delay here, but that caused layout + // to load asynchrously, which is BAD, so try skipping delay for now + + // process pane contents and callbacks, and init/resize child-layout if exists + $.each(_c.allPanes, function (idx, pane) { + afterInitPane(pane); + }); + } + + /** + * Add a pane to the layout - subroutine of initPanes() + * + * @see initPanes() + * @param {string} pane The pane to process + * @param {boolean=} [force=false] Size content after init + */ +, addPane = function (pane, force) { + if (!force && !isInitialized()) return; + var + o = options[pane] + , s = state[pane] + , c = _c[pane] + , dir = c.dir + , fx = s.fx + , spacing = o.spacing_open || 0 + , isCenter = (pane === "center") + , CSS = {} + , $P = $Ps[pane] + , size, minSize, maxSize, child + ; + // if pane-pointer already exists, remove the old one first + if ($P) + removePane( pane, false, true, false ); + else + $Cs[pane] = false; // init + + $P = $Ps[pane] = getPane(pane); + if (!$P.length) { + $Ps[pane] = false; // logic + return; + } + + // SAVE original Pane CSS + if (!$P.data("layoutCSS")) { + var props = "position,top,left,bottom,right,width,height,overflow,zIndex,display,backgroundColor,padding,margin,border"; + $P.data("layoutCSS", styles($P, props)); + } + + // create alias for pane data in Instance - initHandles will add more + Instance[pane] = { + name: pane + , pane: $Ps[pane] + , content: $Cs[pane] + , options: options[pane] + , state: state[pane] + , children: children[pane] + }; + + // add classes, attributes & events + $P .data({ + parentLayout: Instance // pointer to Layout Instance + , layoutPane: Instance[pane] // NEW pointer to pane-alias-object + , layoutEdge: pane + , layoutRole: "pane" + }) + .css(c.cssReq).css("zIndex", options.zIndexes.pane_normal) + .css(o.applyDemoStyles ? c.cssDemo : {}) // demo styles + .addClass( o.paneClass +" "+ o.paneClass+"-"+pane ) // default = "ui-layout-pane ui-layout-pane-west" - may be a dupe of 'paneSelector' + .bind("mouseenter."+ sID, addHover ) + .bind("mouseleave."+ sID, removeHover ) + ; + var paneMethods = { + hide: '' + , show: '' + , toggle: '' + , close: '' + , open: '' + , slideOpen: '' + , slideClose: '' + , slideToggle: '' + , size: 'sizePane' + , sizePane: 'sizePane' + , sizeContent: '' + , sizeHandles: '' + , enableClosable: '' + , disableClosable: '' + , enableSlideable: '' + , disableSlideable: '' + , enableResizable: '' + , disableResizable: '' + , swapPanes: 'swapPanes' + , swap: 'swapPanes' + , move: 'swapPanes' + , removePane: 'removePane' + , remove: 'removePane' + , createChildren: '' + , resizeChildren: '' + , resizeAll: 'resizeAll' + , resizeLayout: 'resizeAll' + } + , name; + // loop hash and bind all methods - include layoutID namespacing + for (name in paneMethods) { + $P.bind("layoutpane"+ name.toLowerCase() +"."+ sID, Instance[ paneMethods[name] || name ]); + } + + // see if this pane has a 'scrolling-content element' + initContent(pane, false); // false = do NOT sizeContent() - called later + + if (!isCenter) { + // call _parseSize AFTER applying pane classes & styles - but before making visible (if hidden) + // if o.size is auto or not valid, then MEASURE the pane and use that as its 'size' + size = s.size = _parseSize(pane, o.size); + minSize = _parseSize(pane,o.minSize) || 1; + maxSize = _parseSize(pane,o.maxSize) || 100000; + if (size > 0) size = max(min(size, maxSize), minSize); + s.autoResize = o.autoResize; // used with percentage sizes + + // state for border-panes + s.isClosed = false; // true = pane is closed + s.isSliding = false; // true = pane is currently open by 'sliding' over adjacent panes + s.isResizing= false; // true = pane is in process of being resized + s.isHidden = false; // true = pane is hidden - no spacing, resizer or toggler is visible! + + // array for 'pin buttons' whose classNames are auto-updated on pane-open/-close + if (!s.pins) s.pins = []; + } + // states common to ALL panes + s.tagName = $P[0].tagName; + s.edge = pane; // useful if pane is (or about to be) 'swapped' - easy find out where it is (or is going) + s.noRoom = false; // true = pane 'automatically' hidden due to insufficient room - will unhide automatically + s.isVisible = true; // false = pane is invisible - closed OR hidden - simplify logic + + // init pane positioning + setPanePosition( pane ); + + // if pane is not visible, + if (dir === "horz") // north or south pane + CSS.height = cssH($P, size); + else if (dir === "vert") // east or west pane + CSS.width = cssW($P, size); + //else if (isCenter) {} + + $P.css(CSS); // apply size -- top, bottom & height will be set by sizeMidPanes + if (dir != "horz") sizeMidPanes(pane, true); // true = skipCallback + + // if manually adding a pane AFTER layout initialization, then... + if (state.initialized) { + initHandles( pane ); + initHotkeys( pane ); + } + + // close or hide the pane if specified in settings + if (o.initClosed && o.closable && !o.initHidden) + close(pane, true, true); // true, true = force, noAnimation + else if (o.initHidden || o.initClosed) + hide(pane); // will be completely invisible - no resizer or spacing + else if (!s.noRoom) + // make the pane visible - in case was initially hidden + $P.css("display","block"); + // ELSE setAsOpen() - called later by initHandles() + + // RESET visibility now - pane will appear IF display:block + $P.css("visibility","visible"); + + // check option for auto-handling of pop-ups & drop-downs + if (o.showOverflowOnHover) + $P.hover( allowOverflow, resetOverflow ); + + // if manually adding a pane AFTER layout initialization, then... + if (state.initialized) { + afterInitPane( pane ); + } + } + +, afterInitPane = function (pane) { + var $P = $Ps[pane] + , s = state[pane] + , o = options[pane] + ; + if (!$P) return; + + // see if there is a directly-nested layout inside this pane + if ($P.data("layout")) + refreshChildren( pane, $P.data("layout") ); + + // process pane contents and callbacks, and init/resize child-layout if exists + if (s.isVisible) { // pane is OPEN + if (state.initialized) // this pane was added AFTER layout was created + resizeAll(); // will also sizeContent + else + sizeContent(pane); + + if (o.triggerEventsOnLoad) + _runCallbacks("onresize_end", pane); + else // automatic if onresize called, otherwise call it specifically + // resize child - IF inner-layout already exists (created before this layout) + resizeChildren(pane, true); // a previously existing childLayout + } + + // init childLayouts - even if pane is not visible + if (o.initChildren && o.children) + createChildren(pane); + } + + /** + * @param {string=} panes The pane(s) to process + */ +, setPanePosition = function (panes) { + panes = panes ? panes.split(",") : _c.borderPanes; + + // create toggler DIVs for each pane, and set object pointers for them, eg: $R.north = north toggler DIV + $.each(panes, function (i, pane) { + var $P = $Ps[pane] + , $R = $Rs[pane] + , o = options[pane] + , s = state[pane] + , side = _c[pane].side + , CSS = {} + ; + if (!$P) return; // pane does not exist - skip + + // set css-position to account for container borders & padding + switch (pane) { + case "north": CSS.top = sC.inset.top; + CSS.left = sC.inset.left; + CSS.right = sC.inset.right; + break; + case "south": CSS.bottom = sC.inset.bottom; + CSS.left = sC.inset.left; + CSS.right = sC.inset.right; + break; + case "west": CSS.left = sC.inset.left; // top, bottom & height set by sizeMidPanes() + break; + case "east": CSS.right = sC.inset.right; // ditto + break; + case "center": // top, left, width & height set by sizeMidPanes() + } + // apply position + $P.css(CSS); + + // update resizer position + if ($R && s.isClosed) + $R.css(side, sC.inset[side]); + else if ($R && !s.isHidden) + $R.css(side, sC.inset[side] + getPaneSize(pane)); + }); + } + + /** + * Initialize module objects, styling, size and position for all resize bars and toggler buttons + * + * @see _create() + * @param {string=} [panes=""] The edge(s) to process + */ +, initHandles = function (panes) { + panes = panes ? panes.split(",") : _c.borderPanes; + + // create toggler DIVs for each pane, and set object pointers for them, eg: $R.north = north toggler DIV + $.each(panes, function (i, pane) { + var $P = $Ps[pane]; + $Rs[pane] = false; // INIT + $Ts[pane] = false; + if (!$P) return; // pane does not exist - skip + + var o = options[pane] + , s = state[pane] + , c = _c[pane] + , paneId = o.paneSelector.substr(0,1) === "#" ? o.paneSelector.substr(1) : "" + , rClass = o.resizerClass + , tClass = o.togglerClass + , spacing = (s.isVisible ? o.spacing_open : o.spacing_closed) + , _pane = "-"+ pane // used for classNames + , _state = (s.isVisible ? "-open" : "-closed") // used for classNames + , I = Instance[pane] + // INIT RESIZER BAR + , $R = I.resizer = $Rs[pane] = $("
      ") + // INIT TOGGLER BUTTON + , $T = I.toggler = (o.closable ? $Ts[pane] = $("
      ") : false) + ; + + //if (s.isVisible && o.resizable) ... handled by initResizable + if (!s.isVisible && o.slidable) + $R.attr("title", o.tips.Slide).css("cursor", o.sliderCursor); + + $R // if paneSelector is an ID, then create a matching ID for the resizer, eg: "#paneLeft" => "paneLeft-resizer" + .attr("id", paneId ? paneId +"-resizer" : "" ) + .data({ + parentLayout: Instance + , layoutPane: Instance[pane] // NEW pointer to pane-alias-object + , layoutEdge: pane + , layoutRole: "resizer" + }) + .css(_c.resizers.cssReq).css("zIndex", options.zIndexes.resizer_normal) + .css(o.applyDemoStyles ? _c.resizers.cssDemo : {}) // add demo styles + .addClass(rClass +" "+ rClass+_pane) + .hover(addHover, removeHover) // ALWAYS add hover-classes, even if resizing is not enabled - handle with CSS instead + .hover(onResizerEnter, onResizerLeave) // ALWAYS NEED resizer.mouseleave to balance toggler.mouseenter + .appendTo($N) // append DIV to container + ; + if (o.resizerDblClickToggle) + $R.bind("dblclick."+ sID, toggle ); + + if ($T) { + $T // if paneSelector is an ID, then create a matching ID for the resizer, eg: "#paneLeft" => "#paneLeft-toggler" + .attr("id", paneId ? paneId +"-toggler" : "" ) + .data({ + parentLayout: Instance + , layoutPane: Instance[pane] // NEW pointer to pane-alias-object + , layoutEdge: pane + , layoutRole: "toggler" + }) + .css(_c.togglers.cssReq) // add base/required styles + .css(o.applyDemoStyles ? _c.togglers.cssDemo : {}) // add demo styles + .addClass(tClass +" "+ tClass+_pane) + .hover(addHover, removeHover) // ALWAYS add hover-classes, even if toggling is not enabled - handle with CSS instead + .bind("mouseenter", onResizerEnter) // NEED toggler.mouseenter because mouseenter MAY NOT fire on resizer + .appendTo($R) // append SPAN to resizer DIV + ; + // ADD INNER-SPANS TO TOGGLER + if (o.togglerContent_open) // ui-layout-open + $(""+ o.togglerContent_open +"") + .data({ + layoutEdge: pane + , layoutRole: "togglerContent" + }) + .data("layoutRole", "togglerContent") + .data("layoutEdge", pane) + .addClass("content content-open") + .css("display","none") + .appendTo( $T ) + //.hover( addHover, removeHover ) // use ui-layout-toggler-west-hover .content-open instead! + ; + if (o.togglerContent_closed) // ui-layout-closed + $(""+ o.togglerContent_closed +"") + .data({ + layoutEdge: pane + , layoutRole: "togglerContent" + }) + .addClass("content content-closed") + .css("display","none") + .appendTo( $T ) + //.hover( addHover, removeHover ) // use ui-layout-toggler-west-hover .content-closed instead! + ; + // ADD TOGGLER.click/.hover + enableClosable(pane); + } + + // add Draggable events + initResizable(pane); + + // ADD CLASSNAMES & SLIDE-BINDINGS - eg: class="resizer resizer-west resizer-open" + if (s.isVisible) + setAsOpen(pane); // onOpen will be called, but NOT onResize + else { + setAsClosed(pane); // onClose will be called + bindStartSlidingEvents(pane, true); // will enable events IF option is set + } + + }); + + // SET ALL HANDLE DIMENSIONS + sizeHandles(); + } + + + /** + * Initialize scrolling ui-layout-content div - if exists + * + * @see initPane() - or externally after an Ajax injection + * @param {string} pane The pane to process + * @param {boolean=} [resize=true] Size content after init + */ +, initContent = function (pane, resize) { + if (!isInitialized()) return; + var + o = options[pane] + , sel = o.contentSelector + , I = Instance[pane] + , $P = $Ps[pane] + , $C + ; + if (sel) $C = I.content = $Cs[pane] = (o.findNestedContent) + ? $P.find(sel).eq(0) // match 1-element only + : $P.children(sel).eq(0) + ; + if ($C && $C.length) { + $C.data("layoutRole", "content"); + // SAVE original Content CSS + if (!$C.data("layoutCSS")) + $C.data("layoutCSS", styles($C, "height")); + $C.css( _c.content.cssReq ); + if (o.applyDemoStyles) { + $C.css( _c.content.cssDemo ); // add padding & overflow: auto to content-div + $P.css( _c.content.cssDemoPane ); // REMOVE padding/scrolling from pane + } + // ensure no vertical scrollbar on pane - will mess up measurements + if ($P.css("overflowX").match(/(scroll|auto)/)) { + $P.css("overflow", "hidden"); + } + state[pane].content = {}; // init content state + if (resize !== false) sizeContent(pane); + // sizeContent() is called AFTER init of all elements + } + else + I.content = $Cs[pane] = false; + } + + + /** + * Add resize-bars to all panes that specify it in options + * -dependancy: $.fn.resizable - will skip if not found + * + * @see _create() + * @param {string=} [panes=""] The edge(s) to process + */ +, initResizable = function (panes) { + var draggingAvailable = $.layout.plugins.draggable + , side // set in start() + ; + panes = panes ? panes.split(",") : _c.borderPanes; + + $.each(panes, function (idx, pane) { + var o = options[pane]; + if (!draggingAvailable || !$Ps[pane] || !o.resizable) { + o.resizable = false; + return true; // skip to next + } + + var s = state[pane] + , z = options.zIndexes + , c = _c[pane] + , side = c.dir=="horz" ? "top" : "left" + , $P = $Ps[pane] + , $R = $Rs[pane] + , base = o.resizerClass + , lastPos = 0 // used when live-resizing + , r, live // set in start because may change + // 'drag' classes are applied to the ORIGINAL resizer-bar while dragging is in process + , resizerClass = base+"-drag" // resizer-drag + , resizerPaneClass = base+"-"+pane+"-drag" // resizer-north-drag + // 'helper' class is applied to the CLONED resizer-bar while it is being dragged + , helperClass = base+"-dragging" // resizer-dragging + , helperPaneClass = base+"-"+pane+"-dragging" // resizer-north-dragging + , helperLimitClass = base+"-dragging-limit" // resizer-drag + , helperPaneLimitClass = base+"-"+pane+"-dragging-limit" // resizer-north-drag + , helperClassesSet = false // logic var + ; + + if (!s.isClosed) + $R.attr("title", o.tips.Resize) + .css("cursor", o.resizerCursor); // n-resize, s-resize, etc + + $R.draggable({ + containment: $N[0] // limit resizing to layout container + , axis: (c.dir=="horz" ? "y" : "x") // limit resizing to horz or vert axis + , delay: 0 + , distance: 1 + , grid: o.resizingGrid + // basic format for helper - style it using class: .ui-draggable-dragging + , helper: "clone" + , opacity: o.resizerDragOpacity + , addClasses: false // avoid ui-state-disabled class when disabled + //, iframeFix: o.draggableIframeFix // TODO: consider using when bug is fixed + , zIndex: z.resizer_drag + + , start: function (e, ui) { + // REFRESH options & state pointers in case we used swapPanes + o = options[pane]; + s = state[pane]; + // re-read options + live = o.livePaneResizing; + + // ondrag_start callback - will CANCEL hide if returns false + // TODO: dragging CANNOT be cancelled like this, so see if there is a way? + if (false === _runCallbacks("ondrag_start", pane)) return false; + + s.isResizing = true; // prevent pane from closing while resizing + state.paneResizing = pane; // easy to see if ANY pane is resizing + timer.clear(pane+"_closeSlider"); // just in case already triggered + + // SET RESIZER LIMITS - used in drag() + setSizeLimits(pane); // update pane/resizer state + r = s.resizerPosition; + lastPos = ui.position[ side ] + + $R.addClass( resizerClass +" "+ resizerPaneClass ); // add drag classes + helperClassesSet = false; // reset logic var - see drag() + + // DISABLE TEXT SELECTION (probably already done by resizer.mouseOver) + $('body').disableSelection(); + + // MASK PANES CONTAINING IFRAMES, APPLETS OR OTHER TROUBLESOME ELEMENTS + showMasks( pane, { resizing: true }); + } + + , drag: function (e, ui) { + if (!helperClassesSet) { // can only add classes after clone has been added to the DOM + //$(".ui-draggable-dragging") + ui.helper + .addClass( helperClass +" "+ helperPaneClass ) // add helper classes + .css({ right: "auto", bottom: "auto" }) // fix dir="rtl" issue + .children().css("visibility","hidden") // hide toggler inside dragged resizer-bar + ; + helperClassesSet = true; + // draggable bug!? RE-SET zIndex to prevent E/W resize-bar showing through N/S pane! + if (s.isSliding) $Ps[pane].css("zIndex", z.pane_sliding); + } + // CONTAIN RESIZER-BAR TO RESIZING LIMITS + var limit = 0; + if (ui.position[side] < r.min) { + ui.position[side] = r.min; + limit = -1; + } + else if (ui.position[side] > r.max) { + ui.position[side] = r.max; + limit = 1; + } + // ADD/REMOVE dragging-limit CLASS + if (limit) { + ui.helper.addClass( helperLimitClass +" "+ helperPaneLimitClass ); // at dragging-limit + window.defaultStatus = (limit>0 && pane.match(/(north|west)/)) || (limit<0 && pane.match(/(south|east)/)) ? o.tips.maxSizeWarning : o.tips.minSizeWarning; + } + else { + ui.helper.removeClass( helperLimitClass +" "+ helperPaneLimitClass ); // not at dragging-limit + window.defaultStatus = ""; + } + // DYNAMICALLY RESIZE PANES IF OPTION ENABLED + // won't trigger unless resizer has actually moved! + if (live && Math.abs(ui.position[side] - lastPos) >= o.liveResizingTolerance) { + lastPos = ui.position[side]; + resizePanes(e, ui, pane) + } + } + + , stop: function (e, ui) { + $('body').enableSelection(); // RE-ENABLE TEXT SELECTION + window.defaultStatus = ""; // clear 'resizing limit' message from statusbar + $R.removeClass( resizerClass +" "+ resizerPaneClass ); // remove drag classes from Resizer + s.isResizing = false; + state.paneResizing = false; // easy to see if ANY pane is resizing + resizePanes(e, ui, pane, true); // true = resizingDone + } + + }); + }); + + /** + * resizePanes + * + * Sub-routine called from stop() - and drag() if livePaneResizing + * + * @param {!Object} evt + * @param {!Object} ui + * @param {string} pane + * @param {boolean=} [resizingDone=false] + */ + var resizePanes = function (evt, ui, pane, resizingDone) { + var dragPos = ui.position + , c = _c[pane] + , o = options[pane] + , s = state[pane] + , resizerPos + ; + switch (pane) { + case "north": resizerPos = dragPos.top; break; + case "west": resizerPos = dragPos.left; break; + case "south": resizerPos = sC.layoutHeight - dragPos.top - o.spacing_open; break; + case "east": resizerPos = sC.layoutWidth - dragPos.left - o.spacing_open; break; + }; + // remove container margin from resizer position to get the pane size + var newSize = resizerPos - sC.inset[c.side]; + + // Disable OR Resize Mask(s) created in drag.start + if (!resizingDone) { + // ensure we meet liveResizingTolerance criteria + if (Math.abs(newSize - s.size) < o.liveResizingTolerance) + return; // SKIP resize this time + // resize the pane + manualSizePane(pane, newSize, false, true); // true = noAnimation + sizeMasks(); // resize all visible masks + } + else { // resizingDone + // ondrag_end callback + if (false !== _runCallbacks("ondrag_end", pane)) + manualSizePane(pane, newSize, false, true); // true = noAnimation + hideMasks(true); // true = force hiding all masks even if one is 'sliding' + if (s.isSliding) // RE-SHOW 'object-masks' so objects won't show through sliding pane + showMasks( pane, { resizing: true }); + } + }; + } + + /** + * sizeMask + * + * Needed to overlay a DIV over an IFRAME-pane because mask CANNOT be *inside* the pane + * Called when mask created, and during livePaneResizing + */ +, sizeMask = function () { + var $M = $(this) + , pane = $M.data("layoutMask") // eg: "west" + , s = state[pane] + ; + // only masks over an IFRAME-pane need manual resizing + if (s.tagName == "IFRAME" && s.isVisible) // no need to mask closed/hidden panes + $M.css({ + top: s.offsetTop + , left: s.offsetLeft + , width: s.outerWidth + , height: s.outerHeight + }); + /* ALT Method... + var $P = $Ps[pane]; + $M.css( $P.position() ).css({ width: $P[0].offsetWidth, height: $P[0].offsetHeight }); + */ + } +, sizeMasks = function () { + $Ms.each( sizeMask ); // resize all 'visible' masks + } + + /** + * @param {string} pane The pane being resized, animated or isSliding + * @param {Object=} [args] (optional) Options: which masks to apply, and to which panes + */ +, showMasks = function (pane, args) { + var c = _c[pane] + , panes = ["center"] + , z = options.zIndexes + , a = $.extend({ + objectsOnly: false + , animation: false + , resizing: true + , sliding: state[pane].isSliding + }, args ) + , o, s + ; + if (a.resizing) + panes.push( pane ); + if (a.sliding) + panes.push( _c.oppositeEdge[pane] ); // ADD the oppositeEdge-pane + + if (c.dir === "horz") { + panes.push("west"); + panes.push("east"); + } + + $.each(panes, function(i,p){ + s = state[p]; + o = options[p]; + if (s.isVisible && ( o.maskObjects || (!a.objectsOnly && o.maskContents) )) { + getMasks(p).each(function(){ + sizeMask.call(this); + this.style.zIndex = s.isSliding ? z.pane_sliding+1 : z.pane_normal+1 + this.style.display = "block"; + }); + } + }); + } + + /** + * @param {boolean=} force Hide masks even if a pane is sliding + */ +, hideMasks = function (force) { + // ensure no pane is resizing - could be a timing issue + if (force || !state.paneResizing) { + $Ms.hide(); // hide ALL masks + } + // if ANY pane is sliding, then DO NOT remove masks from panes with maskObjects enabled + else if (!force && !$.isEmptyObject( state.panesSliding )) { + var i = $Ms.length - 1 + , p, $M; + for (; i >= 0; i--) { + $M = $Ms.eq(i); + p = $M.data("layoutMask"); + if (!options[p].maskObjects) { + $M.hide(); + } + } + } + } + + /** + * @param {string} pane + */ +, getMasks = function (pane) { + var $Masks = $([]) + , $M, i = 0, c = $Ms.length + ; + for (; i CSS + if (sC.tagName === "BODY" && ($N = $("html")).data(css)) // RESET CSS + $N.css( $N.data(css) ).removeData(css); + + // trigger plugins for this layout, if there are any + runPluginCallbacks( Instance, $.layout.onDestroy ); + + // trigger state-management and onunload callback + unload(); + + // clear the Instance of everything except for container & options (so could recreate) + // RE-CREATE: myLayout = myLayout.container.layout( myLayout.options ); + for (var n in Instance) + if (!n.match(/^(container|options)$/)) delete Instance[ n ]; + // add a 'destroyed' flag to make it easy to check + Instance.destroyed = true; + + // if this is a child layout, CLEAR the child-pointer in the parent + /* for now the pointer REMAINS, but with only container, options and destroyed keys + if (parentPane) { + var layout = parentPane.pane.data("parentLayout") + , key = layout.options.instanceKey || 'error'; + // THIS SYNTAX MAY BE WRONG! + parentPane.children[key] = layout.children[ parentPane.name ].children[key] = null; + } + */ + + return Instance; // for coding convenience + } + + /** + * Remove a pane from the layout - subroutine of destroy() + * + * @see destroy() + * @param {(string|Object)} evt_or_pane The pane to process + * @param {boolean=} [remove=false] Remove the DOM element? + * @param {boolean=} [skipResize=false] Skip calling resizeAll()? + * @param {boolean=} [destroyChild=true] Destroy Child-layouts? If not passed, obeys options setting + */ +, removePane = function (evt_or_pane, remove, skipResize, destroyChild) { + if (!isInitialized()) return; + var pane = evtPane.call(this, evt_or_pane) + , $P = $Ps[pane] + , $C = $Cs[pane] + , $R = $Rs[pane] + , $T = $Ts[pane] + ; + // NOTE: elements can still exist even after remove() + // so check for missing data(), which is cleared by removed() + if ($P && $.isEmptyObject( $P.data() )) $P = false; + if ($C && $.isEmptyObject( $C.data() )) $C = false; + if ($R && $.isEmptyObject( $R.data() )) $R = false; + if ($T && $.isEmptyObject( $T.data() )) $T = false; + + if ($P) $P.stop(true, true); + + var o = options[pane] + , s = state[pane] + , d = "layout" + , css = "layoutCSS" + , pC = children[pane] + , hasChildren = $.isPlainObject( pC ) && !$.isEmptyObject( pC ) + , destroy = destroyChild !== undefined ? destroyChild : o.destroyChildren + ; + // FIRST destroy the child-layout(s) + if (hasChildren && destroy) { + $.each( pC, function (key, child) { + if (!child.destroyed) + child.destroy(true);// tell child-layout to destroy ALL its child-layouts too + if (child.destroyed) // destroy was successful + delete pC[key]; + }); + // if no more children, remove the children hash + if ($.isEmptyObject( pC )) { + pC = children[pane] = null; // clear children hash + hasChildren = false; + } + } + + // Note: can't 'remove' a pane element with non-destroyed children + if ($P && remove && !hasChildren) + $P.remove(); // remove the pane-element and everything inside it + else if ($P && $P[0]) { + // create list of ALL pane-classes that need to be removed + var root = o.paneClass // default="ui-layout-pane" + , pRoot = root +"-"+ pane // eg: "ui-layout-pane-west" + , _open = "-open" + , _sliding= "-sliding" + , _closed = "-closed" + , classes = [ root, root+_open, root+_closed, root+_sliding, // generic classes + pRoot, pRoot+_open, pRoot+_closed, pRoot+_sliding ] // pane-specific classes + ; + $.merge(classes, getHoverClasses($P, true)); // ADD hover-classes + // remove all Layout classes from pane-element + $P .removeClass( classes.join(" ") ) // remove ALL pane-classes + .removeData("parentLayout") + .removeData("layoutPane") + .removeData("layoutRole") + .removeData("layoutEdge") + .removeData("autoHidden") // in case set + .unbind("."+ sID) // remove ALL Layout events + // TODO: remove these extra unbind commands when jQuery is fixed + //.unbind("mouseenter"+ sID) + //.unbind("mouseleave"+ sID) + ; + // do NOT reset CSS if this pane/content is STILL the container of a nested layout! + // the nested layout will reset its 'container' CSS when/if it is destroyed + if (hasChildren && $C) { + // a content-div may not have a specific width, so give it one to contain the Layout + $C.width( $C.width() ); + $.each( pC, function (key, child) { + child.resizeAll(); // resize the Layout + }); + } + else if ($C) + $C.css( $C.data(css) ).removeData(css).removeData("layoutRole"); + // remove pane AFTER content in case there was a nested layout + if (!$P.data(d)) + $P.css( $P.data(css) ).removeData(css); + } + + // REMOVE pane resizer and toggler elements + if ($T) $T.remove(); + if ($R) $R.remove(); + + // CLEAR all pointers and state data + Instance[pane] = $Ps[pane] = $Cs[pane] = $Rs[pane] = $Ts[pane] = false; + s = { removed: true }; + + if (!skipResize) + resizeAll(); + } + + +/* + * ########################### + * ACTION METHODS + * ########################### + */ + + /** + * @param {string} pane + */ +, _hidePane = function (pane) { + var $P = $Ps[pane] + , o = options[pane] + , s = $P[0].style + ; + if (o.useOffscreenClose) { + if (!$P.data(_c.offscreenReset)) + $P.data(_c.offscreenReset, { left: s.left, right: s.right }); + $P.css( _c.offscreenCSS ); + } + else + $P.hide().removeData(_c.offscreenReset); + } + + /** + * @param {string} pane + */ +, _showPane = function (pane) { + var $P = $Ps[pane] + , o = options[pane] + , off = _c.offscreenCSS + , old = $P.data(_c.offscreenReset) + , s = $P[0].style + ; + $P .show() // ALWAYS show, just in case + .removeData(_c.offscreenReset); + if (o.useOffscreenClose && old) { + if (s.left == off.left) + s.left = old.left; + if (s.right == off.right) + s.right = old.right; + } + } + + + /** + * Completely 'hides' a pane, including its spacing - as if it does not exist + * The pane is not actually 'removed' from the source, so can use 'show' to un-hide it + * + * @param {(string|Object)} evt_or_pane The pane being hidden, ie: north, south, east, or west + * @param {boolean=} [noAnimation=false] + */ +, hide = function (evt_or_pane, noAnimation) { + if (!isInitialized()) return; + var pane = evtPane.call(this, evt_or_pane) + , o = options[pane] + , s = state[pane] + , $P = $Ps[pane] + , $R = $Rs[pane] + ; + if (!$P || s.isHidden) return; // pane does not exist OR is already hidden + + // onhide_start callback - will CANCEL hide if returns false + if (state.initialized && false === _runCallbacks("onhide_start", pane)) return; + + s.isSliding = false; // just in case + delete state.panesSliding[pane]; + + // now hide the elements + if ($R) $R.hide(); // hide resizer-bar + if (!state.initialized || s.isClosed) { + s.isClosed = true; // to trigger open-animation on show() + s.isHidden = true; + s.isVisible = false; + if (!state.initialized) + _hidePane(pane); // no animation when loading page + sizeMidPanes(_c[pane].dir === "horz" ? "" : "center"); + if (state.initialized || o.triggerEventsOnLoad) + _runCallbacks("onhide_end", pane); + } + else { + s.isHiding = true; // used by onclose + close(pane, false, noAnimation); // adjust all panes to fit + } + } + + /** + * Show a hidden pane - show as 'closed' by default unless openPane = true + * + * @param {(string|Object)} evt_or_pane The pane being opened, ie: north, south, east, or west + * @param {boolean=} [openPane=false] + * @param {boolean=} [noAnimation=false] + * @param {boolean=} [noAlert=false] + */ +, show = function (evt_or_pane, openPane, noAnimation, noAlert) { + if (!isInitialized()) return; + var pane = evtPane.call(this, evt_or_pane) + , o = options[pane] + , s = state[pane] + , $P = $Ps[pane] + , $R = $Rs[pane] + ; + if (!$P || !s.isHidden) return; // pane does not exist OR is not hidden + + // onshow_start callback - will CANCEL show if returns false + if (false === _runCallbacks("onshow_start", pane)) return; + + s.isShowing = true; // used by onopen/onclose + //s.isHidden = false; - will be set by open/close - if not cancelled + s.isSliding = false; // just in case + delete state.panesSliding[pane]; + + // now show the elements + //if ($R) $R.show(); - will be shown by open/close + if (openPane === false) + close(pane, true); // true = force + else + open(pane, false, noAnimation, noAlert); // adjust all panes to fit + } + + + /** + * Toggles a pane open/closed by calling either open or close + * + * @param {(string|Object)} evt_or_pane The pane being toggled, ie: north, south, east, or west + * @param {boolean=} [slide=false] + */ +, toggle = function (evt_or_pane, slide) { + if (!isInitialized()) return; + var evt = evtObj(evt_or_pane) + , pane = evtPane.call(this, evt_or_pane) + , s = state[pane] + ; + if (evt) // called from to $R.dblclick OR triggerPaneEvent + evt.stopImmediatePropagation(); + if (s.isHidden) + show(pane); // will call 'open' after unhiding it + else if (s.isClosed) + open(pane, !!slide); + else + close(pane); + } + + + /** + * Utility method used during init or other auto-processes + * + * @param {string} pane The pane being closed + * @param {boolean=} [setHandles=false] + */ +, _closePane = function (pane, setHandles) { + var + $P = $Ps[pane] + , s = state[pane] + ; + _hidePane(pane); + s.isClosed = true; + s.isVisible = false; + if (setHandles) setAsClosed(pane); + } + + /** + * Close the specified pane (animation optional), and resize all other panes as needed + * + * @param {(string|Object)} evt_or_pane The pane being closed, ie: north, south, east, or west + * @param {boolean=} [force=false] + * @param {boolean=} [noAnimation=false] + * @param {boolean=} [skipCallback=false] + */ +, close = function (evt_or_pane, force, noAnimation, skipCallback) { + var pane = evtPane.call(this, evt_or_pane); + // if pane has been initialized, but NOT the complete layout, close pane instantly + if (!state.initialized && $Ps[pane]) { + _closePane(pane, true); // INIT pane as closed + return; + } + if (!isInitialized()) return; + + var + $P = $Ps[pane] + , $R = $Rs[pane] + , $T = $Ts[pane] + , o = options[pane] + , s = state[pane] + , c = _c[pane] + , doFX, isShowing, isHiding, wasSliding; + + // QUEUE in case another action/animation is in progress + $N.queue(function( queueNext ){ + + if ( !$P + || (!o.closable && !s.isShowing && !s.isHiding) // invalid request // (!o.resizable && !o.closable) ??? + || (!force && s.isClosed && !s.isShowing) // already closed + ) return queueNext(); + + // onclose_start callback - will CANCEL hide if returns false + // SKIP if just 'showing' a hidden pane as 'closed' + var abort = !s.isShowing && false === _runCallbacks("onclose_start", pane); + + // transfer logic vars to temp vars + isShowing = s.isShowing; + isHiding = s.isHiding; + wasSliding = s.isSliding; + // now clear the logic vars (REQUIRED before aborting) + delete s.isShowing; + delete s.isHiding; + + if (abort) return queueNext(); + + doFX = !noAnimation && !s.isClosed && (o.fxName_close != "none"); + s.isMoving = true; + s.isClosed = true; + s.isVisible = false; + // update isHidden BEFORE sizing panes + if (isHiding) s.isHidden = true; + else if (isShowing) s.isHidden = false; + + if (s.isSliding) // pane is being closed, so UNBIND trigger events + bindStopSlidingEvents(pane, false); // will set isSliding=false + else // resize panes adjacent to this one + sizeMidPanes(_c[pane].dir === "horz" ? "" : "center", false); // false = NOT skipCallback + + // if this pane has a resizer bar, move it NOW - before animation + setAsClosed(pane); + + // CLOSE THE PANE + if (doFX) { // animate the close + lockPaneForFX(pane, true); // need to set left/top so animation will work + $P.hide( o.fxName_close, o.fxSettings_close, o.fxSpeed_close, function () { + lockPaneForFX(pane, false); // undo + if (s.isClosed) close_2(); + queueNext(); + }); + } + else { // hide the pane without animation + _hidePane(pane); + close_2(); + queueNext(); + }; + }); + + // SUBROUTINE + function close_2 () { + s.isMoving = false; + bindStartSlidingEvents(pane, true); // will enable if o.slidable = true + + // if opposite-pane was autoClosed, see if it can be autoOpened now + var altPane = _c.oppositeEdge[pane]; + if (state[ altPane ].noRoom) { + setSizeLimits( altPane ); + makePaneFit( altPane ); + } + + if (!skipCallback && (state.initialized || o.triggerEventsOnLoad)) { + // onclose callback - UNLESS just 'showing' a hidden pane as 'closed' + if (!isShowing) _runCallbacks("onclose_end", pane); + // onhide OR onshow callback + if (isShowing) _runCallbacks("onshow_end", pane); + if (isHiding) _runCallbacks("onhide_end", pane); + } + } + } + + /** + * @param {string} pane The pane just closed, ie: north, south, east, or west + */ +, setAsClosed = function (pane) { + if (!$Rs[pane]) return; // handles not initialized yet! + var + $P = $Ps[pane] + , $R = $Rs[pane] + , $T = $Ts[pane] + , o = options[pane] + , s = state[pane] + , side = _c[pane].side + , rClass = o.resizerClass + , tClass = o.togglerClass + , _pane = "-"+ pane // used for classNames + , _open = "-open" + , _sliding= "-sliding" + , _closed = "-closed" + ; + $R + .css(side, sC.inset[side]) // move the resizer + .removeClass( rClass+_open +" "+ rClass+_pane+_open ) + .removeClass( rClass+_sliding +" "+ rClass+_pane+_sliding ) + .addClass( rClass+_closed +" "+ rClass+_pane+_closed ) + ; + // DISABLE 'resizing' when closed - do this BEFORE bindStartSlidingEvents? + if (o.resizable && $.layout.plugins.draggable) + $R + .draggable("disable") + .removeClass("ui-state-disabled") // do NOT apply disabled styling - not suitable here + .css("cursor", "default") + .attr("title","") + ; + + // if pane has a toggler button, adjust that too + if ($T) { + $T + .removeClass( tClass+_open +" "+ tClass+_pane+_open ) + .addClass( tClass+_closed +" "+ tClass+_pane+_closed ) + .attr("title", o.tips.Open) // may be blank + ; + // toggler-content - if exists + $T.children(".content-open").hide(); + $T.children(".content-closed").css("display","block"); + } + + // sync any 'pin buttons' + syncPinBtns(pane, false); + + if (state.initialized) { + // resize 'length' and position togglers for adjacent panes + sizeHandles(); + } + } + + /** + * Open the specified pane (animation optional), and resize all other panes as needed + * + * @param {(string|Object)} evt_or_pane The pane being opened, ie: north, south, east, or west + * @param {boolean=} [slide=false] + * @param {boolean=} [noAnimation=false] + * @param {boolean=} [noAlert=false] + */ +, open = function (evt_or_pane, slide, noAnimation, noAlert) { + if (!isInitialized()) return; + var pane = evtPane.call(this, evt_or_pane) + , $P = $Ps[pane] + , $R = $Rs[pane] + , $T = $Ts[pane] + , o = options[pane] + , s = state[pane] + , c = _c[pane] + , doFX, isShowing + ; + // QUEUE in case another action/animation is in progress + $N.queue(function( queueNext ){ + + if ( !$P + || (!o.resizable && !o.closable && !s.isShowing) // invalid request + || (s.isVisible && !s.isSliding) // already open + ) return queueNext(); + + // pane can ALSO be unhidden by just calling show(), so handle this scenario + if (s.isHidden && !s.isShowing) { + queueNext(); // call before show() because it needs the queue free + show(pane, true); + return; + } + + if (s.autoResize && s.size != o.size) // resize pane to original size set in options + sizePane(pane, o.size, true, true, true); // true=skipCallback/noAnimation/forceResize + else + // make sure there is enough space available to open the pane + setSizeLimits(pane, slide); + + // onopen_start callback - will CANCEL open if returns false + var cbReturn = _runCallbacks("onopen_start", pane); + + if (cbReturn === "abort") + return queueNext(); + + // update pane-state again in case options were changed in onopen_start + if (cbReturn !== "NC") // NC = "No Callback" + setSizeLimits(pane, slide); + + if (s.minSize > s.maxSize) { // INSUFFICIENT ROOM FOR PANE TO OPEN! + syncPinBtns(pane, false); // make sure pin-buttons are reset + if (!noAlert && o.tips.noRoomToOpen) + alert(o.tips.noRoomToOpen); + return queueNext(); // ABORT + } + + if (slide) // START Sliding - will set isSliding=true + bindStopSlidingEvents(pane, true); // BIND trigger events to close sliding-pane + else if (s.isSliding) // PIN PANE (stop sliding) - open pane 'normally' instead + bindStopSlidingEvents(pane, false); // UNBIND trigger events - will set isSliding=false + else if (o.slidable) + bindStartSlidingEvents(pane, false); // UNBIND trigger events + + s.noRoom = false; // will be reset by makePaneFit if 'noRoom' + makePaneFit(pane); + + // transfer logic var to temp var + isShowing = s.isShowing; + // now clear the logic var + delete s.isShowing; + + doFX = !noAnimation && s.isClosed && (o.fxName_open != "none"); + s.isMoving = true; + s.isVisible = true; + s.isClosed = false; + // update isHidden BEFORE sizing panes - WHY??? Old? + if (isShowing) s.isHidden = false; + + if (doFX) { // ANIMATE + // mask adjacent panes with objects + lockPaneForFX(pane, true); // need to set left/top so animation will work + $P.show( o.fxName_open, o.fxSettings_open, o.fxSpeed_open, function() { + lockPaneForFX(pane, false); // undo + if (s.isVisible) open_2(); // continue + queueNext(); + }); + } + else { // no animation + _showPane(pane);// just show pane and... + open_2(); // continue + queueNext(); + }; + }); + + // SUBROUTINE + function open_2 () { + s.isMoving = false; + + // cure iframe display issues + _fixIframe(pane); + + // NOTE: if isSliding, then other panes are NOT 'resized' + if (!s.isSliding) { // resize all panes adjacent to this one + sizeMidPanes(_c[pane].dir=="vert" ? "center" : "", false); // false = NOT skipCallback + } + + // set classes, position handles and execute callbacks... + setAsOpen(pane); + }; + + } + + /** + * @param {string} pane The pane just opened, ie: north, south, east, or west + * @param {boolean=} [skipCallback=false] + */ +, setAsOpen = function (pane, skipCallback) { + var + $P = $Ps[pane] + , $R = $Rs[pane] + , $T = $Ts[pane] + , o = options[pane] + , s = state[pane] + , side = _c[pane].side + , rClass = o.resizerClass + , tClass = o.togglerClass + , _pane = "-"+ pane // used for classNames + , _open = "-open" + , _closed = "-closed" + , _sliding= "-sliding" + ; + $R + .css(side, sC.inset[side] + getPaneSize(pane)) // move the resizer + .removeClass( rClass+_closed +" "+ rClass+_pane+_closed ) + .addClass( rClass+_open +" "+ rClass+_pane+_open ) + ; + if (s.isSliding) + $R.addClass( rClass+_sliding +" "+ rClass+_pane+_sliding ) + else // in case 'was sliding' + $R.removeClass( rClass+_sliding +" "+ rClass+_pane+_sliding ) + + removeHover( 0, $R ); // remove hover classes + if (o.resizable && $.layout.plugins.draggable) + $R .draggable("enable") + .css("cursor", o.resizerCursor) + .attr("title", o.tips.Resize); + else if (!s.isSliding) + $R.css("cursor", "default"); // n-resize, s-resize, etc + + // if pane also has a toggler button, adjust that too + if ($T) { + $T .removeClass( tClass+_closed +" "+ tClass+_pane+_closed ) + .addClass( tClass+_open +" "+ tClass+_pane+_open ) + .attr("title", o.tips.Close); // may be blank + removeHover( 0, $T ); // remove hover classes + // toggler-content - if exists + $T.children(".content-closed").hide(); + $T.children(".content-open").css("display","block"); + } + + // sync any 'pin buttons' + syncPinBtns(pane, !s.isSliding); + + // update pane-state dimensions - BEFORE resizing content + $.extend(s, elDims($P)); + + if (state.initialized) { + // resize resizer & toggler sizes for all panes + sizeHandles(); + // resize content every time pane opens - to be sure + sizeContent(pane, true); // true = remeasure headers/footers, even if 'pane.isMoving' + } + + if (!skipCallback && (state.initialized || o.triggerEventsOnLoad) && $P.is(":visible")) { + // onopen callback + _runCallbacks("onopen_end", pane); + // onshow callback - TODO: should this be here? + if (s.isShowing) _runCallbacks("onshow_end", pane); + + // ALSO call onresize because layout-size *may* have changed while pane was closed + if (state.initialized) + _runCallbacks("onresize_end", pane); + } + + // TODO: Somehow sizePane("north") is being called after this point??? + } + + + /** + * slideOpen / slideClose / slideToggle + * + * Pass-though methods for sliding + */ +, slideOpen = function (evt_or_pane) { + if (!isInitialized()) return; + var evt = evtObj(evt_or_pane) + , pane = evtPane.call(this, evt_or_pane) + , s = state[pane] + , delay = options[pane].slideDelay_open + ; + // prevent event from triggering on NEW resizer binding created below + if (evt) evt.stopImmediatePropagation(); + + if (s.isClosed && evt && evt.type === "mouseenter" && delay > 0) + // trigger = mouseenter - use a delay + timer.set(pane+"_openSlider", open_NOW, delay); + else + open_NOW(); // will unbind events if is already open + + /** + * SUBROUTINE for timed open + */ + function open_NOW () { + if (!s.isClosed) // skip if no longer closed! + bindStopSlidingEvents(pane, true); // BIND trigger events to close sliding-pane + else if (!s.isMoving) + open(pane, true); // true = slide - open() will handle binding + }; + } + +, slideClose = function (evt_or_pane) { + if (!isInitialized()) return; + var evt = evtObj(evt_or_pane) + , pane = evtPane.call(this, evt_or_pane) + , o = options[pane] + , s = state[pane] + , delay = s.isMoving ? 1000 : 300 // MINIMUM delay - option may override + ; + if (s.isClosed || s.isResizing) + return; // skip if already closed OR in process of resizing + else if (o.slideTrigger_close === "click") + close_NOW(); // close immediately onClick + else if (o.preventQuickSlideClose && s.isMoving) + return; // handle Chrome quick-close on slide-open + else if (o.preventPrematureSlideClose && evt && $.layout.isMouseOverElem(evt, $Ps[pane])) + return; // handle incorrect mouseleave trigger, like when over a SELECT-list in IE + else if (evt) // trigger = mouseleave - use a delay + // 1 sec delay if 'opening', else .3 sec + timer.set(pane+"_closeSlider", close_NOW, max(o.slideDelay_close, delay)); + else // called programically + close_NOW(); + + /** + * SUBROUTINE for timed close + */ + function close_NOW () { + if (s.isClosed) // skip 'close' if already closed! + bindStopSlidingEvents(pane, false); // UNBIND trigger events - TODO: is this needed here? + else if (!s.isMoving) + close(pane); // close will handle unbinding + }; + } + + /** + * @param {(string|Object)} evt_or_pane The pane being opened, ie: north, south, east, or west + */ +, slideToggle = function (evt_or_pane) { + var pane = evtPane.call(this, evt_or_pane); + toggle(pane, true); + } + + + /** + * Must set left/top on East/South panes so animation will work properly + * + * @param {string} pane The pane to lock, 'east' or 'south' - any other is ignored! + * @param {boolean} doLock true = set left/top, false = remove + */ +, lockPaneForFX = function (pane, doLock) { + var $P = $Ps[pane] + , s = state[pane] + , o = options[pane] + , z = options.zIndexes + ; + if (doLock) { + showMasks( pane, { animation: true, objectsOnly: true }); + $P.css({ zIndex: z.pane_animate }); // overlay all elements during animation + if (pane=="south") + $P.css({ top: sC.inset.top + sC.innerHeight - $P.outerHeight() }); + else if (pane=="east") + $P.css({ left: sC.inset.left + sC.innerWidth - $P.outerWidth() }); + } + else { // animation DONE - RESET CSS + hideMasks(); + $P.css({ zIndex: (s.isSliding ? z.pane_sliding : z.pane_normal) }); + if (pane=="south") + $P.css({ top: "auto" }); + // if pane is positioned 'off-screen', then DO NOT screw with it! + else if (pane=="east" && !$P.css("left").match(/\-99999/)) + $P.css({ left: "auto" }); + // fix anti-aliasing in IE - only needed for animations that change opacity + if (browser.msie && o.fxOpacityFix && o.fxName_open != "slide" && $P.css("filter") && $P.css("opacity") == 1) + $P[0].style.removeAttribute('filter'); + } + } + + + /** + * Toggle sliding functionality of a specific pane on/off by adding removing 'slide open' trigger + * + * @see open(), close() + * @param {string} pane The pane to enable/disable, 'north', 'south', etc. + * @param {boolean} enable Enable or Disable sliding? + */ +, bindStartSlidingEvents = function (pane, enable) { + var o = options[pane] + , $P = $Ps[pane] + , $R = $Rs[pane] + , evtName = o.slideTrigger_open.toLowerCase() + ; + if (!$R || (enable && !o.slidable)) return; + + // make sure we have a valid event + if (evtName.match(/mouseover/)) + evtName = o.slideTrigger_open = "mouseenter"; + else if (!evtName.match(/(click|dblclick|mouseenter)/)) + evtName = o.slideTrigger_open = "click"; + + // must remove double-click-toggle when using dblclick-slide + if (o.resizerDblClickToggle && evtName.match(/click/)) { + $R[enable ? "unbind" : "bind"]('dblclick.'+ sID, toggle) + } + + $R + // add or remove event + [enable ? "bind" : "unbind"](evtName +'.'+ sID, slideOpen) + // set the appropriate cursor & title/tip + .css("cursor", enable ? o.sliderCursor : "default") + .attr("title", enable ? o.tips.Slide : "") + ; + } + + /** + * Add or remove 'mouseleave' events to 'slide close' when pane is 'sliding' open or closed + * Also increases zIndex when pane is sliding open + * See bindStartSlidingEvents for code to control 'slide open' + * + * @see slideOpen(), slideClose() + * @param {string} pane The pane to process, 'north', 'south', etc. + * @param {boolean} enable Enable or Disable events? + */ +, bindStopSlidingEvents = function (pane, enable) { + var o = options[pane] + , s = state[pane] + , c = _c[pane] + , z = options.zIndexes + , evtName = o.slideTrigger_close.toLowerCase() + , action = (enable ? "bind" : "unbind") + , $P = $Ps[pane] + , $R = $Rs[pane] + ; + timer.clear(pane+"_closeSlider"); // just in case + + if (enable) { + s.isSliding = true; + state.panesSliding[pane] = true; + // remove 'slideOpen' event from resizer + // ALSO will raise the zIndex of the pane & resizer + bindStartSlidingEvents(pane, false); + } + else { + s.isSliding = false; + delete state.panesSliding[pane]; + } + + // RE/SET zIndex - increases when pane is sliding-open, resets to normal when not + $P.css("zIndex", enable ? z.pane_sliding : z.pane_normal); + $R.css("zIndex", enable ? z.pane_sliding+2 : z.resizer_normal); // NOTE: mask = pane_sliding+1 + + // make sure we have a valid event + if (!evtName.match(/(click|mouseleave)/)) + evtName = o.slideTrigger_close = "mouseleave"; // also catches 'mouseout' + + // add/remove slide triggers + $R[action](evtName, slideClose); // base event on resize + // need extra events for mouseleave + if (evtName === "mouseleave") { + // also close on pane.mouseleave + $P[action]("mouseleave."+ sID, slideClose); + // cancel timer when mouse moves between 'pane' and 'resizer' + $R[action]("mouseenter."+ sID, cancelMouseOut); + $P[action]("mouseenter."+ sID, cancelMouseOut); + } + + if (!enable) + timer.clear(pane+"_closeSlider"); + else if (evtName === "click" && !o.resizable) { + // IF pane is not resizable (which already has a cursor and tip) + // then set the a cursor & title/tip on resizer when sliding + $R.css("cursor", enable ? o.sliderCursor : "default"); + $R.attr("title", enable ? o.tips.Close : ""); // use Toggler-tip, eg: "Close Pane" + } + + // SUBROUTINE for mouseleave timer clearing + function cancelMouseOut (evt) { + timer.clear(pane+"_closeSlider"); + evt.stopPropagation(); + } + } + + + /** + * Hides/closes a pane if there is insufficient room - reverses this when there is room again + * MUST have already called setSizeLimits() before calling this method + * + * @param {string} pane The pane being resized + * @param {boolean=} [isOpening=false] Called from onOpen? + * @param {boolean=} [skipCallback=false] Should the onresize callback be run? + * @param {boolean=} [force=false] + */ +, makePaneFit = function (pane, isOpening, skipCallback, force) { + var o = options[pane] + , s = state[pane] + , c = _c[pane] + , $P = $Ps[pane] + , $R = $Rs[pane] + , isSidePane = c.dir==="vert" + , hasRoom = false + ; + // special handling for center & east/west panes + if (pane === "center" || (isSidePane && s.noVerticalRoom)) { + // see if there is enough room to display the pane + // ERROR: hasRoom = s.minHeight <= s.maxHeight && (isSidePane || s.minWidth <= s.maxWidth); + hasRoom = (s.maxHeight >= 0); + if (hasRoom && s.noRoom) { // previously hidden due to noRoom, so show now + _showPane(pane); + if ($R) $R.show(); + s.isVisible = true; + s.noRoom = false; + if (isSidePane) s.noVerticalRoom = false; + _fixIframe(pane); + } + else if (!hasRoom && !s.noRoom) { // not currently hidden, so hide now + _hidePane(pane); + if ($R) $R.hide(); + s.isVisible = false; + s.noRoom = true; + } + } + + // see if there is enough room to fit the border-pane + if (pane === "center") { + // ignore center in this block + } + else if (s.minSize <= s.maxSize) { // pane CAN fit + hasRoom = true; + if (s.size > s.maxSize) // pane is too big - shrink it + sizePane(pane, s.maxSize, skipCallback, true, force); // true = noAnimation + else if (s.size < s.minSize) // pane is too small - enlarge it + sizePane(pane, s.minSize, skipCallback, true, force); // true = noAnimation + // need s.isVisible because new pseudoClose method keeps pane visible, but off-screen + else if ($R && s.isVisible && $P.is(":visible")) { + // make sure resizer-bar is positioned correctly + // handles situation where nested layout was 'hidden' when initialized + var pos = s.size + sC.inset[c.side]; + if ($.layout.cssNum( $R, c.side ) != pos) $R.css( c.side, pos ); + } + + // if was previously hidden due to noRoom, then RESET because NOW there is room + if (s.noRoom) { + // s.noRoom state will be set by open or show + if (s.wasOpen && o.closable) { + if (o.autoReopen) + open(pane, false, true, true); // true = noAnimation, true = noAlert + else // leave the pane closed, so just update state + s.noRoom = false; + } + else + show(pane, s.wasOpen, true, true); // true = noAnimation, true = noAlert + } + } + else { // !hasRoom - pane CANNOT fit + if (!s.noRoom) { // pane not set as noRoom yet, so hide or close it now... + s.noRoom = true; // update state + s.wasOpen = !s.isClosed && !s.isSliding; + if (s.isClosed){} // SKIP + else if (o.closable) // 'close' if possible + close(pane, true, true); // true = force, true = noAnimation + else // 'hide' pane if cannot just be closed + hide(pane, true); // true = noAnimation + } + } + } + + + /** + * manualSizePane is an exposed flow-through method allowing extra code when pane is 'manually resized' + * + * @param {(string|Object)} evt_or_pane The pane being resized + * @param {number} size The *desired* new size for this pane - will be validated + * @param {boolean=} [skipCallback=false] Should the onresize callback be run? + * @param {boolean=} [noAnimation=false] + * @param {boolean=} [force=false] Force resizing even if does not seem necessary + */ +, manualSizePane = function (evt_or_pane, size, skipCallback, noAnimation, force) { + if (!isInitialized()) return; + var pane = evtPane.call(this, evt_or_pane) + , o = options[pane] + , s = state[pane] + // if resizing callbacks have been delayed and resizing is now DONE, force resizing to complete... + , forceResize = force || (o.livePaneResizing && !s.isResizing) + ; + // ANY call to manualSizePane disables autoResize - ie, percentage sizing + s.autoResize = false; + // flow-through... + sizePane(pane, size, skipCallback, noAnimation, forceResize); // will animate resize if option enabled + } + + /** + * sizePane is called only by internal methods whenever a pane needs to be resized + * + * @param {(string|Object)} evt_or_pane The pane being resized + * @param {number} size The *desired* new size for this pane - will be validated + * @param {boolean=} [skipCallback=false] Should the onresize callback be run? + * @param {boolean=} [noAnimation=false] + * @param {boolean=} [force=false] Force resizing even if does not seem necessary + */ +, sizePane = function (evt_or_pane, size, skipCallback, noAnimation, force) { + if (!isInitialized()) return; + var pane = evtPane.call(this, evt_or_pane) // probably NEVER called from event? + , o = options[pane] + , s = state[pane] + , $P = $Ps[pane] + , $R = $Rs[pane] + , side = _c[pane].side + , dimName = _c[pane].sizeType.toLowerCase() + , skipResizeWhileDragging = s.isResizing && !o.triggerEventsDuringLiveResize + , doFX = noAnimation !== true && o.animatePaneSizing + , oldSize, newSize + ; + // QUEUE in case another action/animation is in progress + $N.queue(function( queueNext ){ + // calculate 'current' min/max sizes + setSizeLimits(pane); // update pane-state + oldSize = s.size; + size = _parseSize(pane, size); // handle percentages & auto + size = max(size, _parseSize(pane, o.minSize)); + size = min(size, s.maxSize); + if (size < s.minSize) { // not enough room for pane! + queueNext(); // call before makePaneFit() because it needs the queue free + makePaneFit(pane, false, skipCallback); // will hide or close pane + return; + } + + // IF newSize is same as oldSize, then nothing to do - abort + if (!force && size === oldSize) + return queueNext(); + + s.newSize = size; + + // onresize_start callback CANNOT cancel resizing because this would break the layout! + if (!skipCallback && state.initialized && s.isVisible) + _runCallbacks("onresize_start", pane); + + // resize the pane, and make sure its visible + newSize = cssSize(pane, size); + + if (doFX && $P.is(":visible")) { // ANIMATE + var fx = $.layout.effects.size[pane] || $.layout.effects.size.all + , easing = o.fxSettings_size.easing || fx.easing + , z = options.zIndexes + , props = {}; + props[ dimName ] = newSize +'px'; + s.isMoving = true; + // overlay all elements during animation + $P.css({ zIndex: z.pane_animate }) + .show().animate( props, o.fxSpeed_size, easing, function(){ + // reset zIndex after animation + $P.css({ zIndex: (s.isSliding ? z.pane_sliding : z.pane_normal) }); + s.isMoving = false; + delete s.newSize; + sizePane_2(); // continue + queueNext(); + }); + } + else { // no animation + $P.css( dimName, newSize ); // resize pane + delete s.newSize; + // if pane is visible, then + if ($P.is(":visible")) + sizePane_2(); // continue + else { + // pane is NOT VISIBLE, so just update state data... + // when pane is *next opened*, it will have the new size + s.size = size; // update state.size + $.extend(s, elDims($P)); // update state dimensions + } + queueNext(); + }; + + }); + + // SUBROUTINE + function sizePane_2 () { + /* Panes are sometimes not sized precisely in some browsers!? + * This code will resize the pane up to 3 times to nudge the pane to the correct size + */ + var actual = dimName==='width' ? $P.outerWidth() : $P.outerHeight() + , tries = [{ + pane: pane + , count: 1 + , target: size + , actual: actual + , correct: (size === actual) + , attempt: size + , cssSize: newSize + }] + , lastTry = tries[0] + , thisTry = {} + , msg = 'Inaccurate size after resizing the '+ pane +'-pane.' + ; + while ( !lastTry.correct ) { + thisTry = { pane: pane, count: lastTry.count+1, target: size }; + + if (lastTry.actual > size) + thisTry.attempt = max(0, lastTry.attempt - (lastTry.actual - size)); + else // lastTry.actual < size + thisTry.attempt = max(0, lastTry.attempt + (size - lastTry.actual)); + + thisTry.cssSize = cssSize(pane, thisTry.attempt); + $P.css( dimName, thisTry.cssSize ); + + thisTry.actual = dimName=='width' ? $P.outerWidth() : $P.outerHeight(); + thisTry.correct = (size === thisTry.actual); + + // log attempts and alert the user of this *non-fatal error* (if showDebugMessages) + if ( tries.length === 1) { + _log(msg, false, true); + _log(lastTry, false, true); + } + _log(thisTry, false, true); + // after 4 tries, is as close as its gonna get! + if (tries.length > 3) break; + + tries.push( thisTry ); + lastTry = tries[ tries.length - 1 ]; + } + // END TESTING CODE + + // update pane-state dimensions + s.size = size; + $.extend(s, elDims($P)); + + if (s.isVisible && $P.is(":visible")) { + // reposition the resizer-bar + if ($R) $R.css( side, size + sC.inset[side] ); + // resize the content-div + sizeContent(pane); + } + + if (!skipCallback && !skipResizeWhileDragging && state.initialized && s.isVisible) + _runCallbacks("onresize_end", pane); + + // resize all the adjacent panes, and adjust their toggler buttons + // when skipCallback passed, it means the controlling method will handle 'other panes' + if (!skipCallback) { + // also no callback if live-resize is in progress and NOT triggerEventsDuringLiveResize + if (!s.isSliding) sizeMidPanes(_c[pane].dir=="horz" ? "" : "center", skipResizeWhileDragging, force); + sizeHandles(); + } + + // if opposite-pane was autoClosed, see if it can be autoOpened now + var altPane = _c.oppositeEdge[pane]; + if (size < oldSize && state[ altPane ].noRoom) { + setSizeLimits( altPane ); + makePaneFit( altPane, false, skipCallback ); + } + + // DEBUG - ALERT user/developer so they know there was a sizing problem + if (tries.length > 1) + _log(msg +'\nSee the Error Console for details.', true, true); + } + } + + /** + * @see initPanes(), sizePane(), resizeAll(), open(), close(), hide() + * @param {(Array.|string)} panes The pane(s) being resized, comma-delmited string + * @param {boolean=} [skipCallback=false] Should the onresize callback be run? + * @param {boolean=} [force=false] + */ +, sizeMidPanes = function (panes, skipCallback, force) { + panes = (panes ? panes : "east,west,center").split(","); + + $.each(panes, function (i, pane) { + if (!$Ps[pane]) return; // NO PANE - skip + var + o = options[pane] + , s = state[pane] + , $P = $Ps[pane] + , $R = $Rs[pane] + , isCenter= (pane=="center") + , hasRoom = true + , CSS = {} + // if pane is not visible, show it invisibly NOW rather than for *each call* in this script + , visCSS = $.layout.showInvisibly($P) + + , newCenter = calcNewCenterPaneDims() + ; + + // update pane-state dimensions + $.extend(s, elDims($P)); + + if (pane === "center") { + if (!force && s.isVisible && newCenter.width === s.outerWidth && newCenter.height === s.outerHeight) { + $P.css(visCSS); + return true; // SKIP - pane already the correct size + } + // set state for makePaneFit() logic + $.extend(s, cssMinDims(pane), { + maxWidth: newCenter.width + , maxHeight: newCenter.height + }); + CSS = newCenter; + s.newWidth = CSS.width; + s.newHeight = CSS.height; + // convert OUTER width/height to CSS width/height + CSS.width = cssW($P, CSS.width); + // NEW - allow pane to extend 'below' visible area rather than hide it + CSS.height = cssH($P, CSS.height); + hasRoom = CSS.width >= 0 && CSS.height >= 0; // height >= 0 = ALWAYS TRUE NOW + + // during layout init, try to shrink east/west panes to make room for center + if (!state.initialized && o.minWidth > newCenter.width) { + var + reqPx = o.minWidth - s.outerWidth + , minE = options.east.minSize || 0 + , minW = options.west.minSize || 0 + , sizeE = state.east.size + , sizeW = state.west.size + , newE = sizeE + , newW = sizeW + ; + if (reqPx > 0 && state.east.isVisible && sizeE > minE) { + newE = max( sizeE-minE, sizeE-reqPx ); + reqPx -= sizeE-newE; + } + if (reqPx > 0 && state.west.isVisible && sizeW > minW) { + newW = max( sizeW-minW, sizeW-reqPx ); + reqPx -= sizeW-newW; + } + // IF we found enough extra space, then resize the border panes as calculated + if (reqPx === 0) { + if (sizeE && sizeE != minE) + sizePane('east', newE, true, true, force); // true = skipCallback/noAnimation - initPanes will handle when done + if (sizeW && sizeW != minW) + sizePane('west', newW, true, true, force); // true = skipCallback/noAnimation + // now start over! + sizeMidPanes('center', skipCallback, force); + $P.css(visCSS); + return; // abort this loop + } + } + } + else { // for east and west, set only the height, which is same as center height + // set state.min/maxWidth/Height for makePaneFit() logic + if (s.isVisible && !s.noVerticalRoom) + $.extend(s, elDims($P), cssMinDims(pane)) + if (!force && !s.noVerticalRoom && newCenter.height === s.outerHeight) { + $P.css(visCSS); + return true; // SKIP - pane already the correct size + } + // east/west have same top, bottom & height as center + CSS.top = newCenter.top; + CSS.bottom = newCenter.bottom; + s.newSize = newCenter.height + // NEW - allow pane to extend 'below' visible area rather than hide it + CSS.height = cssH($P, newCenter.height); + s.maxHeight = CSS.height; + hasRoom = (s.maxHeight >= 0); // ALWAYS TRUE NOW + if (!hasRoom) s.noVerticalRoom = true; // makePaneFit() logic + } + + if (hasRoom) { + // resizeAll passes skipCallback because it triggers callbacks after ALL panes are resized + if (!skipCallback && state.initialized) + _runCallbacks("onresize_start", pane); + + $P.css(CSS); // apply the CSS to pane + if (pane !== "center") + sizeHandles(pane); // also update resizer length + if (s.noRoom && !s.isClosed && !s.isHidden) + makePaneFit(pane); // will re-open/show auto-closed/hidden pane + if (s.isVisible) { + $.extend(s, elDims($P)); // update pane dimensions + if (state.initialized) sizeContent(pane); // also resize the contents, if exists + } + } + else if (!s.noRoom && s.isVisible) // no room for pane + makePaneFit(pane); // will hide or close pane + + // reset visibility, if necessary + $P.css(visCSS); + + delete s.newSize; + delete s.newWidth; + delete s.newHeight; + + if (!s.isVisible) + return true; // DONE - next pane + + /* + * Extra CSS for IE6 or IE7 in Quirks-mode - add 'width' to NORTH/SOUTH panes + * Normally these panes have only 'left' & 'right' positions so pane auto-sizes + * ALSO required when pane is an IFRAME because will NOT default to 'full width' + * TODO: Can I use width:100% for a north/south iframe? + * TODO: Sounds like a job for $P.outerWidth( sC.innerWidth ) SETTER METHOD + */ + if (pane === "center") { // finished processing midPanes + var fix = browser.isIE6 || !browser.boxModel; + if ($Ps.north && (fix || state.north.tagName=="IFRAME")) + $Ps.north.css("width", cssW($Ps.north, sC.innerWidth)); + if ($Ps.south && (fix || state.south.tagName=="IFRAME")) + $Ps.south.css("width", cssW($Ps.south, sC.innerWidth)); + } + + // resizeAll passes skipCallback because it triggers callbacks after ALL panes are resized + if (!skipCallback && state.initialized) + _runCallbacks("onresize_end", pane); + }); + } + + + /** + * @see window.onresize(), callbacks or custom code + * @param {(Object|boolean)=} evt_or_refresh If 'true', then also reset pane-positioning + */ +, resizeAll = function (evt_or_refresh) { + var oldW = sC.innerWidth + , oldH = sC.innerHeight + ; + // stopPropagation if called by trigger("layoutdestroy") - use evtPane utility + evtPane(evt_or_refresh); + + // cannot size layout when 'container' is hidden or collapsed + if (!$N.is(":visible")) return; + + if (!state.initialized) { + _initLayoutElements(); + return; // no need to resize since we just initialized! + } + + if (evt_or_refresh === true && $.isPlainObject(options.outset)) { + // update container CSS in case outset option has changed + $N.css( options.outset ); + } + // UPDATE container dimensions + $.extend(sC, elDims( $N, options.inset )); + if (!sC.outerHeight) return; + + // if 'true' passed, refresh pane & handle positioning too + if (evt_or_refresh === true) { + setPanePosition(); + } + + // onresizeall_start will CANCEL resizing if returns false + // state.container has already been set, so user can access this info for calcuations + if (false === _runCallbacks("onresizeall_start")) return false; + + var // see if container is now 'smaller' than before + shrunkH = (sC.innerHeight < oldH) + , shrunkW = (sC.innerWidth < oldW) + , $P, o, s + ; + // NOTE special order for sizing: S-N-E-W + $.each(["south","north","east","west"], function (i, pane) { + if (!$Ps[pane]) return; // no pane - SKIP + o = options[pane]; + s = state[pane]; + if (s.autoResize && s.size != o.size) // resize pane to original size set in options + sizePane(pane, o.size, true, true, true); // true=skipCallback/noAnimation/forceResize + else { + setSizeLimits(pane); + makePaneFit(pane, false, true, true); // true=skipCallback/forceResize + } + }); + + sizeMidPanes("", true, true); // true=skipCallback/forceResize + sizeHandles(); // reposition the toggler elements + + // trigger all individual pane callbacks AFTER layout has finished resizing + $.each(_c.allPanes, function (i, pane) { + $P = $Ps[pane]; + if (!$P) return; // SKIP + if (state[pane].isVisible) // undefined for non-existent panes + _runCallbacks("onresize_end", pane); // callback - if exists + }); + + _runCallbacks("onresizeall_end"); + //_triggerLayoutEvent(pane, 'resizeall'); + } + + /** + * Whenever a pane resizes or opens that has a nested layout, trigger resizeAll + * + * @param {(string|Object)} evt_or_pane The pane just resized or opened + */ +, resizeChildren = function (evt_or_pane, skipRefresh) { + var pane = evtPane.call(this, evt_or_pane); + + if (!options[pane].resizeChildren) return; + + // ensure the pane-children are up-to-date + if (!skipRefresh) refreshChildren( pane ); + var pC = children[pane]; + if ($.isPlainObject( pC )) { + // resize one or more children + $.each( pC, function (key, child) { + if (!child.destroyed) child.resizeAll(); + }); + } + } + + /** + * IF pane has a content-div, then resize all elements inside pane to fit pane-height + * + * @param {(string|Object)} evt_or_panes The pane(s) being resized + * @param {boolean=} [remeasure=false] Should the content (header/footer) be remeasured? + */ +, sizeContent = function (evt_or_panes, remeasure) { + if (!isInitialized()) return; + + var panes = evtPane.call(this, evt_or_panes); + panes = panes ? panes.split(",") : _c.allPanes; + + $.each(panes, function (idx, pane) { + var + $P = $Ps[pane] + , $C = $Cs[pane] + , o = options[pane] + , s = state[pane] + , m = s.content // m = measurements + ; + if (!$P || !$C || !$P.is(":visible")) return true; // NOT VISIBLE - skip + + // if content-element was REMOVED, update OR remove the pointer + if (!$C.length) { + initContent(pane, false); // false = do NOT sizeContent() - already there! + if (!$C) return; // no replacement element found - pointer have been removed + } + + // onsizecontent_start will CANCEL resizing if returns false + if (false === _runCallbacks("onsizecontent_start", pane)) return; + + // skip re-measuring offsets if live-resizing + if ((!s.isMoving && !s.isResizing) || o.liveContentResizing || remeasure || m.top == undefined) { + _measure(); + // if any footers are below pane-bottom, they may not measure correctly, + // so allow pane overflow and re-measure + if (m.hiddenFooters > 0 && $P.css("overflow") === "hidden") { + $P.css("overflow", "visible"); + _measure(); // remeasure while overflowing + $P.css("overflow", "hidden"); + } + } + // NOTE: spaceAbove/Below *includes* the pane paddingTop/Bottom, but not pane.borders + var newH = s.innerHeight - (m.spaceAbove - s.css.paddingTop) - (m.spaceBelow - s.css.paddingBottom); + + if (!$C.is(":visible") || m.height != newH) { + // size the Content element to fit new pane-size - will autoHide if not enough room + setOuterHeight($C, newH, true); // true=autoHide + m.height = newH; // save new height + }; + + if (state.initialized) + _runCallbacks("onsizecontent_end", pane); + + function _below ($E) { + return max(s.css.paddingBottom, (parseInt($E.css("marginBottom"), 10) || 0)); + }; + + function _measure () { + var + ignore = options[pane].contentIgnoreSelector + , $Fs = $C.nextAll().not(".ui-layout-mask").not(ignore || ":lt(0)") // not :lt(0) = ALL + , $Fs_vis = $Fs.filter(':visible') + , $F = $Fs_vis.filter(':last') + ; + m = { + top: $C[0].offsetTop + , height: $C.outerHeight() + , numFooters: $Fs.length + , hiddenFooters: $Fs.length - $Fs_vis.length + , spaceBelow: 0 // correct if no content footer ($E) + } + m.spaceAbove = m.top; // just for state - not used in calc + m.bottom = m.top + m.height; + if ($F.length) + //spaceBelow = (LastFooter.top + LastFooter.height) [footerBottom] - Content.bottom + max(LastFooter.marginBottom, pane.paddingBotom) + m.spaceBelow = ($F[0].offsetTop + $F.outerHeight()) - m.bottom + _below($F); + else // no footer - check marginBottom on Content element itself + m.spaceBelow = _below($C); + }; + }); + } + + + /** + * Called every time a pane is opened, closed, or resized to slide the togglers to 'center' and adjust their length if necessary + * + * @see initHandles(), open(), close(), resizeAll() + * @param {(string|Object)=} evt_or_panes The pane(s) being resized + */ +, sizeHandles = function (evt_or_panes) { + var panes = evtPane.call(this, evt_or_panes) + panes = panes ? panes.split(",") : _c.borderPanes; + + $.each(panes, function (i, pane) { + var + o = options[pane] + , s = state[pane] + , $P = $Ps[pane] + , $R = $Rs[pane] + , $T = $Ts[pane] + , $TC + ; + if (!$P || !$R) return; + + var + dir = _c[pane].dir + , _state = (s.isClosed ? "_closed" : "_open") + , spacing = o["spacing"+ _state] + , togAlign = o["togglerAlign"+ _state] + , togLen = o["togglerLength"+ _state] + , paneLen + , left + , offset + , CSS = {} + ; + + if (spacing === 0) { + $R.hide(); + return; + } + else if (!s.noRoom && !s.isHidden) // skip if resizer was hidden for any reason + $R.show(); // in case was previously hidden + + // Resizer Bar is ALWAYS same width/height of pane it is attached to + if (dir === "horz") { // north/south + //paneLen = $P.outerWidth(); // s.outerWidth || + paneLen = sC.innerWidth; // handle offscreen-panes + s.resizerLength = paneLen; + left = $.layout.cssNum($P, "left") + $R.css({ + width: cssW($R, paneLen) // account for borders & padding + , height: cssH($R, spacing) // ditto + , left: left > -9999 ? left : sC.inset.left // handle offscreen-panes + }); + } + else { // east/west + paneLen = $P.outerHeight(); // s.outerHeight || + s.resizerLength = paneLen; + $R.css({ + height: cssH($R, paneLen) // account for borders & padding + , width: cssW($R, spacing) // ditto + , top: sC.inset.top + getPaneSize("north", true) // TODO: what if no North pane? + //, top: $.layout.cssNum($Ps["center"], "top") + }); + } + + // remove hover classes + removeHover( o, $R ); + + if ($T) { + if (togLen === 0 || (s.isSliding && o.hideTogglerOnSlide)) { + $T.hide(); // always HIDE the toggler when 'sliding' + return; + } + else + $T.show(); // in case was previously hidden + + if (!(togLen > 0) || togLen === "100%" || togLen > paneLen) { + togLen = paneLen; + offset = 0; + } + else { // calculate 'offset' based on options.PANE.togglerAlign_open/closed + if (isStr(togAlign)) { + switch (togAlign) { + case "top": + case "left": offset = 0; + break; + case "bottom": + case "right": offset = paneLen - togLen; + break; + case "middle": + case "center": + default: offset = round((paneLen - togLen) / 2); // 'default' catches typos + } + } + else { // togAlign = number + var x = parseInt(togAlign, 10); // + if (togAlign >= 0) offset = x; + else offset = paneLen - togLen + x; // NOTE: x is negative! + } + } + + if (dir === "horz") { // north/south + var width = cssW($T, togLen); + $T.css({ + width: width // account for borders & padding + , height: cssH($T, spacing) // ditto + , left: offset // TODO: VERIFY that toggler positions correctly for ALL values + , top: 0 + }); + // CENTER the toggler content SPAN + $T.children(".content").each(function(){ + $TC = $(this); + $TC.css("marginLeft", round((width-$TC.outerWidth())/2)); // could be negative + }); + } + else { // east/west + var height = cssH($T, togLen); + $T.css({ + height: height // account for borders & padding + , width: cssW($T, spacing) // ditto + , top: offset // POSITION the toggler + , left: 0 + }); + // CENTER the toggler content SPAN + $T.children(".content").each(function(){ + $TC = $(this); + $TC.css("marginTop", round((height-$TC.outerHeight())/2)); // could be negative + }); + } + + // remove ALL hover classes + removeHover( 0, $T ); + } + + // DONE measuring and sizing this resizer/toggler, so can be 'hidden' now + if (!state.initialized && (o.initHidden || s.isHidden)) { + $R.hide(); + if ($T) $T.hide(); + } + }); + } + + + /** + * @param {(string|Object)} evt_or_pane + */ +, enableClosable = function (evt_or_pane) { + if (!isInitialized()) return; + var pane = evtPane.call(this, evt_or_pane) + , $T = $Ts[pane] + , o = options[pane] + ; + if (!$T) return; + o.closable = true; + $T .bind("click."+ sID, function(evt){ evt.stopPropagation(); toggle(pane); }) + .css("visibility", "visible") + .css("cursor", "pointer") + .attr("title", state[pane].isClosed ? o.tips.Open : o.tips.Close) // may be blank + .show(); + } + /** + * @param {(string|Object)} evt_or_pane + * @param {boolean=} [hide=false] + */ +, disableClosable = function (evt_or_pane, hide) { + if (!isInitialized()) return; + var pane = evtPane.call(this, evt_or_pane) + , $T = $Ts[pane] + ; + if (!$T) return; + options[pane].closable = false; + // is closable is disable, then pane MUST be open! + if (state[pane].isClosed) open(pane, false, true); + $T .unbind("."+ sID) + .css("visibility", hide ? "hidden" : "visible") // instead of hide(), which creates logic issues + .css("cursor", "default") + .attr("title", ""); + } + + + /** + * @param {(string|Object)} evt_or_pane + */ +, enableSlidable = function (evt_or_pane) { + if (!isInitialized()) return; + var pane = evtPane.call(this, evt_or_pane) + , $R = $Rs[pane] + ; + if (!$R || !$R.data('draggable')) return; + options[pane].slidable = true; + if (state[pane].isClosed) + bindStartSlidingEvents(pane, true); + } + /** + * @param {(string|Object)} evt_or_pane + */ +, disableSlidable = function (evt_or_pane) { + if (!isInitialized()) return; + var pane = evtPane.call(this, evt_or_pane) + , $R = $Rs[pane] + ; + if (!$R) return; + options[pane].slidable = false; + if (state[pane].isSliding) + close(pane, false, true); + else { + bindStartSlidingEvents(pane, false); + $R .css("cursor", "default") + .attr("title", ""); + removeHover(null, $R[0]); // in case currently hovered + } + } + + + /** + * @param {(string|Object)} evt_or_pane + */ +, enableResizable = function (evt_or_pane) { + if (!isInitialized()) return; + var pane = evtPane.call(this, evt_or_pane) + , $R = $Rs[pane] + , o = options[pane] + ; + if (!$R || !$R.data('draggable')) return; + o.resizable = true; + $R.draggable("enable"); + if (!state[pane].isClosed) + $R .css("cursor", o.resizerCursor) + .attr("title", o.tips.Resize); + } + /** + * @param {(string|Object)} evt_or_pane + */ +, disableResizable = function (evt_or_pane) { + if (!isInitialized()) return; + var pane = evtPane.call(this, evt_or_pane) + , $R = $Rs[pane] + ; + if (!$R || !$R.data('draggable')) return; + options[pane].resizable = false; + $R .draggable("disable") + .css("cursor", "default") + .attr("title", ""); + removeHover(null, $R[0]); // in case currently hovered + } + + + /** + * Move a pane from source-side (eg, west) to target-side (eg, east) + * If pane exists on target-side, move that to source-side, ie, 'swap' the panes + * + * @param {(string|Object)} evt_or_pane1 The pane/edge being swapped + * @param {string} pane2 ditto + */ +, swapPanes = function (evt_or_pane1, pane2) { + if (!isInitialized()) return; + var pane1 = evtPane.call(this, evt_or_pane1); + // change state.edge NOW so callbacks can know where pane is headed... + state[pane1].edge = pane2; + state[pane2].edge = pane1; + // run these even if NOT state.initialized + if (false === _runCallbacks("onswap_start", pane1) + || false === _runCallbacks("onswap_start", pane2) + ) { + state[pane1].edge = pane1; // reset + state[pane2].edge = pane2; + return; + } + + var + oPane1 = copy( pane1 ) + , oPane2 = copy( pane2 ) + , sizes = {} + ; + sizes[pane1] = oPane1 ? oPane1.state.size : 0; + sizes[pane2] = oPane2 ? oPane2.state.size : 0; + + // clear pointers & state + $Ps[pane1] = false; + $Ps[pane2] = false; + state[pane1] = {}; + state[pane2] = {}; + + // ALWAYS remove the resizer & toggler elements + if ($Ts[pane1]) $Ts[pane1].remove(); + if ($Ts[pane2]) $Ts[pane2].remove(); + if ($Rs[pane1]) $Rs[pane1].remove(); + if ($Rs[pane2]) $Rs[pane2].remove(); + $Rs[pane1] = $Rs[pane2] = $Ts[pane1] = $Ts[pane2] = false; + + // transfer element pointers and data to NEW Layout keys + move( oPane1, pane2 ); + move( oPane2, pane1 ); + + // cleanup objects + oPane1 = oPane2 = sizes = null; + + // make panes 'visible' again + if ($Ps[pane1]) $Ps[pane1].css(_c.visible); + if ($Ps[pane2]) $Ps[pane2].css(_c.visible); + + // fix any size discrepancies caused by swap + resizeAll(); + + // run these even if NOT state.initialized + _runCallbacks("onswap_end", pane1); + _runCallbacks("onswap_end", pane2); + + return; + + function copy (n) { // n = pane + var + $P = $Ps[n] + , $C = $Cs[n] + ; + return !$P ? false : { + pane: n + , P: $P ? $P[0] : false + , C: $C ? $C[0] : false + , state: $.extend(true, {}, state[n]) + , options: $.extend(true, {}, options[n]) + } + }; + + function move (oPane, pane) { + if (!oPane) return; + var + P = oPane.P + , C = oPane.C + , oldPane = oPane.pane + , c = _c[pane] + // save pane-options that should be retained + , s = $.extend(true, {}, state[pane]) + , o = options[pane] + // RETAIN side-specific FX Settings - more below + , fx = { resizerCursor: o.resizerCursor } + , re, size, pos + ; + $.each("fxName,fxSpeed,fxSettings".split(","), function (i, k) { + fx[k +"_open"] = o[k +"_open"]; + fx[k +"_close"] = o[k +"_close"]; + fx[k +"_size"] = o[k +"_size"]; + }); + + // update object pointers and attributes + $Ps[pane] = $(P) + .data({ + layoutPane: Instance[pane] // NEW pointer to pane-alias-object + , layoutEdge: pane + }) + .css(_c.hidden) + .css(c.cssReq) + ; + $Cs[pane] = C ? $(C) : false; + + // set options and state + options[pane] = $.extend(true, {}, oPane.options, fx); + state[pane] = $.extend(true, {}, oPane.state); + + // change classNames on the pane, eg: ui-layout-pane-east ==> ui-layout-pane-west + re = new RegExp(o.paneClass +"-"+ oldPane, "g"); + P.className = P.className.replace(re, o.paneClass +"-"+ pane); + + // ALWAYS regenerate the resizer & toggler elements + initHandles(pane); // create the required resizer & toggler + + // if moving to different orientation, then keep 'target' pane size + if (c.dir != _c[oldPane].dir) { + size = sizes[pane] || 0; + setSizeLimits(pane); // update pane-state + size = max(size, state[pane].minSize); + // use manualSizePane to disable autoResize - not useful after panes are swapped + manualSizePane(pane, size, true, true); // true/true = skipCallback/noAnimation + } + else // move the resizer here + $Rs[pane].css(c.side, sC.inset[c.side] + (state[pane].isVisible ? getPaneSize(pane) : 0)); + + + // ADD CLASSNAMES & SLIDE-BINDINGS + if (oPane.state.isVisible && !s.isVisible) + setAsOpen(pane, true); // true = skipCallback + else { + setAsClosed(pane); + bindStartSlidingEvents(pane, true); // will enable events IF option is set + } + + // DESTROY the object + oPane = null; + }; + } + + + /** + * INTERNAL method to sync pin-buttons when pane is opened or closed + * Unpinned means the pane is 'sliding' - ie, over-top of the adjacent panes + * + * @see open(), setAsOpen(), setAsClosed() + * @param {string} pane These are the params returned to callbacks by layout() + * @param {boolean} doPin True means set the pin 'down', False means 'up' + */ +, syncPinBtns = function (pane, doPin) { + if ($.layout.plugins.buttons) + $.each(state[pane].pins, function (i, selector) { + $.layout.buttons.setPinState(Instance, $(selector), pane, doPin); + }); + } + +; // END var DECLARATIONS + + /** + * Capture keys when enableCursorHotkey - toggle pane if hotkey pressed + * + * @see document.keydown() + */ + function keyDown (evt) { + if (!evt) return true; + var code = evt.keyCode; + if (code < 33) return true; // ignore special keys: ENTER, TAB, etc + + var + PANE = { + 38: "north" // Up Cursor - $.ui.keyCode.UP + , 40: "south" // Down Cursor - $.ui.keyCode.DOWN + , 37: "west" // Left Cursor - $.ui.keyCode.LEFT + , 39: "east" // Right Cursor - $.ui.keyCode.RIGHT + } + , ALT = evt.altKey // no worky! + , SHIFT = evt.shiftKey + , CTRL = evt.ctrlKey + , CURSOR = (CTRL && code >= 37 && code <= 40) + , o, k, m, pane + ; + + if (CURSOR && options[PANE[code]].enableCursorHotkey) // valid cursor-hotkey + pane = PANE[code]; + else if (CTRL || SHIFT) // check to see if this matches a custom-hotkey + $.each(_c.borderPanes, function (i, p) { // loop each pane to check its hotkey + o = options[p]; + k = o.customHotkey; + m = o.customHotkeyModifier; // if missing or invalid, treated as "CTRL+SHIFT" + if ((SHIFT && m=="SHIFT") || (CTRL && m=="CTRL") || (CTRL && SHIFT)) { // Modifier matches + if (k && code === (isNaN(k) || k <= 9 ? k.toUpperCase().charCodeAt(0) : k)) { // Key matches + pane = p; + return false; // BREAK + } + } + }); + + // validate pane + if (!pane || !$Ps[pane] || !options[pane].closable || state[pane].isHidden) + return true; + + toggle(pane); + + evt.stopPropagation(); + evt.returnValue = false; // CANCEL key + return false; + }; + + +/* + * ###################################### + * UTILITY METHODS + * called externally or by initButtons + * ###################################### + */ + + /** + * Change/reset a pane overflow setting & zIndex to allow popups/drop-downs to work + * + * @param {Object=} [el] (optional) Can also be 'bound' to a click, mouseOver, or other event + */ + function allowOverflow (el) { + if (!isInitialized()) return; + if (this && this.tagName) el = this; // BOUND to element + var $P; + if (isStr(el)) + $P = $Ps[el]; + else if ($(el).data("layoutRole")) + $P = $(el); + else + $(el).parents().each(function(){ + if ($(this).data("layoutRole")) { + $P = $(this); + return false; // BREAK + } + }); + if (!$P || !$P.length) return; // INVALID + + var + pane = $P.data("layoutEdge") + , s = state[pane] + ; + + // if pane is already raised, then reset it before doing it again! + // this would happen if allowOverflow is attached to BOTH the pane and an element + if (s.cssSaved) + resetOverflow(pane); // reset previous CSS before continuing + + // if pane is raised by sliding or resizing, or its closed, then abort + if (s.isSliding || s.isResizing || s.isClosed) { + s.cssSaved = false; + return; + } + + var + newCSS = { zIndex: (options.zIndexes.resizer_normal + 1) } + , curCSS = {} + , of = $P.css("overflow") + , ofX = $P.css("overflowX") + , ofY = $P.css("overflowY") + ; + // determine which, if any, overflow settings need to be changed + if (of != "visible") { + curCSS.overflow = of; + newCSS.overflow = "visible"; + } + if (ofX && !ofX.match(/(visible|auto)/)) { + curCSS.overflowX = ofX; + newCSS.overflowX = "visible"; + } + if (ofY && !ofY.match(/(visible|auto)/)) { + curCSS.overflowY = ofX; + newCSS.overflowY = "visible"; + } + + // save the current overflow settings - even if blank! + s.cssSaved = curCSS; + + // apply new CSS to raise zIndex and, if necessary, make overflow 'visible' + $P.css( newCSS ); + + // make sure the zIndex of all other panes is normal + $.each(_c.allPanes, function(i, p) { + if (p != pane) resetOverflow(p); + }); + + }; + /** + * @param {Object=} [el] (optional) Can also be 'bound' to a click, mouseOver, or other event + */ + function resetOverflow (el) { + if (!isInitialized()) return; + if (this && this.tagName) el = this; // BOUND to element + var $P; + if (isStr(el)) + $P = $Ps[el]; + else if ($(el).data("layoutRole")) + $P = $(el); + else + $(el).parents().each(function(){ + if ($(this).data("layoutRole")) { + $P = $(this); + return false; // BREAK + } + }); + if (!$P || !$P.length) return; // INVALID + + var + pane = $P.data("layoutEdge") + , s = state[pane] + , CSS = s.cssSaved || {} + ; + // reset the zIndex + if (!s.isSliding && !s.isResizing) + $P.css("zIndex", options.zIndexes.pane_normal); + + // reset Overflow - if necessary + $P.css( CSS ); + + // clear var + s.cssSaved = false; + }; + +/* + * ##################### + * CREATE/RETURN LAYOUT + * ##################### + */ + + // validate that container exists + var $N = $(this).eq(0); // FIRST matching Container element + if (!$N.length) { + return _log( options.errors.containerMissing ); + }; + + // Users retrieve Instance of a layout with: $N.layout() OR $N.data("layout") + // return the Instance-pointer if layout has already been initialized + if ($N.data("layoutContainer") && $N.data("layout")) + return $N.data("layout"); // cached pointer + + // init global vars + var + $Ps = {} // Panes x5 - set in initPanes() + , $Cs = {} // Content x5 - set in initPanes() + , $Rs = {} // Resizers x4 - set in initHandles() + , $Ts = {} // Togglers x4 - set in initHandles() + , $Ms = $([]) // Masks - up to 2 masks per pane (IFRAME + DIV) + // aliases for code brevity + , sC = state.container // alias for easy access to 'container dimensions' + , sID = state.id // alias for unique layout ID/namespace - eg: "layout435" + ; + + // create Instance object to expose data & option Properties, and primary action Methods + var Instance = { + // layout data + options: options // property - options hash + , state: state // property - dimensions hash + // object pointers + , container: $N // property - object pointers for layout container + , panes: $Ps // property - object pointers for ALL Panes: panes.north, panes.center + , contents: $Cs // property - object pointers for ALL Content: contents.north, contents.center + , resizers: $Rs // property - object pointers for ALL Resizers, eg: resizers.north + , togglers: $Ts // property - object pointers for ALL Togglers, eg: togglers.north + // border-pane open/close + , hide: hide // method - ditto + , show: show // method - ditto + , toggle: toggle // method - pass a 'pane' ("north", "west", etc) + , open: open // method - ditto + , close: close // method - ditto + , slideOpen: slideOpen // method - ditto + , slideClose: slideClose // method - ditto + , slideToggle: slideToggle // method - ditto + // pane actions + , setSizeLimits: setSizeLimits // method - pass a 'pane' - update state min/max data + , _sizePane: sizePane // method -intended for user by plugins only! + , sizePane: manualSizePane // method - pass a 'pane' AND an 'outer-size' in pixels or percent, or 'auto' + , sizeContent: sizeContent // method - pass a 'pane' + , swapPanes: swapPanes // method - pass TWO 'panes' - will swap them + , showMasks: showMasks // method - pass a 'pane' OR list of panes - default = all panes with mask option set + , hideMasks: hideMasks // method - ditto' + // pane element methods + , initContent: initContent // method - ditto + , addPane: addPane // method - pass a 'pane' + , removePane: removePane // method - pass a 'pane' to remove from layout, add 'true' to delete the pane-elem + , createChildren: createChildren // method - pass a 'pane' and (optional) layout-options (OVERRIDES options[pane].children + , refreshChildren: refreshChildren // method - pass a 'pane' and a layout-instance + // special pane option setting + , enableClosable: enableClosable // method - pass a 'pane' + , disableClosable: disableClosable // method - ditto + , enableSlidable: enableSlidable // method - ditto + , disableSlidable: disableSlidable // method - ditto + , enableResizable: enableResizable // method - ditto + , disableResizable: disableResizable// method - ditto + // utility methods for panes + , allowOverflow: allowOverflow // utility - pass calling element (this) + , resetOverflow: resetOverflow // utility - ditto + // layout control + , destroy: destroy // method - no parameters + , initPanes: isInitialized // method - no parameters + , resizeAll: resizeAll // method - no parameters + // callback triggering + , runCallbacks: _runCallbacks // method - pass evtName & pane (if a pane-event), eg: trigger("onopen", "west") + // alias collections of options, state and children - created in addPane and extended elsewhere + , hasParentLayout: false // set by initContainer() + , children: children // pointers to child-layouts, eg: Instance.children.west.layoutName + , north: false // alias group: { name: pane, pane: $Ps[pane], options: options[pane], state: state[pane], children: children[pane] } + , south: false // ditto + , west: false // ditto + , east: false // ditto + , center: false // ditto + }; + + // create the border layout NOW + if (_create() === 'cancel') // onload_start callback returned false to CANCEL layout creation + return null; + else // true OR false -- if layout-elements did NOT init (hidden or do not exist), can auto-init later + return Instance; // return the Instance object + +} + + +})( jQuery ); +// END Layout - keep internal vars internal! + + + +// START Plugins - shared wrapper, no global vars +(function ($) { + + +/** + * jquery.layout.state 1.0 + * $Date: 2011-07-16 08:00:00 (Sat, 16 July 2011) $ + * + * Copyright (c) 2012 + * Kevin Dalman (http://allpro.net) + * + * Dual licensed under the GPL (http://www.gnu.org/licenses/gpl.html) + * and MIT (http://www.opensource.org/licenses/mit-license.php) licenses. + * + * @requires: UI Layout 1.3.0.rc30.1 or higher + * @requires: $.ui.cookie (above) + * + * @see: http://groups.google.com/group/jquery-ui-layout + */ +/* + * State-management options stored in options.stateManagement, which includes a .cookie hash + * Default options saves ALL KEYS for ALL PANES, ie: pane.size, pane.isClosed, pane.isHidden + * + * // STATE/COOKIE OPTIONS + * @example $(el).layout({ + stateManagement: { + enabled: true + , stateKeys: "east.size,west.size,east.isClosed,west.isClosed" + , cookie: { name: "appLayout", path: "/" } + } + }) + * @example $(el).layout({ stateManagement__enabled: true }) // enable auto-state-management using cookies + * @example $(el).layout({ stateManagement__cookie: { name: "appLayout", path: "/" } }) + * @example $(el).layout({ stateManagement__cookie__name: "appLayout", stateManagement__cookie__path: "/" }) + * + * // STATE/COOKIE METHODS + * @example myLayout.saveCookie( "west.isClosed,north.size,south.isHidden", {expires: 7} ); + * @example myLayout.loadCookie(); + * @example myLayout.deleteCookie(); + * @example var JSON = myLayout.readState(); // CURRENT Layout State + * @example var JSON = myLayout.readCookie(); // SAVED Layout State (from cookie) + * @example var JSON = myLayout.state.stateData; // LAST LOADED Layout State (cookie saved in layout.state hash) + * + * CUSTOM STATE-MANAGEMENT (eg, saved in a database) + * @example var JSON = myLayout.readState( "west.isClosed,north.size,south.isHidden" ); + * @example myLayout.loadState( JSON ); + */ + +/** + * UI COOKIE UTILITY + * + * A $.cookie OR $.ui.cookie namespace *should be standard*, but until then... + * This creates $.ui.cookie so Layout does not need the cookie.jquery.js plugin + * NOTE: This utility is REQUIRED by the layout.state plugin + * + * Cookie methods in Layout are created as part of State Management + */ +if (!$.ui) $.ui = {}; +$.ui.cookie = { + + // cookieEnabled is not in DOM specs, but DOES works in all browsers,including IE6 + acceptsCookies: !!navigator.cookieEnabled + +, read: function (name) { + var c = document.cookie + , cs = c ? c.split(';') : [] + , pair // loop var + ; + for (var i=0, n=cs.length; i < n; i++) { + pair = $.trim(cs[i]).split('='); // name=value pair + if (pair[0] == name) // found the layout cookie + return decodeURIComponent(pair[1]); + } + return null; + } + +, write: function (name, val, cookieOpts) { + var params = "" + , date = "" + , clear = false + , o = cookieOpts || {} + , x = o.expires || null + , t = $.type(x) + ; + if (t === "date") + date = x; + else if (t === "string" && x > 0) { + x = parseInt(x,10); + t = "number"; + } + if (t === "number") { + date = new Date(); + if (x > 0) + date.setDate(date.getDate() + x); + else { + date.setFullYear(1970); + clear = true; + } + } + if (date) params += ";expires="+ date.toUTCString(); + if (o.path) params += ";path="+ o.path; + if (o.domain) params += ";domain="+ o.domain; + if (o.secure) params += ";secure"; + document.cookie = name +"="+ (clear ? "" : encodeURIComponent( val )) + params; // write or clear cookie + } + +, clear: function (name) { + $.ui.cookie.write(name, "", {expires: -1}); + } + +}; +// if cookie.jquery.js is not loaded, create an alias to replicate it +// this may be useful to other plugins or code dependent on that plugin +if (!$.cookie) $.cookie = function (k, v, o) { + var C = $.ui.cookie; + if (v === null) + C.clear(k); + else if (v === undefined) + return C.read(k); + else + C.write(k, v, o); +}; + + +// tell Layout that the state plugin is available +$.layout.plugins.stateManagement = true; + +// Add State-Management options to layout.defaults +$.layout.config.optionRootKeys.push("stateManagement"); +$.layout.defaults.stateManagement = { + enabled: false // true = enable state-management, even if not using cookies +, autoSave: true // Save a state-cookie when page exits? +, autoLoad: true // Load the state-cookie when Layout inits? +, animateLoad: true // animate panes when loading state into an active layout +, includeChildren: true // recurse into child layouts to include their state as well + // List state-data to save - must be pane-specific +, stateKeys: "north.size,south.size,east.size,west.size,"+ + "north.isClosed,south.isClosed,east.isClosed,west.isClosed,"+ + "north.isHidden,south.isHidden,east.isHidden,west.isHidden" +, cookie: { + name: "" // If not specified, will use Layout.name, else just "Layout" + , domain: "" // blank = current domain + , path: "" // blank = current page, "/" = entire website + , expires: "" // 'days' to keep cookie - leave blank for 'session cookie' + , secure: false + } +}; +// Set stateManagement as a layout-option, NOT a pane-option +$.layout.optionsMap.layout.push("stateManagement"); + +/* + * State Management methods + */ +$.layout.state = { + + /** + * Get the current layout state and save it to a cookie + * + * myLayout.saveCookie( keys, cookieOpts ) + * + * @param {Object} inst + * @param {(string|Array)=} keys + * @param {Object=} cookieOpts + */ + saveCookie: function (inst, keys, cookieOpts) { + var o = inst.options + , sm = o.stateManagement + , oC = $.extend(true, {}, sm.cookie, cookieOpts || null) + , data = inst.state.stateData = inst.readState( keys || sm.stateKeys ) // read current panes-state + ; + $.ui.cookie.write( oC.name || o.name || "Layout", $.layout.state.encodeJSON(data), oC ); + return $.extend(true, {}, data); // return COPY of state.stateData data + } + + /** + * Remove the state cookie + * + * @param {Object} inst + */ +, deleteCookie: function (inst) { + var o = inst.options; + $.ui.cookie.clear( o.stateManagement.cookie.name || o.name || "Layout" ); + } + + /** + * Read & return data from the cookie - as JSON + * + * @param {Object} inst + */ +, readCookie: function (inst) { + var o = inst.options; + var c = $.ui.cookie.read( o.stateManagement.cookie.name || o.name || "Layout" ); + // convert cookie string back to a hash and return it + return c ? $.layout.state.decodeJSON(c) : {}; + } + + /** + * Get data from the cookie and USE IT to loadState + * + * @param {Object} inst + */ +, loadCookie: function (inst) { + var c = $.layout.state.readCookie(inst); // READ the cookie + if (c) { + inst.state.stateData = $.extend(true, {}, c); // SET state.stateData + inst.loadState(c); // LOAD the retrieved state + } + return c; + } + + /** + * Update layout options from the cookie, if one exists + * + * @param {Object} inst + * @param {Object=} stateData + * @param {boolean=} animate + */ +, loadState: function (inst, data, opts) { + if (!$.isPlainObject( data ) || $.isEmptyObject( data )) return; + + // normalize data & cache in the state object + data = inst.state.stateData = $.layout.transformData( data ); // panes = default subkey + + // add missing/default state-restore options + var smo = inst.options.stateManagement; + opts = $.extend({ + animateLoad: false //smo.animateLoad + , includeChildren: smo.includeChildren + }, opts ); + + if (!inst.state.initialized) { + /* + * layout NOT initialized, so just update its options + */ + // MUST remove pane.children keys before applying to options + // use a copy so we don't remove keys from original data + var o = $.extend(true, {}, data); + //delete o.center; // center has no state-data - only children + $.each($.layout.config.allPanes, function (idx, pane) { + if (o[pane]) delete o[pane].children; + }); + // update CURRENT layout-options with saved state data + $.extend(true, inst.options, o); + } + else { + /* + * layout already initialized, so modify layout's configuration + */ + var noAnimate = !opts.animateLoad + , o, c, h, state, open + ; + $.each($.layout.config.borderPanes, function (idx, pane) { + o = data[ pane ]; + if (!$.isPlainObject( o )) return; // no key, skip pane + + s = o.size; + c = o.initClosed; + h = o.initHidden; + ar = o.autoResize + state = inst.state[pane]; + open = state.isVisible; + + // reset autoResize + if (ar) + state.autoResize = ar; + // resize BEFORE opening + if (!open) + inst._sizePane(pane, s, false, false, false); // false=skipCallback/noAnimation/forceResize + // open/close as necessary - DO NOT CHANGE THIS ORDER! + if (h === true) inst.hide(pane, noAnimate); + else if (c === true) inst.close(pane, false, noAnimate); + else if (c === false) inst.open (pane, false, noAnimate); + else if (h === false) inst.show (pane, false, noAnimate); + // resize AFTER any other actions + if (open) + inst._sizePane(pane, s, false, false, noAnimate); // animate resize if option passed + }); + + /* + * RECURSE INTO CHILD-LAYOUTS + */ + if (opts.includeChildren) { + var paneStateChildren, childState; + $.each(inst.children, function (pane, paneChildren) { + paneStateChildren = data[pane] ? data[pane].children : 0; + if (paneStateChildren && paneChildren) { + $.each(paneChildren, function (stateKey, child) { + childState = paneStateChildren[stateKey]; + if (child && childState) + child.loadState( childState ); + }); + } + }); + } + } + } + + /** + * Get the *current layout state* and return it as a hash + * + * @param {Object=} inst // Layout instance to get state for + * @param {object=} [opts] // State-Managements override options + */ +, readState: function (inst, opts) { + // backward compatility + if ($.type(opts) === 'string') opts = { keys: opts }; + if (!opts) opts = {}; + var sm = inst.options.stateManagement + , ic = opts.includeChildren + , recurse = ic !== undefined ? ic : sm.includeChildren + , keys = opts.stateKeys || sm.stateKeys + , alt = { isClosed: 'initClosed', isHidden: 'initHidden' } + , state = inst.state + , panes = $.layout.config.allPanes + , data = {} + , pair, pane, key, val + , ps, pC, child, array, count, branch + ; + if ($.isArray(keys)) keys = keys.join(","); + // convert keys to an array and change delimiters from '__' to '.' + keys = keys.replace(/__/g, ".").split(','); + // loop keys and create a data hash + for (var i=0, n=keys.length; i < n; i++) { + pair = keys[i].split("."); + pane = pair[0]; + key = pair[1]; + if ($.inArray(pane, panes) < 0) continue; // bad pane! + val = state[ pane ][ key ]; + if (val == undefined) continue; + if (key=="isClosed" && state[pane]["isSliding"]) + val = true; // if sliding, then *really* isClosed + ( data[pane] || (data[pane]={}) )[ alt[key] ? alt[key] : key ] = val; + } + + // recurse into the child-layouts for each pane + if (recurse) { + $.each(panes, function (idx, pane) { + pC = inst.children[pane]; + ps = state.stateData[pane]; + if ($.isPlainObject( pC ) && !$.isEmptyObject( pC )) { + // ensure a key exists for this 'pane', eg: branch = data.center + branch = data[pane] || (data[pane] = {}); + if (!branch.children) branch.children = {}; + $.each( pC, function (key, child) { + // ONLY read state from an initialize layout + if ( child.state.initialized ) + branch.children[ key ] = $.layout.state.readState( child ); + // if we have PREVIOUS (onLoad) state for this child-layout, KEEP IT! + else if ( ps && ps.children && ps.children[ key ] ) { + branch.children[ key ] = $.extend(true, {}, ps.children[ key ] ); + } + }); + } + }); + } + + return data; + } + + /** + * Stringify a JSON hash so can save in a cookie or db-field + */ +, encodeJSON: function (JSON) { + return parse(JSON); + function parse (h) { + var D=[], i=0, k, v, t // k = key, v = value + , a = $.isArray(h) + ; + for (k in h) { + v = h[k]; + t = typeof v; + if (t == 'string') // STRING - add quotes + v = '"'+ v +'"'; + else if (t == 'object') // SUB-KEY - recurse into it + v = parse(v); + D[i++] = (!a ? '"'+ k +'":' : '') + v; + } + return (a ? '[' : '{') + D.join(',') + (a ? ']' : '}'); + }; + } + + /** + * Convert stringified JSON back to a hash object + * @see $.parseJSON(), adding in jQuery 1.4.1 + */ +, decodeJSON: function (str) { + try { return $.parseJSON ? $.parseJSON(str) : window["eval"]("("+ str +")") || {}; } + catch (e) { return {}; } + } + + +, _create: function (inst) { + var _ = $.layout.state + , o = inst.options + , sm = o.stateManagement + ; + // ADD State-Management plugin methods to inst + $.extend( inst, { + // readCookie - update options from cookie - returns hash of cookie data + readCookie: function () { return _.readCookie(inst); } + // deleteCookie + , deleteCookie: function () { _.deleteCookie(inst); } + // saveCookie - optionally pass keys-list and cookie-options (hash) + , saveCookie: function (keys, cookieOpts) { return _.saveCookie(inst, keys, cookieOpts); } + // loadCookie - readCookie and use to loadState() - returns hash of cookie data + , loadCookie: function () { return _.loadCookie(inst); } + // loadState - pass a hash of state to use to update options + , loadState: function (stateData, opts) { _.loadState(inst, stateData, opts); } + // readState - returns hash of current layout-state + , readState: function (keys) { return _.readState(inst, keys); } + // add JSON utility methods too... + , encodeJSON: _.encodeJSON + , decodeJSON: _.decodeJSON + }); + + // init state.stateData key, even if plugin is initially disabled + inst.state.stateData = {}; + + // autoLoad MUST BE one of: data-array, data-hash, callback-function, or TRUE + if ( !sm.autoLoad ) return; + + // When state-data exists in the autoLoad key USE IT, + // even if stateManagement.enabled == false + if ($.isPlainObject( sm.autoLoad )) { + if (!$.isEmptyObject( sm.autoLoad )) { + inst.loadState( sm.autoLoad ); + } + } + else if ( sm.enabled ) { + // update the options from cookie or callback + // if options is a function, call it to get stateData + if ($.isFunction( sm.autoLoad )) { + var d = {}; + try { + d = sm.autoLoad( inst, inst.state, inst.options, inst.options.name || '' ); // try to get data from fn + } catch (e) {} + if (d && $.isPlainObject( d ) && !$.isEmptyObject( d )) + inst.loadState(d); + } + else // any other truthy value will trigger loadCookie + inst.loadCookie(); + } + } + +, _unload: function (inst) { + var sm = inst.options.stateManagement; + if (sm.enabled && sm.autoSave) { + // if options is a function, call it to save the stateData + if ($.isFunction( sm.autoSave )) { + try { + sm.autoSave( inst, inst.state, inst.options, inst.options.name || '' ); // try to get data from fn + } catch (e) {} + } + else // any truthy value will trigger saveCookie + inst.saveCookie(); + } + } + +}; + +// add state initialization method to Layout's onCreate array of functions +$.layout.onCreate.push( $.layout.state._create ); +$.layout.onUnload.push( $.layout.state._unload ); + + + + +/** + * jquery.layout.buttons 1.0 + * $Date: 2011-07-16 08:00:00 (Sat, 16 July 2011) $ + * + * Copyright (c) 2012 + * Kevin Dalman (http://allpro.net) + * + * Dual licensed under the GPL (http://www.gnu.org/licenses/gpl.html) + * and MIT (http://www.opensource.org/licenses/mit-license.php) licenses. + * + * @requires: UI Layout 1.3.0.rc30.1 or higher + * + * @see: http://groups.google.com/group/jquery-ui-layout + * + * Docs: [ to come ] + * Tips: [ to come ] + */ + +// tell Layout that the state plugin is available +$.layout.plugins.buttons = true; + +// Add buttons options to layout.defaults +$.layout.defaults.autoBindCustomButtons = false; +// Specify autoBindCustomButtons as a layout-option, NOT a pane-option +$.layout.optionsMap.layout.push("autoBindCustomButtons"); + +/* + * Button methods + */ +$.layout.buttons = { + + /** + * Searches for .ui-layout-button-xxx elements and auto-binds them as layout-buttons + * + * @see _create() + * + * @param {Object} inst Layout Instance object + */ + init: function (inst) { + var pre = "ui-layout-button-" + , layout = inst.options.name || "" + , name; + $.each("toggle,open,close,pin,toggle-slide,open-slide".split(","), function (i, action) { + $.each($.layout.config.borderPanes, function (ii, pane) { + $("."+pre+action+"-"+pane).each(function(){ + // if button was previously 'bound', data.layoutName was set, but is blank if layout has no 'name' + name = $(this).data("layoutName") || $(this).attr("layoutName"); + if (name == undefined || name === layout) + inst.bindButton(this, action, pane); + }); + }); + }); + } + + /** + * Helper function to validate params received by addButton utilities + * + * Two classes are added to the element, based on the buttonClass... + * The type of button is appended to create the 2nd className: + * - ui-layout-button-pin // action btnClass + * - ui-layout-button-pin-west // action btnClass + pane + * - ui-layout-button-toggle + * - ui-layout-button-open + * - ui-layout-button-close + * + * @param {Object} inst Layout Instance object + * @param {(string|!Object)} selector jQuery selector (or element) for button, eg: ".ui-layout-north .toggle-button" + * @param {string} pane Name of the pane the button is for: 'north', 'south', etc. + * + * @return {Array.} If both params valid, the element matching 'selector' in a jQuery wrapper - otherwise returns null + */ +, get: function (inst, selector, pane, action) { + var $E = $(selector) + , o = inst.options + , err = o.errors.addButtonError + ; + if (!$E.length) { // element not found + $.layout.msg(err +" "+ o.errors.selector +": "+ selector, true); + } + else if ($.inArray(pane, $.layout.config.borderPanes) < 0) { // invalid 'pane' sepecified + $.layout.msg(err +" "+ o.errors.pane +": "+ pane, true); + $E = $(""); // NO BUTTON + } + else { // VALID + var btn = o[pane].buttonClass +"-"+ action; + $E .addClass( btn +" "+ btn +"-"+ pane ) + .data("layoutName", o.name); // add layout identifier - even if blank! + } + return $E; + } + + + /** + * NEW syntax for binding layout-buttons - will eventually replace addToggle, addOpen, etc. + * + * @param {Object} inst Layout Instance object + * @param {(string|!Object)} selector jQuery selector (or element) for button, eg: ".ui-layout-north .toggle-button" + * @param {string} action + * @param {string} pane + */ +, bind: function (inst, selector, action, pane) { + var _ = $.layout.buttons; + switch (action.toLowerCase()) { + case "toggle": _.addToggle (inst, selector, pane); break; + case "open": _.addOpen (inst, selector, pane); break; + case "close": _.addClose (inst, selector, pane); break; + case "pin": _.addPin (inst, selector, pane); break; + case "toggle-slide": _.addToggle (inst, selector, pane, true); break; + case "open-slide": _.addOpen (inst, selector, pane, true); break; + } + return inst; + } + + /** + * Add a custom Toggler button for a pane + * + * @param {Object} inst Layout Instance object + * @param {(string|!Object)} selector jQuery selector (or element) for button, eg: ".ui-layout-north .toggle-button" + * @param {string} pane Name of the pane the button is for: 'north', 'south', etc. + * @param {boolean=} slide true = slide-open, false = pin-open + */ +, addToggle: function (inst, selector, pane, slide) { + $.layout.buttons.get(inst, selector, pane, "toggle") + .click(function(evt){ + inst.toggle(pane, !!slide); + evt.stopPropagation(); + }); + return inst; + } + + /** + * Add a custom Open button for a pane + * + * @param {Object} inst Layout Instance object + * @param {(string|!Object)} selector jQuery selector (or element) for button, eg: ".ui-layout-north .toggle-button" + * @param {string} pane Name of the pane the button is for: 'north', 'south', etc. + * @param {boolean=} slide true = slide-open, false = pin-open + */ +, addOpen: function (inst, selector, pane, slide) { + $.layout.buttons.get(inst, selector, pane, "open") + .attr("title", inst.options[pane].tips.Open) + .click(function (evt) { + inst.open(pane, !!slide); + evt.stopPropagation(); + }); + return inst; + } + + /** + * Add a custom Close button for a pane + * + * @param {Object} inst Layout Instance object + * @param {(string|!Object)} selector jQuery selector (or element) for button, eg: ".ui-layout-north .toggle-button" + * @param {string} pane Name of the pane the button is for: 'north', 'south', etc. + */ +, addClose: function (inst, selector, pane) { + $.layout.buttons.get(inst, selector, pane, "close") + .attr("title", inst.options[pane].tips.Close) + .click(function (evt) { + inst.close(pane); + evt.stopPropagation(); + }); + return inst; + } + + /** + * Add a custom Pin button for a pane + * + * Four classes are added to the element, based on the paneClass for the associated pane... + * Assuming the default paneClass and the pin is 'up', these classes are added for a west-pane pin: + * - ui-layout-pane-pin + * - ui-layout-pane-west-pin + * - ui-layout-pane-pin-up + * - ui-layout-pane-west-pin-up + * + * @param {Object} inst Layout Instance object + * @param {(string|!Object)} selector jQuery selector (or element) for button, eg: ".ui-layout-north .toggle-button" + * @param {string} pane Name of the pane the pin is for: 'north', 'south', etc. + */ +, addPin: function (inst, selector, pane) { + var _ = $.layout.buttons + , $E = _.get(inst, selector, pane, "pin"); + if ($E.length) { + var s = inst.state[pane]; + $E.click(function (evt) { + _.setPinState(inst, $(this), pane, (s.isSliding || s.isClosed)); + if (s.isSliding || s.isClosed) inst.open( pane ); // change from sliding to open + else inst.close( pane ); // slide-closed + evt.stopPropagation(); + }); + // add up/down pin attributes and classes + _.setPinState(inst, $E, pane, (!s.isClosed && !s.isSliding)); + // add this pin to the pane data so we can 'sync it' automatically + // PANE.pins key is an array so we can store multiple pins for each pane + s.pins.push( selector ); // just save the selector string + } + return inst; + } + + /** + * Change the class of the pin button to make it look 'up' or 'down' + * + * @see addPin(), syncPins() + * + * @param {Object} inst Layout Instance object + * @param {Array.} $Pin The pin-span element in a jQuery wrapper + * @param {string} pane These are the params returned to callbacks by layout() + * @param {boolean} doPin true = set the pin 'down', false = set it 'up' + */ +, setPinState: function (inst, $Pin, pane, doPin) { + var updown = $Pin.attr("pin"); + if (updown && doPin === (updown=="down")) return; // already in correct state + var + o = inst.options[pane] + , pin = o.buttonClass +"-pin" + , side = pin +"-"+ pane + , UP = pin +"-up "+ side +"-up" + , DN = pin +"-down "+side +"-down" + ; + $Pin + .attr("pin", doPin ? "down" : "up") // logic + .attr("title", doPin ? o.tips.Unpin : o.tips.Pin) + .removeClass( doPin ? UP : DN ) + .addClass( doPin ? DN : UP ) + ; + } + + /** + * INTERNAL function to sync 'pin buttons' when pane is opened or closed + * Unpinned means the pane is 'sliding' - ie, over-top of the adjacent panes + * + * @see open(), close() + * + * @param {Object} inst Layout Instance object + * @param {string} pane These are the params returned to callbacks by layout() + * @param {boolean} doPin True means set the pin 'down', False means 'up' + */ +, syncPinBtns: function (inst, pane, doPin) { + // REAL METHOD IS _INSIDE_ LAYOUT - THIS IS HERE JUST FOR REFERENCE + $.each(inst.state[pane].pins, function (i, selector) { + $.layout.buttons.setPinState(inst, $(selector), pane, doPin); + }); + } + + +, _load: function (inst) { + var _ = $.layout.buttons; + // ADD Button methods to Layout Instance + // Note: sel = jQuery Selector string + $.extend( inst, { + bindButton: function (sel, action, pane) { return _.bind(inst, sel, action, pane); } + // DEPRECATED METHODS + , addToggleBtn: function (sel, pane, slide) { return _.addToggle(inst, sel, pane, slide); } + , addOpenBtn: function (sel, pane, slide) { return _.addOpen(inst, sel, pane, slide); } + , addCloseBtn: function (sel, pane) { return _.addClose(inst, sel, pane); } + , addPinBtn: function (sel, pane) { return _.addPin(inst, sel, pane); } + }); + + // init state array to hold pin-buttons + for (var i=0; i<4; i++) { + var pane = $.layout.config.borderPanes[i]; + inst.state[pane].pins = []; + } + + // auto-init buttons onLoad if option is enabled + if ( inst.options.autoBindCustomButtons ) + _.init(inst); + } + +, _unload: function (inst) { + // TODO: unbind all buttons??? + } + +}; + +// add initialization method to Layout's onLoad array of functions +$.layout.onLoad.push( $.layout.buttons._load ); +//$.layout.onUnload.push( $.layout.buttons._unload ); + + + +/** + * jquery.layout.browserZoom 1.0 + * $Date: 2011-12-29 08:00:00 (Thu, 29 Dec 2011) $ + * + * Copyright (c) 2012 + * Kevin Dalman (http://allpro.net) + * + * Dual licensed under the GPL (http://www.gnu.org/licenses/gpl.html) + * and MIT (http://www.opensource.org/licenses/mit-license.php) licenses. + * + * @requires: UI Layout 1.3.0.rc30.1 or higher + * + * @see: http://groups.google.com/group/jquery-ui-layout + * + * TODO: Extend logic to handle other problematic zooming in browsers + * TODO: Add hotkey/mousewheel bindings to _instantly_ respond to these zoom event + */ + +// tell Layout that the plugin is available +$.layout.plugins.browserZoom = true; + +$.layout.defaults.browserZoomCheckInterval = 1000; +$.layout.optionsMap.layout.push("browserZoomCheckInterval"); + +/* + * browserZoom methods + */ +$.layout.browserZoom = { + + _init: function (inst) { + // abort if browser does not need this check + if ($.layout.browserZoom.ratio() !== false) + $.layout.browserZoom._setTimer(inst); + } + +, _setTimer: function (inst) { + // abort if layout destroyed or browser does not need this check + if (inst.destroyed) return; + var o = inst.options + , s = inst.state + // don't need check if inst has parentLayout, but check occassionally in case parent destroyed! + // MINIMUM 100ms interval, for performance + , ms = inst.hasParentLayout ? 5000 : Math.max( o.browserZoomCheckInterval, 100 ) + ; + // set the timer + setTimeout(function(){ + if (inst.destroyed || !o.resizeWithWindow) return; + var d = $.layout.browserZoom.ratio(); + if (d !== s.browserZoom) { + s.browserZoom = d; + inst.resizeAll(); + } + // set a NEW timeout + $.layout.browserZoom._setTimer(inst); + } + , ms ); + } + +, ratio: function () { + var w = window + , s = screen + , d = document + , dE = d.documentElement || d.body + , b = $.layout.browser + , v = b.version + , r, sW, cW + ; + // we can ignore all browsers that fire window.resize event onZoom + if ((b.msie && v > 8) + || !b.msie + ) return false; // don't need to track zoom + + if (s.deviceXDPI && s.systemXDPI) // syntax compiler hack + return calc(s.deviceXDPI, s.systemXDPI); + // everything below is just for future reference! + if (b.webkit && (r = d.body.getBoundingClientRect)) + return calc((r.left - r.right), d.body.offsetWidth); + if (b.webkit && (sW = w.outerWidth)) + return calc(sW, w.innerWidth); + if ((sW = s.width) && (cW = dE.clientWidth)) + return calc(sW, cW); + return false; // no match, so cannot - or don't need to - track zoom + + function calc (x,y) { return (parseInt(x,10) / parseInt(y,10) * 100).toFixed(); } + } + +}; +// add initialization method to Layout's onLoad array of functions +$.layout.onReady.push( $.layout.browserZoom._init ); + + +})( jQuery ); \ No newline at end of file diff --git a/client/js/libs/tag-it.js b/client/js/libs/tag-it.js new file mode 100644 index 000000000..60a41ee00 --- /dev/null +++ b/client/js/libs/tag-it.js @@ -0,0 +1,386 @@ +/* +* 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: { + itemName : 'item', + fieldName : 'tags', + availableTags : [], + tagSource : null, + removeConfirmation: false, + caseSensitive : true, + + // When enabled, quotes are not neccesary + // for inputting multi-word tags. + allowSpaces: false, + + // Whether to animate tag removals or not. + animate: true, + + // 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, + + 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, + // ignoring settings.itemName. + singleFieldNode: null, + + // Optionally set a tabindex attribute on the input that gets + // created for tag-it. + tabIndex: null, + + + // Event callbacks. + onTagAdded : null, + onTagRemoved: null, + onTagClicked: null + }, + + + _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.css('display', 'none'); + } else { + this.tagList = this.element.find('ul, ol').andSelf().last(); + } + + this._tagInput = $('').addClass('ui-widget-content'); + if (this.options.tabIndex) { + this._tagInput.attr('tabindex', this.options.tabIndex); + } + + this.options.tagSource = this.options.tagSource || 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); + }); + showChoices(this._subtractArray(choices, this.assignedTags())); + }; + + // Bind tagSource callback functions to this context. + if ($.isFunction(this.options.tagSource)) { + this.options.tagSource = $.proxy(this.options.tagSource, this); + } + + this.tagList + .addClass('tagit unstyled') + .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')) { + that._trigger('onTagClicked', e, target.closest('.tagit-choice')); + } 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(); + } + }); + + // Add existing tags from the list, if any. + this.tagList.children('li').each(function() { + if (!$(this).hasClass('tagit-new')) { + that.createTag($(this).html(), $(this).attr('class')); + $(this).remove(); + } + }); + + // Single field support. + 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); + }); + } else { + // Create our single field input after our list. + this.options.singleFieldNode = this.tagList.after(''); + } + } + + // 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.COMMA || + 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 + ) + ) + ) + ) { + event.preventDefault(); + that.createTag(that._cleanedInput()); + + // The autocomplete doesn't close automatically when TAB is pressed. + // So let's ensure that it closes. + that._tagInput.autocomplete('close'); + } + }).blur(function(e){ + // Create a tag when the element loses focus (unless it's empty). + that.createTag(that._cleanedInput()); + }); + + + // Autocomplete. + if (this.options.availableTags || this.options.tagSource) { + this._tagInput.autocomplete({ + source: this.options.tagSource, + select: function(event, ui) { + // Delete the last tag if we autocomplete something despite the input being empty + // This happens because the input's blur event causes the tag to be created when + // the user clicks an autocomplete item. + // The only artifact of this is that while the user holds down the mouse button + // on the selected autocomplete item, a tag is shown with the pre-autocompleted text, + // and is changed to the autocompleted text upon mouseup. + if (that._tagInput.val() === '') { + that.removeTag(that._lastTag(), false); + } + that.createTag(ui.item.value); + // Preventing the tag input to be updated with the chosen value. + return false; + } + }); + } + }, + + _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.children('.tagit-choice:last'); + }, + + 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.tagList.children('.tagit-choice').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)).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).children('.tagit-label').text(); + } else { + return $(tag).children('input').val(); + } + }, + + _isNew: function(value) { + var that = this; + var isNew = true; + this.tagList.children('.tagit-choice').each(function(i) { + if (that._formatStr(value) == that._formatStr(that.tagLabel(this))) { + isNew = false; + return false; + } + }); + return isNew; + }, + + _formatStr: function(str) { + if (this.options.caseSensitive) { + return str; + } + return $.trim(str.toLowerCase()); + }, + + createTag: function(value, additionalClass) { + var that = this; + // Automatically trims the value of leading and trailing whitespace. + value = $.trim(value); + + if (!this._isNew(value) || value === '') { + 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); + + // Button for removing the tag. + var removeTagIcon = $('') + .addClass('icon-remove'); + 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 tags = this.assignedTags(); + tags.push(value); + this._updateSingleTagsField(tags); + } else { + var escapedValue = label.html(); + tag.append(''); + } + + this._trigger('onTagAdded', null, tag); + + // Cleaning the input. + this._tagInput.val(''); + + // insert tag + this._tagInput.parent().before(tag); + }, + + removeTag: function(tag, animate) { + animate = animate || this.options.animate; + + tag = $(tag); + + this._trigger('onTagRemoved', null, tag); + + if (this.options.singleField) { + var tags = this.assignedTags(); + var removedTagLabel = this.tagLabel(tag); + tags = $.grep(tags, function(el){ + return el != removedTagLabel; + }); + this._updateSingleTagsField(tags); + } + // Animate the removal. + if (animate) { + tag.fadeOut('fast').hide('blind', {direction: 'horizontal'}, 'fast', function(){ + tag.remove(); + }).dequeue(); + } else { + tag.remove(); + } + }, + + removeAll: function() { + // Removes all tags. + var that = this; + this.tagList.children('.tagit-choice').each(function(index, tag) { + that.removeTag(tag, false); + }); + } + + }); + +})(jQuery); diff --git a/client/js/libs/tmpl.min.js b/client/js/libs/tmpl.min.js new file mode 100644 index 000000000..34600d18f --- /dev/null +++ b/client/js/libs/tmpl.min.js @@ -0,0 +1,86 @@ +/* + * JavaScript Templates 2.3.0 + * https://github.com/blueimp/JavaScript-Templates + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + * + * Inspired by John Resig's JavaScript Micro-Templating: + * http://ejohn.org/blog/javascript-micro-templating/ + */ + +/*jslint evil: true, regexp: true, unparam: true */ +/*global document, define */ + +(function ($) { + "use strict"; + var tmpl = function (str, data) { + var f = !/[^\w\-\.:]/.test(str) ? tmpl.cache[str] = tmpl.cache[str] || + tmpl(tmpl.load(str)) : + new Function( + tmpl.arg + ',tmpl', + "var _e=tmpl.encode" + tmpl.helper + ",_s='" + + str.replace(tmpl.regexp, tmpl.func) + + "';return _s;" + ); + return data ? f(data, tmpl) : function (data) { + return f(data, tmpl); + }; + }; + tmpl.cache = {}; + tmpl.load = function (id) { + return document.getElementById(id).innerHTML; + }; + tmpl.regexp = /([\s'\\])(?!(?:[^{]|\{(?!%))*%\})|(?:\{%(=|#)([\s\S]+?)%\})|(\{%)|(%\})/g; + tmpl.func = function (s, p1, p2, p3, p4, p5) { + if (p1) { // whitespace, quote and backspace in HTML context + return { + "\n": "\\n", + "\r": "\\r", + "\t": "\\t", + " " : " " + }[p1] || "\\" + p1; + } + if (p2) { // interpolation: {%=prop%}, or unescaped: {%#prop%} + if (p2 === "=") { + return "'+_e(" + p3 + ")+'"; + } + return "'+" + p3 + "+'"; + } + if (p4) { // evaluation start tag: {% + return "';"; + } + if (p5) { // evaluation end tag: %} + return "_s+='"; + } + }; + tmpl.encReg = /[<>&"'\x00]/g; + tmpl.encMap = { + "<" : "<", + ">" : ">", + "&" : "&", + "\"" : """, + "'" : "'" + }; + tmpl.encode = function (s) { + return String(s).replace( + tmpl.encReg, + function (c) { + return tmpl.encMap[c] || ""; + } + ); + }; + tmpl.arg = "o"; + tmpl.helper = ",print=function(s,e){_s+=e&&(s||'')||_e(s);}" + + ",include=function(s,d){_s+=tmpl(s,d);}"; + if (typeof define === "function" && define.amd) { + define(function () { + return tmpl; + }); + } else { + $.tmpl = tmpl; + } +}(this)); \ No newline at end of file diff --git a/client/js/libs/underscore.js b/client/js/libs/underscore.js new file mode 100644 index 000000000..63f2ecb27 --- /dev/null +++ b/client/js/libs/underscore.js @@ -0,0 +1,1396 @@ +// Underscore.js 1.6.0 +// http://underscorejs.org +// (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Underscore may be freely distributed under the MIT license. + +(function() { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `exports` on the server. + var root = this; + + // Save the previous value of the `_` variable. + var previousUnderscore = root._; + + // Establish the object that gets returned to break out of a loop iteration. + var breaker = {}; + + // Save bytes in the minified (but not gzipped) version: + var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; + + // Create quick reference variables for speed access to core prototypes. + var + push = ArrayProto.push, + slice = ArrayProto.slice, + concat = ArrayProto.concat, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + + // All **ECMAScript 5** native function implementations that we hope to use + // are declared here. + var + nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeBind = FuncProto.bind; + + // Create a safe reference to the Underscore object for use below. + var _ = function(obj) { + if (obj instanceof _) return obj; + if (!(this instanceof _)) return new _(obj); + this._wrapped = obj; + }; + + // Export the Underscore object for **Node.js**, with + // backwards-compatibility for the old `require()` API. If we're in + // the browser, add `_` as a global object via a string identifier, + // for Closure Compiler "advanced" mode. + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = _; + } + exports._ = _; + } else { + root._ = _; + } + + // Current version. + _.VERSION = '1.6.0'; + + // Internal function: creates a callback bound to its context if supplied + var createCallback = function(func, context, argCount) { + if (!context) return func; + switch (argCount == null ? 3 : argCount) { + case 1: return function(value) { + return func.call(context, value); + }; + case 2: return function(value, other) { + return func.call(context, value, other); + }; + case 3: return function(value, index, collection) { + return func.call(context, value, index, collection); + }; + case 4: return function(accumulator, value, index, collection) { + return func.call(context, accumulator, value, index, collection); + }; + } + return function() { + return func.apply(this, arguments); + }; + }; + + // An internal function to generate lookup iterators. + var lookupIterator = function(value, context, argCount) { + if (value == null) return _.identity; + if (_.isFunction(value)) return createCallback(value, context, argCount); + if (_.isObject(value)) return _.matches(value); + return _.property(value); + }; + + // Collection Functions + // -------------------- + + // The cornerstone, an `each` implementation, aka `forEach`. + // Handles raw objects in addition to array-likes. Treats all + // sparse array-likes as if they were dense. + _.each = _.forEach = function(obj, iterator, context) { + var i, length; + if (obj == null) return obj; + iterator = createCallback(iterator, context); + if (obj.length === +obj.length) { + for (i = 0, length = obj.length; i < length; i++) { + if (iterator(obj[i], i, obj) === breaker) break; + } + } else { + var keys = _.keys(obj); + for (i = 0, length = keys.length; i < length; i++) { + if (iterator(obj[keys[i]], keys[i], obj) === breaker) break; + } + } + return obj; + }; + + // Return the results of applying the iterator to each element. + _.map = _.collect = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + iterator = lookupIterator(iterator, context); + _.each(obj, function(value, index, list) { + results.push(iterator(value, index, list)); + }); + return results; + }; + + var reduceError = 'Reduce of empty array with no initial value'; + + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. + _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + iterator = createCallback(iterator, context, 4); + _.each(obj, function(value, index, list) { + if (!initial) { + memo = value; + initial = true; + } else { + memo = iterator(memo, value, index, list); + } + }); + if (!initial) throw TypeError(reduceError); + return memo; + }; + + // The right-associative version of reduce, also known as `foldr`. + _.reduceRight = _.foldr = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + var length = obj.length; + iterator = createCallback(iterator, context, 4); + if (length !== +length) { + var keys = _.keys(obj); + length = keys.length; + } + _.each(obj, function(value, index, list) { + index = keys ? keys[--length] : --length; + if (!initial) { + memo = obj[index]; + initial = true; + } else { + memo = iterator(memo, obj[index], index, list); + } + }); + if (!initial) throw TypeError(reduceError); + return memo; + }; + + // Return the first value which passes a truth test. Aliased as `detect`. + _.find = _.detect = function(obj, predicate, context) { + var result; + predicate = lookupIterator(predicate, context); + _.some(obj, function(value, index, list) { + if (predicate(value, index, list)) { + result = value; + return true; + } + }); + return result; + }; + + // Return all the elements that pass a truth test. + // Aliased as `select`. + _.filter = _.select = function(obj, predicate, context) { + var results = []; + if (obj == null) return results; + predicate = lookupIterator(predicate, context); + _.each(obj, function(value, index, list) { + if (predicate(value, index, list)) results.push(value); + }); + return results; + }; + + // Return all the elements for which a truth test fails. + _.reject = function(obj, predicate, context) { + return _.filter(obj, _.negate(lookupIterator(predicate)), context); + }; + + // Determine whether all of the elements match a truth test. + // Aliased as `all`. + _.every = _.all = function(obj, predicate, context) { + var result = true; + if (obj == null) return result; + predicate = lookupIterator(predicate, context); + _.each(obj, function(value, index, list) { + result = predicate(value, index, list); + if (!result) return breaker; + }); + return !!result; + }; + + // Determine if at least one element in the object matches a truth test. + // Aliased as `any`. + _.some = _.any = function(obj, predicate, context) { + var result = false; + if (obj == null) return result; + predicate = lookupIterator(predicate, context); + _.each(obj, function(value, index, list) { + result = predicate(value, index, list); + if (result) return breaker; + }); + return !!result; + }; + + // Determine if the array or object contains a given value (using `===`). + // Aliased as `include`. + _.contains = _.include = function(obj, target) { + if (obj == null) return false; + if (obj.length === +obj.length) return _.indexOf(obj, target) >= 0; + return _.some(obj, function(value) { + return value === target; + }); + }; + + // Invoke a method (with arguments) on every item in a collection. + _.invoke = function(obj, method) { + var args = slice.call(arguments, 2); + var isFunc = _.isFunction(method); + return _.map(obj, function(value) { + return (isFunc ? method : value[method]).apply(value, args); + }); + }; + + // Convenience version of a common use case of `map`: fetching a property. + _.pluck = function(obj, key) { + return _.map(obj, _.property(key)); + }; + + // Convenience version of a common use case of `filter`: selecting only objects + // containing specific `key:value` pairs. + _.where = function(obj, attrs) { + return _.filter(obj, _.matches(attrs)); + }; + + // Convenience version of a common use case of `find`: getting the first object + // containing specific `key:value` pairs. + _.findWhere = function(obj, attrs) { + return _.find(obj, _.matches(attrs)); + }; + + // Return the maximum element or (element-based computation). + _.max = function(obj, iterator, context) { + var result = -Infinity, lastComputed = -Infinity, + value, computed; + if (!iterator && _.isArray(obj)) { + for (var i = 0, length = obj.length; i < length; i++) { + value = obj[i]; + if (value > result) { + result = value; + } + } + } else { + iterator = lookupIterator(iterator, context); + _.each(obj, function(value, index, list) { + computed = iterator ? iterator(value, index, list) : value; + if (computed > lastComputed || computed === -Infinity && result === -Infinity) { + result = value; + lastComputed = computed; + } + }); + } + return result; + }; + + // Return the minimum element (or element-based computation). + _.min = function(obj, iterator, context) { + var result = Infinity, lastComputed = Infinity, + value, computed; + if (!iterator && _.isArray(obj)) { + for (var i = 0, length = obj.length; i < length; i++) { + value = obj[i]; + if (value < result) { + result = value; + } + } + } else { + iterator = lookupIterator(iterator, context); + _.each(obj, function(value, index, list) { + computed = iterator ? iterator(value, index, list) : value; + if (computed < lastComputed || computed === Infinity && result === Infinity) { + result = value; + lastComputed = computed; + } + }); + } + return result; + }; + + // Shuffle an array, using the modern version of the + // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). + _.shuffle = function(obj) { + var rand; + var index = 0; + var shuffled = []; + _.each(obj, function(value) { + rand = _.random(index++); + shuffled[index - 1] = shuffled[rand]; + shuffled[rand] = value; + }); + return shuffled; + }; + + // Sample **n** random values from a collection. + // If **n** is not specified, returns a single random element. + // The internal `guard` argument allows it to work with `map`. + _.sample = function(obj, n, guard) { + if (n == null || guard) { + if (obj.length !== +obj.length) obj = _.values(obj); + return obj[_.random(obj.length - 1)]; + } + return _.shuffle(obj).slice(0, Math.max(0, n)); + }; + + // Sort the object's values by a criterion produced by an iterator. + _.sortBy = function(obj, iterator, context) { + iterator = lookupIterator(iterator, context); + return _.pluck(_.map(obj, function(value, index, list) { + return { + value: value, + index: index, + criteria: iterator(value, index, list) + }; + }).sort(function(left, right) { + var a = left.criteria; + var b = right.criteria; + if (a !== b) { + if (a > b || a === void 0) return 1; + if (a < b || b === void 0) return -1; + } + return left.index - right.index; + }), 'value'); + }; + + // An internal function used for aggregate "group by" operations. + var group = function(behavior) { + return function(obj, iterator, context) { + var result = {}; + iterator = lookupIterator(iterator, context); + _.each(obj, function(value, index) { + var key = iterator(value, index, obj); + behavior(result, value, key); + }); + return result; + }; + }; + + // Groups the object's values by a criterion. Pass either a string attribute + // to group by, or a function that returns the criterion. + _.groupBy = group(function(result, value, key) { + if (_.has(result, key)) result[key].push(value); else result[key] = [value]; + }); + + // Indexes the object's values by a criterion, similar to `groupBy`, but for + // when you know that your index values will be unique. + _.indexBy = group(function(result, value, key) { + result[key] = value; + }); + + // Counts instances of an object that group by a certain criterion. Pass + // either a string attribute to count by, or a function that returns the + // criterion. + _.countBy = group(function(result, value, key) { + if (_.has(result, key)) result[key]++; else result[key] = 1; + }); + + // Use a comparator function to figure out the smallest index at which + // an object should be inserted so as to maintain order. Uses binary search. + _.sortedIndex = function(array, obj, iterator, context) { + iterator = lookupIterator(iterator, context, 1); + var value = iterator(obj); + var low = 0, high = array.length; + while (low < high) { + var mid = (low + high) >>> 1; + if (iterator(array[mid]) < value) low = mid + 1; else high = mid; + } + return low; + }; + + // Safely create a real, live array from anything iterable. + _.toArray = function(obj) { + if (!obj) return []; + if (_.isArray(obj)) return slice.call(obj); + if (obj.length === +obj.length) return _.map(obj, _.identity); + return _.values(obj); + }; + + // Return the number of elements in an object. + _.size = function(obj) { + if (obj == null) return 0; + return obj.length === +obj.length ? obj.length : _.keys(obj).length; + }; + + // Array Functions + // --------------- + + // Get the first element of an array. Passing **n** will return the first N + // values in the array. Aliased as `head` and `take`. The **guard** check + // allows it to work with `_.map`. + _.first = _.head = _.take = function(array, n, guard) { + if (array == null) return void 0; + if (n == null || guard) return array[0]; + if (n < 0) return []; + return slice.call(array, 0, n); + }; + + // Returns everything but the last entry of the array. Especially useful on + // the arguments object. Passing **n** will return all the values in + // the array, excluding the last N. The **guard** check allows it to work with + // `_.map`. + _.initial = function(array, n, guard) { + return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n))); + }; + + // Get the last element of an array. Passing **n** will return the last N + // values in the array. The **guard** check allows it to work with `_.map`. + _.last = function(array, n, guard) { + if (array == null) return void 0; + if (n == null || guard) return array[array.length - 1]; + return slice.call(array, Math.max(array.length - n, 0)); + }; + + // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. + // Especially useful on the arguments object. Passing an **n** will return + // the rest N values in the array. The **guard** + // check allows it to work with `_.map`. + _.rest = _.tail = _.drop = function(array, n, guard) { + return slice.call(array, n == null || guard ? 1 : n); + }; + + // Trim out all falsy values from an array. + _.compact = function(array) { + return _.filter(array, _.identity); + }; + + // Internal implementation of a recursive `flatten` function. + var flatten = function(input, shallow, strict, output) { + if (shallow && _.every(input, _.isArray)) { + return concat.apply(output, input); + } + for (var i = 0, length = input.length; i < length; i++) { + var value = input[i]; + if (!_.isArray(value) && !_.isArguments(value)) { + if (!strict) output.push(value); + } else if (shallow) { + push.apply(output, value); + } else { + flatten(value, shallow, strict, output); + } + } + return output; + }; + + // Flatten out an array, either recursively (by default), or just one level. + _.flatten = function(array, shallow) { + return flatten(array, shallow, false, []); + }; + + // Return a version of the array that does not contain the specified value(s). + _.without = function(array) { + return _.difference(array, slice.call(arguments, 1)); + }; + + // Split an array into two arrays: one whose elements all satisfy the given + // predicate, and one whose elements all do not satisfy the predicate. + _.partition = function(obj, predicate, context) { + predicate = lookupIterator(predicate, context, 1); + var pass = [], fail = []; + _.each(obj, function(value, key, obj) { + (predicate(value, key, obj) ? pass : fail).push(value); + }); + return [pass, fail]; + }; + + // Produce a duplicate-free version of the array. If the array has already + // been sorted, you have the option of using a faster algorithm. + // Aliased as `unique`. + _.uniq = _.unique = function(array, isSorted, iterator, context) { + if (array == null) return []; + if (_.isFunction(isSorted)) { + context = iterator; + iterator = isSorted; + isSorted = false; + } + if (iterator) iterator = lookupIterator(iterator, context); + var result = []; + var seen = []; + for (var i = 0, length = array.length; i < length; i++) { + var value = array[i]; + if (iterator) value = iterator(value, i, array); + if (isSorted ? !i || seen !== value : !_.contains(seen, value)) { + if (isSorted) seen = value; + else seen.push(value); + result.push(array[i]); + } + } + return result; + }; + + // Produce an array that contains the union: each distinct element from all of + // the passed-in arrays. + _.union = function() { + return _.uniq(flatten(arguments, true, true, [])); + }; + + // Produce an array that contains every item shared between all the + // passed-in arrays. + _.intersection = function(array) { + if (array == null) return []; + var result = []; + var argsLength = arguments.length; + for (var i = 0, length = array.length; i < length; i++) { + var item = array[i]; + if (_.contains(result, item)) continue; + for (var j = 1; j < argsLength; j++) { + if (!_.contains(arguments[j], item)) break; + } + if (j === argsLength) result.push(item); + } + return result; + }; + + // Take the difference between one array and a number of other arrays. + // Only the elements present in just the first array will remain. + _.difference = function(array) { + var rest = flatten(slice.call(arguments, 1), true, true, []); + return _.filter(array, function(value){ + return !_.contains(rest, value); + }); + }; + + // Zip together multiple lists into a single array -- elements that share + // an index go together. + _.zip = function() { + var length = _.max(_.pluck(arguments, 'length').concat(0)); + var results = Array(length); + for (var i = 0; i < length; i++) { + results[i] = _.pluck(arguments, '' + i); + } + return results; + }; + + // Converts lists into objects. Pass either a single array of `[key, value]` + // pairs, or two parallel arrays of the same length -- one of keys, and one of + // the corresponding values. + _.object = function(list, values) { + if (list == null) return {}; + var result = {}; + for (var i = 0, length = list.length; i < length; i++) { + if (values) { + result[list[i]] = values[i]; + } else { + result[list[i][0]] = list[i][1]; + } + } + return result; + }; + + // Return the position of the first occurrence of an item in an array, + // or -1 if the item is not included in the array. + // If the array is large and already in sort order, pass `true` + // for **isSorted** to use binary search. + _.indexOf = function(array, item, isSorted) { + if (array == null) return -1; + var i = 0, length = array.length; + if (isSorted) { + if (typeof isSorted == 'number') { + i = isSorted < 0 ? Math.max(0, length + isSorted) : isSorted; + } else { + i = _.sortedIndex(array, item); + return array[i] === item ? i : -1; + } + } + for (; i < length; i++) if (array[i] === item) return i; + return -1; + }; + + _.lastIndexOf = function(array, item, from) { + if (array == null) return -1; + var i = from == null ? array.length : from; + while (i--) if (array[i] === item) return i; + return -1; + }; + + // Generate an integer Array containing an arithmetic progression. A port of + // the native Python `range()` function. See + // [the Python documentation](http://docs.python.org/library/functions.html#range). + _.range = function(start, stop, step) { + if (arguments.length <= 1) { + stop = start || 0; + start = 0; + } + step = arguments[2] || 1; + + var length = Math.max(Math.ceil((stop - start) / step), 0); + var idx = 0; + var range = Array(length); + + while (idx < length) { + range[idx++] = start; + start += step; + } + + return range; + }; + + // Function (ahem) Functions + // ------------------ + + // Reusable constructor function for prototype setting. + var Ctor = function(){}; + + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if + // available. + _.bind = function(func, context) { + var args, bound; + if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); + if (!_.isFunction(func)) throw TypeError('Bind must be called on a function'); + args = slice.call(arguments, 2); + bound = function() { + if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); + Ctor.prototype = func.prototype; + var self = new Ctor; + Ctor.prototype = null; + var result = func.apply(self, args.concat(slice.call(arguments))); + if (Object(result) === result) return result; + return self; + }; + return bound; + }; + + // Partially apply a function by creating a version that has had some of its + // arguments pre-filled, without changing its dynamic `this` context. _ acts + // as a placeholder, allowing any combination of arguments to be pre-filled. + _.partial = function(func) { + var boundArgs = slice.call(arguments, 1); + return function() { + var position = 0; + var args = boundArgs.slice(); + for (var i = 0, length = args.length; i < length; i++) { + if (args[i] === _) args[i] = arguments[position++]; + } + while (position < arguments.length) args.push(arguments[position++]); + return func.apply(this, args); + }; + }; + + // Bind a number of an object's methods to that object. Remaining arguments + // are the method names to be bound. Useful for ensuring that all callbacks + // defined on an object belong to it. + _.bindAll = function(obj) { + var funcs = slice.call(arguments, 1); + if (funcs.length === 0) throw Error('bindAll must be passed function names'); + _.each(funcs, function(f) { + obj[f] = _.bind(obj[f], obj); + }); + return obj; + }; + + // Memoize an expensive function by storing its results. + _.memoize = function(func, hasher) { + if (!hasher) hasher = _.identity; + var memoize = function() { + var cache = memoize.cache; + var key = hasher.apply(this, arguments); + if (!_.has(cache, key)) cache[key] = func.apply(this, arguments); + return cache[key]; + }; + memoize.cache = {}; + return memoize; + }; + + // Delays a function for the given number of milliseconds, and then calls + // it with the arguments supplied. + _.delay = function(func, wait) { + var args = slice.call(arguments, 2); + return setTimeout(function(){ + return func.apply(null, args); + }, wait); + }; + + // Defers a function, scheduling it to run after the current call stack has + // cleared. + _.defer = function(func) { + return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); + }; + + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. Normally, the throttled function will run + // as much as it can, without ever going more than once per `wait` duration; + // but if you'd like to disable the execution on the leading edge, pass + // `{leading: false}`. To disable execution on the trailing edge, ditto. + _.throttle = function(func, wait, options) { + var context, args, result; + var timeout = null; + var previous = 0; + if (!options) options = {}; + var later = function() { + previous = options.leading === false ? 0 : _.now(); + timeout = null; + result = func.apply(context, args); + if (!timeout) context = args = null; + }; + return function() { + var now = _.now(); + if (!previous && options.leading === false) previous = now; + var remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0 || remaining > wait) { + clearTimeout(timeout); + timeout = null; + previous = now; + result = func.apply(context, args); + if (!timeout) context = args = null; + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }; + + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. If `immediate` is passed, trigger the function on the + // leading edge, instead of the trailing. + _.debounce = function(func, wait, immediate) { + var timeout, args, context, timestamp, result; + + var later = function() { + var last = _.now() - timestamp; + + if (last < wait && last > 0) { + timeout = setTimeout(later, wait - last); + } else { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + if (!timeout) context = args = null; + } + } + }; + + return function() { + context = this; + args = arguments; + timestamp = _.now(); + var callNow = immediate && !timeout; + if (!timeout) timeout = setTimeout(later, wait); + if (callNow) { + result = func.apply(context, args); + context = args = null; + } + + return result; + }; + }; + + // Returns a function that will be executed at most one time, no matter how + // often you call it. Useful for lazy initialization. + _.once = function(func) { + var ran = false, memo; + return function() { + if (ran) return memo; + ran = true; + memo = func.apply(this, arguments); + func = null; + return memo; + }; + }; + + // Returns the first function passed as an argument to the second, + // allowing you to adjust arguments, run code before and after, and + // conditionally execute the original function. + _.wrap = function(func, wrapper) { + return _.partial(wrapper, func); + }; + + // Returns a negated version of the passed-in predicate. + _.negate = function(predicate) { + return function() { + return !predicate.apply(this, arguments); + }; + }; + + // Returns a function that is the composition of a list of functions, each + // consuming the return value of the function that follows. + _.compose = function() { + var funcs = arguments; + return function() { + var args = arguments; + for (var i = funcs.length - 1; i >= 0; i--) { + args = [funcs[i].apply(this, args)]; + } + return args[0]; + }; + }; + + // Returns a function that will only be executed after being called N times. + _.after = function(times, func) { + return function() { + if (--times < 1) { + return func.apply(this, arguments); + } + }; + }; + + // Object Functions + // ---------------- + + // Retrieve the names of an object's properties. + // Delegates to **ECMAScript 5**'s native `Object.keys` + _.keys = function(obj) { + if (!_.isObject(obj)) return []; + if (nativeKeys) return nativeKeys(obj); + var keys = []; + for (var key in obj) if (_.has(obj, key)) keys.push(key); + return keys; + }; + + // Retrieve the values of an object's properties. + _.values = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var values = Array(length); + for (var i = 0; i < length; i++) { + values[i] = obj[keys[i]]; + } + return values; + }; + + // Convert an object into a list of `[key, value]` pairs. + _.pairs = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var pairs = Array(length); + for (var i = 0; i < length; i++) { + pairs[i] = [keys[i], obj[keys[i]]]; + } + return pairs; + }; + + // Invert the keys and values of an object. The values must be serializable. + _.invert = function(obj) { + var result = {}; + var keys = _.keys(obj); + for (var i = 0, length = keys.length; i < length; i++) { + result[obj[keys[i]]] = keys[i]; + } + return result; + }; + + // Return a sorted list of the function names available on the object. + // Aliased as `methods` + _.functions = _.methods = function(obj) { + var names = []; + for (var key in obj) { + if (_.isFunction(obj[key])) names.push(key); + } + return names.sort(); + }; + + // Extend a given object with all the properties in passed-in object(s). + _.extend = function(obj) { + if (!_.isObject(obj)) return obj; + _.each(slice.call(arguments, 1), function(source) { + for (var prop in source) { + obj[prop] = source[prop]; + } + }); + return obj; + }; + + // Return a copy of the object only containing the whitelisted properties. + _.pick = function(obj, iterator, context) { + var result = {}, key; + if (_.isFunction(iterator)) { + for (key in obj) { + var value = obj[key]; + if (iterator.call(context, value, key, obj)) result[key] = value; + } + } else { + var keys = concat.apply([], slice.call(arguments, 1)); + for (var i = 0, length = keys.length; i < length; i++) { + key = keys[i]; + if (key in obj) result[key] = obj[key]; + } + } + return result; + }; + + // Return a copy of the object without the blacklisted properties. + _.omit = function(obj, iterator, context) { + var keys; + if (_.isFunction(iterator)) { + iterator = _.negate(iterator); + } else { + keys = _.map(concat.apply([], slice.call(arguments, 1)), String); + iterator = function(value, key) { + return !_.contains(keys, key); + }; + } + return _.pick(obj, iterator, context); + }; + + // Fill in a given object with default properties. + _.defaults = function(obj) { + if (!_.isObject(obj)) return obj; + _.each(slice.call(arguments, 1), function(source) { + for (var prop in source) { + if (obj[prop] === void 0) obj[prop] = source[prop]; + } + }); + return obj; + }; + + // Create a (shallow-cloned) duplicate of an object. + _.clone = function(obj) { + if (!_.isObject(obj)) return obj; + return _.isArray(obj) ? obj.slice() : _.extend({}, obj); + }; + + // Invokes interceptor with the obj, and then returns obj. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. + _.tap = function(obj, interceptor) { + interceptor(obj); + return obj; + }; + + // Internal recursive comparison function for `isEqual`. + var eq = function(a, b, aStack, bStack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) return a !== 0 || 1 / a === 1 / b; + // A strict comparison is necessary because `null == undefined`. + if (a == null || b == null) return a === b; + // Unwrap any wrapped objects. + if (a instanceof _) a = a._wrapped; + if (b instanceof _) b = b._wrapped; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className !== toString.call(b)) return false; + switch (className) { + // RegExps are coerced to strings for comparison. + case '[object RegExp]': + // Strings, numbers, dates, and booleans are compared by value. + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `String("5")`. + return '' + a === '' + b; + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. + if (a != +a) return b != +b; + // An `egal` comparison is performed for other numeric values. + return a == 0 ? 1 / a == 1 / b : a == +b; + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a === +b; + } + if (typeof a != 'object' || typeof b != 'object') return false; + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] === a) return bStack[length] === b; + } + // Objects with different constructors are not equivalent, but `Object`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if ( + aCtor !== bCtor && 'constructor' in a && 'constructor' in b && + !(_.isFunction(aCtor) && aCtor instanceof aCtor && + _.isFunction(bCtor) && bCtor instanceof bCtor) + ) { + return false; + } + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + var size = 0, result = true; + // Recursively compare objects and arrays. + if (className === '[object Array]') { + // Compare array lengths to determine if a deep comparison is necessary. + size = a.length; + result = size === b.length; + if (result) { + // Deep compare the contents, ignoring non-numeric properties. + while (size--) { + if (!(result = eq(a[size], b[size], aStack, bStack))) break; + } + } + } else { + // Deep compare objects. + for (var key in a) { + if (_.has(a, key)) { + // Count the expected number of properties. + size++; + // Deep compare each member. + if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; + } + } + // Ensure that both objects contain the same number of properties. + if (result) { + for (key in b) { + if (_.has(b, key) && !size--) break; + } + result = !size; + } + } + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + return result; + }; + + // Perform a deep comparison to check if two objects are equal. + _.isEqual = function(a, b) { + return eq(a, b, [], []); + }; + + // Is a given array, string, or object empty? + // An "empty" object has no enumerable own-properties. + _.isEmpty = function(obj) { + if (obj == null) return true; + if (_.isArray(obj) || _.isString(obj) || _.isArguments(obj)) return obj.length === 0; + for (var key in obj) if (_.has(obj, key)) return false; + return true; + }; + + // Is a given value a DOM element? + _.isElement = function(obj) { + return !!(obj && obj.nodeType === 1); + }; + + // Is a given value an array? + // Delegates to ECMA5's native Array.isArray + _.isArray = nativeIsArray || function(obj) { + return toString.call(obj) === '[object Array]'; + }; + + // Is a given variable an object? + _.isObject = function(obj) { + return obj === Object(obj); + }; + + // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. + _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { + _['is' + name] = function(obj) { + return toString.call(obj) === '[object ' + name + ']'; + }; + }); + + // Define a fallback version of the method in browsers (ahem, IE), where + // there isn't any inspectable "Arguments" type. + if (!_.isArguments(arguments)) { + _.isArguments = function(obj) { + return _.has(obj, 'callee'); + }; + } + + // Optimize `isFunction` if appropriate. + if (typeof /./ !== 'function') { + _.isFunction = function(obj) { + return typeof obj === 'function'; + }; + } + + // Is a given object a finite number? + _.isFinite = function(obj) { + return isFinite(obj) && !isNaN(parseFloat(obj)); + }; + + // Is the given value `NaN`? (NaN is the only number which does not equal itself). + _.isNaN = function(obj) { + return _.isNumber(obj) && obj !== +obj; + }; + + // Is a given value a boolean? + _.isBoolean = function(obj) { + return obj === true || obj === false || toString.call(obj) === '[object Boolean]'; + }; + + // Is a given value equal to null? + _.isNull = function(obj) { + return obj === null; + }; + + // Is a given variable undefined? + _.isUndefined = function(obj) { + return obj === void 0; + }; + + // Shortcut function for checking if an object has a given property directly + // on itself (in other words, not on a prototype). + _.has = function(obj, key) { + return obj != null && hasOwnProperty.call(obj, key); + }; + + // Utility Functions + // ----------------- + + // Run Underscore.js in *noConflict* mode, returning the `_` variable to its + // previous owner. Returns a reference to the Underscore object. + _.noConflict = function() { + root._ = previousUnderscore; + return this; + }; + + // Keep the identity function around for default iterators. + _.identity = function(value) { + return value; + }; + + _.constant = function(value) { + return function() { + return value; + }; + }; + + _.noop = function(){}; + + _.property = function(key) { + return function(obj) { + return obj[key]; + }; + }; + + // Returns a predicate for checking whether an object has a given set of `key:value` pairs. + _.matches = function(attrs) { + return function(obj) { + if (obj == null) return _.isEmpty(attrs); + if (obj === attrs) return true; + for (var key in attrs) if (attrs[key] !== obj[key]) return false; + return true; + }; + }; + + // Run a function **n** times. + _.times = function(n, iterator, context) { + var accum = Array(Math.max(0, n)); + iterator = createCallback(iterator, context, 1); + for (var i = 0; i < n; i++) accum[i] = iterator(i); + return accum; + }; + + // Return a random integer between min and max (inclusive). + _.random = function(min, max) { + if (max == null) { + max = min; + min = 0; + } + return min + Math.floor(Math.random() * (max - min + 1)); + }; + + // A (possibly faster) way to get the current timestamp as an integer. + _.now = Date.now || function() { + return new Date().getTime(); + }; + + // List of HTML entities for escaping. + var entityMap = { + escape: { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + } + }; + entityMap.unescape = _.invert(entityMap.escape); + + // Regexes containing the keys and values listed immediately above. + var entityRegexes = { + escape: RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), + unescape: RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') + }; + + // Functions for escaping and unescaping strings to/from HTML interpolation. + _.each(['escape', 'unescape'], function(method) { + _[method] = function(string) { + if (string == null) return ''; + return ('' + string).replace(entityRegexes[method], function(match) { + return entityMap[method][match]; + }); + }; + }); + + // If the value of the named `property` is a function then invoke it with the + // `object` as context; otherwise, return it. + _.result = function(object, property) { + if (object == null) return void 0; + var value = object[property]; + return _.isFunction(value) ? object[property]() : value; + }; + + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + var idCounter = 0; + _.uniqueId = function(prefix) { + var id = ++idCounter + ''; + return prefix ? prefix + id : id; + }; + + // By default, Underscore uses ERB-style template delimiters, change the + // following template settings to use alternative delimiters. + _.templateSettings = { + evaluate : /<%([\s\S]+?)%>/g, + interpolate : /<%=([\s\S]+?)%>/g, + escape : /<%-([\s\S]+?)%>/g + }; + + // When customizing `templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /(.)^/; + + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escaper = /\\|'|\r|\n|\u2028|\u2029/g; + + var escapeChar = function(match) { + return '\\' + escapes[match]; + }; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + _.template = function(text, data, settings) { + settings = _.defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset).replace(escaper, escapeChar); + index = offset + match.length; + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } else if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } else if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + + // Adobe VMs need the match returned to produce the correct offest. + return match; + }); + source += "';\n"; + + // If a variable is not specified, place data values in local scope. + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + 'return __p;\n'; + + try { + var render = Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + if (data) return render(data, _); + var template = function(data) { + return render.call(this, data, _); + }; + + // Provide the compiled source as a convenience for precompilation. + var argument = settings.variable || 'obj'; + template.source = 'function(' + argument + '){\n' + source + '}'; + + return template; + }; + + // Add a "chain" function, which will delegate to the wrapper. + _.chain = function(obj) { + return _(obj).chain(); + }; + + // OOP + // --------------- + // If Underscore is called as a function, it returns a wrapped object that + // can be used OO-style. This wrapper holds altered versions of all the + // underscore functions. Wrapped objects may be chained. + + // Helper function to continue chaining intermediate results. + var result = function(obj) { + return this._chain ? _(obj).chain() : obj; + }; + + // Add your own custom functions to the Underscore object. + _.mixin = function(obj) { + _.each(_.functions(obj), function(name) { + var func = _[name] = obj[name]; + _.prototype[name] = function() { + var args = [this._wrapped]; + push.apply(args, arguments); + return result.call(this, func.apply(_, args)); + }; + }); + }; + + // Add all of the Underscore functions to the wrapper object. + _.mixin(_); + + // Add all mutator Array functions to the wrapper. + _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + var obj = this._wrapped; + method.apply(obj, arguments); + if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0]; + return result.call(this, obj); + }; + }); + + // Add all accessor Array functions to the wrapper. + _.each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + return result.call(this, method.apply(this._wrapped, arguments)); + }; + }); + + _.extend(_.prototype, { + + // Start chaining a wrapped Underscore object. + chain: function() { + this._chain = true; + return this; + }, + + // Extracts the result from a wrapped and chained object. + value: function() { + return this._wrapped; + } + + }); + + // AMD registration happens at the end for compatibility with AMD loaders + // that may not enforce next-turn semantics on modules. Even though general + // practice for AMD registration is to be anonymous, underscore registers + // as a named module because, like jQuery, it is a base library that is + // popular enough to be bundled in a third party lib, but not be part of + // an AMD load request. Those cases could generate an error when an + // anonymous define() is called outside of a loader request. + if (typeof define === 'function' && define.amd) { + define('underscore', [], function() { + return _; + }); + } +}.call(this)); diff --git a/client/js/models/acl.js b/client/js/models/acl.js new file mode 100644 index 000000000..de673a7a4 --- /dev/null +++ b/client/js/models/acl.js @@ -0,0 +1,14 @@ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ACL Model + * @class ACL + * @constructor + * @extends Backbone.Model + */ +App.ACL = Backbone.Model.extend({ + initialize: function() { + this.acl_links_roles = new App.RoleSettingsCollection(); + } +}); diff --git a/client/js/models/activity.js b/client/js/models/activity.js new file mode 100644 index 000000000..5228e82d0 --- /dev/null +++ b/client/js/models/activity.js @@ -0,0 +1,18 @@ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Activity Model + * @class Activity + * @constructor + * @extends Backbone.Model + */ +App.Activity = Backbone.Model.extend({ + storeName: 'comment', + initialize: function() { + this.user = new App.User(); + this.cards = new App.CardCollection(); + this.lists = new App.ListCollection(); + this.boards = new App.BoardCollection(); + } +}); diff --git a/client/js/models/board.js b/client/js/models/board.js new file mode 100644 index 000000000..b7b60e211 --- /dev/null +++ b/client/js/models/board.js @@ -0,0 +1,53 @@ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Board Model + * @class Board + * @constructor + * @extends Backbone.Model + */ +App.Board = Backbone.Model.extend({ + url: function() { + return api_url + 'boards/' + this.id + '.json'; + }, + storeName: 'board', + initialize: function(args) { + this.lists = new App.ListCollection(); + this.lists.url = api_url + 'boards/' + this.id + '/lists.json'; + this.lists.board = this; + + this.board_subscribers = new App.BoardSubscriberCollection(); + this.board_subscribers.url = api_url + 'boards/' + this.id + '/board_subscribers.json'; + this.board_subscribers.board = this; + + this.board_stars = new App.BoardSubscriberCollection(); + this.board_stars.board = this; + + this.activities = new App.ActivityCollection(); + this.activities.url = api_url + 'boards/' + this.id + '/activities.json'; + this.activities.board = this; + + this.board_subscriber = new App.BoardSubscriber(); + this.board_subscriber.url = api_url + 'boards/' + this.id + '/subscriber.json'; + this.board_subscriber.board = this; + + this.board_star = new App.BoardStar(); + this.board_star.url = api_url + 'boards/' + this.id + '/subscriber.json'; + this.board_star.board = this; + + this.board_users = new App.BoardsUserCollection(); + this.board_users.url = api_url + 'boards/' + this.id + '/users.json'; + this.board_users.board = this; + + this.custom_attachments = new App.CardAttachmentCollection(); + + this.attachments = new App.CardAttachmentCollection(); + + this.cards = new App.CardCollection(); + this.labels = new App.CardLabelCollection(); + this.checklists = new App.CardCheckListCollection(); + this.checklist_items = new App.CheckListItemCollection(); + this.boards_stars = new App.BoardStarCollection(); + } +}); diff --git a/client/js/models/boards_star.js b/client/js/models/boards_star.js new file mode 100644 index 000000000..30502f606 --- /dev/null +++ b/client/js/models/boards_star.js @@ -0,0 +1,14 @@ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Board Star Model + * @class BoardStar + * @constructor + * @extends Backbone.Model + */ +App.BoardStar = Backbone.Model.extend({ + url: function() { + return api_url + 'board_stars/' + this.id + '.json'; + } +}); diff --git a/client/js/models/boards_subscriber.js b/client/js/models/boards_subscriber.js new file mode 100644 index 000000000..a03ca0e63 --- /dev/null +++ b/client/js/models/boards_subscriber.js @@ -0,0 +1,14 @@ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Board Subscriber Model + * @class BoardSubscriber + * @constructor + * @extends Backbone.Model + */ +App.BoardSubscriber = Backbone.Model.extend({ + url: function() { + return api_url + 'board_subscribers/' + this.id + '.json'; + } +}); diff --git a/client/js/models/boards_user.js b/client/js/models/boards_user.js new file mode 100644 index 000000000..679692ef1 --- /dev/null +++ b/client/js/models/boards_user.js @@ -0,0 +1,12 @@ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Boards User Model + * @class BoardsUser + * @constructor + * @extends Backbone.Model + */ +App.BoardsUser = Backbone.Model.extend({ + storeName: 'board_user' +}); diff --git a/client/js/models/card.js b/client/js/models/card.js new file mode 100644 index 000000000..ae8ff9b49 --- /dev/null +++ b/client/js/models/card.js @@ -0,0 +1,75 @@ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Card Model + * @class Card + * @constructor + * @extends Backbone.Model + */ +App.Card = Backbone.Model.extend({ + storeName: 'card', + initialize: function() { + this.url = api_url + 'boards/' + this.attributes.board_id + '/lists/' + this.attributes.list_id + '/cards.json'; + + this.attachments = new App.CardAttachmentCollection(); + this.attachments.card = this; + + this.labels = new App.CardLabelCollection(); + this.labels.card = this; + + this.board_labels = new App.CardLabelCollection(); + + this.checklists = new App.CardCheckListCollection(); + this.checklists.card = this; + + this.board_users = new App.BoardsUserCollection(); + this.board_users.card = this; + + this.cards = new App.CardCollection(); + this.users = new App.UserCollection(); + this.card_voters = new App.CardVoterCollection(); + this.board_activities = new App.ActivityCollection(); + this.cards_subscribers = new App.CardSubscriberCollection(); + }, + moveAfter: function(beforeId) { + var before = this.collection.get(beforeId); + var after = this.collection.next(before); + if (typeof after == 'undefined') { + afterPosition = before.position() + 2; + } else { + afterPosition = after.position(); + } + var difference = (afterPosition - before.position()) / 2; + var newPosition = difference + before.position(); + this.set({ + position: newPosition + }, { + silent: true + }); + return this; + }, + moveBefore: function(afterId) { + var after = this.collection.get(afterId); + var before = this.collection.previous(after); + if (typeof before == 'undefined') { + beforePosition = 0.0; + } else { + beforePosition = before.position(); + } + var difference = (after.position() - beforePosition) / 2; + var newPosition = difference + beforePosition; + this.set({ + position: newPosition + }, { + silent: true + }); + this.collection.sort({ + silent: true + }); + return this; + }, + position: function() { + return parseFloat(this.attributes.position); + } +}); diff --git a/client/js/models/card_attachment.js b/client/js/models/card_attachment.js new file mode 100644 index 000000000..def27f34a --- /dev/null +++ b/client/js/models/card_attachment.js @@ -0,0 +1,14 @@ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Card Attachment Model + * @class Card Attachment + * @constructor + * @extends Backbone.Model + */ +App.CardAttachment = Backbone.Model.extend({ + initialize: function() { + this.card = new App.Card(); + } +}); diff --git a/client/js/models/card_subscriber.js b/client/js/models/card_subscriber.js new file mode 100644 index 000000000..02121b10b --- /dev/null +++ b/client/js/models/card_subscriber.js @@ -0,0 +1,12 @@ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Card Subscriber Model + * @class CardSubscriber + * @constructor + * @extends Backbone.Model + */ +App.CardSubscriber = Backbone.Model.extend({ + storeName: 'card_subscriber' +}); diff --git a/client/js/models/card_user.js b/client/js/models/card_user.js new file mode 100644 index 000000000..e45166a61 --- /dev/null +++ b/client/js/models/card_user.js @@ -0,0 +1,12 @@ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Card User Model + * @class CardUser + * @constructor + * @extends Backbone.Model + */ +App.CardUser = Backbone.Model.extend({ + storeName: 'card_user' +}); diff --git a/client/js/models/card_voter.js b/client/js/models/card_voter.js new file mode 100644 index 000000000..6e5253325 --- /dev/null +++ b/client/js/models/card_voter.js @@ -0,0 +1,12 @@ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Card Voter Model + * @class CardVoter + * @constructor + * @extends Backbone.Model + */ +App.CardVoter = Backbone.Model.extend({ + storeName: 'card_voter' +}); diff --git a/client/js/models/checklist.js b/client/js/models/checklist.js new file mode 100644 index 000000000..b4f831a76 --- /dev/null +++ b/client/js/models/checklist.js @@ -0,0 +1,53 @@ +if (typeof App == 'undefined') { + App = {}; +} +/** + * CheckList Model + * @class CheckList + * @constructor + * @extends Backbone.Model + */ +App.CheckList = Backbone.Model.extend({ + storeName: 'checklist', + initialize: function() { + this.checklist_items = new App.CheckListItemCollection(); + this.board_users = new App.BoardsUserCollection(); + this.card = new App.Card(); + }, + moveAfter: function(beforeId) { + var before = this.collection.get(beforeId); + var after = this.collection.next(before); + if (typeof after == 'undefined') { + afterPosition = before.position() + 2; + } else { + afterPosition = after.position(); + } + var difference = (afterPosition - before.position()) / 2; + var newPosition = difference + before.position(); + this.set({ + position: newPosition + }); + return this; + }, + moveBefore: function(afterId) { + var after = this.collection.get(afterId); + var before = this.collection.previous(after); + if (typeof before == 'undefined') { + beforePosition = 0.0; + } else { + beforePosition = before.position(); + } + var difference = (after.position() - beforePosition) / 2; + var newPosition = difference + beforePosition; + this.set({ + position: newPosition + }); + this.collection.sort({ + silent: true + }); + return this; + }, + position: function() { + return parseFloat(this.attributes.position); + } +}); diff --git a/client/js/models/checklist_item.js b/client/js/models/checklist_item.js new file mode 100644 index 000000000..f588748db --- /dev/null +++ b/client/js/models/checklist_item.js @@ -0,0 +1,52 @@ +if (typeof App == 'undefined') { + App = {}; +} +/** + * CheckList Item Model + * @class CheckListItem + * @constructor + * @extends Backbone.Model + */ +App.CheckListItem = Backbone.Model.extend({ + storeName: 'item', + initialize: function() { + this.board_users = new App.BoardsUserCollection(); + this.cards = new App.CardCollection(); + }, + moveAfter: function(beforeId) { + var before = this.collection.get(beforeId); + var after = this.collection.next(before); + if (typeof after == 'undefined') { + afterPosition = before.position() + 2; + } else { + afterPosition = after.position(); + } + var difference = (afterPosition - before.position()) / 2; + var newPosition = difference + before.position(); + this.set({ + position: newPosition + }); + return this; + }, + moveBefore: function(afterId) { + var after = this.collection.get(afterId); + var before = this.collection.previous(after); + if (typeof before == 'undefined') { + beforePosition = 0.0; + } else { + beforePosition = before.position(); + } + var difference = (after.position() - beforePosition) / 2; + var newPosition = difference + beforePosition; + this.set({ + position: newPosition + }); + this.collection.sort({ + silent: true + }); + return this; + }, + position: function() { + return parseFloat(this.attributes.position); + } +}); diff --git a/client/js/models/elasticsearch.js b/client/js/models/elasticsearch.js new file mode 100644 index 000000000..6f5539ace --- /dev/null +++ b/client/js/models/elasticsearch.js @@ -0,0 +1,10 @@ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ElasticSearch Model + * @class ElasticSearch + * @constructor + * @extends Backbone.Model + */ +App.ElasticSearch = Backbone.Model.extend({}); diff --git a/client/js/models/email_template.js b/client/js/models/email_template.js new file mode 100644 index 000000000..0760a9ddd --- /dev/null +++ b/client/js/models/email_template.js @@ -0,0 +1,10 @@ +if (typeof App == 'undefined') { + App = {}; +} +/** + * EmailTemplate Model + * @class EmailTemplate + * @constructor + * @extends Backbone.Model + */ +App.EmailTemplate = Backbone.Model.extend({}); diff --git a/client/js/models/flickr.js b/client/js/models/flickr.js new file mode 100644 index 000000000..2c9e1be27 --- /dev/null +++ b/client/js/models/flickr.js @@ -0,0 +1,15 @@ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Flickr Model + * @class Flickr + * @constructor + * @extends Backbone.Model + */ +App.Flickr = Backbone.Model.extend({ + initialize: function() { + this.url = 'https://api.flickr.com/services/rest/?api_key=' + FLICKR_API_KEY + '&format=json&method=flickr.photos.getRecent&nojsoncallback=1&page=1&per_page=20'; + }, + storeName: 'flickr' +}); diff --git a/client/js/models/instant_card_add.js b/client/js/models/instant_card_add.js new file mode 100644 index 000000000..279e8b957 --- /dev/null +++ b/client/js/models/instant_card_add.js @@ -0,0 +1,10 @@ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Card Subscriber Model + * @class CardSubscriber + * @constructor + * @extends Backbone.Model + */ +App.InstantCardAdd = Backbone.Model.extend({}); diff --git a/client/js/models/label.js b/client/js/models/label.js new file mode 100644 index 000000000..64b87ee07 --- /dev/null +++ b/client/js/models/label.js @@ -0,0 +1,15 @@ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Label Model + * @class Label + * @constructor + * @extends Backbone.Model + */ +App.Label = Backbone.Model.extend({ + storeName: 'label', + initialize: function() { + this.card = new App.Card(); + } +}); diff --git a/client/js/models/list.js b/client/js/models/list.js new file mode 100644 index 000000000..ccfdca35a --- /dev/null +++ b/client/js/models/list.js @@ -0,0 +1,67 @@ +if (typeof App == 'undefined') { + App = {}; +} +/** + * List Model + * @class List + * @constructor + * @extends Backbone.Model + */ +App.List = Backbone.Model.extend({ + initialize: function() { + this.url = api_url + 'boards/' + this.attributes.board_id + '/lists.json'; + + this.cards = new App.CardCollection(); + this.cards.url = api_url + 'boards/' + this.attributes.board_id + '/lists/' + this.id + '/cards.json'; + this.cards.list = this; + + this.boards = new App.BoardCollection(); + this.board_users = new App.BoardsUserCollection(); + this.attachments = new App.CardAttachmentCollection(); + this.activities = new App.ActivityCollection(); + this.subscriber = new App.ListSubscriber(); + this.labels = new App.CardLabelCollection(); + this.lists_subscribers = new App.ListSubscriberCollection(); + }, + storeName: 'list', + moveAfter: function(beforeId) { + var before = this.collection.get(beforeId); + var after = this.collection.next(before); + if (typeof after == 'undefined') { + afterPosition = before.position() + 2; + } else { + afterPosition = after.position(); + } + var difference = (afterPosition - before.position()) / 2; + var newPosition = difference + before.position(); + this.set({ + position: newPosition + }, { + silent: true + }); + return this; + }, + moveBefore: function(afterId) { + var after = this.collection.get(afterId); + var before = this.collection.previous(after); + if (typeof before == 'undefined') { + beforePosition = 0.0; + } else { + beforePosition = before.position(); + } + var difference = (after.position() - beforePosition) / 2; + var newPosition = difference + beforePosition; + this.set({ + position: newPosition + }, { + silent: true + }); + this.collection.sort({ + silent: true + }); + return this; + }, + position: function() { + return parseFloat(this.attributes.position); + } +}); diff --git a/client/js/models/list_subscriber.js b/client/js/models/list_subscriber.js new file mode 100644 index 000000000..e0d9302d0 --- /dev/null +++ b/client/js/models/list_subscriber.js @@ -0,0 +1,10 @@ +if (typeof App == 'undefined') { + App = {}; +} +/** + * List Subscriber Model + * @class ListSubscriber + * @constructor + * @extends Backbone.Model + */ +App.ListSubscriber = Backbone.Model.extend({}); diff --git a/client/js/models/oauth.js b/client/js/models/oauth.js new file mode 100644 index 000000000..0823ba3ce --- /dev/null +++ b/client/js/models/oauth.js @@ -0,0 +1,14 @@ +if (typeof App == 'undefined') { + App = {}; +} +/** + * OAuth Model + * @class OAuth + * @constructor + * @extends Backbone.Model + */ +App.OAuth = Backbone.Model.extend({ + url: function() { + return api_url + 'oauth.json'; + } +}); diff --git a/client/js/models/organization.js b/client/js/models/organization.js new file mode 100644 index 000000000..3256351a7 --- /dev/null +++ b/client/js/models/organization.js @@ -0,0 +1,16 @@ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Organization Model + * @class Organization + * @constructor + * @extends Backbone.Model + */ +App.Organization = Backbone.Model.extend({ + initialize: function(args) { + this.boards = new App.BoardCollection(); + this.board_users = new App.BoardsUserCollection(); + this.organizations_users = new App.OrganizationsUserCollection(); + } +}); diff --git a/client/js/models/organizations_user.js b/client/js/models/organizations_user.js new file mode 100644 index 000000000..448118c5f --- /dev/null +++ b/client/js/models/organizations_user.js @@ -0,0 +1,10 @@ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Organizations User Model + * @class OrganizationsUser + * @constructor + * @extends Backbone.Model + */ +App.OrganizationsUser = Backbone.Model.extend({}); diff --git a/client/js/models/role.js b/client/js/models/role.js new file mode 100644 index 000000000..e889208a8 --- /dev/null +++ b/client/js/models/role.js @@ -0,0 +1,10 @@ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Role Model + * @class Role + * @constructor + * @extends Backbone.Model + */ +App.Role = Backbone.Model.extend({}); diff --git a/client/js/models/role_setting.js b/client/js/models/role_setting.js new file mode 100644 index 000000000..3dcea8fef --- /dev/null +++ b/client/js/models/role_setting.js @@ -0,0 +1,10 @@ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Role Setting Model + * @class RoleSetting + * @constructor + * @extends Backbone.Model + */ +App.RoleSetting = Backbone.Model.extend({}); diff --git a/client/js/models/setting_category.js b/client/js/models/setting_category.js new file mode 100644 index 000000000..737740018 --- /dev/null +++ b/client/js/models/setting_category.js @@ -0,0 +1,10 @@ +if (typeof App == 'undefined') { + App = {}; +} +/** + * SettingCategory Model + * @class SettingCategory + * @constructor + * @extends Backbone.Model + */ +App.SettingCategory = Backbone.Model.extend({}); diff --git a/client/js/models/user.js b/client/js/models/user.js new file mode 100644 index 000000000..71d75c984 --- /dev/null +++ b/client/js/models/user.js @@ -0,0 +1,24 @@ +if (typeof App == 'undefined') { + App = {}; +} +/** + * User Model + * @class User + * @constructor + * @extends Backbone.Model + */ +App.User = Backbone.Model.extend({ + initialize: function(args) { + this.activities = new App.ActivityCollection(); + this.activities.url = api_url + 'users/' + this.id + '/activities.json'; + + this.cards = new App.CardUserCollection(); + this.cards.url = api_url + 'users/' + this.id + '/cards.json'; + + this.boards_stars = new App.BoardStarCollection(); + + this.boards_users = new App.BoardsUserCollection(); + + this.organizations = new App.OrganizationCollection(); + } +}); diff --git a/client/js/models/workflow_template.js b/client/js/models/workflow_template.js new file mode 100644 index 000000000..30d2dcfc4 --- /dev/null +++ b/client/js/models/workflow_template.js @@ -0,0 +1,10 @@ +if (typeof App == 'undefined') { + App = {}; +} +/** + * WorkFlow Template Model + * @class WorkFlowTemplate + * @constructor + * @extends Backbone.Model + */ +App.WorkFlowTemplate = Backbone.Model.extend({}); diff --git a/client/js/templates/about_us.jst.ejs b/client/js/templates/about_us.jst.ejs new file mode 100644 index 000000000..0725f51ae --- /dev/null +++ b/client/js/templates/about_us.jst.ejs @@ -0,0 +1,31 @@ + \ No newline at end of file diff --git a/client/js/templates/activity.jst.ejs b/client/js/templates/activity.jst.ejs new file mode 100644 index 000000000..f138bb666 --- /dev/null +++ b/client/js/templates/activity.jst.ejs @@ -0,0 +1,106 @@ +<% if(!_.isEmpty(activity) && activity != null){ %> +
        + + <% if(!_.isEmpty(activity.attributes.profile_picture_path)) { + var profile_picture_path = activity.showImage('User', activity.attributes.user_id, 'small_thumb' ); + %> + [Image: <%-activity.attributes.username %>] + <% } else {%> + <%- activity.attributes.initials %> + <% } %> + +
        + <% if(activity.attributes.type == 'add_comment' && activity.attributes.type == 'edit_comment' ) { %> +
        + + <%= converter.makeHtml(makeLink(''+ _.escape(activity.attributes.comment), activity.attributes.board_id)) %> +
        + <% } else {%> + <% + var cardLink = '' + _.escape(activity.attributes.card_name) + ''; + if(activity.attributes.type != 'add_comment' && activity.attributes.type != 'edit_comment') { + activity.attributes.comment = activity.attributes.comment.replace('##CARD_LINK##', cardLink); + activity.attributes.comment = activity.attributes.comment.replace('##LABEL_NAME##', _.escape(activity.attributes.label_name)); + activity.attributes.comment = activity.attributes.comment.replace('##CARD_NAME##', _.escape(activity.attributes.card_name)); + activity.attributes.comment = activity.attributes.comment.replace('##DESCRIPTION##', _.escape(activity.attributes.card_description)); + activity.attributes.comment = activity.attributes.comment.replace('##LIST_NAME##', _.escape(activity.attributes.list_name)); + activity.attributes.comment = activity.attributes.comment.replace('##BOARD_NAME##', _.escape(activity.attributes.board_name)); + activity.attributes.comment = activity.attributes.comment.replace('##USER_NAME##', _.escape(activity.attributes.username)); + activity.attributes.comment = activity.attributes.comment.replace('##CHECKLIST_ITEM_NAME##', _.escape(activity.attributes.checklist_item_name)); + activity.attributes.comment = activity.attributes.comment.replace('##CHECKLIST_ITEM_PARENT_NAME##', _.escape(activity.attributes.checklist_item_parent_name)); + activity.attributes.comment = activity.attributes.comment.replace('##CHECKLIST_NAME##', _.escape(activity.attributes.checklist_name)); + } else { + if(!_.isUndefined(activity.from_footer)) { + var comment = activity.attributes.username + ' commented in card ' + cardLink; + } + } + %> +
        +
        +
        + + <% if((activity.attributes.type == 'add_comment' || activity.attributes.type == 'edit_comment')) { %> + <%if(!_.isUndefined(activity.from_footer)) { %> + <%= comment %> + <% } %> + <%if(!_.isUndefined(activity.from_footer)) { %> +
        + <% } %> + <%= converter.makeHtml(makeLink(_.escape(activity.attributes.comment), activity.attributes.board_id)) %> + <%if(!_.isUndefined(activity.from_footer)) { %> +
        + <% } %> + <% } else {%> + <%= converter.makeHtml(makeLink(activity.attributes.comment, activity.attributes.board_id)) %> + <% }%> +
        + <% if(activity.attributes.difference != null && activity.attributes.type != 'change_background' && activity.attributes.type != 'add_background' && activity.attributes.type != 'change_visibility' && activity.attributes.type != 'edit_card_duedate'&& activity.attributes.type != 'add_card_desc' && activity.attributes.type != 'update_card_checklist_item' && activity.attributes.type != 'change_card_position') { %> +
        <%= converter.makeHtml(activity.attributes.difference[0]) %>
        + <% } %> + +
        +
        +
        + <% } %> +
        +
        +
        +
        + + <%- activity.attributes.created %> + <% if(type == 'all') { %> +  on <%- activity.attributes.board_name %> + <% } %> + <% if(!_.isUndefined(authuser.user) && activity.attributes.type == "add_comment" && type != "all") { %> +
        +
          + <% if(!_.isEmpty(role_links.where({slug: "edit_comment"}))){ %> +
        • Edit
        • + <% } %> +
        • Reply
        • + <% if(!_.isEmpty(role_links.where({slug: "delete_comment"}))){ %> + + + <% } %> +
        +
        + <% } else if(activity.attributes.revisions != null && activity.attributes.revisions != "" && (!_.isUndefined(authuser.user) && parseInt(authuser.user.id) == 1 || current_user_can_undo_it == true )){ %> + <% if(!_.isEmpty(role_links.where({slug: "undo_activity"}))){ %> + <% if(!_.isUndefined(activity.from_footer)) { %> + + <% } else { %> + + <% }%> + <% } %> + <% } %> + +
        +
        +
        +
        +<% }else{ %>
        No activities available.
        <% } %> diff --git a/client/js/templates/activity_add_form.jst.ejs b/client/js/templates/activity_add_form.jst.ejs new file mode 100644 index 000000000..00496d2c1 --- /dev/null +++ b/client/js/templates/activity_add_form.jst.ejs @@ -0,0 +1 @@ +
        \ No newline at end of file diff --git a/client/js/templates/activity_card_search.jst.ejs b/client/js/templates/activity_card_search.jst.ejs new file mode 100644 index 000000000..01c3ef661 --- /dev/null +++ b/client/js/templates/activity_card_search.jst.ejs @@ -0,0 +1,5 @@ +<% if(card != null){ %> +<%- card.attributes.name %> +<%}else{%> +No cards available. +<%}%> diff --git a/client/js/templates/activity_delete_confirm.jst.ejs b/client/js/templates/activity_delete_confirm.jst.ejs new file mode 100644 index 000000000..4227a32a5 --- /dev/null +++ b/client/js/templates/activity_delete_confirm.jst.ejs @@ -0,0 +1 @@ +
        Delete Comment?

        Deleting a comment is forever. There is no undo.

        Delete
        \ No newline at end of file diff --git a/client/js/templates/activity_index.jst.ejs b/client/js/templates/activity_index.jst.ejs new file mode 100644 index 000000000..89eed9058 --- /dev/null +++ b/client/js/templates/activity_index.jst.ejs @@ -0,0 +1,5 @@ +
        +
          + +
        +
        Load more activities
        \ No newline at end of file diff --git a/client/js/templates/activity_reply_form.jst.ejs b/client/js/templates/activity_reply_form.jst.ejs new file mode 100644 index 000000000..afd61e3bd --- /dev/null +++ b/client/js/templates/activity_reply_form.jst.ejs @@ -0,0 +1 @@ +
        \ No newline at end of file diff --git a/client/js/templates/activity_user_add_search_result.jst.ejs b/client/js/templates/activity_user_add_search_result.jst.ejs new file mode 100644 index 000000000..696dc871b --- /dev/null +++ b/client/js/templates/activity_user_add_search_result.jst.ejs @@ -0,0 +1,6 @@ +<% if(user != null) { %>
      • +<% if(!_.isEmpty(user.attributes.profile_picture_path)) { + var profile_picture_path = user.showImage('User', user.attributes.user_id, 'micro_thumb' ); +%> +[Image: <%-user.attributes.username %>]<% } else {%> <%- user.attributes.initials %><% } %> <%-user.attributes.username %>
      • <% }else{ %>
      • No users available.
      • <%}%> + diff --git a/client/js/templates/admin_activity_index.jst.ejs b/client/js/templates/admin_activity_index.jst.ejs new file mode 100644 index 000000000..72f8c3a70 --- /dev/null +++ b/client/js/templates/admin_activity_index.jst.ejs @@ -0,0 +1,100 @@ +<% if(!_.isEmpty(activity) && activity != null){ %> +<% + var cardLink = '' + _.escape(activity.attributes.card_name) + ''; + if(activity.attributes.type != 'add_comment' && activity.attributes.type != 'edit_comment') { + activity.attributes.comment = activity.attributes.comment.replace('##CARD_LINK##', cardLink); + activity.attributes.comment = activity.attributes.comment.replace('##LABEL_NAME##', _.escape(activity.attributes.label_name)); + activity.attributes.comment = activity.attributes.comment.replace('##CARD_NAME##', _.escape(activity.attributes.card_name)); + activity.attributes.comment = activity.attributes.comment.replace('##DESCRIPTION##', _.escape(activity.attributes.card_description)); + activity.attributes.comment = activity.attributes.comment.replace('##LIST_NAME##', _.escape(activity.attributes.list_name)); + activity.attributes.comment = activity.attributes.comment.replace('##BOARD_NAME##', _.escape(activity.attributes.board_name)); + activity.attributes.comment = activity.attributes.comment.replace('##USER_NAME##', _.escape(activity.attributes.username)); + activity.attributes.comment = activity.attributes.comment.replace('##CHECKLIST_ITEM_NAME##', _.escape(activity.attributes.checklist_item_name)); + activity.attributes.comment = activity.attributes.comment.replace('##CHECKLIST_ITEM_PARENT_NAME##', _.escape(activity.attributes.checklist_item_parent_name)); + activity.attributes.comment = activity.attributes.comment.replace('##CHECKLIST_NAME##', _.escape(activity.attributes.checklist_name)); + } else { + var comment = activity.attributes.username + ' commented in card ' + cardLink; + } +%> +
        + + <% if(!_.isEmpty(activity.attributes.profile_picture_path)) { + var profile_picture_path = activity.showImage('User', activity.attributes.user_id, 'small_thumb' ); + %> + [Image: <%-activity.attributes.username %>] + <% } else {%> + <%- activity.attributes.initials %> + <% } %> + +
        + <% if(activity.attributes.type != 'add_comment' && activity.attributes.type != 'add_card_duedate') { %> +
        + + <% if(activity.attributes.type == 'add_comment' || activity.attributes.type == 'edit_comment') { %> + <%= comment %> + <%= converter.makeHtml(makeLink(_.escape(activity.attributes.comment), activity.attributes.board_id)) %> + <% } else{%> + <%= converter.makeHtml(makeLink(activity.attributes.comment, activity.attributes.board_id)) %> + <% } %> + + <% if(activity.attributes.difference != null) { %> +
        <%= activity.attributes.difference %>
        + <% } %> +
        + <% } else {%> +
        +
        +
        + + <% if(activity.attributes.type == 'add_comment' || activity.attributes.type == 'edit_comment') { %> + <%= comment %> + <%= converter.makeHtml(makeLink(_.escape(activity.attributes.comment), activity.attributes.board_id)) %> + <% } else{%> + <%= converter.makeHtml(makeLink(activity.attributes.comment, activity.attributes.board_id)) %> + <% } %> + + <% if(activity.attributes.difference != null && activity.attributes.type != 'change_background' && activity.attributes.type != 'add_background' && activity.attributes.type != 'change_visibility' && activity.attributes.type != 'edit_card_duedate'&& activity.attributes.type != 'add_card_desc' && activity.attributes.type != 'update_card_checklist_item' && activity.attributes.type != 'change_card_position') { %> +
        <%= activity.attributes.difference %>
        + <% } %> + +
        +
        +
        + <% } %> +
        +
        +
        +
        + + <%- activity.attributes.created %> on <%- activity.attributes.board_name %> + <% if(!_.isUndefined(authuser.user) && activity.attributes.type == "add_comment" && type != "all") { %> +
        +
          + <% if(!_.isEmpty(role_links.where({slug: "edit_comment"}))){ %> +
        • Edit
        • + <% } %> +
        • Reply
        • + <% if(!_.isEmpty(role_links.where({slug: "delete_comment"}))){ %> + + + <% } %> +
        +
        + <% } else if(activity.attributes.revisions != null && activity.attributes.revisions != "" && (parseInt(authuser.user.id) == 1 || current_user_can_undo_it == true )){ %> + <% if(!_.isEmpty(role_links.where({slug: "undo_activity"}))){ %> + <% if(_.isUndefined(activity.from_footer)) { %> + + <% }%> + <% } %> + <% } %> + +
        +
        +
        +
        +<% }else{ %>
        No activities available.
        <% } %> diff --git a/client/js/templates/admin_user_add.jst.ejs b/client/js/templates/admin_user_add.jst.ejs new file mode 100644 index 000000000..16b7f7396 --- /dev/null +++ b/client/js/templates/admin_user_add.jst.ejs @@ -0,0 +1,25 @@ +
        +
        +
        Add User
        +
        +
        +
        + + +
        +
        + + +
        +
        + + +
        +
        + + +
        +
        +
        +
        +
        \ No newline at end of file diff --git a/client/js/templates/archived_card.jst.ejs b/client/js/templates/archived_card.jst.ejs new file mode 100644 index 000000000..1dcf330e8 --- /dev/null +++ b/client/js/templates/archived_card.jst.ejs @@ -0,0 +1,5 @@ +<% if(card != null){ %> +<%-card.attributes.name%>
        +<% }else{ %> + No cards available. +<%}%> diff --git a/client/js/templates/archived_cards.jst.ejs b/client/js/templates/archived_cards.jst.ejs new file mode 100644 index 000000000..71d58f9be --- /dev/null +++ b/client/js/templates/archived_cards.jst.ejs @@ -0,0 +1 @@ +
        \ No newline at end of file diff --git a/client/js/templates/archived_items.jst.ejs b/client/js/templates/archived_items.jst.ejs new file mode 100644 index 000000000..a824df515 --- /dev/null +++ b/client/js/templates/archived_items.jst.ejs @@ -0,0 +1 @@ +
        • Archived Items
        \ No newline at end of file diff --git a/client/js/templates/archived_list.jst.ejs b/client/js/templates/archived_list.jst.ejs new file mode 100644 index 000000000..d10596389 --- /dev/null +++ b/client/js/templates/archived_list.jst.ejs @@ -0,0 +1,5 @@ +<% if(list != null){ %> +<%- list.attributes.name %>
        +<% }else{ %> + No lists available. +<%}%> \ No newline at end of file diff --git a/client/js/templates/archived_lists.jst.ejs b/client/js/templates/archived_lists.jst.ejs new file mode 100644 index 000000000..b81cc9518 --- /dev/null +++ b/client/js/templates/archived_lists.jst.ejs @@ -0,0 +1 @@ +
        \ No newline at end of file diff --git a/client/js/templates/attachment.jst.ejs b/client/js/templates/attachment.jst.ejs new file mode 100644 index 000000000..a1a492fcc --- /dev/null +++ b/client/js/templates/attachment.jst.ejs @@ -0,0 +1,40 @@ +<% if(attachment != null){ %> + + <% if(attachment.get("name").match(/\.(jpg|jpeg|png|gif)$/)){ + var picture_path = attachment.showImage('CardAttachment', attachment.attributes.id, 'large_thumb' ); + %> + + <% } else{ + var extension = attachment.attributes.name.split('.'); + %> +

        <% if(!_.isUndefined(extension) && extension.length > 1) { %><%- extension[extension.length - 1].toUpperCase() %><% }%>

        + <% }%> +
        +
        + <%- attachment.get('name') %> + Added <%- attachment.get("created") %> + + <%if(!_.isUndefined(authuser.user)) {%> + + <% } %> +
        +<% }else{ %> + No attachments available +<% } %> \ No newline at end of file diff --git a/client/js/templates/attachment_delete_confirm.jst.ejs b/client/js/templates/attachment_delete_confirm.jst.ejs new file mode 100644 index 000000000..e56a921cb --- /dev/null +++ b/client/js/templates/attachment_delete_confirm.jst.ejs @@ -0,0 +1 @@ +
        Delete Attachment?

        Deleting an attachment is permanent. There is no undo.

        Delete
        \ No newline at end of file diff --git a/client/js/templates/attachment_delete_confirm_form.jst.ejs b/client/js/templates/attachment_delete_confirm_form.jst.ejs new file mode 100644 index 000000000..16dbfa090 --- /dev/null +++ b/client/js/templates/attachment_delete_confirm_form.jst.ejs @@ -0,0 +1 @@ +
        Delete Attachment?

        Deleting an attachment is permanent. There is no undo.

        Delete
        \ No newline at end of file diff --git a/client/js/templates/board.jst.ejs b/client/js/templates/board.jst.ejs new file mode 100644 index 000000000..b4e3d09de --- /dev/null +++ b/client/js/templates/board.jst.ejs @@ -0,0 +1,54 @@ +<% + if(board.attributes.is_closed) { + var style = ''; + if (board.attributes.background_picture_url || board.attributes.background_pattern_url || board.attributes.background_color) { + style = 'color:#ffffff;'; + } +%> + +
        + Board is closed + +
        + <% } else {%> +
        +
        +
        + <% if(!_.isUndefined(authuser.user) && !_.isEmpty(role_links.where({slug: "add_list"}))){ %> +
        +
        Add a list +
        +
        + + +
        +
        + +
        +
        +
        +
        + <% } %> +
        +
        +
        + <% } %> \ No newline at end of file diff --git a/client/js/templates/board_404.jst.ejs b/client/js/templates/board_404.jst.ejs new file mode 100644 index 000000000..7da7e36fe --- /dev/null +++ b/client/js/templates/board_404.jst.ejs @@ -0,0 +1,5 @@ +
        + Board not found +
        This board may be private. <% if(!_.isEmpty(role_links.where({slug: "users_login"})) && _.isUndefined(authuser.user)){ %>You may be able to view it by logging in. <% } %> +
        +
        \ No newline at end of file diff --git a/client/js/templates/board_add.jst.ejs b/client/js/templates/board_add.jst.ejs new file mode 100644 index 000000000..5e632b50d --- /dev/null +++ b/client/js/templates/board_add.jst.ejs @@ -0,0 +1,41 @@ +
      • +
        + + Create Board + +
        +
      • +
      • +
      • +
        +
        + + +
        + <% if (templates.models.length > 0 && !_.isEmpty(role_links.where({slug: "view_organization_visibility"}))) { %> +
        + +
        <% } %> +
        + + +
      • \ No newline at end of file diff --git a/client/js/templates/board_add_organization_form.jst.ejs b/client/js/templates/board_add_organization_form.jst.ejs new file mode 100644 index 000000000..5870574b5 --- /dev/null +++ b/client/js/templates/board_add_organization_form.jst.ejs @@ -0,0 +1,2 @@ + +<% var organization_options = ''; _.each(organizations.models, function(organization) { organization_options += ''; }); %> \ No newline at end of file diff --git a/client/js/templates/board_additional_settings.jst.ejs b/client/js/templates/board_additional_settings.jst.ejs new file mode 100644 index 000000000..618832d10 --- /dev/null +++ b/client/js/templates/board_additional_settings.jst.ejs @@ -0,0 +1 @@ +
        Additional Settings
        • Card Cover Images Enabled
        • Enable Card Cover Images
        \ No newline at end of file diff --git a/client/js/templates/board_background.jst.ejs b/client/js/templates/board_background.jst.ejs new file mode 100644 index 000000000..4d5d6c4b4 --- /dev/null +++ b/client/js/templates/board_background.jst.ejs @@ -0,0 +1,207 @@ +
        + Change Background +
        +
        +
        +
          + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        +

        Photos

        + <% if (!_.isEmpty(board.attributes.background_picture_url) && board.attributes.background_picture_url != 'NULL') { + var background_picture_url = (board.attributes.background_picture_url).replace("_XXXX.jpg", "_s.jpg"); + %> +
          + +
        + <% } else { %> + Choose... + <% } %> + +

        Patterns and Textures

        + <% if (!_.isEmpty(board.attributes.background_pattern_url) && board.attributes.background_pattern_url != 'NULL') { + var background_pattern_url = (board.attributes.background_pattern_url).replace("_XXXX.jpg", "_s.jpg"); + %> +
          + +
        + <% } else { %> + Choose... + <% } %> + + + +

        Custom

        +
          + <% board.custom_attachments.each(function(custom_attachment) { %> + + <% }); %> +
        + +<% if(!_.isEmpty(role_links.where({slug: "add_custom_background"}))){ %> +
        +
        +
        + Drop files to upload +
        +
        +
        +<% } %> +
        +

        Productivity Beats

        +<% if (!_.isEmpty(board.attributes.music_content) && board.attributes.music_content != 'NULL') { %> +
          +
        • + +
            + +
          + <% } else { %> + Add ... + <% } %> +
        +
        \ No newline at end of file diff --git a/client/js/templates/board_custom_background.jst.ejs b/client/js/templates/board_custom_background.jst.ejs new file mode 100644 index 000000000..5c667f1cc --- /dev/null +++ b/client/js/templates/board_custom_background.jst.ejs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/js/templates/board_filter.jst.ejs b/client/js/templates/board_filter.jst.ejs new file mode 100644 index 000000000..e504fd294 --- /dev/null +++ b/client/js/templates/board_filter.jst.ejs @@ -0,0 +1,51 @@ +
        + + + + Filter Cards +
        +
        +
        + <% if (board.labels.length > 0) { %> + +
        + <% } %> + <% if (board.board_users.length > 0) { %> + +
        + <% } %> + +
        \ No newline at end of file diff --git a/client/js/templates/board_header.jst.ejs b/client/js/templates/board_header.jst.ejs new file mode 100644 index 000000000..a27e3d6f8 --- /dev/null +++ b/client/js/templates/board_header.jst.ejs @@ -0,0 +1,288 @@ +
        +
        + + + +
        +
        diff --git a/client/js/templates/board_index.jst.ejs b/client/js/templates/board_index.jst.ejs new file mode 100644 index 000000000..d81424cc7 --- /dev/null +++ b/client/js/templates/board_index.jst.ejs @@ -0,0 +1,7 @@ + <% if(!_.isEmpty(role_links.where({slug: "view_my_boards"}))){ %> +
        +
        + +
        +
        + <% } %> diff --git a/client/js/templates/board_index_header.jst.ejs b/client/js/templates/board_index_header.jst.ejs new file mode 100644 index 000000000..d5c83c59b --- /dev/null +++ b/client/js/templates/board_index_header.jst.ejs @@ -0,0 +1,21 @@ +
        +
        + + +
        +
        \ No newline at end of file diff --git a/client/js/templates/board_member_add_search_result.jst.ejs b/client/js/templates/board_member_add_search_result.jst.ejs new file mode 100644 index 000000000..d827afec4 --- /dev/null +++ b/client/js/templates/board_member_add_search_result.jst.ejs @@ -0,0 +1,13 @@ +<% if(user != null){ %> + + <% if(!_.isEmpty(user.attributes.profile_picture_path)) { + var profile_picture_path = user.showImage('User', user.attributes.id, 'micro_thumb' ); + %> + [Image: <%-user.attributes.username %>] + <% } else {%> + <%- user.attributes.initials %> + <% } %> + <%- user.attributes.username %> +<% } else{ %> + No users available. +<% } %> \ No newline at end of file diff --git a/client/js/templates/board_organization_form.jst.ejs b/client/js/templates/board_organization_form.jst.ejs new file mode 100644 index 000000000..e76670418 --- /dev/null +++ b/client/js/templates/board_organization_form.jst.ejs @@ -0,0 +1,22 @@ +<% + + var organization_options = ''; + var is_org_found = false; + var display_options = false; + _.each(organizations.models, function(organization) { + display_options = true; + if(parseInt(organization.id) === board.attributes.organization_id){ + is_org_found = true; + } + var selected = (parseInt(organization.id) === board.attributes.organization_id) ? 'selected="selected"' : ''; + var current = (parseInt(organization.id) === board.attributes.organization_id) ? ' (current)' : ''; + organization_options += ''; + }); + if(!is_org_found && board.attributes.organization_id != 0){ + display_options = true; + organization_options += ''; + } +%> +
        +
        disabled<%}%>>
        +
        \ No newline at end of file diff --git a/client/js/templates/board_sidebar.jst.ejs b/client/js/templates/board_sidebar.jst.ejs new file mode 100644 index 000000000..08fc35232 --- /dev/null +++ b/client/js/templates/board_sidebar.jst.ejs @@ -0,0 +1,73 @@ +<% + var background_color = board.attributes.background_color; + var background_picture_url = board.attributes.background_picture_url; + var background_pattern_url = board.attributes.background_pattern_url; + var preview = ''; + if (!_.isEmpty(background_picture_url) && background_picture_url != 'NULL') { + background_picture_url = background_picture_url.replace("_XXXX.jpg", "_b.jpg"); + preview = ''; + } else if (!_.isEmpty(background_pattern_url) && background_pattern_url != 'NULL') { + background_pattern_url = background_pattern_url.replace("_XXXX.jpg", "_s.jpg"); + preview = ''; + } else if (!_.isEmpty(background_color) && background_color != 'NULL') { + preview = ''; + } +%> +
      • Filter Cards
      • +<% if(is_admin || (!_.isUndefined(authuser.user) && authuser.user.role_id == 1)) {%> + <% if(!_.isEmpty(role_links.where({slug: "view_archived_lists"})) || !_.isEmpty(role_links.where({slug: "view_archived_cards"}))){ %> +
      • Archived Items
      • + <% } %> +<% } %> +<%if(!_.isUndefined(authuser.user)) {%> +<% if(!_.isEmpty(role_links.where({slug: "view_sync_calendar"}))){ %> + +<% } %> +
      • +<% if(is_admin || (!_.isUndefined(authuser.user) && authuser.user.role_id == 1)) {%> +
      • Change Background
      • +<% }%> +<%if(!_.isUndefined(authuser.user) && !is_offline_data) {%> +
      • Copy Board +
      • +<% }} %> +
      • +
      • Show Attachments
      • +<% if(is_admin || (!_.isUndefined(authuser.user) && authuser.user.role_id == 1)) {%> +
      • Additional Settings
      • + +<% } %> \ No newline at end of file diff --git a/client/js/templates/board_simple_view.jst.ejs b/client/js/templates/board_simple_view.jst.ejs new file mode 100644 index 000000000..6014a1ac2 --- /dev/null +++ b/client/js/templates/board_simple_view.jst.ejs @@ -0,0 +1,82 @@ +<% if(board != null){ %> +
        + <% if(!_.isEmpty(board.subscribers)){ var subscriber = board.subscribers.findWhere({ user_id: parseInt(authuser.user.id) }); } %> +
        +
        + + <% if(board.attributes.is_admin || (!_.isUndefined(authuser.user) && authuser.user.role_id == 1)) {%> + + <% if(board.attributes.board_visibility == 0) { %> + + <% } else if(board.attributes.board_visibility == 1) { %> + + <% } else if(board.attributes.board_visibility == 2) { %> + + <% } %> + + + + <% } else {%> + + <% if(board.attributes.board_visibility == 0) { %> + + <% } else if(board.attributes.board_visibility == 1) { %> + + <% } else if(board.attributes.board_visibility == 2) { %> + + <% } %> + + <% } %> + <% if(!_.isEmpty(role_links.where({slug: "starred_board"}))){ %> + <% if(!_.isUndefined(starred_boards) && starred_boards.map( Number ).indexOf(board.attributes.id) != -1){ %> + + <% } else {%> + + <% } %> + <% } %> +
        +
        +
          + <% + var style = ''; + if (board.attributes.background_picture_url) { + var background_picture_url = board.attributes.background_picture_url.replace("_XXXX.jpg", "_n.jpg"); + style = 'background:url(' + background_picture_url + ') 25% 25%; background-size: cover'; + } else if (board.attributes.background_pattern_url) { + var background_pattern_url = board.attributes.background_pattern_url.replace("_XXXX.jpg", "_s.jpg"); + style = 'background: transparent url(' + background_pattern_url + ') repeat scroll 0% 0%;'; + } else if (board.attributes.background_color){ + style = 'background:' + board.attributes.background_color; + } + %> +
        • + +
          +
          +
        • +
        +
        +
        +<% }else{ %> +
        + <%= message %> +
        +<%}%> \ No newline at end of file diff --git a/client/js/templates/board_user_actions.jst.ejs b/client/js/templates/board_user_actions.jst.ejs new file mode 100644 index 000000000..55cb3e12f --- /dev/null +++ b/client/js/templates/board_user_actions.jst.ejs @@ -0,0 +1,47 @@ +
      • +
        + + <% if(!_.isEmpty(user.attributes.profile_picture_path)) { + var profile_picture_path = user.showImage('User', user.attributes.user_id, 'micro_thumb' ); + %> + [Image: <%-user.attributes.username %>] + <% } else {%> + <%- user.attributes.initials %> + <% } %> + <%- user.attributes.username %> + + + +
        +
        + + +
      • \ No newline at end of file diff --git a/client/js/templates/board_user_activity.jst.ejs b/client/js/templates/board_user_activity.jst.ejs new file mode 100644 index 000000000..07c4b12af --- /dev/null +++ b/client/js/templates/board_user_activity.jst.ejs @@ -0,0 +1 @@ +
        Member Activities
        • User activities......
        \ No newline at end of file diff --git a/client/js/templates/board_user_remove_confirm.jst.ejs b/client/js/templates/board_user_remove_confirm.jst.ejs new file mode 100644 index 000000000..c72b4ca1f --- /dev/null +++ b/client/js/templates/board_user_remove_confirm.jst.ejs @@ -0,0 +1,19 @@ +
      • +
        +

        Remove Member? + + + +

        +
        +
        + +
      • \ No newline at end of file diff --git a/client/js/templates/board_users_view.jst.ejs b/client/js/templates/board_users_view.jst.ejs new file mode 100644 index 000000000..c81f2bd6d --- /dev/null +++ b/client/js/templates/board_users_view.jst.ejs @@ -0,0 +1,17 @@ + + <% if(!_.isEmpty(user.attributes.profile_picture_path)) { + var profile_picture_path = user.showImage('User', user.attributes.user_id, 'small_thumb' ); + %> + [Image: <%-user.attributes.username %>] + <% } else {%> + <%- user.attributes.initials %> + <% } %> + <% if(is_admin && user.attributes.is_admin) { %> + Owner + <% } %> + + \ No newline at end of file diff --git a/client/js/templates/board_visibility.jst.ejs b/client/js/templates/board_visibility.jst.ejs new file mode 100644 index 000000000..49381810e --- /dev/null +++ b/client/js/templates/board_visibility.jst.ejs @@ -0,0 +1,19 @@ +
      • + +Private + <% if(board.attributes.board_visibility == 0){ %> + + <%}%> + This board is private. Only people added to the board can view and edit it.
      • +
      • +
      • Organization + <% if(board.attributes.board_visibility == 1){ %> + + <%}%> + This board is visible to members of the organization. Only people added to the board can edit. The board must be added to an org to enable this.
      • +
      • +
      • Public + <% if(board.attributes.board_visibility == 2){ %> + + <%}%> + This board is public. It's visible to anyone with the link and will show up in search engines like Google. Only people added to the board can edit.
      • diff --git a/client/js/templates/card.jst.ejs b/client/js/templates/card.jst.ejs new file mode 100644 index 000000000..b51ea8a4d --- /dev/null +++ b/client/js/templates/card.jst.ejs @@ -0,0 +1,94 @@ +
        + <% if(!_.isEmpty(role_links.where({slug: "view_card_labels"}))){ %> +
        + <% + card.labels.each(function(label) { + if (!_.isUndefined(label) && label.attributes.name !== "") { %> + + + <% } + });%> + + +
        + <% } %> + <% if(!_.isEmpty(card.attachments) && card.attachments.length > 0 && card.attachments.at(0).attributes.name.toLowerCase().match(/\.(jpg|jpeg|png|gif)$/)){ %> + + <% } %> + <%- card.attributes.name %> +
          + <% + if(!_.isUndefined(authuser) && !_.isUndefined(authuser.user)){ + var cards_subscribers = card.cards_subscribers.where({ + is_subscribed: true, + user_id: parseInt(authuser.user.id) + }); + } + %> + <% if(!_.isUndefined(cards_subscribers) && !_.isEmpty(cards_subscribers)){ %> +
        • + <% } %> + <% if(card.card_voters.length > 0){ %> +
        • <%- card.card_voters.length %>
        • + <% } %> + <% + var comment = card.list.collection.board.activities.where({card_id: card.attributes.id, type: "add_comment"}); + if(!_.isEmpty(comment) && comment.length > 0){ %> +
        • <%- comment.length %>
        • + <% } %> + <% if(!_.isEmpty(card.attributes.description)){ %> +
        • + <% } %> + <% if(card.attributes.checklist_item_count > 0){ %> +
        • <% if(card.attributes.checklist_item_completed_count == card.attributes.checklist_item_count) { %>
          <% } %><%- card.attributes.checklist_item_completed_count %>/<%- card.attributes.checklist_item_count %><% if(card.attributes.checklist_item_completed_count == card.attributes.checklist_item_count) { %>
          <% } %>
        • + <% } %> + <% if(card.attributes.due_date > 0){ %> +
        • <%- card.attributes.due_date %>
        • + <% } %> + <% if(!_.isEmpty(card.attachments) && card.attachments.length > 0){ %> +
        • + + + + <%- card.attachments.length %> + + +
        • + <% } %> + <% if(!_.isUndefined(card.id) && (_.isUndefined(card.attributes.is_offline) || card.attributes.is_offline == false)){ %> +
        • #<%- card.id %>
        • + <%}%> + <% if(!_.isEmpty(card.attributes.due_date) && card.attributes.due_date != 'NULL'){ + var date_time = card.attributes.due_date.split('T'); + date_time = date_time[0].split(' '); + %> +
        • <%= dateFormat(date_time[0], 'mediumDate') %>
        • + <% } %> +
        +
        +
          + <% + card.users.each(function(card_user) { + if (!_.isUndefined(card_user)) { + %> +
        • + <% if(!_.isEmpty(card_user.attributes.profile_picture_path)) { + var profile_picture_path = card.showImage('User', card_user.attributes.user_id, 'small_thumb' ); + %> + [Image: <%-card_user.attributes.username %>] + <% } else {%> + <%- card_user.attributes.initials %> + <% } %> +
        • + <% + } + }); + %> +
        +
        +
        \ No newline at end of file diff --git a/client/js/templates/card_actions.jst.ejs b/client/js/templates/card_actions.jst.ejs new file mode 100644 index 000000000..276309b73 --- /dev/null +++ b/client/js/templates/card_actions.jst.ejs @@ -0,0 +1 @@ +
      • Options
      • <% if(!_.isEmpty(role_links.where({slug: "add_card_user"}))){ %>
      • Members
      • <% } %><% if(!_.isEmpty(role_links.where({slug: "add_labels"}))){ %>
      • Labels
      • <% } %>
      • Position
      • \ No newline at end of file diff --git a/client/js/templates/card_add.jst.ejs b/client/js/templates/card_add.jst.ejs new file mode 100644 index 000000000..c80ff263a --- /dev/null +++ b/client/js/templates/card_add.jst.ejs @@ -0,0 +1,23 @@ +
        + + + + + +
        + +
        +
        +
        + +
        +
          +
        • + +
        • + +
        +
        +
        diff --git a/client/js/templates/card_attachment.jst.ejs b/client/js/templates/card_attachment.jst.ejs new file mode 100644 index 000000000..a1686a10c --- /dev/null +++ b/client/js/templates/card_attachment.jst.ejs @@ -0,0 +1,31 @@ +<% if(attachment.attributes.name.toLowerCase().match(/\.(jpg|jpeg|png|gif)$/)){ +var picture_path = attachment.showImage('CardAttachment', attachment.attributes.id, 'large_thumb' ); +%> +<% } else{ var extension = attachment.attributes.name.split('.'); %>

        <% if(!_.isUndefined(extension) && extension.length > 1) { %><%- extension[extension.length - 1].toUpperCase() %><% }%>

        <% }%>
        + +
        +<%- attachment.attributes.name %>Added <%- attachment.attributes.created %> +<%if(!_.isUndefined(authuser.user)) {%> + +<% } %> +
        \ No newline at end of file diff --git a/client/js/templates/card_checklist.jst.ejs b/client/js/templates/card_checklist.jst.ejs new file mode 100644 index 000000000..8a02e432d --- /dev/null +++ b/client/js/templates/card_checklist.jst.ejs @@ -0,0 +1,39 @@ +
        +

        + + + <%- checklist.get('name') %> + +

        + <% if(!_.isUndefined(authuser.user) && !_.isEmpty(role_links.where({slug: "delete_checklist"}))){ %> + + <% } %> +
        +
        +<% + var percentage = ((parseInt(checklist.get('checklist_item_completed_count')) / parseInt(checklist.get('checklist_item_count'))) * 100); + %> + <% + if(isNaN(percentage)){ + percentage_val = 0; + } else { + percentage_val = Math.round(percentage, 2); + } + %> +
        + + <%= percentage_val%>%  
        <%= Math.round(percentage, 2) %>%
        \ No newline at end of file diff --git a/client/js/templates/card_checklist_item.jst.ejs b/client/js/templates/card_checklist_item.jst.ejs new file mode 100644 index 000000000..bce5854a0 --- /dev/null +++ b/client/js/templates/card_checklist_item.jst.ejs @@ -0,0 +1,10 @@ +
        +
        + checked<% } %> <% if(_.isUndefined(authuser.user)){%> disabled<%}%> > + + +
        +
        \ No newline at end of file diff --git a/client/js/templates/card_copy.jst.ejs b/client/js/templates/card_copy.jst.ejs new file mode 100644 index 000000000..9c93415e7 --- /dev/null +++ b/client/js/templates/card_copy.jst.ejs @@ -0,0 +1 @@ +
      • Search for Card to Copy...
        • \ No newline at end of file diff --git a/client/js/templates/card_duedate_from.jst.ejs b/client/js/templates/card_duedate_from.jst.ejs new file mode 100644 index 000000000..90bf3bcc3 --- /dev/null +++ b/client/js/templates/card_duedate_from.jst.ejs @@ -0,0 +1 @@ +<% var date = ''; var time = ''; if (card.attributes.due_date !== null && card.attributes.due_date !== 'NULL') { var date_time = card.attributes.due_date.split('T'); date = date_time[0]; time = date_time[1]; } %>
          <% if(!_.isEmpty(card.attributes.due_date) && card.attributes.due_date != 'NULL') { %>
          <% } %>
          \ No newline at end of file diff --git a/client/js/templates/card_label.jst.ejs b/client/js/templates/card_label.jst.ejs new file mode 100644 index 000000000..1af9598c1 --- /dev/null +++ b/client/js/templates/card_label.jst.ejs @@ -0,0 +1 @@ +<%- label.get('name') %> \ No newline at end of file diff --git a/client/js/templates/card_label_form.jst.ejs b/client/js/templates/card_label_form.jst.ejs new file mode 100644 index 000000000..ab5db541a --- /dev/null +++ b/client/js/templates/card_label_form.jst.ejs @@ -0,0 +1,4 @@ +
          + + Labels +
          \ No newline at end of file diff --git a/client/js/templates/card_labels_form.jst.ejs b/client/js/templates/card_labels_form.jst.ejs new file mode 100644 index 000000000..e3be0888d --- /dev/null +++ b/client/js/templates/card_labels_form.jst.ejs @@ -0,0 +1 @@ +
          Labels
          \ No newline at end of file diff --git a/client/js/templates/card_list_view.jst.ejs b/client/js/templates/card_list_view.jst.ejs new file mode 100644 index 000000000..6f5bce3d7 --- /dev/null +++ b/client/js/templates/card_list_view.jst.ejs @@ -0,0 +1,32 @@ +<% if(card != null && !_.isEmpty(card)){ %> + +
            +
          +
            +
          +
            +
          • +
          +
          +
          + +
          +
          + +<% } else{ %>No cards available.<% } %> \ No newline at end of file diff --git a/client/js/templates/card_member_form.jst.ejs b/client/js/templates/card_member_form.jst.ejs new file mode 100644 index 000000000..973e43f37 --- /dev/null +++ b/client/js/templates/card_member_form.jst.ejs @@ -0,0 +1,39 @@ +
        • +
          + + Members + +
          +
        • +
        • + <% if (!_.isEmpty(card.board_users.models)) { + card.board_users.each(function(board_user) { + var added_user = card.users.findWhere({ user_id: board_user.get('user_id') }); + if (_.isEmpty(added_user)) { %> +
        • + + <% if(!_.isEmpty(board_user.attributes.profile_picture_path)) { + var profile_picture_path = board_user.showImage('User', board_user.attributes.id, 'micro_thumb' ); + %> + [Image: <%-board_user.attributes.username %>] + <% } else {%> + <%- board_user.attributes.initials %> + <% } %> + <%- board_user.get('username') %> +
        • + <% } else { %> +
        • + <%-board_user.get('initials') %> + <% if(!_.isEmpty(board_user.attributes.profile_picture_path)) { + var profile_picture_path = board_user.showImage('User', board_user.attributes.id, 'micro_thumb' ); + %> + [Image: <%-board_user.attributes.username %>] + <% } else {%> + <%- board_user.attributes.initials %> + <% } %> + <%-board_user.get('username') %>
        • + <% } + }); + }else{ %> +
        • No members available.
        • + <% } %> diff --git a/client/js/templates/card_positions_form.jst.ejs b/client/js/templates/card_positions_form.jst.ejs new file mode 100644 index 000000000..3b345cfc4 --- /dev/null +++ b/client/js/templates/card_positions_form.jst.ejs @@ -0,0 +1,46 @@ +<% +var content_list = ''; +content_position += ''; +var content = content_list + content_position; +%> + +
          + Position +
          +
          +
          + <%= content %> + +
          + diff --git a/client/js/templates/card_search_result.jst.ejs b/client/js/templates/card_search_result.jst.ejs new file mode 100644 index 000000000..75f948e0d --- /dev/null +++ b/client/js/templates/card_search_result.jst.ejs @@ -0,0 +1 @@ +<%- card.attributes.name %> \ No newline at end of file diff --git a/client/js/templates/card_search_users_result.jst.ejs b/client/js/templates/card_search_users_result.jst.ejs new file mode 100644 index 000000000..7c1411f85 --- /dev/null +++ b/client/js/templates/card_search_users_result.jst.ejs @@ -0,0 +1,18 @@ +<% + if(user != null){ +%> + data-card-user-id="<%-added_user.id %><%}%>" data-user-name="<%- user.attributes.username %>" data-user-initial="<%-user.attributes.initials %>" data-user-profile-picture-path="<%=user.attributes.profile_picture_path%>" data-user-id="<% if (!_.isUndefined(user.attributes.user_id)) { %><%- user.attributes.user_id %><%}else{%><%- user.attributes.id %><%}%>"> + + <% if(!_.isEmpty(user.attributes.profile_picture_path)) { + var user_id = (!_.isUndefined(user.attributes.user_id))? user.attributes.user_id:user.attributes.id ; + var profile_picture_path = user.showImage('User', user_id, 'micro_thumb' ); + %> + [Image: <%-user.attributes.username %>] + <% } else {%> + <%-user.attributes.initials %> + <% } %> + <%- user.attributes.username %> + <% if(!_.isUndefined(user.attributes.is_existing_user) || is_added_user){ %><%}%> +<% } else{ %> +
          No users available.
          +<% } %> \ No newline at end of file diff --git a/client/js/templates/card_voters_list.jst.ejs b/client/js/templates/card_voters_list.jst.ejs new file mode 100644 index 000000000..78e2fbd56 --- /dev/null +++ b/client/js/templates/card_voters_list.jst.ejs @@ -0,0 +1,25 @@ +
        • +
          + Voters +
          +
        • +
        • +<% + if (!_.isEmpty(card.card_voters.models)){ + card.card_voters.each(function(voter) { + %> +
        • + + <% if(!_.isEmpty(voter.attributes.profile_picture_path)) { + var profile_picture_path = card.showImage('User', voter.attributes.user_id, 'micro_thumb' ); + %> + [Image: <%-voter.attributes.username %>] + <% } else {%> + <%- voter.attributes.initials %> + <% } %> + + <%- voter.attributes.username %>
        • + <% }); + }else{ %> +
        • No voters available. + <% } %> diff --git a/client/js/templates/change_password.jst.ejs b/client/js/templates/change_password.jst.ejs new file mode 100644 index 000000000..051b43300 --- /dev/null +++ b/client/js/templates/change_password.jst.ejs @@ -0,0 +1,25 @@ +
          +
          +
          Change Password
          +
          +
          +
          + + +
          +
          + + +
          +
          + + +
          +
          + + +
          +
          +
          +
          +
          \ No newline at end of file diff --git a/client/js/templates/chat.jst.ejs b/client/js/templates/chat.jst.ejs new file mode 100644 index 000000000..7370b02b5 --- /dev/null +++ b/client/js/templates/chat.jst.ejs @@ -0,0 +1 @@ +

          Coming soon.....

          \ No newline at end of file diff --git a/client/js/templates/checklist_actions.jst.ejs b/client/js/templates/checklist_actions.jst.ejs new file mode 100644 index 000000000..c7301a7b4 --- /dev/null +++ b/client/js/templates/checklist_actions.jst.ejs @@ -0,0 +1 @@ +Delete This Checklist \ No newline at end of file diff --git a/client/js/templates/checklist_add_form.jst.ejs b/client/js/templates/checklist_add_form.jst.ejs new file mode 100644 index 000000000..aa7c2fd00 --- /dev/null +++ b/client/js/templates/checklist_add_form.jst.ejs @@ -0,0 +1,32 @@ +
          + + +
          +
          + + + + + +
          +
          + + +
          \ No newline at end of file diff --git a/client/js/templates/checklist_delete_confirm_form.jst.ejs b/client/js/templates/checklist_delete_confirm_form.jst.ejs new file mode 100644 index 000000000..af2b01e6a --- /dev/null +++ b/client/js/templates/checklist_delete_confirm_form.jst.ejs @@ -0,0 +1,8 @@ +
          + Delete Checklist? +
          +
          +
          +

          Deleting a checklist is permanent and there is no way to get it back.

          + Delete +
          \ No newline at end of file diff --git a/client/js/templates/checklist_edit_form.jst.ejs b/client/js/templates/checklist_edit_form.jst.ejs new file mode 100644 index 000000000..58e0b3811 --- /dev/null +++ b/client/js/templates/checklist_edit_form.jst.ejs @@ -0,0 +1,12 @@ +
          + +
          + +
          +
          +
          +
          + + +
          +
          \ No newline at end of file diff --git a/client/js/templates/checklist_item_actions.jst.ejs b/client/js/templates/checklist_item_actions.jst.ejs new file mode 100644 index 000000000..1d4c9c67d --- /dev/null +++ b/client/js/templates/checklist_item_actions.jst.ejs @@ -0,0 +1,9 @@ +
          + Options +
          +
          + \ No newline at end of file diff --git a/client/js/templates/checklist_item_add_form.jst.ejs b/client/js/templates/checklist_item_add_form.jst.ejs new file mode 100644 index 000000000..4303c4e6f --- /dev/null +++ b/client/js/templates/checklist_item_add_form.jst.ejs @@ -0,0 +1,16 @@ +
          + +
          + +
          +
          +
          + + + +
          \ No newline at end of file diff --git a/client/js/templates/checklist_item_add_link.jst.ejs b/client/js/templates/checklist_item_add_link.jst.ejs new file mode 100644 index 000000000..9709b02a3 --- /dev/null +++ b/client/js/templates/checklist_item_add_link.jst.ejs @@ -0,0 +1,4 @@ +Add Item +
          + +
          \ No newline at end of file diff --git a/client/js/templates/checklist_item_delete_confirm_form.jst.ejs b/client/js/templates/checklist_item_delete_confirm_form.jst.ejs new file mode 100644 index 000000000..d1d2ad40b --- /dev/null +++ b/client/js/templates/checklist_item_delete_confirm_form.jst.ejs @@ -0,0 +1,8 @@ +
          + Delete Checklist? +
          +
          +
          +

          Deleting a item is permanent and there is no way to get it back.

          + Delete +
          \ No newline at end of file diff --git a/client/js/templates/checklist_item_edit_form.jst.ejs b/client/js/templates/checklist_item_edit_form.jst.ejs new file mode 100644 index 000000000..a77b6fb81 --- /dev/null +++ b/client/js/templates/checklist_item_edit_form.jst.ejs @@ -0,0 +1,44 @@ +
          + + +
          +
          +
          +
            +
          • +
            + +
            +
          • +
          • + +
          • +
          • +
              + <% if (!_.isEmpty(role_links.where({slug: "delete_checklist_item"}))) { %> + + <% } %> + <% + if (!_.isEmpty(role_links.where({ + slug: "convert_item_to_card" + }))) { %> +
            • + Convert to Card +
            • + <% } %> + +
            +
          • +
          +
          +
          diff --git a/client/js/templates/checklist_item_mention_member.jst.ejs b/client/js/templates/checklist_item_mention_member.jst.ejs new file mode 100644 index 000000000..7cf90be7b --- /dev/null +++ b/client/js/templates/checklist_item_mention_member.jst.ejs @@ -0,0 +1,16 @@ +<% if(board_user != null){ %> + + + <% if(!_.isEmpty(board_user.attributes.profile_picture_path)) { + var profile_picture_path = board_user.showImage('User', board_user.attributes.user_id, 'small_thumb' ); + %> + [Image: <%-board_user.attributes.username %>] + <% } else {%> + <%-board_user.attributes.initials %> + <% } %> + <%- board_user.attributes.username %> + + +<% }else{%> + No users available. +<%}%> diff --git a/client/js/templates/checklist_item_mention_member_search_form.jst.ejs b/client/js/templates/checklist_item_mention_member_search_form.jst.ejs new file mode 100644 index 000000000..f29515c45 --- /dev/null +++ b/client/js/templates/checklist_item_mention_member_search_form.jst.ejs @@ -0,0 +1,21 @@ +
          + Mention Member +
          +
          +
          +
            +
          • +
            +
            + + +
            +
            +
          • +
          • + Search for a person in <%- SITE_NAME %> by name or email address. +
          • +
          +
          + + diff --git a/client/js/templates/closed_boards_index.jst.ejs b/client/js/templates/closed_boards_index.jst.ejs new file mode 100644 index 000000000..825c7619c --- /dev/null +++ b/client/js/templates/closed_boards_index.jst.ejs @@ -0,0 +1,6 @@ + <% if(!_.isEmpty(role_links.where({slug: "view_closed_boards"}))){ %> +
          +
          +
          +
          + <% } %> diff --git a/client/js/templates/closed_boards_listing.jst.ejs b/client/js/templates/closed_boards_listing.jst.ejs new file mode 100644 index 000000000..5737cbb56 --- /dev/null +++ b/client/js/templates/closed_boards_listing.jst.ejs @@ -0,0 +1,45 @@ +<% if(board != null){ +var style = ''; +if (board.attributes.background_picture_url) { + var background_picture_url = board.attributes.background_picture_url.replace("_XXXX.jpg", "_s.jpg"); + style = 'background-image:url(' + background_picture_url + '); background-size:cover;'; +} else if (board.attributes.background_pattern_url) { + var background_pattern_url = board.attributes.background_pattern_url.replace("_XXXX.jpg", "_s.jpg"); + style = 'background-image:url(' + background_pattern_url + '); background-size:cover;'; +} else if (board.attributes.background_color){ + style = 'background-color:' + board.attributes.background_color + ';color:#ffffff;'; +} else { + style = ''; +} +%> + + + <%- board.attributes.name %> + + + +<% }else{ %> +
          + No boards available. +
          +<%}%> \ No newline at end of file diff --git a/client/js/templates/copy_board_visibility.jst.ejs b/client/js/templates/copy_board_visibility.jst.ejs new file mode 100644 index 000000000..79269c889 --- /dev/null +++ b/client/js/templates/copy_board_visibility.jst.ejs @@ -0,0 +1,10 @@ + + This board will be + Private. + +Change +<% if (name == 'org') { %> + This board will be Organization.Change +<% } else if (name == 'public') { %> + This board will be Public.Change +<% } %> \ No newline at end of file diff --git a/client/js/templates/copy_card.jst.ejs b/client/js/templates/copy_card.jst.ejs new file mode 100644 index 000000000..d478c5327 --- /dev/null +++ b/client/js/templates/copy_card.jst.ejs @@ -0,0 +1,96 @@ +<% if (card.attachments.length > 0 || card.activities.length > 0 || card.labels.length > 0 || card.checklists.length > 0 || card.users.length > 0) { %> +
          +

          Keep...

          + <% if (card.attachments.length > 0) {%> +
          +
          + + +
          +
          + <% } + var comment = card.list.collection.board.activities.where({card_id: card.attributes.id, type: "add_comment"}); + if (comment.length > 0) { %> +
          +
          + + +
          +
          + <% } + if (card.labels.length > 0) { %> +
          +
          + + +
          +
          + <%} + if (card.checklists.length > 0) { %> +
          +
          + + +
          +
          + <% } + if (card.users.length > 0) { %> +
          +
          + + +
          +
          + <% } %> +
          +<% } + var content_board = '
          '; + var content_position = '
          '; + content_list += '
          '; + content_position += ''; + var content = content_board + content_list + content_position; +%> +<%= content %> \ No newline at end of file diff --git a/client/js/templates/copy_from_existing_card.jst.ejs b/client/js/templates/copy_from_existing_card.jst.ejs new file mode 100644 index 000000000..6d1d73084 --- /dev/null +++ b/client/js/templates/copy_from_existing_card.jst.ejs @@ -0,0 +1,66 @@ +
        • Copy Card

        • +
        • +
        • +
          +
          +
          + + +
          +
          +
          + <% + var content = ''; + var content_board = '
          '; + var content_position = '
          '; + board.lists.add(board.attributes.lists); + var is_first_list = true; + board.lists.each(function(list) { + is_first = false; + if (card.attributes.list_id == list.id) { + is_first_list = true; + content_list += ''; + board.cards.add(board.attributes.cards); + } else { + content_list += ''; + } + if(is_first_list){ + content_position = '
          '; + content_list += '
          '; + content_position += '
          '; + content = content_board + content_list + content_position; + %> + <%= content%> +
          +
          + + + +
          +
          +
        • + + \ No newline at end of file diff --git a/client/js/templates/copy_list.jst.ejs b/client/js/templates/copy_list.jst.ejs new file mode 100644 index 000000000..3c4fce897 --- /dev/null +++ b/client/js/templates/copy_list.jst.ejs @@ -0,0 +1,17 @@ +
          + Copy List +
          +
          +
          +
          +
          + + + + +
          +
          + +
          +
          +
          \ No newline at end of file diff --git a/client/js/templates/edit_activity_form.jst.ejs b/client/js/templates/edit_activity_form.jst.ejs new file mode 100644 index 000000000..6e9ef4f7a --- /dev/null +++ b/client/js/templates/edit_activity_form.jst.ejs @@ -0,0 +1,8 @@ +
          + + +
          +
          + + +
          \ No newline at end of file diff --git a/client/js/templates/edit_board_member_permission_to_admin.jst.ejs b/client/js/templates/edit_board_member_permission_to_admin.jst.ejs new file mode 100644 index 000000000..8bfeffa60 --- /dev/null +++ b/client/js/templates/edit_board_member_permission_to_admin.jst.ejs @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/client/js/templates/edit_board_member_permission_to_normal.jst.ejs b/client/js/templates/edit_board_member_permission_to_normal.jst.ejs new file mode 100644 index 000000000..7506ec3c6 --- /dev/null +++ b/client/js/templates/edit_board_member_permission_to_normal.jst.ejs @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/client/js/templates/email_templates.jst.ejs b/client/js/templates/email_templates.jst.ejs new file mode 100644 index 000000000..5e42fad77 --- /dev/null +++ b/client/js/templates/email_templates.jst.ejs @@ -0,0 +1,75 @@ + + +
          +
          +
          + +
          + +
          +
          + <% if (list.models.length > 0 ) { + var j = 0; + _.each(list.models, function(email_tempalte) { + var template = email_tempalte.get('template'); + %> + + <% + j++; + }); + } + %> +
          +
          +
          +
          + \ No newline at end of file diff --git a/client/js/templates/error_404.jst.ejs b/client/js/templates/error_404.jst.ejs new file mode 100644 index 000000000..67858a229 --- /dev/null +++ b/client/js/templates/error_404.jst.ejs @@ -0,0 +1 @@ +404 Page not found. \ No newline at end of file diff --git a/client/js/templates/flickr.jst.ejs b/client/js/templates/flickr.jst.ejs new file mode 100644 index 000000000..52434652a --- /dev/null +++ b/client/js/templates/flickr.jst.ejs @@ -0,0 +1,10 @@ +<% if(photo != null){ %> + <% + var path = 'http://farm' + photo.farm + '.static.flickr.com/' + photo.server + '/' + photo.id + '_' + photo.secret + '_s.jpg'; + %> +
          + + [Image: <%= photo.title%>] + +
          +<% } %> \ No newline at end of file diff --git a/client/js/templates/footer.jst.ejs b/client/js/templates/footer.jst.ejs new file mode 100644 index 000000000..c622abc45 --- /dev/null +++ b/client/js/templates/footer.jst.ejs @@ -0,0 +1,199 @@ + +<% if(!_.isUndefined(authuser.user)){%> + + <% } %> +
          + +
          +
          \ No newline at end of file diff --git a/client/js/templates/header.jst.ejs b/client/js/templates/header.jst.ejs new file mode 100644 index 000000000..7d51a7a42 --- /dev/null +++ b/client/js/templates/header.jst.ejs @@ -0,0 +1,143 @@ +<% if(_.isEmpty(current_param) || ((typeof(user) == "undefined" || (typeof(user) != "undefined" && user.role_id != 1)) || (current_param !== 'activities' && current_param !== 'users' && current_param !== 'roles' && current_param !== 'settings' && current_param !== 'email_templates'))){ %> + +<% } else if(typeof(user) != "undefined" && !_.isEmpty(current_param) && (current_param === 'activities' || current_param === 'users' || current_param === 'roles' || current_param === 'settings' || current_param === 'email_templates')){ %> +<% + if(!_.isEmpty(current_param) && (current_param === 'activities')) { + current_title = 'Activities'; + } else if(!_.isEmpty(current_param) && (current_param === 'users')){ + current_title = 'Users'; + } else if(!_.isEmpty(current_param) && (current_param === 'roles')){ + current_title = 'Roles'; + } else if(!_.isEmpty(current_param) && (current_param === 'settings')){ + current_title = 'Settings'; + } else if(!_.isEmpty(current_param) && (current_param === 'email_templates')){ + current_title = 'Email Templates'; + } +%> + +
          + + +
          +<% } %> \ No newline at end of file diff --git a/client/js/templates/instant_card_add.jst.ejs b/client/js/templates/instant_card_add.jst.ejs new file mode 100644 index 000000000..171539706 --- /dev/null +++ b/client/js/templates/instant_card_add.jst.ejs @@ -0,0 +1,97 @@ +<% if (boards.models.length == 0 || _.isEmpty(boards.models[0])) { %> + <% if (boards.models.length == 0) { %> + Please add board. + <% }else if ( _.isEmpty(boards.models[0])){ %> + Please add list. + <% }%> +<% }else { %> +
          +
          +
          + +
          + + +
          +
          + <% if (boards.models.length > 0) { %> +
          +
          + +
          +
          + + <% var position = _.max(first_board.attributes.lists, function(list) { + return list.position; + }); + if (isNaN(position.position)) { + position.position = first_board.attributes.board_lists.length; + } + %> + <% } %> +
          +
          + + <% } %> +
          + +
          + +
          +
          +
          +
          +
            + +
          • + + +
          • + +
          + +
          + +
          +
          +
          +
          +
          +<% }%> \ No newline at end of file diff --git a/client/js/templates/instant_card_add_labels_form.jst.ejs b/client/js/templates/instant_card_add_labels_form.jst.ejs new file mode 100644 index 000000000..78457e0e0 --- /dev/null +++ b/client/js/templates/instant_card_add_labels_form.jst.ejs @@ -0,0 +1,7 @@ +
          Labels
          +
          +
          +
          + +
          +
          \ No newline at end of file diff --git a/client/js/templates/instant_card_add_members_form.jst.ejs b/client/js/templates/instant_card_add_members_form.jst.ejs new file mode 100644 index 000000000..e03746053 --- /dev/null +++ b/client/js/templates/instant_card_add_members_form.jst.ejs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/js/templates/list.jst.ejs b/client/js/templates/list.jst.ejs new file mode 100644 index 000000000..b8e1f2ac4 --- /dev/null +++ b/client/js/templates/list.jst.ejs @@ -0,0 +1,35 @@ + +
          + +
          +
          +
          + <% if(!_.isUndefined(authuser.user) && !_.isEmpty(role_links.where({slug: "add_card"}))){ %> + + <% } %> + \ No newline at end of file diff --git a/client/js/templates/list_actions.jst.ejs b/client/js/templates/list_actions.jst.ejs new file mode 100644 index 000000000..874a05965 --- /dev/null +++ b/client/js/templates/list_actions.jst.ejs @@ -0,0 +1,42 @@ +<% + var is_subscribed = ''; + var subscribe_class = 'js-list-subscribe'; + var subscribe = subscribers.findWhere({ + user_id: parseInt(authuser.id) + }); + var subscribe_id = ''; + if (!_.isEmpty(subscribe) && subscribe.attributes.is_subscribed === true) { + is_subscribed = ''; + subscribe_class = 'js-list-unsubscribe'; + subscribe_id = subscribe.attributes.id; + } +%> +
        • List Actions
        • +
        • +<% if(!_.isEmpty(role_links.where({slug: "add_card"}))){ %> +
        • Add Card
        • +<% } %> +<% if(!_.isEmpty(role_links.where({slug: "add_list"})) && !is_offline_data){ %> +
        • Copy List
        • +<% } %> +<% if(!_.isEmpty(role_links.where({slug: "edit_list"}))){ %> +
        • Move List
        • +<% } %> +<% if(!_.isEmpty(role_links.where({slug: "subscribe_list"}))){ %> +
        • +<% } %> +
        • +<% if(!_.isEmpty(role_links.where({slug: "move_list_cards"}))){ %> +
        • Move All Cards in This List
        • +<% } %> +<% if(!_.isEmpty(role_links.where({slug: "edit_list"}))){ %> +
        • Archive All Cards in This List
        • +<% } %> +
        • +
        • Show Attachments
        • +<% if(!_.isEmpty(role_links.where({slug: "edit_list"}))){ %> +
        • Archive This List
        • +<% } %> +<% if(!_.isEmpty(role_links.where({slug: "delete_list"}))){ %> +
        • Delete This List
        • +<% } %> \ No newline at end of file diff --git a/client/js/templates/list_add.jst.ejs b/client/js/templates/list_add.jst.ejs new file mode 100644 index 000000000..fc32f0e5e --- /dev/null +++ b/client/js/templates/list_add.jst.ejs @@ -0,0 +1,15 @@ +
          + Add a list + +
          +
          + + +
          +
          + + + +
          +
          +
          \ No newline at end of file diff --git a/client/js/templates/list_archive_confirm.jst.ejs b/client/js/templates/list_archive_confirm.jst.ejs new file mode 100644 index 000000000..07a18213c --- /dev/null +++ b/client/js/templates/list_archive_confirm.jst.ejs @@ -0,0 +1,8 @@ +
          + Archive This List +
          +
          +
          +

          This will remove list from the board. To view archived lists and bring them back to the board, click "Settings" > "Archived Items" > "Switch to lists".

          + Archive +
          \ No newline at end of file diff --git a/client/js/templates/list_cards_archive_confirm.jst.ejs b/client/js/templates/list_cards_archive_confirm.jst.ejs new file mode 100644 index 000000000..5246f4964 --- /dev/null +++ b/client/js/templates/list_cards_archive_confirm.jst.ejs @@ -0,0 +1,8 @@ +
          + Archive All Cards in this List? +
          +
          +
          +

          This will remove all the cards in this list from the board. To view archived cards and bring them back to the board, click "Menu" > "Archived Items".

          + Archive All +
          \ No newline at end of file diff --git a/client/js/templates/list_delete_confirm.jst.ejs b/client/js/templates/list_delete_confirm.jst.ejs new file mode 100644 index 000000000..6e07226ad --- /dev/null +++ b/client/js/templates/list_delete_confirm.jst.ejs @@ -0,0 +1,8 @@ +
          + Delete This List +
          +
          +
          +

          This will remove list from the board. You can't undo the list after delete.

          + Delete +
          \ No newline at end of file diff --git a/client/js/templates/login.jst.ejs b/client/js/templates/login.jst.ejs new file mode 100644 index 000000000..77163fef2 --- /dev/null +++ b/client/js/templates/login.jst.ejs @@ -0,0 +1,44 @@ +
          +
          +
          + Login +
          +
          +
          +
          + <% if(!_.isEmpty(LDAP_LOGIN_ENABLED) && LDAP_LOGIN_ENABLED == "true") { + loginPlaceholder = 'LDAP Login'; + } else { + loginPlaceholder = 'Email or Username'; + } + %> + + +
          +
          + + +
          +
          + + +
          +
            + <% if(!_.isEmpty(role_links.where({slug: "users_forgotpassword"}))){ %> +
          • Forgot your password?
          • + <% + } + if(!_.isEmpty(role_links.where({slug: "users_register"}))){ + if(!_.isEmpty(LDAP_LOGIN_ENABLED) && LDAP_LOGIN_ENABLED === "false") { + %> +
          • |
          • +
          • Register
          • + <% + } + } + %> +
          +
          +
          +
          +
          \ No newline at end of file diff --git a/client/js/templates/modal_activity_view.jst.ejs b/client/js/templates/modal_activity_view.jst.ejs new file mode 100644 index 000000000..c34810031 --- /dev/null +++ b/client/js/templates/modal_activity_view.jst.ejs @@ -0,0 +1,18 @@ + \ No newline at end of file diff --git a/client/js/templates/modal_card_member_form.jst.ejs b/client/js/templates/modal_card_member_form.jst.ejs new file mode 100644 index 000000000..fb65445ce --- /dev/null +++ b/client/js/templates/modal_card_member_form.jst.ejs @@ -0,0 +1,17 @@ +
        • + Members +
        • +
        • +<% + board_users.each(function(board_user) { + var added_user = users.findWhere({ + card_id: card.id, + user_id: board_user.get('user_id') + }); + if (_.isUndefined(added_user)) { +%> +
        • <%- board_user.get('username') %>
        • + <% } else { %> +
        • <%-board_user.get('initials') %><%-board_user.get('username') %>
        • +<% } +}); %> \ No newline at end of file diff --git a/client/js/templates/modal_card_view.jst.ejs b/client/js/templates/modal_card_view.jst.ejs new file mode 100644 index 000000000..671c3fd3a --- /dev/null +++ b/client/js/templates/modal_card_view.jst.ejs @@ -0,0 +1,524 @@ +
          +
          +
          + + +
          Drop Files Here
          + +
          + + <%if(!_.isUndefined(authuser.user)) {%> + + <%}%> + +
          diff --git a/client/js/templates/modal_flickr_photo_view.jst.ejs b/client/js/templates/modal_flickr_photo_view.jst.ejs new file mode 100644 index 000000000..9c24defb0 --- /dev/null +++ b/client/js/templates/modal_flickr_photo_view.jst.ejs @@ -0,0 +1,25 @@ + \ No newline at end of file diff --git a/client/js/templates/modal_list_view.jst.ejs b/client/js/templates/modal_list_view.jst.ejs new file mode 100644 index 000000000..0f0909978 --- /dev/null +++ b/client/js/templates/modal_list_view.jst.ejs @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/client/js/templates/modal_music_view.jst.ejs b/client/js/templates/modal_music_view.jst.ejs new file mode 100644 index 000000000..158850e26 --- /dev/null +++ b/client/js/templates/modal_music_view.jst.ejs @@ -0,0 +1,37 @@ + \ No newline at end of file diff --git a/client/js/templates/modal_user_activities_list_view.jst.ejs b/client/js/templates/modal_user_activities_list_view.jst.ejs new file mode 100644 index 000000000..d38eaadcd --- /dev/null +++ b/client/js/templates/modal_user_activities_list_view.jst.ejs @@ -0,0 +1,17 @@ + \ No newline at end of file diff --git a/client/js/templates/move_card.jst.ejs b/client/js/templates/move_card.jst.ejs new file mode 100644 index 000000000..c5ae8d40e --- /dev/null +++ b/client/js/templates/move_card.jst.ejs @@ -0,0 +1,51 @@ +<% +var content_board = '
          '; +var content_position = '
          '; +content_list += '
          '; +content_position += ''; +var content = content_board + content_list + content_position; +%> +<%= content %> \ No newline at end of file diff --git a/client/js/templates/move_cards_from_list.jst.ejs b/client/js/templates/move_cards_from_list.jst.ejs new file mode 100644 index 000000000..ec6814ae2 --- /dev/null +++ b/client/js/templates/move_cards_from_list.jst.ejs @@ -0,0 +1,17 @@ +<% + var list_li = ''; + _.each(filtered_lists, function(filtered_list) { + if (filtered_list.id == list.attributes.id) { + list_li += ''; + } else { + list_li += ''; + } + }); +%> +
        • +
          + Move All Cards in List +
          +
        • +
        • +<%= list_li%> \ No newline at end of file diff --git a/client/js/templates/move_list.jst.ejs b/client/js/templates/move_list.jst.ejs new file mode 100644 index 000000000..022a4ebe3 --- /dev/null +++ b/client/js/templates/move_list.jst.ejs @@ -0,0 +1,43 @@ +<% +var content_board = '
          '; +var board_lists = new App.ListCollection(); +board_lists.sortByColumn('position'); +var current_position = board_lists.indexOf(board_lists.findWhere({id: list.attributes.id})) + 1; +if(current_position == 0){ + current_position = 1; +} +boards.each(function(board) { + if (list.attributes.board_id == board.id) { + var list_count = board.lists.where({is_archived: false, is_deleted: false}).length; + content_board += ''; + for(var i = 1; i <= list_count; i++) { + if (i == list.attributes.position) { + content_position += ''; + } else { + content_position += ''; + } + } + if (list.attributes.board_id != board.attributes.id) { + var next_position = parseInt(list_count) + 1; + content_position += ''; + } + } else { + if (board.attributes.is_closed == false || board.attributes.is_closed == 'f') { + content_board += ''; + } + } + +}); +content_board += '
          '; +content_position += ''; +var content = content_board + content_position; +%> +
          Move List
          +
          +<%= content %> +
          + + +
          +
          \ No newline at end of file diff --git a/client/js/templates/my_boards_listing.jst.ejs b/client/js/templates/my_boards_listing.jst.ejs new file mode 100644 index 000000000..ec5e16e0d --- /dev/null +++ b/client/js/templates/my_boards_listing.jst.ejs @@ -0,0 +1,43 @@ +<% if(board != null){ + var style = ''; + board.board_subscribers.add(board.attributes.boards_subscribers); + var subscriber = board.board_subscribers.findWhere({ + board_id: parseInt(board.id), + user_id: parseInt(authuser.user.id) + }); + if (board.attributes.background_picture_url) { + var background_picture_url = board.attributes.background_picture_url.replace("_XXXX.jpg", "_s.jpg"); + style = 'background-image:url(' + background_picture_url + '); background-size:cover;'; + } else if (board.attributes.background_pattern_url) { + var background_pattern_url = board.attributes.background_pattern_url.replace("_XXXX.jpg", "_s.jpg"); + style = 'background-image:url(' + background_pattern_url + '); background-size:cover;'; + } else if (board.attributes.background_color){ + style = 'background-color:' + board.attributes.background_color + ';color:#ffffff;'; + } +%> + + + + <%- board.attributes.name %> + <% + if (!_.isUndefined(subscriber) && !_.isEmpty(subscriber) && subscriber.attributes.is_subscribe === false) { + %> + + <% + } else { + %> + + <% + } + %> + + + + +<% }else{ %> +
          + No boards available. +
          +<%}%> + + \ No newline at end of file diff --git a/client/js/templates/notification_menu.jst.ejs b/client/js/templates/notification_menu.jst.ejs new file mode 100644 index 000000000..e4746182d --- /dev/null +++ b/client/js/templates/notification_menu.jst.ejs @@ -0,0 +1,2 @@ +
        • All
        • +
        • In this board
        • \ No newline at end of file diff --git a/client/js/templates/organization_add.jst.ejs b/client/js/templates/organization_add.jst.ejs new file mode 100644 index 000000000..dddc5cc22 --- /dev/null +++ b/client/js/templates/organization_add.jst.ejs @@ -0,0 +1,20 @@ +
          + Create Organization +
          +
          +
          +
          +
          + + +
          +
          + + +
          +
          + +
          +

          An organization is a group of boards and members.

          +
          +
          \ No newline at end of file diff --git a/client/js/templates/organization_board.jst.ejs b/client/js/templates/organization_board.jst.ejs new file mode 100644 index 000000000..eb9dc2324 --- /dev/null +++ b/client/js/templates/organization_board.jst.ejs @@ -0,0 +1,76 @@ +<% if(board != null){ %> +
          +
          +
          + + <% if(!_.isEmpty(role_links.where({slug: "starred_board"}))){ %> + + + <% if(board.attributes.board_visibility == 0) { %> + + <% } else if(board.attributes.board_visibility == 1) { %> + + <% } else if(board.attributes.board_visibility == 2) { %> + + <% } %> + + + + <% } %> + <% if(!_.isEmpty(role_links.where({slug: "starred_board"}))){ %> + <% if(!_.isEmpty(stared)){ %> + + <% } else {%> + + <% } %> + <% } %> +
          +
          +
            + <% + var style = ''; + if (board.attributes.background_picture_url) { + var background_picture_url = board.attributes.background_picture_url.replace("_XXXX.jpg", "_n.jpg"); + style = 'background:url(' + background_picture_url + ') 25% 25%; background-size: cover'; + } else if (board.attributes.background_pattern_url) { + var background_pattern_url = board.attributes.background_pattern_url.replace("_XXXX.jpg", "_n.jpg"); + style = 'background:url(' + background_pattern_url + ') repeat scroll 0% 0%;'; + } else if (board.attributes.background_color){ + style = 'background:' + board.attributes.background_color; + } + %> + <% if(!_.isEmpty(role_links.where({slug: "starred_board"}))){ %> +
          • + +
            +
            +
          • + <% } else { %> +
          • +
            +
          • + <% } %> +
          +
          +
          +<% }else{ %> +
          + No boards available. +
          +<%}%> \ No newline at end of file diff --git a/client/js/templates/organization_delete_form.jst.ejs b/client/js/templates/organization_delete_form.jst.ejs new file mode 100644 index 000000000..35e2cdd11 --- /dev/null +++ b/client/js/templates/organization_delete_form.jst.ejs @@ -0,0 +1,14 @@ +
          + Delete Organization? +
          +
          +
          +
            +
          • + Deleting an Organization is permanent. There is no undo. + +
          • +
          +
          \ No newline at end of file diff --git a/client/js/templates/organization_header.jst.ejs b/client/js/templates/organization_header.jst.ejs new file mode 100644 index 000000000..824362c24 --- /dev/null +++ b/client/js/templates/organization_header.jst.ejs @@ -0,0 +1,69 @@ +
          + + <% if(!_.isUndefined(authuser.user)){%> + +<% } %> \ No newline at end of file diff --git a/client/js/templates/organization_member_confirm_remove_form.jst.ejs b/client/js/templates/organization_member_confirm_remove_form.jst.ejs new file mode 100644 index 000000000..3287736bf --- /dev/null +++ b/client/js/templates/organization_member_confirm_remove_form.jst.ejs @@ -0,0 +1,13 @@ +
          + Leave organization +
          +
          + \ No newline at end of file diff --git a/client/js/templates/organization_member_permission_form.jst.ejs b/client/js/templates/organization_member_permission_form.jst.ejs new file mode 100644 index 000000000..2b4ea375c --- /dev/null +++ b/client/js/templates/organization_member_permission_form.jst.ejs @@ -0,0 +1,49 @@ +<% + var change_normal = ''; + var change_admin = ''; + if (organization_user.collection.where({ + is_admin: true + }).length > 1) { + change_normal = 'navbar-btn h6 js-edit-organization-member-permission-to-normal'; + } else { + change_admin = 'navbar-btn h6 js-edit-organization-member-permission-to-admin'; + } +%> +
        • +
          + Change Permissions +
          +
        • +
        • +
        • + <% + if(!_.isEmpty(change_admin)){ + %> + + Owner <% if(organization_user.attributes.is_admin == true || organization_user.attributes.is_admin == 't'){ %><%}%> + Can view, create and edit org boards, and change settings for the organization. + + <% } else { %> + + Owner <% if(organization_user.attributes.is_admin == true || organization_user.attributes.is_admin == 't'){ %><%}%> + Can view, create and edit org boards, and change settings for the organization. + + <% } %> +
        • +
        • +
        • + <% + if(!_.isEmpty(change_normal)){ + %> + + Member <% if(organization_user.attributes.is_admin == false || organization_user.attributes.is_admin == 'f'){ %><%}%> + Can view, create, and edit org boards, but not change settings. + + <% } else { %> + + Member<% if(organization_user.attributes.is_admin == false || organization_user.attributes.is_admin == 'f'){ %><%}%> + Can view, create, and edit org boards, but not change settings. + + <% } %> +
        • + diff --git a/client/js/templates/organization_member_remove_form.jst.ejs b/client/js/templates/organization_member_remove_form.jst.ejs new file mode 100644 index 000000000..7438b87db --- /dev/null +++ b/client/js/templates/organization_member_remove_form.jst.ejs @@ -0,0 +1,6 @@ +
        • + + Remove from organization + Remove all access to the organization. The member will remain on all their boards in this organization. + +
        • diff --git a/client/js/templates/organization_view.jst.ejs b/client/js/templates/organization_view.jst.ejs new file mode 100644 index 000000000..bc6f66eb4 --- /dev/null +++ b/client/js/templates/organization_view.jst.ejs @@ -0,0 +1,48 @@ + +
          +
          + +
          +
          + +
          +
          +
          +
          +
          +
          +
          +
          +
          +
          +
          +
          + <% + var logo_path = "img/default-organization.png"; + var is_logo_added = false; + if (!_.isUndefined(organization.attributes.logo_url) && organization.attributes.logo_url != null) { + is_logo_added = true; + logo_path = organization.showImage('Organization', organization.attributes.id, 'medium_thumb' ) +'?'+ new Date().getTime(); + } + %> +
          id="dropzone" <%}%>class="navbar-btn btn-xs"><% if(!_.isUndefined(authuser.user) && is_logo_added){ %><%}%>[Image: <%= organization.attributes.name%>] +
          + +
          +
          +
          +
          Drop Files Here
          +
          + + +
          + +
          +
          +
          + \ No newline at end of file diff --git a/client/js/templates/organization_visibility_form.jst.ejs b/client/js/templates/organization_visibility_form.jst.ejs new file mode 100644 index 000000000..4ddb23ec4 --- /dev/null +++ b/client/js/templates/organization_visibility_form.jst.ejs @@ -0,0 +1,17 @@ +
        • +
          Select Visibility
          +
        • +
        • +
        • + + Private<% if(organization.attributes.organization_visibility == 2){%><%}%> + This organization is private. It\'s not indexed or visible to those outside the org. + +
        • +
        • +
        • + + Public<% if(organization.attributes.organization_visibility == 1){%><%}%> + This organization is public. It's visible to anyone with the link and will show up in search engines like Google. Only those invited to the org can add and edit org boards. + +
        • \ No newline at end of file diff --git a/client/js/templates/organizations_board_form_view.jst.ejs b/client/js/templates/organizations_board_form_view.jst.ejs new file mode 100644 index 000000000..007b27efb --- /dev/null +++ b/client/js/templates/organizations_board_form_view.jst.ejs @@ -0,0 +1,33 @@ +
        • +
          Add
          +
        • + +<% if(!_.isEmpty(role_links.where({slug: "add_board"}))){ %> +
        • + + + Import Board from Trello + + Upload json file exported from Trello. + +
          + +
          +
        • + +
        • + + New Board + A board is a collection of cards ordered in a list of lists. Use it to manage a project, track a collection, or organize anything. + +
        • + +<% } %> +<% if(!_.isEmpty(role_links.where({slug: "add_organization"}))){ %> +
        • + + New Organization + An organization is a group of boards and people. Use it to group boards in your company, team, or family. + +
        • +<% } %> \ No newline at end of file diff --git a/client/js/templates/organizations_list_view.jst.ejs b/client/js/templates/organizations_list_view.jst.ejs new file mode 100644 index 000000000..3861529bb --- /dev/null +++ b/client/js/templates/organizations_list_view.jst.ejs @@ -0,0 +1,111 @@ +<% if(organization != null){ %> + +
          + <% + var logo_path = "img/default-organization.png"; + if (!_.isUndefined(organization.attributes.logo_url) && organization.attributes.logo_url != null) { + logo_path = organization.showImage('Organization', organization.attributes.id, 'small_thumb' ); + } + %> + [Images: <%- organization.attributes.name %>] +
          + + <% if(organization.attributes.description != ''){ %> +
          +

          <%- organization.attributes.description%>

          +
          + <%}%> +
          +
          + + + + +<%}else{%> +
          + No organizations available. +
          +<%}%> \ No newline at end of file diff --git a/client/js/templates/organizations_lists_header.jst.ejs b/client/js/templates/organizations_lists_header.jst.ejs new file mode 100644 index 000000000..c04aff216 --- /dev/null +++ b/client/js/templates/organizations_lists_header.jst.ejs @@ -0,0 +1,23 @@ + \ No newline at end of file diff --git a/client/js/templates/organizations_lists_view.jst.ejs b/client/js/templates/organizations_lists_view.jst.ejs new file mode 100644 index 000000000..212c6365b --- /dev/null +++ b/client/js/templates/organizations_lists_view.jst.ejs @@ -0,0 +1,9 @@ +
          +
          + + + + +
          +
          +
          \ No newline at end of file diff --git a/client/js/templates/organizations_user_view.jst.ejs b/client/js/templates/organizations_user_view.jst.ejs new file mode 100644 index 000000000..381ed7646 --- /dev/null +++ b/client/js/templates/organizations_user_view.jst.ejs @@ -0,0 +1,157 @@ +
          +<% if(!_.isEmpty(role_links.where({slug: "add_organization_user"}))){ %> +
          +
            + +
              +
          +<% } %> +<% if(!_.isEmpty(role_links.where({slug: "view_organization_user_listing"}))){ %> +
          + + + <% if(!_.isEmpty(organizations_users)){ %> + <% _.each(organizations_users.models, function(organizations_user){ %> + + + + + <% }); %> + <% } else { %> + + <% } %> + +
          +
          + <% if(!_.isEmpty(role_links.where({slug: "starred_board"}))){ %> + + <% } %> + <% if(!_.isEmpty(organizations_user.attributes.profile_picture_path)) { + var profile_picture_path = organizations_users.showImage('User', organizations_user.attributes.user_id, 'small_thumb' ); + %> + [Image: <%-organizations_user.attributes.username %>] + <% } else {%> + <%- organizations_user.attributes.initials %> + <% } %> + <% if(!_.isEmpty(role_links.where({slug: "starred_board"}))){ %> + + <% } %> +
          +

          <%- organizations_user.attributes.full_name %>

          + <% if(!_.isEmpty(role_links.where({slug: "starred_board"}))){ %> + <%- organizations_user.attributes.username %> + <% } else { %> + <%- organizations_user.attributes.username %> + <% } %> +
          +
          +
          +
            + + + <% if(!_.isEmpty(role_links.where({slug: "edit_organization_user"}))){ %> + <% if(!_.isUndefined(authuser.user)){%> + + + <% } else{ %> + + <%} }%> +
          +
          + +
          No members available
          +
          +<% } %> +
          \ No newline at end of file diff --git a/client/js/templates/role_settings.jst.ejs b/client/js/templates/role_settings.jst.ejs new file mode 100644 index 000000000..d1f46e55a --- /dev/null +++ b/client/js/templates/role_settings.jst.ejs @@ -0,0 +1,249 @@ +
          +
          + +
          +
          \ No newline at end of file diff --git a/client/js/templates/roles.jst.ejs b/client/js/templates/roles.jst.ejs new file mode 100644 index 000000000..cf031c7f7 --- /dev/null +++ b/client/js/templates/roles.jst.ejs @@ -0,0 +1,15 @@ + + <%- acl_link.attributes.name%> + + + <% + for(var i = 1; i <= 3; i++){ + var is_enabled = acl_link.acl_links_roles.findWhere({ + role_id: i + }) + %> +
          + checked="checked" <% } %>/> +
          + <%}%> + diff --git a/client/js/templates/search_result.jst.ejs b/client/js/templates/search_result.jst.ejs new file mode 100644 index 000000000..e3bbdf786 --- /dev/null +++ b/client/js/templates/search_result.jst.ejs @@ -0,0 +1,18 @@ +<% +_.each(hits, function(hit) { + if (hit._index === 'boards') { +%> +
        • <%- hit._source.doc.board_name %>
        • +<% + } else if (hit._index === 'lists') { +%> +
        • <%- hit._source.doc.list_name %>
        • +<% + } else if (hit._index === 'cards') { +%> +
        • <%- hit._source.doc.card_name %>
        • +<% + } + +}); +%> diff --git a/client/js/templates/select_board_visibility.jst.ejs b/client/js/templates/select_board_visibility.jst.ejs new file mode 100644 index 000000000..b650a7916 --- /dev/null +++ b/client/js/templates/select_board_visibility.jst.ejs @@ -0,0 +1,10 @@ +<% if (name == 'private') { +%> + Private +<% } else if (name == 'public') { +%> + Public +<% } else if (!_.isUndefined(data.organization_id)) { +%> + Organization +<% } %> \ No newline at end of file diff --git a/client/js/templates/selected_board_visibility.jst.ejs b/client/js/templates/selected_board_visibility.jst.ejs new file mode 100644 index 000000000..92abf7f1c --- /dev/null +++ b/client/js/templates/selected_board_visibility.jst.ejs @@ -0,0 +1,12 @@ + + This board will be + +
          + + +
          \ No newline at end of file diff --git a/client/js/templates/setting_list.jst.ejs b/client/js/templates/setting_list.jst.ejs new file mode 100644 index 000000000..403b4a3d8 --- /dev/null +++ b/client/js/templates/setting_list.jst.ejs @@ -0,0 +1,85 @@ + +
          +
          +
          + +
          +
          +
          + <% if (list.models.length > 0 ) { + var j = 0; + _.each(list.models, function(settingCategory) { + var settings = settingCategory.get('settings'); + %> + + <% + j++; + }); + } + %> +
          +
          +
          +
          + \ No newline at end of file diff --git a/client/js/templates/show_all_visibility.jst.ejs b/client/js/templates/show_all_visibility.jst.ejs new file mode 100644 index 000000000..6613fc126 --- /dev/null +++ b/client/js/templates/show_all_visibility.jst.ejs @@ -0,0 +1,29 @@ +<% + var private_visibility = ""; + var org_visibility = ""; + var public_visibility = ""; + + if (visibility == 0) { + private_visibility = ''; + } else if (visibility == 1) { + org_visibility = ''; + } else if (visibility == 2) { + public_visibility = ''; + } +%> +
        • + + Private <%= private_visibility %> + This board is private. Only people added to the board can view and edit it. + +
        • + +
        • + + Organization <%= org_visibility %> + This board is visible to members of the organization. Only people added to the board can edit. The board must be added to an org to enable this. +
        • + +
        • + Public <%= public_visibility %> This board is public. It's visible to anyone with the link and will show up in search engines like Google. Only people added to the board can edit. +
        • \ No newline at end of file diff --git a/client/js/templates/show_board_member_permission_form.jst.ejs b/client/js/templates/show_board_member_permission_form.jst.ejs new file mode 100644 index 000000000..4989ae203 --- /dev/null +++ b/client/js/templates/show_board_member_permission_form.jst.ejs @@ -0,0 +1,18 @@ +
          + Change Permissions +
          +
          + \ No newline at end of file diff --git a/client/js/templates/show_board_visibility.jst.ejs b/client/js/templates/show_board_visibility.jst.ejs new file mode 100644 index 000000000..cc3bac36f --- /dev/null +++ b/client/js/templates/show_board_visibility.jst.ejs @@ -0,0 +1,5 @@ +
        • Private <% if (visibility == 0) { %> <% } %> This board is private. Only people added to the board can view and edit it.
        • + +
        • Organization <% if (visibility == 1) { %> <% } %> This board is visible to members of the organization. Only people added to the board can edit. The board must be added to an org to enable this.
        • + +
        • Public <% if (visibility == 2) { %> <% } %> This board is public. It's visible to anyone with the link and will show up in search engines like Google. Only people added to the board can edit.
        • \ No newline at end of file diff --git a/client/js/templates/show_copy_board.jst.ejs b/client/js/templates/show_copy_board.jst.ejs new file mode 100644 index 000000000..e490a8834 --- /dev/null +++ b/client/js/templates/show_copy_board.jst.ejs @@ -0,0 +1,57 @@ +
          +
          + + + + Copy Board +
          +
          +
          +
          +
          +
          + + +
          + +
          + +
          +
          + + +
          +
          +
          + +
          +
          +
          +
          +
          \ No newline at end of file diff --git a/client/js/templates/show_search_boards.jst.ejs b/client/js/templates/show_search_boards.jst.ejs new file mode 100644 index 000000000..120311cc0 --- /dev/null +++ b/client/js/templates/show_search_boards.jst.ejs @@ -0,0 +1,16 @@ +<% if(!_.isEmpty(board) && board != null){ %> + + + + + <%- board.attributes.name %> + + + + + +<% } else{ %> +
          + No boards available. +
          +<% } %> \ No newline at end of file diff --git a/client/js/templates/show_search_message.jst.ejs b/client/js/templates/show_search_message.jst.ejs new file mode 100644 index 000000000..b88afe697 --- /dev/null +++ b/client/js/templates/show_search_message.jst.ejs @@ -0,0 +1 @@ +You can use search operators like @member, #label, is:archived, and has:attachments to refine your search. \ No newline at end of file diff --git a/client/js/templates/show_sync_google_calendar.jst.ejs b/client/js/templates/show_sync_google_calendar.jst.ejs new file mode 100644 index 000000000..9d1af3fbd --- /dev/null +++ b/client/js/templates/show_sync_google_calendar.jst.ejs @@ -0,0 +1,16 @@ +
          +
          + + + + URL +
          +
          +
          +
          +
          + +
          +
          +
          +
          \ No newline at end of file diff --git a/client/js/templates/starred_boards_index.jst.ejs b/client/js/templates/starred_boards_index.jst.ejs new file mode 100644 index 000000000..78e5c39fd --- /dev/null +++ b/client/js/templates/starred_boards_index.jst.ejs @@ -0,0 +1,6 @@ + <% if(!_.isEmpty(role_links.where({slug: "view_stared_boards"}))){ %> +
          +
          +
          +
          + <% } %> diff --git a/client/js/templates/started_boards_listing.jst.ejs b/client/js/templates/started_boards_listing.jst.ejs new file mode 100644 index 000000000..9d1981e97 --- /dev/null +++ b/client/js/templates/started_boards_listing.jst.ejs @@ -0,0 +1,27 @@ +<% if(board != null){ + var style = ''; + if (board.attributes.background_picture_url) { + var background_picture_url = board.attributes.background_picture_url.replace("_XXXX.jpg", "_s.jpg"); + style = 'background-image:url(' + background_picture_url + '); background-size:cover;'; + } else if (board.attributes.background_pattern_url) { + var background_pattern_url = board.attributes.background_pattern_url.replace("_XXXX.jpg", "_s.jpg"); + style = 'background-image:url(' + background_pattern_url + '); background-size:cover;'; + } else if (board.attributes.background_color){ + style = 'background-color:' + board.attributes.background_color + ';color:#ffffff;'; + } else { + style = ''; + } +%> + + + + + <%- board.attributes.name %> + + + +<% }else{ %> +
          + No boards available. +
          +<%}%> \ No newline at end of file diff --git a/client/js/templates/switch_to_list_form.jst.ejs b/client/js/templates/switch_to_list_form.jst.ejs new file mode 100644 index 000000000..d9d2b22cf --- /dev/null +++ b/client/js/templates/switch_to_list_form.jst.ejs @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/client/js/templates/user.jst.ejs b/client/js/templates/user.jst.ejs new file mode 100644 index 000000000..f0f97577f --- /dev/null +++ b/client/js/templates/user.jst.ejs @@ -0,0 +1,145 @@ + + + + + + \ No newline at end of file diff --git a/client/js/templates/user_activity.jst.ejs b/client/js/templates/user_activity.jst.ejs new file mode 100644 index 000000000..de7f9b78c --- /dev/null +++ b/client/js/templates/user_activity.jst.ejs @@ -0,0 +1,77 @@ +<% if(!_.isEmpty(activity) && activity != null){ %> + <% + var cardLink = '' + _.escape(activity.attributes.card_name) + ''; + if(activity.attributes.type != 'add_comment' && activity.attributes.type != 'edit_comment') { + activity.attributes.comment = activity.attributes.comment.replace('##CARD_LINK##', cardLink); + activity.attributes.comment = activity.attributes.comment.replace('##LABEL_NAME##', _.escape(activity.attributes.label_name)); + activity.attributes.comment = activity.attributes.comment.replace('##CARD_NAME##', _.escape(activity.attributes.card_name)); + activity.attributes.comment = activity.attributes.comment.replace('##DESCRIPTION##', _.escape(activity.attributes.card_description)); + activity.attributes.comment = activity.attributes.comment.replace('##LIST_NAME##', _.escape(activity.attributes.list_name)); + activity.attributes.comment = activity.attributes.comment.replace('##BOARD_NAME##', _.escape(activity.attributes.board_name)); + activity.attributes.comment = activity.attributes.comment.replace('##USER_NAME##', _.escape(activity.attributes.username)); + activity.attributes.comment = activity.attributes.comment.replace('##CHECKLIST_ITEM_NAME##', _.escape(activity.attributes.checklist_item_name)); + activity.attributes.comment = activity.attributes.comment.replace('##CHECKLIST_ITEM_PARENT_NAME##', _.escape(activity.attributes.checklist_item_parent_name)); + activity.attributes.comment = activity.attributes.comment.replace('##CHECKLIST_NAME##', _.escape(activity.attributes.checklist_name)); + } else { + var comment = activity.attributes.username + ' commented in card ' + cardLink; + } + %> + +<% }else{ %> + No activities available. +<% } %> diff --git a/client/js/templates/user_activity_menu.jst.ejs b/client/js/templates/user_activity_menu.jst.ejs new file mode 100644 index 000000000..0b2c38c82 --- /dev/null +++ b/client/js/templates/user_activity_menu.jst.ejs @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/client/js/templates/user_board_list.jst.ejs b/client/js/templates/user_board_list.jst.ejs new file mode 100644 index 000000000..3bf6d5c96 --- /dev/null +++ b/client/js/templates/user_board_list.jst.ejs @@ -0,0 +1,20 @@ +<% + var style = ''; + if (user_board.attributes.background_picture_url) { + var background_picture_url = user_board.attributes.background_picture_url.replace("_XXXX.jpg", "_s.jpg"); + style = 'background-image:url(' + background_picture_url + ');background-size:cover;'; + } else if (user_board.attributes.background_pattern_url) { + background_pattern_url = user_board.attributes.background_pattern_url.replace("_XXXX.jpg", "_s.jpg"); + style = 'background-image:url(' + background_pattern_url + ');background-size:cover;'; + } else if (user_board.attributes.background_color){ + style = 'background-color:' + user_board.attributes.background_color + ';color:#ffffff;'; + } else { + style = ''; + } +%> + + + + <%- user_board.attributes.name %> + + \ No newline at end of file diff --git a/client/js/templates/user_boards_listing_menu.jst.ejs b/client/js/templates/user_boards_listing_menu.jst.ejs new file mode 100644 index 000000000..42864c63a --- /dev/null +++ b/client/js/templates/user_boards_listing_menu.jst.ejs @@ -0,0 +1,15 @@ +
          +
          + Boards for <%- boards.username%> +
          +
          +
          + <% + boards.each(function(board) { + %> +
        • <%- board.attributes.board_name%>
        • + <% + }); + %> +
          +
          \ No newline at end of file diff --git a/client/js/templates/user_cards.jst.ejs b/client/js/templates/user_cards.jst.ejs new file mode 100644 index 000000000..563b51c59 --- /dev/null +++ b/client/js/templates/user_cards.jst.ejs @@ -0,0 +1,66 @@ +

          <%- key %>

          +
          + + + + + + + + <% _.each(card_user, function(user_card) { %> + + + + <% }); %> + +
          + +
          +
          +
          + +
          +
          +
          +
          \ No newline at end of file diff --git a/client/js/templates/user_index_container.jst.ejs b/client/js/templates/user_index_container.jst.ejs new file mode 100644 index 000000000..b4e48e6f8 --- /dev/null +++ b/client/js/templates/user_index_container.jst.ejs @@ -0,0 +1,5 @@ +
          +
          +
          +
          +
          \ No newline at end of file diff --git a/client/js/templates/user_search_result.jst.ejs b/client/js/templates/user_search_result.jst.ejs new file mode 100644 index 000000000..8a1131f82 --- /dev/null +++ b/client/js/templates/user_search_result.jst.ejs @@ -0,0 +1,15 @@ +<% if(user != null && _.isUndefined(q)){ %> + +<% if(!_.isEmpty(user.attributes.profile_picture_path)) { + var profile_picture_path = user.showImage('User', user.attributes.id, 'micro_thumb' ); +%> + [Image: <%-user.attributes.username %>] +<% } else {%> + <%- user.attributes.initials %> +<% } %> + <%- user.attributes.username %> +<% }else if(user == null && _.isUndefined(q)){ %> + No users avaailable. +<% } else{ %> + Search for a person in <%- SITE_NAME %> by name or email address. +<% } %> diff --git a/client/js/templates/user_view.jst.ejs b/client/js/templates/user_view.jst.ejs new file mode 100644 index 000000000..a6c4b06c6 --- /dev/null +++ b/client/js/templates/user_view.jst.ejs @@ -0,0 +1,89 @@ + +
          +
          +
          + +
          +
          +
          + <% if (!_.isEmpty(role_links.where({slug: "view_user_activities"}))) { %> + + <% } %> + <% if(!_.isEmpty(role_links.where({slug: "view_user_cards"}))){ %> + + <% } %> + <% if(!_.isEmpty(role_links.where({slug: "edit_user_details"}))){ %> + + <% } %> +
          +
          +
          +
          + \ No newline at end of file diff --git a/client/js/templates/user_view_header.jst.ejs b/client/js/templates/user_view_header.jst.ejs new file mode 100644 index 000000000..2501ec298 --- /dev/null +++ b/client/js/templates/user_view_header.jst.ejs @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/client/js/templates/users_forgot_password.jst.ejs b/client/js/templates/users_forgot_password.jst.ejs new file mode 100644 index 000000000..3316225a5 --- /dev/null +++ b/client/js/templates/users_forgot_password.jst.ejs @@ -0,0 +1,23 @@ +
          Enter your Email, and we will send resetting your password.
          +
          +
          +
          Forgot Password
          +
          +
          +
          + + +
          +
          + + +
          + +
          +
          +
          +
          \ No newline at end of file diff --git a/client/js/templates/users_register.jst.ejs b/client/js/templates/users_register.jst.ejs new file mode 100644 index 000000000..e816c8df3 --- /dev/null +++ b/client/js/templates/users_register.jst.ejs @@ -0,0 +1,39 @@ +
          +
          +
          Register
          +
          +
          +
          + + +
          +
          + + +
          +
          + + +
          +
          + + +
          +
            + <% + if(!_.isEmpty(role_links.where({slug: "users_login"}))){ + %> +
          • Login
          • + <% + } + %> + <% if(!_.isEmpty(role_links.where({slug: "users_forgotpassword"}))){ %> +
          • |
          • +
          • Forgot your password?
          • + + <% }%> +
          +
          +
          +
          +
          \ No newline at end of file diff --git a/client/js/views/about_us_view.js b/client/js/views/about_us_view.js new file mode 100644 index 000000000..7650190f1 --- /dev/null +++ b/client/js/views/about_us_view.js @@ -0,0 +1,38 @@ +/** + * @fileOverview This file has functions related to about us page view. This view calling from application view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * About us View + * @class AboutusView + * @constructor + * @extends Backbone.View + */ +App.AboutusView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + this.render(); + }, + template: JST['templates/about_us'], + tagName: 'section', + className: 'row', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template()); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/activity_add_form_view.js b/client/js/views/activity_add_form_view.js new file mode 100644 index 000000000..6eb92f9e1 --- /dev/null +++ b/client/js/views/activity_add_form_view.js @@ -0,0 +1,45 @@ +/** + * @fileOverview This file has functions related to activity add view. This view calling from modal card view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : card model and it's related values. It contain all board based object @see Available Object in App.ModalCardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ActivityAddForm View + * @class ActivityAddFormView + * @constructor + * @extends Backbone.View + */ +App.ActivityAddFormView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/activity_add_form'], + tagName: 'form', + className: 'js-add-comment', + id: 'AddActivityForm', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + card: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/activity_card_search_view.js b/client/js/views/activity_card_search_view.js new file mode 100644 index 000000000..c777dd6db --- /dev/null +++ b/client/js/views/activity_card_search_view.js @@ -0,0 +1,43 @@ +/** + * @fileOverview This file has functions related to activity add view. This view calling from modal card view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : card model and it's related values. It contain all board based object @see Available Object in App.ModalCardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ActivityCardSearch View + * @class ActivityCardSearchView + * @constructor + * @extends Backbone.View + */ +App.ActivityCardSearchView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/activity_card_search'], + tagName: 'li', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + card: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/activity_delete_confirm_view.js b/client/js/views/activity_delete_confirm_view.js new file mode 100644 index 000000000..3a33ed2dc --- /dev/null +++ b/client/js/views/activity_delete_confirm_view.js @@ -0,0 +1,43 @@ +/** + * @fileOverview This file has functions related to activity delete view. This view calling from modal card view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : activity ID + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ActivityDeleteConfirm View + * @class ActivityDeleteConfirmView + * @constructor + * @extends Backbone.View + */ +App.ActivityDeleteConfirmView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/activity_delete_confirm'], + tagName: 'div', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + activity_id: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/activity_index_view.js b/client/js/views/activity_index_view.js new file mode 100644 index 000000000..314b7f0c5 --- /dev/null +++ b/client/js/views/activity_index_view.js @@ -0,0 +1,126 @@ +/** + * @fileOverview This file has functions related to activity index view. This view calling from application view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : undefined + */ +if (typeof App == 'undefined') { + App = {}; +} +App.ActivityIndexView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.id = options.id; + this.getListing(); + this.render(); + this.last_activity_id = 0; + }, + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click #js-admin-activites-load-more': 'loadActivities', + }, + template: JST['templates/activity_index'], + converter: new Showdown.converter(), + /** + * getListing() + * get settings + * @return false + */ + getListing: function() { + self = this; + if (_.isUndefined(this.id)) { + this.id = 1; + } + var activities = new App.ActivityCollection(); + activities.url = api_url + 'activities.json'; + activities.fetch({ + cache: false, + abortPending: true, + success: function(collections, response) { + if (!_.isEmpty(activities.models)) { + self.renderActivitiesCollection(activities); + var last_activity = _.min(activities.models, function(activity) { + return activity.id; + }); + self.last_activity_id = last_activity.id; + self.$('#js-admin-activites-load-more').removeClass('hide'); + } else { + var view_activity = this.$('#js-admin-activity-list'); + var view = new App.AdminActivityIndexView({ + model: null, + }); + view_activity.html(view.el); + } + + } + }); + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function(collections) { + this.$el.html(this.template({ + type: 'all', + })); + + this.$('.timeago').timeago(); + $('.js-admin-activity-menu').addClass('active'); + $('.js-admin-email-menu, .js-admin-user-menu, .js-admin-role-menu, .js-admin-setting-menu').removeClass('active'); + return this; + }, + /** + * loadActivities() + * display load Activities + */ + loadActivities: function() { + //e.preventDefault(); + var self = this; + var query_string = (this.last_activity_id !== 0) ? '?last_activity_id=' + this.last_activity_id : ''; + var activities = new App.ActivityCollection(); + activities.url = api_url + 'activities.json' + query_string; + activities.fetch({ + cache: false, + success: function(user, response) { + if (!_.isEmpty(activities) && !_.isEmpty(activities.models)) { + var last_activity = _.min(activities.models, function(activity) { + return activity.id; + }); + self.last_activity_id = last_activity.id; + self.renderActivitiesCollection(activities); + } + } + }); + }, + /** + * renderActivitiesCollection() + * display card activities + */ + renderActivitiesCollection: function(activities) { + $('.js-admin-activity-menu').addClass('active'); + $('.js-admin-email-menu, .js-admin-user-menu, .js-admin-role-menu, .js-admin-setting-menu').removeClass('active'); + var self = this; + var view_activity = this.$('#js-admin-activity-list'); + if (!_.isEmpty(activities)) { + for (var i = 0; i < activities.models.length; i++) { + var activity = activities.models[i]; + var view = new App.AdminActivityIndexView({ + model: activity, + }); + view_activity.append(view.el).find('.timeago').timeago(); + } + } + } +}); diff --git a/client/js/views/activity_reply_form_view.js b/client/js/views/activity_reply_form_view.js new file mode 100644 index 000000000..45c7b930e --- /dev/null +++ b/client/js/views/activity_reply_form_view.js @@ -0,0 +1,45 @@ +/** + * @fileOverview This file has functions related to activity reply view. This view calling from modal card view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : activitiy model and it's related values + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ActivityReplyForm View + * @class ActivityReplyFormView + * @constructor + * @extends Backbone.View + */ +App.ActivityReplyFormView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/activity_reply_form'], + tagName: 'form', + className: 'js-add-comment panel-body js-reply-form', + id: 'AddActivityForm', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + activity: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/activity_user_add_search_result_view.js b/client/js/views/activity_user_add_search_result_view.js new file mode 100644 index 000000000..237d7cb44 --- /dev/null +++ b/client/js/views/activity_user_add_search_result_view.js @@ -0,0 +1,44 @@ +/** + * @fileOverview This file has functions related to activity user search view. This view calling from modal card view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : user model and it's related values + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ActivityUserAddSearchResult View + * @class ActivityUserAddSearchResultView + * @constructor + * @extends Backbone.View + */ +App.ActivityUserAddSearchResultView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/activity_user_add_search_result'], + tagName: 'ul', + className: 'list-unstyled navbar-btn', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.el = this.template({ + user: this.model + }); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/activity_view.js b/client/js/views/activity_view.js new file mode 100644 index 000000000..2793edf83 --- /dev/null +++ b/client/js/views/activity_view.js @@ -0,0 +1,162 @@ +/** + * @fileOverview This file has functions related to activity view. + * This view calling from board, card checklist, card checklist item, footer, list, modal card, modal activity and modal user activity view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : activity model and it's related values + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Activity View + * @class ActivityView + * @constructor + * @extends Backbone.View + */ +App.ActivityView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.type = options.type; + this.board = options.board; + }, + converter: new Showdown.converter(), + template: JST['templates/activity'], + tagName: 'li', + className: 'btn-block col-xs-12 js-activity', + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-undo': 'undo', + 'click .js-undo2': 'undo_all', + 'click .js-open-card-view': 'triggerOpenModal', + + }, + triggerOpenModal: function(e) { + var self = this; + var target = $(e.target); + var card_id = target.data('card_id'); + if (card_id !== undefined && card_id !== null && ($('#js-card-' + card_id).length !== 0)) { + $('#js-card-' + card_id).trigger('click'); + } else { + return true; + } + return false; + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + var current_user_can_undo_it = false; + if (!_.isUndefined(App.boards) && !_.isEmpty(this.model) && !_.isUndefined(this.model.attributes.board_id) && !_.isUndefined(App.boards.get(this.model.attributes.board_id))) { + var board_users = App.boards.get(this.model.attributes.board_id).attributes.users; + _.each(board_users, function(board_user) { + if (parseInt(board_user.user_id) === parseInt(authuser.user.id) && board_user.is_admin === true) { + current_user_can_undo_it = true; + } + }); + } + this.$el.html(this.template({ + activity: this.model, + type: this.type, + converter: this.converter, + current_user_can_undo_it: current_user_can_undo_it + })); + if (!_.isEmpty(this.model)) { + this.$el.addClass('js-list-activity-' + this.model.attributes.id); + if (this.model.attributes.depth !== 0) { + var col_offset = parseInt(this.model.attributes.depth); + this.$el.addClass('col-lg-offset-' + col_offset); + } + } + this.showTooltip(); + return this; + }, + /** + * undo() + * revert changes to the previous version + * @param NULL + * @return false + */ + undo: function(e) { + var self = this; + e.preventDefault(); + this.model.url = api_url + 'activities/undo/' + this.model.id + '.json'; + this.model.save({}, { + patch: true, + success: function(model, response) { + if (!_.isUndefined(response.undo.card)) { + var card = self.board.cards.findWhere({ + id: parseInt(response.undo.card.id) + }); + card.set(response.undo.card); + _.each(response.undo.card, function(val, key) { + if (key === 'id' || key === 'list_id' || key === 'board_id') { + card.set(key, parseInt(val)); + } + var activity = new App.Activity(); + activity.set(response.activity); + card.activities.unshift(activity); + }); + var activity = new App.Activity(); + activity.set(response.activity); + var view = new App.ActivityView({ + model: activity, + board: self.board + }); + var view_activity = $('#js-card-activities-' + card.attributes.id); + view_activity.prepend(view.render().el).find('.timeago').timeago(); + } else if (!_.isUndefined(response.undo.list)) { + var list = self.board.lists.findWhere({ + id: parseInt(response.undo.list.id) + }); + list.set(response.undo.list); + _.each(response.undo.list, function(val, key) { + if (key === 'id' || key === 'board_id') { + list.set(key, parseInt(val)); + } + }); + } else if (!_.isUndefined(response.undo.board)) { + board.set(response.undo.board); + _.each(response.undo.board, function(val, key) { + if (key === 'id' || key === 'user_id') { + board.set(key, parseInt(val)); + } + }); + } + return false; + } + }); + return false; + }, + /** + * undo_all() + * revert changes to the previous version + * @param NULL + * @return false + */ + undo_all: function(e) { + var self = this; + e.preventDefault(); + this.model.url = api_url + 'activities/undo/' + this.model.id + '.json'; + this.model.save({}, { + patch: true, + success: function(model, response) { + self.flash('danger', "Undo Succeed"); + } + }); + return false; + } +}); diff --git a/client/js/views/admin_activity_index_view.js b/client/js/views/admin_activity_index_view.js new file mode 100644 index 000000000..ce3c26285 --- /dev/null +++ b/client/js/views/admin_activity_index_view.js @@ -0,0 +1,75 @@ +/** + * @fileOverview This file has functions related to admin activity index view. This view calling from activity index view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : activity model and it's related values + */ +if (typeof App == 'undefined') { + App = {}; +} +App.AdminActivityIndexView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/admin_activity_index'], + converter: new Showdown.converter(), + tagName: 'li', + className: 'row col-xs-12', + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-undo2': 'undo_all' + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + var current_user_can_undo_it = false; + if (!_.isUndefined(App.boards) && !_.isEmpty(this.model) && !_.isUndefined(this.model.attributes.board_id) && !_.isUndefined(App.boards.get(this.model.attributes.board_id))) { + var board_users = App.boards.get(this.model.attributes.board_id).attributes.users; + _.each(board_users, function(board_user) { + if (parseInt(board_user.user_id) === parseInt(authuser.user.id) && board_user.is_admin === true) { + current_user_can_undo_it = true; + } + }); + } + this.$el.html(this.template({ + activity: this.model, + type: 'all', + converter: this.converter, + current_user_can_undo_it: current_user_can_undo_it + })); + this.$('.timeago').timeago(); + }, + /** + * undo_all() + * revert changes to the previous version + * @param NULL + * @return false + */ + undo_all: function(e) { + var self = this; + e.preventDefault(); + this.model.url = api_url + 'activities/undo/' + this.model.id + '.json'; + this.model.save({}, { + patch: true, + success: function(model, response) { + self.flash('danger', "Undo Succeed"); + } + }); + return false; + } +}); diff --git a/client/js/views/admin_user_add_view.js b/client/js/views/admin_user_add_view.js new file mode 100644 index 000000000..a9f34fcf0 --- /dev/null +++ b/client/js/views/admin_user_add_view.js @@ -0,0 +1,80 @@ +/** + * @fileOverview This file has functions related to admin user add view. This view calling from application view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : user model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Admin user add View + * @class AdminUserAddView + * @constructor + * @extends Backbone.View + */ +App.AdminUserAddView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/admin_user_add'], + tagName: 'article', + className: 'clearfix', + id: 'admin-user-add', + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'submit form#AdminUserAddForm': 'adminUserAdd' + }, + /** + * adminUserAdd() + * save user + * @return false + */ + adminUserAdd: function(e) { + var target = $(e.target); + var data = target.serializeObject(); + var self = this; + var user = new App.User(); + user.url = api_url + 'users/admin_user_add.json'; + user.save(data, { + success: function(model, response) { + if (!_.isEmpty(response.error)) { + self.flash('danger', response.error); + $('#inputPassword').val(''); + } else { + self.flash('success', 'User added successfully.'); + target[0].reset(); + app.navigate('#/users', { + trigger: true, + replace: true + }); + } + } + }); + return false; + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template()); + $('.js-admin-user-menu').addClass('active'); + $('.js-admin-activity-menu, .js-admin-setting-menu, .js-admin-email-menu, .js-admin-role-menu').removeClass('active'); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/application_view.js b/client/js/views/application_view.js new file mode 100644 index 000000000..6dcb070c3 --- /dev/null +++ b/client/js/views/application_view.js @@ -0,0 +1,772 @@ +/** + * @fileOverview This file has functions related to application view. This view calling from application. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : undefined + */ +if (typeof App == 'undefined') { + App = {}; +} +var loginExceptionUrl = ['register', 'login', 'forgotpassword', 'user_activation', 'aboutus']; +var adminUrl = ['roles', 'activities', 'users', 'settings', 'email_templates']; +/** + * Application View + * @class ApplicationView + * @constructor + * @extends Backbone.View + */ + +App.ApplicationView = Backbone.View.extend({ + /** + * Constructor + * Check api token is present if + * initialize default values and actions + */ + initialize: function(options) { + var page = this; + page.page_view_type = options.type; + page.page_view_id = options.id; + page.page_view_hash = options.hash; + last_user_activity_id = load_more_last_board_activity_id = last_board_activity_id = 0; + if (_.isUndefined(App.music)) { + App.music = {}; + } + if (!_.isUndefined(App.music)) { + App.music.music_content = ''; + if (!_.isUndefined(App.music.inst)) { + App.music.inst.silence(); + } + } + if (window.sessionStorage.getItem('auth') !== undefined && window.sessionStorage.getItem('auth') !== null && !api_token) { + var Auth = JSON.parse(window.sessionStorage.getItem('auth')); + api_token = Auth.access_token; + page.authuser = Auth; + authuser = Auth; + var organizations = authuser.user.organizations; + authuser.user.organizations = new App.OrganizationCollection(); + authuser.user.organizations.add(organizations); + } else { + if (_.isUndefined(window.sessionStorage.getItem("music_play")) || window.sessionStorage.getItem("music_play") === null) { + window.sessionStorage.setItem('music_play', "1"); + } + } + if (role_links.length === 0 && window.sessionStorage.getItem('links') !== undefined && window.sessionStorage.getItem('links') !== null) { + role_links.add(JSON.parse(window.sessionStorage.getItem('links'))); + } + if (page.model !== 'boards_view') { + viewed_board = new App.Board(); + } + if (!is_offline_data) { + if (api_token === '') { + page.authuser = new App.OAuth(); + page.authuser.url = api_url + 'oauth.json'; + page.authuser.fetch({ + cache: false, + abortPending: true, + success: function(model, response) { + api_token = response.access_token; + window.sessionStorage.setItem('links', response.links); + role_links.add(JSON.parse(response.links)); + settings.url = api_url + 'settings.json'; + settings.fetch({ + cache: false, + abortPending: true, + success: function(collection, settings_response) { + SITE_NAME = settings_response.SITE_NAME; + page.set_page_title(); + FLICKR_API_KEY = settings_response.FLICKR_API_KEY; + DROPBOX_APPKEY = settings_response.DROPBOX_APPKEY; + LABEL_ICON = settings_response.LABEL_ICON; + SITE_TIMEZONE = settings_response.SITE_TIMEZONE; + LDAP_LOGIN_ENABLED = settings_response.LDAP_LOGIN_ENABLED; + if (page.model === "admin_user_add" || page.model === "register") { + if (!_.isEmpty(LDAP_LOGIN_ENABLED) && LDAP_LOGIN_ENABLED === "false") { + page.call_function(); + } else { + changeTitle('404 not found'); + this.headerView = new App.HeaderView({ + model: authuser + }); + $('#header').html(this.headerView.el); + var view = new App.Error404View(); + $('#content').html(view.el); + return; + } + } else { + page.call_function(); + } + } + }); + }, + error: function() { + app.navigate('#/users/login', { + trigger: true, + replace: true + }); + }, + }); + } else { + if (settings.length === 0) { + settings.fetch({ + cache: false, + abortPending: true, + success: function(collection, settings_response) { + SITE_NAME = settings_response.SITE_NAME; + page.set_page_title(); + FLICKR_API_KEY = settings_response.FLICKR_API_KEY; + DROPBOX_APPKEY = settings_response.DROPBOX_APPKEY; + LABEL_ICON = settings_response.LABEL_ICON; + SITE_TIMEZONE = settings_response.SITE_TIMEZONE; + LDAP_LOGIN_ENABLED = settings_response.LDAP_LOGIN_ENABLED; + if (page.model === "admin_user_add" || page.model === "register") { + if (!_.isEmpty(LDAP_LOGIN_ENABLED) && LDAP_LOGIN_ENABLED === "false") { + page.call_function(); + } else { + changeTitle('404 not found'); + this.headerView = new App.HeaderView({ + model: authuser + }); + $('#header').html(this.headerView.el); + var view = new App.Error404View(); + $('#content').html(view.el); + return; + } + } else { + page.call_function(); + } + } + }); + } else { + if (page.model === "admin_user_add" || page.model === "register") { + if (!_.isEmpty(LDAP_LOGIN_ENABLED) && LDAP_LOGIN_ENABLED === "false") { + page.call_function(); + } else { + changeTitle('404 not found'); + this.headerView = new App.HeaderView({ + model: authuser + }); + $('#header').html(this.headerView.el); + var view = new App.Error404View(); + $('#content').html(view.el); + return; + } + } else { + page.call_function(); + } + } + } + } else { + return false; + } + }, + set_page_title: function() { + if (this.model == 'login') { + changeTitle('Login'); + } + if (this.model == 'aboutus') { + changeTitle('About'); + } + if (this.model == 'admin_user_add') { + changeTitle('Admin Add User'); + } + if (this.model == 'register') { + changeTitle('Register'); + } + if (this.model == 'forgotpassword') { + changeTitle('Forgot Password'); + } + if (this.model == 'user_activation') { + changeTitle('User Activation'); + } + if (this.model == 'changepassword') { + changeTitle('Change Password'); + } + if (this.model == 'settings') { + changeTitle('Settings'); + } + if (this.model == 'boards_index') { + changeTitle('Boards'); + } + if (this.model == 'starred_boards_index') { + changeTitle('Starred Boards'); + } + if (this.model == 'closed_boards_index') { + changeTitle('Closed Boards'); + } + if (this.model == 'organizations_view') { + changeTitle('Organization'); + } + if (this.model == 'organizations_view_type') { + changeTitle('Organization'); + } + if (this.model == 'organizations_user_view') { + changeTitle('Organization User'); + } + if (this.model == 'users_index') { + changeTitle('Users'); + } + if (this.model == 'role_settings') { + changeTitle('Role Settings'); + } + if (this.model == 'organizations_index') { + changeTitle('Organizations'); + } + if (this.model == 'email_templates') { + changeTitle('Email Templates'); + } + if (this.model == 'email_template_type') { + changeTitle('Email Templates'); + } + if (this.model == 'activity_index') { + changeTitle('Activities'); + } + }, + /** + * board_view() + * render the board view + * @return Object + */ + board_view: function() { + var self = this; + if (viewed_board.id !== parseInt(self.id)) { + var Board = new App.Board({ + id: self.id + }); + Board.url = api_url + 'boards/' + self.id + '.json'; + Board.id = self.id; + Board.fetch({ + cache: false, + abortPending: true, + success: function(model, response) { + if (!_.isUndefined(response.error)) { + $('#content').html(new App.Board404View({ + model: authuser + }).el); + this.headerView = new App.HeaderView({ + model: authuser + }); + $('#header').html(this.headerView.el); + } else { + Board.authuser = self.authuser; + viewed_board = Board; + $('#content').html(new App.BoardView({ + model: Board + }).el); + $('[data-spy="affix"]').each(function() { + var $spy = $(this); + var data = $spy.data(); + + data.offset = data.offset || {}; + + if (data.offsetBottom) data.offset.bottom = data.offsetBottom; + if (data.offsetTop) data.offset.top = data.offsetTop; + + $spy.affix(data); + }); + if (view_type === 'list') { + $('.js-switch-list-view').trigger('click'); + view_type = null; + } else if (view_type === 'calendar') { + $('.js-switch-calendar-view').trigger('click'); + view_type = null; + } else if (view_type === 'attachments') { + $('.js-show-board-modal').trigger('click'); + view_type = null; + } else if (view_type === null || view_type === '') { + $('.js-switch-grid-view').trigger('click'); + view_type = null; + } + this.footerView = new App.FooterView({ + model: authuser, + board_id: self.id, + board: Board + }).render(); + $('#footer').html(this.footerView.el); + if (!_.isUndefined(authuser.user)) { + var count = authuser.user.notify_count; + if (count > 0) { + if (count >= 100) { + count = '100+'; + } + $('#js-notification-count').removeClass('hide').html(count); + favicon.badge(count); + } + } + var current_param = Backbone.history.fragment; + var current_param_split = current_param.split('/'); + if (current_param.indexOf('list') === -1 && current_param.indexOf('calendar') === -1) { + $('a.js-switch-grid-view').parent().addClass('active'); + } + } + self.board_view_height(); + } + }); + + } else { + if (view_type === 'list') { + $('.js-switch-list-view').trigger('click'); + view_type = null; + } else if (view_type === 'calendar') { + $('.js-switch-calendar-view').trigger('click'); + view_type = null; + } else if (view_type === 'attachments') { + $('.js-show-board-modal').trigger('click'); + view_type = null; + } else if (view_type === null || view_type === '') { + view_type = null; + } + } + + return this; + }, + /** + * organization_view() + * render the organization view + * @return Object + */ + organization_view: function() { + var self = this; + var Organization = new App.Organization(); + Organization.organization_id = self.id; + Organization.url = api_url + 'organizations/' + self.id + '.json'; + Organization.fetch({ + cache: false, + abortPending: true, + success: function(model, response) { + if (!_.isUndefined(response.error) && response.error.message == 'Unauthorized') { + app.navigate('#/users/login', { + trigger: true, + replace: true + }); + } else { + Organization.boards.add(Organization.attributes.boards_listing); + $('#header').html(new App.OrganizationHeaderView({ + model: Organization, + type: self.page_view_type + }).el); + $('#content').html(new App.OrganizationsView({ + model: Organization, + type: self.page_view_type + }).el); + } + } + }); + + return this; + }, + /** + * call_function() + * call the render function based on model + * @default '' + * @type String + */ + call_function: function() { + var page = this; + this.headerView = new App.HeaderView({ + model: authuser + }); + var load_boards = false; + if (page.model == 'boards_view' && App.boards !== undefined) { + if (_.isUndefined(App.boards.get(parseInt(page.id)))) { + load_boards = true; + } + } + if ((App.boards === undefined || load_boards) && !_.isUndefined(authuser.user)) { + var boards = new App.BoardCollection(); + boards.url = api_url + 'boards.json?type=simple'; + boards.fetch({ + cache: false, + abortPending: true, + success: function(model, response) { + App.boards = boards; + page.populateLists(); + page.populateBoardStarred(); + if ((_.indexOf(adminUrl, Backbone.history.fragment) >= 0 && !_.isEmpty(authuser.user) && authuser.user.role_id == 1) || _.indexOf(adminUrl, Backbone.history.fragment) < 0) { + page.callback(); + } else { + app.navigate('#/boards', { + trigger: true, + replace: true + }); + } + } + }); + } else { + if ((_.indexOf(adminUrl, Backbone.history.fragment) >= 0 && !_.isEmpty(authuser.user) && authuser.user.role_id == 1) || _.indexOf(adminUrl, Backbone.history.fragment) < 0) { + page.callback(); + } else { + app.navigate('#/boards', { + trigger: true, + replace: true + }); + } + } + if (page.model !== 'boards_view' && page.model !== 'users_index') { + $('#header').html(this.headerView.el); + } + }, + populateLists: function() { + App.boards.each(function(board) { + board.lists.add(board.attributes.lists); + }); + }, + populateBoardStarred: function() { + App.boards.each(function(board) { + board.boards_stars.add(board.attributes.stars); + }); + }, + callback: function() { + var page = this; + if (page.model == 'boards_view') { + $('body').css('background', 'transparent'); + if (!_.isEmpty(role_links.where({ + slug: 'view_board' + }))) { + page.board_view(); + } + } else if (page.model == 'organizations_view') { + page.organization_view(); + } else if (_.isEmpty(authuser.user) && _.indexOf(loginExceptionUrl, page.model) <= -1) { + app.navigate('#/users/login', { + trigger: true, + replace: true + }); + } else if (!_.isEmpty(authuser.user) && _.indexOf(loginExceptionUrl, page.model) > -1) { + if (page.model == 'aboutus') { + this.pageView = new App.AboutusView(); + $('#content').html(this.pageView.el); + } else { + app.navigate('#/boards', { + trigger: true, + replace: true, + trigger_function: false, + }); + Backbone.history.loadUrl('#/boards'); + } + } else { + if (page.model == 'admin_user_add') { + var AdminUser = new App.User(); + this.pageView = new App.AdminUserAddView({ + model: AdminUser + }); + $('#content').html(this.pageView.el); + } else if (page.model == 'register') { + var User = new App.User(); + this.pageView = new App.RegisterView({ + model: User + }); + $('#content').html(this.pageView.el); + } else if (page.model == 'login') { + var LoginUser = new App.User(); + this.pageView = new App.LoginView({ + model: LoginUser + }); + $('#content').html(this.pageView.el); + } else if (page.model == 'forgotpassword') { + var ForgotPasswordUser = new App.User(); + this.pageView = new App.ForgotpasswordView({ + model: ForgotPasswordUser + }); + $('#content').html(this.pageView.el); + } else if (page.model == 'user_activation') { + var UserActivation = new App.User(); + UserActivation.user_id = page.id; + UserActivation.hash = page.page_view_hash; + this.pageView = new App.UseractivationView({ + model: UserActivation + }); + } else if (page.model == 'changepassword') { + var ChangePasswordUser = new App.User(); + ChangePasswordUser.user_id = page.id; + this.pageView = new App.ChangepasswordView({ + model: ChangePasswordUser + }); + $('#content').html(this.pageView.el); + } else if (page.model == 'aboutus') { + this.pageView = new App.AboutusView(); + $('#content').html(this.pageView.el); + } else if (page.model == 'boards_index' || page.model == 'starred_boards_index' || page.model == 'closed_boards_index') { + var page_title = 'My Boards'; + if (page.model == 'starred_boards_index') { + page_title = 'Starred Boards'; + } else if (page.model == 'closed_boards_index') { + page_title = 'Closed Boards'; + } + this.headerView = new App.BoardIndexHeaderView({ + model: page_title, + }); + $('#header').html(new App.BoardIndexHeaderView({ + model: page_title, + }).el); + var board_index = $('#content'); + board_index.html(''); + var self = this; + var user_boards = new App.BoardCollection(); + user_boards.url = api_url + 'users/' + authuser.user.id + '/boards.json'; + user_boards.fetch({ + cache: false, + abortPending: true, + success: function(model, response) { + var boards = new App.BoardCollection(); + boards.url = api_url + 'boards.json?type=simple'; + boards.fetch({ + cache: false, + abortPending: true, + success: function(board_model, board_response) { + App.boards = boards; + self.populateLists(); + self.populateBoardStarred(); + if (page.model == 'starred_boards_index') { + board_index.append(new App.StarredBoardsIndexView().el); + if (!_.isEmpty(role_links.where({ + slug: 'view_stared_boards' + }))) { + if (!_.isEmpty(response.starred_boards)) { + _.each(response.starred_boards, function(starred_board) { + var board = App.boards.findWhere({ + id: parseInt(starred_board), + is_closed: false + }); + if (!_.isUndefined(board)) { + board.board_subscribers.add(board.attributes.boards_subscribers); + filter = _.matches({ + is_archived: false + }); + filtered_lists = _.filter(board.attributes.lists, filter); + board.lists.add(filtered_lists); + $('.js-header-starred-boards').append(new App.BoardSimpleView({ + model: board, + id: 'js-starred-board-' + board.attributes.id, + className: 'col-lg-3 col-md-4 col-sm-4 col-xs-12 mob-no-pad js-board-view js-board-view-' + board.attributes.id, + starred_boards: response.starred_boards + }).el); + } + }); + if ($('.js-header-starred-boards > .js-board-view').length === 0) { + $('.js-header-starred-boards').append(new App.BoardSimpleView({ + model: null, + message: 'No starred boards available.', + id: 'js-starred-board-empty', + className: 'col-lg-3 col-md-3 col-sm-4 col-xs-12' + }).el); + } + } else { + $('.js-header-starred-boards').append(new App.BoardSimpleView({ + model: null, + message: 'No starred boards available.', + id: 'js-starred-board-empty', + className: 'col-lg-3 col-md-3 col-sm-4 col-xs-12' + }).el); + } + + } + } else if (page.model == 'closed_boards_index') { + board_index.append(new App.ClosedBoardsIndexView().el); + if (!_.isEmpty(role_links.where({ + slug: 'view_closed_boards' + }))) { + var closed_boards = App.boards.where({ + is_closed: true + }); + if (!_.isEmpty(closed_boards)) { + _.each(closed_boards, function(closed_board) { + closed_board.board_subscribers.add(closed_board.attributes.boards_subscribers); + filter = _.matches({ + is_archived: false + }); + filtered_lists = _.filter(closed_board.attributes.lists, filter); + closed_board.lists.add(filtered_lists); + $('.js-header-closed-boards').append(new App.BoardSimpleView({ + model: closed_board, + id: 'js-closed-board-' + closed_board.attributes.id, + className: 'col-lg-3 col-md-4 col-sm-4 col-xs-12 mob-no-pad js-board-view js-board-view-' + closed_board.attributes.id, + starred_boards: response.starred_boards + }).el); + }); + if ($('.js-header-closed-boards > .js-board-view').length === 0) { + $('.js-header-closed-boards').append(new App.BoardSimpleView({ + model: null, + message: 'No closed boards available.', + id: 'js-closed-board-empty', + className: 'col-lg-3 col-md-3 col-sm-4 col-xs-12' + }).el); + } + } else { + $('.js-header-closed-boards').append(new App.BoardSimpleView({ + model: null, + message: 'No closed boards available.', + id: 'js-closed-board-empty', + className: 'col-lg-3 col-md-3 col-sm-4 col-xs-12' + }).el); + } + + } + } else { + board_index.append(new App.BoardsIndexView().el); + var my_boards = ''; + my_boards = App.boards.where({ + is_closed: false + }); + if (!_.isEmpty(role_links.where({ + slug: 'view_my_boards' + }))) { + if (!_.isEmpty(my_boards)) { + _.each(my_boards, function(my_board) { + var my_board_filter = _.matches({ + is_archived: false + }); + var my_board_filtered_lists = _.filter(my_board.attributes.lists, my_board_filter); + my_board.lists.add(my_board_filtered_lists); + $('.js-my-boards').append(new App.BoardSimpleView({ + model: my_board, + id: 'js-my-board-' + my_board.attributes.id, + className: 'col-lg-3 col-md-4 col-sm-4 col-xs-12 mob-no-pad js-board-view js-board-view-' + my_board.attributes.id, + starred_boards: response.starred_boards + }).el); + }); + } else { + $('.js-my-boards').append(new App.BoardSimpleView({ + model: null, + message: 'No boards available.', + id: 'js-my-board-empty', + className: 'col-lg-3 col-md-3 col-sm-4 col-xs-12' + }).el); + } + } + } + + } + }); + } + }); + } else if (page.model == 'users_index') { + var users = new App.UserCollection(); + users.url = api_url + 'users.json'; + users.fetch({ + cache: false, + abortPending: true, + success: function(collections, response) { + this.headerView = new App.HeaderView({ + model: authuser, + users: users + }); + $('#header').html(this.headerView.el); + $('#js-navbar-default').remove(); + var view = $('#content').html(new App.UserIndexContainerView({ + model: user + }).el); + users.each(function(user) { + $('.js-user-list').append(new App.UserIndex({ + model: user + }).el); + }); + $('.js-user-list').find('.timeago').timeago(); + } + }); + } else if (page.model == 'settings') { + $('#js-navbar-default').remove(); + $('#content').html(new App.SettingView({ + id: page.page_view_id + }).el); + } else if (page.model == 'user_view') { + if (!_.isUndefined(authuser.user) && (authuser.user.id === page.id || authuser.user.id === '1')) { + // User View + var user = new App.User(); + user.url = api_url + 'users/' + page.id + '.json'; + user.fetch({ + cache: false, + abortPending: true, + success: function(user, response) { + $('#header').html(new App.UserViewHeaderView({ + model: user, + type: page.page_view_type + }).el); + $('#content').html(new App.UserView({ + model: user, + type: page.page_view_type + }).el); + } + }); + } else { + app.navigate('#/boards', { + trigger: true, + replace: true, + trigger_function: false, + }); + Backbone.history.loadUrl('#/boards'); + page.flash('danger', 'Permission denied'); + } + } else if (page.model == 'role_settings') { + // User View + var acl_links = new App.ACLCollection(); + acl_links.url = api_url + 'acl_links.json'; + acl_links.fetch({ + cache: false, + abortPending: true, + success: function(acl_links, response) { + acl_links.reset(response.acl_links); + var roles = new App.RoleCollection(); + roles.reset(response.roles); + $('#js-navbar-default').remove(); + $('#content').html(new App.RoleSettingsView({ + model: acl_links, + roles: roles + }).el); + } + }); + } else if (page.model == 'organizations_index') { + var organizations = new App.OrganizationCollection(); + organizations.url = api_url + 'organizations.json'; + organizations.fetch({ + cache: false, + abortPending: true, + success: function(collections, response) { + $('#header').html(new App.OrganizationsListsHeaderView({ + model: organizations, + }).el); + } + }); + } else if (page.model == 'email_template_type') { + $('#js-navbar-default').remove(); + $('#content').html(new App.EmailTemplateView({ + id: page.page_view_id + }).el); + } else if (page.model == 'activity_index') { + $('#js-navbar-default').remove(); + $('#content').html(new App.ActivityIndexView({ + id: page.page_view_id + }).el); + } + } + if (page.model == 'boards_view') { + authuser.board_id = this.id; + } else { + authuser.board_id = 0; + } + if ((window.sessionStorage.getItem('auth') !== undefined && window.sessionStorage.getItem('auth') !== null) || page.model == 'organizations_view') { + this.footerView = new App.FooterView({ + model: authuser + }).render(); + $('#footer').html(this.footerView.el); + if (!_.isUndefined(authuser.user)) { + var count = authuser.user.notify_count; + if (count > 0) { + if (count >= 100) { + count = '100+'; + } + $('#js-notification-count').removeClass('hide').html(count); + favicon.badge(count); + } + } + } else { + if (Backbone.history.fragment.indexOf('board/') != -1 || Backbone.history.fragment.indexOf('organization/') != -1) { + this.footerView = new App.FooterView({ + model: authuser, + }).render(); + $('#footer').html(this.footerView.el); + } else { + $('#footer').html(''); + } + } + } +}); diff --git a/client/js/views/archived_card_view.js b/client/js/views/archived_card_view.js new file mode 100644 index 000000000..71054ef30 --- /dev/null +++ b/client/js/views/archived_card_view.js @@ -0,0 +1,69 @@ +/** + * @fileOverview This file has functions related to archived card view. This view calling from board header view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : card model and it's related values. It contain all card based object @see Available Object in App.CardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ArchivedCard View + * @class ArchivedCardView + * @constructor + * @extends Backbone.View + */ +App.ArchivedCardView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/archived_card'], + tagName: 'li', + className: 'list-group-item clearfix', + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-delete-archived-card': 'deleteArchivedCard', + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + card: this.model, + })); + this.showTooltip(); + return this; + }, + /** + * deleteArchivedCard() + * delete archived card + * @param e + * @type Object(DOM event) + * @return false + * + */ + deleteArchivedCard: function(e) { + e.preventDefault(); + this.model.collection.remove([{ + id: this.model.attributes.id + }]); + this.$el.remove(); + this.model.url = api_url + 'boards/' + this.model.attributes.board_id + '/lists/' + this.model.attributes.list_id + '/cards/' + this.model.attributes.id + '.json'; + this.model.destroy(); + return false; + }, +}); diff --git a/client/js/views/archived_cards_view.js b/client/js/views/archived_cards_view.js new file mode 100644 index 000000000..3eb00d3ec --- /dev/null +++ b/client/js/views/archived_cards_view.js @@ -0,0 +1,44 @@ +/** + * @fileOverview This file has functions related to archived cards view. This view calling from board header view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board model and it's related values. It contain all board based object @see Available Object in App.BoardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ArchivedCards View + * @class ArchivedCards + * @constructor + * @extends Backbone.View + */ +App.ArchivedCardsView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/archived_cards'], + tagName: 'div', + className: 'clearfix col-xs-12', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + board: this.model, + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/archived_items_view.js b/client/js/views/archived_items_view.js new file mode 100644 index 000000000..d1433effb --- /dev/null +++ b/client/js/views/archived_items_view.js @@ -0,0 +1,43 @@ +/** + * @fileOverview This file has functions related to archived items view. This view calling from board header view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board model and it's related values. It contain all board based object @see Available Object in App.BoardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ArchivedItems View + * @class ArchivedItemsView + * @constructor + * @extends Backbone.View + */ +App.ArchivedItemsView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/archived_items'], + tagName: 'li', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + board: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/archived_list_view.js b/client/js/views/archived_list_view.js new file mode 100644 index 000000000..9123f12fa --- /dev/null +++ b/client/js/views/archived_list_view.js @@ -0,0 +1,69 @@ +/** + * @fileOverview This file has functions related to archived list view. This view calling from board header view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : list model and it's related values. It contain all list based object @see Available Object in App.ListView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * UserCards View + * @class UserCardsView + * @constructor + * @extends Backbone.View + */ +App.ArchivedListView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/archived_list'], + tagName: 'li', + className: 'list-group-item clearfix', + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-delete-archived-list': 'deleteArchivedList', + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + list: this.model, + })); + this.showTooltip(); + return this; + }, + /** + * deleteArchivedList() + * delete archived list + * @param e + * @type Object(DOM event) + * @return false + * + */ + deleteArchivedList: function(e) { + e.preventDefault(); + this.model.collection.remove([{ + id: this.model.attributes.id + }]); + this.$el.remove(); + this.model.url = api_url + 'boards/' + this.model.attributes.board_id + '/lists/' + this.model.attributes.id + '.json'; + this.model.destroy(); + return false; + } +}); diff --git a/client/js/views/archived_lists_view.js b/client/js/views/archived_lists_view.js new file mode 100644 index 000000000..dde8c12fe --- /dev/null +++ b/client/js/views/archived_lists_view.js @@ -0,0 +1,44 @@ +/** + * @fileOverview This file has functions related to archived lists view. This view calling from board header view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board model and it's related values. It contain all list based object @see Available Object in App.BoardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ArchivedLists View + * @class ArchivedListsView + * @constructor + * @extends Backbone.View + */ +App.ArchivedListsView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/archived_lists'], + tagName: 'div', + className: 'clearfix col-xs-12', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + board: this.model, + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/attachment_delete_confirm_form_view.js b/client/js/views/attachment_delete_confirm_form_view.js new file mode 100644 index 000000000..4d0ea1d88 --- /dev/null +++ b/client/js/views/attachment_delete_confirm_form_view.js @@ -0,0 +1,43 @@ +/** + * @fileOverview This file has functions related to attachment delete view. This view calling from attachment view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : attachment model and it's related values + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * AttachmentDeleteConfirmForm View + * @class AttachmentDeleteConfirmFormView + * @constructor + * @extends Backbone.View + */ +App.AttachmentDeleteConfirmFormView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/attachment_delete_confirm_form'], + tagName: 'div', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + attachment: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/attachment_delete_confirm_view.js b/client/js/views/attachment_delete_confirm_view.js new file mode 100644 index 000000000..90686d499 --- /dev/null +++ b/client/js/views/attachment_delete_confirm_view.js @@ -0,0 +1,43 @@ +/** + * @fileOverview This file has functions related to attachment delete confirm view. This view calling from attachment view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : attachment model and it's related values + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ActivityDeleteConfirm View + * @class ActivityDeleteConfirmView + * @constructor + * @extends Backbone.View + */ +App.AttachmentDeleteConfirmView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/attachment_delete_confirm'], + tagName: 'div', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + attachment: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/attachment_view.js b/client/js/views/attachment_view.js new file mode 100644 index 000000000..dab9032f2 --- /dev/null +++ b/client/js/views/attachment_view.js @@ -0,0 +1,86 @@ +/** + * @fileOverview This file has functions related to attachment delete confirm view. This view calling from modal board and modal list view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : attachment model and it's related values + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Attachment View + * @class AttachmentView + * @constructor + * @extends Backbone.View + */ +App.AttachmentView = Backbone.View.extend({ + template: JST['templates/attachment'], + tagName: 'li', + className: 'clearfix col-md-4 col-sm-6 col-xs-12 navbar-btn', + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + this.model.downloadLink = this.downloadLink; + } + }, + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click a.js-show-confirm-delete-attachment': 'showConfirmAttachmentDelete', + 'click .js-close-popup': 'closePopup', + 'click .js-delete-attachment': 'deleteAttachment' + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + attachment: this.model + })); + this.showTooltip(); + return this; + }, + /** + * deleteAttachment() + * delete the attachment + * @param NULL + * @return false + * + */ + deleteAttachment: function() { + this.$el.remove(); + this.model.url = api_url + 'boards/' + this.model.attributes.board_id + '/lists/' + this.model.attributes.list_id + '/cards/' + this.model.attributes.card_id + '/attachments/' + this.model.id + '.json'; + this.model.destroy(); + return false; + }, + /** + * showConfirmAttachmentDelete() + * show the confirm attachment delete + * @param e + * @type Object(DOM event) + */ + showConfirmAttachmentDelete: function(e) { + e.preventDefault(); + $('.js-attachment-confirm-respons-' + this.model.id, this.$el).html(new App.AttachmentDeleteConfirmFormView({ + model: this.model + }).el); + }, + /** + * closePopup() + * close the opened dropdown + * @param e + * @type Object(DOM event) + * @return false + */ + closePopup: function(e) { + var target = $(e.target); + target.parents('li.dropdown').removeClass('open'); + return false; + } +}); diff --git a/client/js/views/board_404_view.js b/client/js/views/board_404_view.js new file mode 100644 index 000000000..ee2e13266 --- /dev/null +++ b/client/js/views/board_404_view.js @@ -0,0 +1,42 @@ +/** + * @fileOverview This file has functions related to board 404 view. This view calling from application view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : undefined + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ActivityAddForm View + * @class ActivityAddFormView + * @constructor + * @extends Backbone.View + */ +App.Board404View = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/board_404'], + tagName: 'section', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + authuser: this.model + })); + return this; + } +}); diff --git a/client/js/views/board_add_organization_form_view.js b/client/js/views/board_add_organization_form_view.js new file mode 100644 index 000000000..f1d31f66c --- /dev/null +++ b/client/js/views/board_add_organization_form_view.js @@ -0,0 +1,44 @@ +/** + * @fileOverview This file has functions related to board add organization view. This view calling from board add view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : organizations collection(based on login user) + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * BoardOrganizationForm View + * @class BoardOrganizationFormView + * @constructor + * @extends Backbone.View + */ +App.BoardAddOrganizationFormView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/board_add_organization_form'], + tagName: 'div', + className: 'form-group', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + organizations: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/board_add_view.js b/client/js/views/board_add_view.js new file mode 100644 index 000000000..0561b203e --- /dev/null +++ b/client/js/views/board_add_view.js @@ -0,0 +1,125 @@ +/** + * @fileOverview This file has functions related to board add view. This view calling from footer view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : workflow template collection + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * BoardAddView View + * @class BoardAddView + * @constructor + * @extends Backbone.View + */ +App.BoardAddView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/board_add'], + tagName: 'ul', + events: { + 'click .js-change-visibility': 'showAllVisibility', + 'click .js-select': 'selectBoardVisibility', + 'submit form#BoardAddForm': 'submitBoardAdd', + }, + className: 'list-unstyled', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + templates: this.model + })); + this.showTooltip(); + return this; + }, + showAllVisibility: function(e) { + e.preventDefault(); + $('.js-open-dropdown').addClass('open'); + $('.js-visibility-container').html(''); + var visibility = $('#inputBoardAddVisibility').val(); + $('.js-visibility-chooser').html(new App.ShowAllVisibilityView({ + model: visibility + }).el); + + return false; + }, + /** + * selectBoardVisibility() + * change the board visibility + * @param e + * @type Object(DOM event) + * @return false + * + */ + selectBoardVisibility: function(e) { + var name = $(e.currentTarget).attr('name'); + var value = 0; + $('#js-board-add-organization').html(''); + if (name == 'org') { + value = 1; + this.showBoardAddeOrganizationForm(e); + } else if (name == 'public') { + value = 2; + } + var content = new App.SelectedBoardVisibilityView({ + model: name + }).el; + $('#inputBoardAddVisibility').val(value); + $('.js-visibility-container').html(content); + content = ''; + $('.js-board-add-dropdown').removeClass('open'); + return false; + }, + submitBoardAdd: function(e) { + e.preventDefault(); + var data = $(e.target).serializeObject(); + var board = new App.Board(); + board.url = api_url + 'boards.json'; + board.save(data, { + success: function(model, response) { + App.boards.add(response.simple_board); + if (response.simple_board.lists !== null) { + App.boards.get(parseInt(response.simple_board.id)).lists.add(response.simple_board.lists); + } + app.navigate('#/board/' + response.id, { + trigger: true, + replace: true + }); + } + }); + return false; + }, + /** + * showChangeOrganizationForm() + * show board organiztion change form + * @param e + * @type Object(DOM event) + * + */ + showBoardAddeOrganizationForm: function(e) { + e.preventDefault(); + var organizations = new App.OrganizationCollection(); + organizations = authuser.user.organizations; + if (authuser.user.organizations !== null && _.isUndefined(authuser.user.organizations.models)) { + organizations.add(JSON.parse(authuser.user.organizations)); + } + authuser.user.organizations = organizations; + $('#js-board-add-organization').html(new App.BoardAddOrganizationFormView({ + model: authuser.user.organizations + }).el); + } +}); diff --git a/client/js/views/board_additional_setting_view.js b/client/js/views/board_additional_setting_view.js new file mode 100644 index 000000000..0bb556bb6 --- /dev/null +++ b/client/js/views/board_additional_setting_view.js @@ -0,0 +1,45 @@ +/** + * @fileOverview This file has functions related to board additional settings view. This view calling from board header view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board model. It contain all board based object @see Available Object in App.BoardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * BoardFilter View + * @class BoardFilterView + * @constructor + * @extends Backbone.View + */ +App.BoardAdditionalSettingsView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + //this.board_labels = options.board_labels; + this.render(); + }, + template: JST['templates/board_additional_settings'], + tagName: 'div', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + board: this.model, + //board_labels: this.board_labels, + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/board_background_view.js b/client/js/views/board_background_view.js new file mode 100644 index 000000000..08482c8c7 --- /dev/null +++ b/client/js/views/board_background_view.js @@ -0,0 +1,43 @@ +/** + * @fileOverview This file has functions related to board background view. This view calling from board header view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board model. It contain all board based object @see Available Object in App.BoardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * BoardBackground View + * @class BoardBackgroundView + * @constructor + * @extends Backbone.View + */ +App.BoardBackgroundView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/board_background'], + tagName: 'div', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + board: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/board_custom_background_view.js b/client/js/views/board_custom_background_view.js new file mode 100644 index 000000000..59cfdaa3f --- /dev/null +++ b/client/js/views/board_custom_background_view.js @@ -0,0 +1,44 @@ +/** + * @fileOverview This file has functions related to board custom background view. This view calling from board header view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board model. It contain all board based object @see Available Object in App.BoardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * BoardCustomBackground View + * @class BoardCustomBackgroundView + * @constructor + * @extends Backbone.View + */ +App.BoardCustomBackgroundView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/board_custom_background'], + tagName: 'div', + className: 'js-change-custom-background board-background-select', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + board_background: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/board_filter_view.js b/client/js/views/board_filter_view.js new file mode 100644 index 000000000..d8c4aaf02 --- /dev/null +++ b/client/js/views/board_filter_view.js @@ -0,0 +1,48 @@ +/** + * @fileOverview This file has functions related to board filter view. This view calling from board header view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board model. It contain all board based object @see Available Object in App.BoardView + * this.model.labels : labels collection + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * BoardFilter View + * @class BoardFilterView + * @constructor + * @extends Backbone.View + */ +App.BoardFilterView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.labels = options.labels; + this.render(); + }, + template: JST['templates/board_filter'], + tagName: 'li', + converter: new Showdown.converter(), + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + board: this.model, + board_labels: this.labels, + converter: this.converter + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/board_header_view.js b/client/js/views/board_header_view.js new file mode 100644 index 000000000..6c861b2cf --- /dev/null +++ b/client/js/views/board_header_view.js @@ -0,0 +1,1705 @@ +/** + * @fileOverview This file has functions related to board header view. This view calling from board view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board model and it's related values + * this.model.activities : activities collection(Based on board) + * this.model.attachments : attachments collection(Based on board) + * this.model.board_stars : starred board collection(Based on logged in user) + * this.model.board_users : board user collection(Based on board) + * this.model.boards_subscribers : board user collection(Based on board) + * this.model.cards : cards collection(Based on board) + * this.model.checklists : checklists collection(Based on card) + * this.model.checklist_items : checklist items collection(Based on checklist) + * this.model.custom_attachments : custom attachments collection(Based on board) + * this.model.lists : lists collection(Based on board) + * this.model.labels : labels collection(Based on board) + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Board Header View + * @class BoardHeaderView + * @constructor + * @extends Backbone.View + */ +App.BoardHeaderView = Backbone.View.extend({ + className: 'navbar navbar-default', + attributes: { + role: 'navigation' + }, + template: JST['templates/board_header'], + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.model.lists.bind('remove', this.showArchivedListLists, this); + this.model.cards.bind('add', this.renderCardsCollection); + this.model.lists.bind('change:is_archived', this.showArchivedListLists, this); + this.model.cards.bind('change:name', this.showArchivedCardsList, this); + this.model.cards.bind('change:due_date', this.showArchivedCardsList, this); + this.model.cards.bind('change:is_archived', this.showArchivedCardsList, this); + this.model.cards.bind('remove', this.showArchivedCardsList, this); + this.model.bind('change:organization_id', this.render, this); + this.model.bind('change:background_picture_url', this.showChangeBackground, this); + this.model.bind('change:background_pattern_url', this.showChangeBackground, this); + this.model.bind('change:music_name', this.showChangeBackground, this); + this.model.bind('change:music_content', this.showChangeBackground, this); + this.model.board_users.bind('add', this.showFilters, this); + this.model.board_users.bind('remove', this.showFilters, this); + this.model.labels.bind('add', this.showFilters, this); + this.model.labels.bind('change', this.showFilters, this); + this.model.labels.bind('remove', this.showFilters, this); + this.authuser = authuser.user; + this.renderAdminBoardUsers(); + }, + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'submit form#js-add-board-member-form': function(e) { + e.preventDefault(); + return false; + }, + 'click .js-star-board': 'starredBoard', + 'click .js-close-popover': 'closePopup', + 'click .js-close-sidebar-popover': 'closeSidebarPopup', + 'click .js-board-visibility': 'showBoardVisibility', + 'click .js-add-board-member-dropdown': 'addBoardMemberDropdown', + 'click .js-close-popover-board-member-dropdown': 'closeBoardMemberDropdown', + 'click .js-subscribe-board': 'subcribeBoard', + 'click .js-switch-grid-view': 'switchGridView', + 'click .js-switch-list-view': 'switchListView', + 'click .js-switch-calendar-view': 'switchCalendarView', + 'click .js-show-filters': 'showFilters', + 'click .js-archived-items': 'showArchivedItems', + 'click .js-sync-google-dropdown': 'syncGoogleDropdown', + 'click .js-show-copy-board': 'showCopyBoard', + 'click .js-syn-google-calendar': 'showSyncGoogleCalendar', + 'click .js-close-sub-popover': 'closeSubPopup', + 'click #js-select-google-sync-url': 'selectGoogleSyncUrl', + 'click .js-change-background': 'showChangeBackground', + 'click .js-open-dropdown': 'openDropdown', + 'click .js-change-visibility': 'showAllVisibility', + 'click .js-show-board-modal': 'showListModal', + 'click .js-additional-settings': 'showAdditionalSettings', + 'click .js-close-board': 'closeBoard', + 'click .js-toggle-label-filter': 'toggleLabelFilter', + 'click .js-toggle-member-filter': 'toggleMemberFilter', + 'click .js-due-filter': 'toggleDueFilter', + 'click .js-back-to-sidebar': 'backToSidebar', + 'click .js-board-user-avatar-click': 'boardUserAvatarDropdown', + 'click .js-close-board-user-avatar': 'closeBoardUserAvatarDropdown', + 'click .js-change-background-image': 'changeBackgroundImage', + 'click .js-change-background-pattern': 'changeBackgroundPattern', + 'click .js-change-custom-background': 'changeCustomBackground', + 'click .js-modal-fliker-trigger': 'modalFlickrTtrigger', + 'click .js-delete-background-img': 'ClearBackground', + 'click .js-modal-music-trigger': 'modalMusicTtrigger', + 'click .js-music-clear': 'ClearMusic', + 'click .js-show-archived-card-lists': 'showArchivedCardsList', + 'click .js-show-archived-lists': 'showArchivedListLists', + 'keyup .js-search-archived-lists': 'showFilteredArchivedListLists', + 'keyup .js-search-archived-cards': 'showFilteredArchivedCardsList', + 'click .js-send-card-to-board': 'sendCardTobard', + 'keyup #inputBoardUserSearch': 'showSearchBoardMembers', + 'click #submitBoardRename': 'boardRename', + 'click .js-close-span-popover': 'closeSpanPopover', + 'click .js-set-privte-board': 'setPrivteBoard', + 'click .js-set-public-board': 'setPublicBoard', + 'click .js-show-board-organization': 'showBoardOrganization', + 'submit .js-save-board-visibility': 'saveBoardVisibility', + 'click .js-change-color': 'changeBackgroundColor', + 'click .js-send-list-to-board': 'sendListToboard', + 'click .js-enable-covers': 'toggleAdditionalSettings', + 'click .js-computer-open-board-background': 'computerOpenBoardBackground', + 'change #js-custom-background-attachment': 'addBoardBackground', + 'click .js-no-action': 'noAction', + 'click .js-back-to-board-visibility': 'showBoardVisibility', + 'click .js-select': 'selectBoardVisibility', + }, + /** + * openDropdown() + * copy the existing card + * @param e + * @type Object(DOM event) + * @return false + * + */ + openDropdown: function(e) { + e.preventDefault(); + var target = $(e.target); + target.parents('li.dropdown:first, div.dropdown:first').addClass('open'); + return false; + }, + + + /** + * syncGoogleDropdown() + * show the sync the board cards duedate to google calander URL + * @param e + * @type Object(DOM event) + * @return false + * + */ + syncGoogleDropdown: function(e) { + e.preventDefault(); + $('.js-sync-google-dropdown').addClass('open'); + return false; + }, + /** + * boardRename() + * close the dropdown + * @param e + * @type Object(DOM event) + * @return false + * + */ + closePopup: function(e) { + var el = this.$el; + var target = el.find(e.target); + target.parents('div.dropdown:first, li.dropdown:first').removeClass('open'); + return false; + }, + /** + * closeSidebarPopup() + * close the dropdown + * @param e + * @type Object(DOM event) + * @return false + * + */ + closeSidebarPopup: function(e) { + var el = this.$el; + var target = el.find(e.target); + target.parents('div.dropdown:first').removeClass('open'); + return false; + }, + /** + * closeSubPopup() + * close the sub dropdown + * @param e + * @type Object(DOM event) + * @return false + * + */ + closeSubPopup: function(e) { + var el = this.$el; + var target = el.find(e.target); + target.parents('li.dropdown').removeClass('open'); + return false; + }, + /** + * showBoardVisibility() + * display the board visibility + * @param e + * @type Object(DOM event) + * + */ + showBoardVisibility: function(e) { + var target = $(e.target); + this.$('.js-back-to-board-visibility').addClass('hide'); + var parent = target.parents('.js-visibility-list-dropdown'); + var visibility = this.model.attributes.board_visibility; + var insert = $('.js-visibility-list', parent); + insert.nextAll().remove(); + $(new App.ShowBoardVisibilityView({ + model: visibility + }).el).insertAfter(insert); + parent.addClass('open'); + return false; + }, + /** + * subcribeBoard() + * subcribe the board + * @param e + * @type Object(DOM event) + * @return false + * + */ + subcribeBoard: function(e) { + e.preventDefault(); + var name = $(e.currentTarget).attr('name'); + var value = 'unsubscribe'; + var content = 'Unsubscribe'; + if (name == 'unsubscribe') { + value = 'subscribe'; + content = 'Subscribe'; + } + $(e.currentTarget).attr('name', value); + $(e.currentTarget).attr('title', value); + $(e.currentTarget).html(content); + var boardSubscriber = new App.BoardSubscriber(); + if (!_.isUndefined(this.model.board_subscriber) && this.model.board_subscriber.attributes.id) { + value = ''; + if ($('#inputBoardSubscribe').val() == 'false') { + value = 'true'; + $('#inputBoardSubscribe').val(value); + } else { + value = 'false'; + $('#inputBoardSubscribe').val(value); + } + var data = $('form#BoardSubscribeForm').serializeObject(); + boardSubscriber.url = api_url + 'boards/' + this.model.id + '/board_subscribers/' + this.model.board_subscriber.attributes.id + '.json'; + boardSubscriber.set('id', this.model.board_subscriber.attributes.id); + boardSubscriber.save(data, { + success: function(model, response) {} + }); + } else { + var subscribe_data = { + board_id: this.model.id, + is_subscribed: true + }; + var self = this; + boardSubscriber.url = api_url + 'boards/' + this.model.id + '/board_subscribers.json'; + boardSubscriber.save(subscribe_data, { + success: function(model, response) { + boardSubscriber.set('id', parseInt(response.id)); + boardSubscriber.set('user_id', parseInt(response.user_id)); + boardSubscriber.set('board_id', parseInt(response.board_id)); + boardSubscriber.set('board_id', (response.is_subscribed === 't') ? true : false); + self.model.board_subscriber = boardSubscriber; + self.model.board_subscribers.add(boardSubscriber); + self.render(); + } + }); + } + return false; + }, + /** + * starredBoard() + * subcribe the board + * @param e + * @type Object(DOM event) + * @return false + * + */ + starredBoard: function(e) { + e.preventDefault(); + $('.js-star-board').addClass('hide'); + $('.js-star-load').removeClass('hide'); + var name = $(e.currentTarget).attr('name'); + var value = 'unstar'; + var is_starred = true; + var self = this; + var content = ''; + if (name == 'unstar') { + value = 'star'; + is_starred = false; + content = ''; + } + $(e.currentTarget).attr('name', value); + $('.js-star-load').addClass('hide'); + $('.js-star-board').removeClass('hide'); + $(e.currentTarget).html(content); + var boardStar = new App.BoardStar(); + if (!_.isEmpty(this.model.board_star) && this.model.board_star.attributes.id) { + value = ''; + if ($('#inputBoardStar').val() == 'false') { + value = 'true'; + is_starred = true; + $('#inputBoardStar').val(value); + } else { + value = 'false'; + is_starred = false; + $('#inputBoardStar').val(value); + } + var data = $('form#BoardStarForm').serializeObject(); + boardStar.url = api_url + 'boards/' + this.model.id + '/boards_stars/' + this.model.board_star.attributes.id + '.json'; + boardStar.set('id', this.model.board_star.attributes.id); + boardStar.save(data, { + success: function(model, response) { + App.boards.get(self.model.attributes.id).boards_stars.get(parseInt(response.id)).set('is_starred', is_starred); + new App.FooterView({ + model: authuser, + board_id: self.model.attributes.id, + board: self.model + }).renderStarredBoards(); + } + }); + } else { + var subscribe_data = {}; + boardStar.url = api_url + 'boards/' + this.model.id + '/boards_stars.json'; + boardStar.set('board_id', this.model.attributes.id); + boardStar.set('user_id', parseInt(authuser.user.id)); + boardStar.set('is_starred', is_starred); + boardStar.save(subscribe_data, { + success: function(model, response) { + boardStar.set('id', parseInt(response.id)); + App.boards.get(self.model.attributes.id).boards_stars.add(boardStar); + self.model.boards_stars.add(response); + self.footerView = new App.FooterView({ + model: authuser, + board_id: self.model.attributes.id + }).renderStarredBoards(); + } + }); + } + return false; + }, + /** + * closeBoard() + * close the board + * @param e + * @type Object(DOM event) + * @return false + * + */ + closeBoard: function(e) { + e.preventDefault(); + this.model.url = api_url + 'boards/' + this.model.id + '.json'; + App.boards.get(this.model.id).set('is_closed', true); + this.footerView = new App.FooterView({ + model: authuser, + board_id: this.model.id + }).renderClosedBoards(); + this.model.save({ + is_closed: true + }, { + patch: true, + success: function(model, response) { + app.navigate('#/board/' + this.model.id, { + trigger: true, + replace: true + }); + } + }); + return false; + }, + showAllVisibility: function(e) { + var target = $(e.target); + var parent = target.parents('.js-visibility-chooser-copy-board-dropdown'); + var visibility = $('#inputBoardVisibility').val(); + var insert = $('.js-visibility-chooser-copy-board', parent); + insert.nextAll().remove(); + $(new App.ShowAllVisibilityView({ + model: visibility + }).el).insertAfter(insert); + parent.addClass('open'); + return false; + }, + /** + * showChangeBackground() + * display the board background change form + * @return false + * + */ + showChangeBackground: function() { + $('.js-side-bar-' + this.model.id).addClass('side-bar-large'); + var el = this.$el; + el.find('.js-setting-response').html(new App.BoardBackgroundView({ + model: this.model + }).el); + var self = this; + _(function() { + Backbone.TemplateManager.baseUrl = '{name}'; + var uploadManager = new Backbone.UploadManager({ + uploadUrl: api_url + 'boards/' + self.model.id + '/custom_backgrounds.json?token=' + api_token, + autoUpload: true, + singleFileUploads: true, + dropZone: $('#custom-background-dropzone'), + formData: $('form.js-cusotm-background-add').serialize(), + fileUploadHTML: '', + }); + uploadManager.on('fileadd', function(file) { + $('#custom-dropzone-cssloader').addClass('cssloader'); + }); + uploadManager.on('filedone', function(file, data) { + $('#custom-dropzone-cssloader').removeClass('cssloader'); + self.model.set({ + background_picture_url: '' + }, { + silent: true + }); + self.model.set({ + background_picture_url: data.result.background_picture_url + "?fx=" + _.random(1000, 9999) + }); + }); + uploadManager.renderTo($('#manager-area')); + }).defer(); + var headerH = $('header').height(); + var windowH = $(window).height(); + var footerH = $('footer').height(); + var boardH = windowH - headerH - footerH - 14; + $('.member-modal.js-pre-scrollable').css({ + 'max-height': boardH - 50, + 'overflow-y': 'auto' + }); + return false; + }, + /** + * showArchivedItems() + * display the archived items + * @param e + * @type Object(DOM event) + * @return false + * + */ + showArchivedItems: function(e) { + var el = this.$el; + e.preventDefault(); + el.find('.side-bar').addClass('side-bar-large'); + el.find('.js-setting-response').html(new App.ArchivedItemsView({ + model: this.model + }).el); + this.showArchivedCardsList(); + var headerH = $('header').height(); + var windowH = $(window).height(); + var footerH = $('footer').height(); + var boardH = windowH - headerH - footerH - 14; + $('.member-modal.js-pre-scrollable').css({ + 'max-height': boardH - 50, + 'overflow-y': 'auto' + }); + return false; + }, + /** + * showArchivedCardsList() + * display the archived cards list + * @return false + * + */ + showArchivedCardsList: function() { + if (!_.isEmpty(role_links.where({ + slug: 'view_archived_cards' + }))) { + var el = this.$el; + var filtered_cards = this.model.cards.where({ + is_archived: true + }); + el.find('.js-archived-items-container').html(new App.ArchivedCardsView({ + model: this.model + }).el); + if (!_.isEmpty(filtered_cards)) { + _.each(filtered_cards, function(card) { + el.find('.js-archived-cards-container').append(new App.ArchivedCardView({ + model: card + }).el); + }); + } else { + el.find('.js-archived-cards-container').append(new App.ArchivedCardView({ + model: null + }).el); + } + } + return false; + }, + /** + * showFilteredArchivedCardsList() + * display the filtered archived cards list + * @return false + * + */ + showFilteredArchivedCardsList: function(e) { + if (!_.isEmpty(role_links.where({ + slug: 'view_archived_cards' + }))) { + var el = this.$el; + var search_q = $(e.currentTarget).val(); + var filtered_cards = ''; + if (!_.isEmpty(search_q)) { + filtered_cards = this.model.cards.filter(function(model) { + return ~model.get('name').indexOf(search_q); + }); + } else { + filtered_cards = this.model.cards.where({ + is_archived: true + }); + } + el.find('.js-archived-cards-container').html(""); + if (!_.isEmpty(filtered_cards)) { + var _i = 0; + _.each(filtered_cards, function(card) { + if (card.attributes.is_archived === true) { + el.find('.js-archived-cards-container').append(new App.ArchivedCardView({ + model: card + }).el); + _i++; + } + }); + if (_i === 0) { + el.find('.js-archived-cards-container').append(new App.ArchivedCardView({ + model: null + }).el); + } + + } else { + el.find('.js-archived-cards-container').append(new App.ArchivedCardView({ + model: null + }).el); + } + } + return false; + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + var self = this; + this.is_admin = false; + changeTitle('Board - ' + _.escape(this.model.attributes.name)); + if (!_.isUndefined(this.authuser)) { + this.model.board_subscriber = this.model.board_subscribers.findWhere({ + user_id: parseInt(this.authuser.id) + }); + this.model.board_star = this.model.board_stars.findWhere({ + user_id: parseInt(this.authuser.id) + }); + var admin = this.model.board_users.findWhere({ + user_id: parseInt(authuser.user.id), + is_admin: true + }); + this.is_admin = (!_.isEmpty(admin) || (!_.isUndefined(authuser.user) && authuser.user.role_id === 1)) ? true : false; + } + this.$el.html(this.template({ + board: this.model, + subscriber: this.model.board_subscriber, + star: this.model.board_star, + is_admin: this.is_admin + })); + $('a.js-switch-grid-view').parent().addClass('active'); + this.showTooltip(); + this.renderBoardUsers(); + return this; + }, + /** + * showAdditionalSettings() + * show the Additional Settings + * @param e + * @type Object(DOM event) + * @return false + * + */ + showAdditionalSettings: function(e) { + e.preventDefault(); + var el = this.$el; + el.find('.js-setting-response').html(new App.BoardAdditionalSettingsView({ + model: this.model, + }).el); + return false; + }, + /** + * showFilters() + * show the filter list + * @param e + * @type Object(DOM event) + * @return false + * + */ + showFilters: function() { + $('.js-side-bar-' + this.model.id).addClass('side-bar-large'); + var el = this.$el; + el.find('.js-setting-response').html(new App.BoardFilterView({ + model: this.model, + labels: this.model.labels + }).el); + var headerH = $('header').height(); + var windowH = $(window).height(); + var footerH = $('footer').height(); + var boardH = windowH - headerH - footerH - 14; + $('.member-modal.js-pre-scrollable').css({ + 'max-height': boardH - 50, + 'overflow-y': 'auto' + }); + return false; + }, + /** + * modalMusicTtrigger() + * display the attachment in the list + * @param e + * @type Object(DOM event) + * @return false + * + */ + modalMusicTtrigger: function(e) { + $('#music-modal').remove(); + var modalView = new App.ModalMusicView({ + model: this.model + }); + modalView.show(); + return false; + }, + /** + * modalFlickrTtrigger() + * display the attachment in the list + * @param e + * @type Object(DOM event) + * @return false + * + */ + modalFlickrTtrigger: function(e) { + var type = $(e.currentTarget).data('type'); + $('#flickr-modal').remove(); + var modalView = new App.ModalFlickrPhotoView({ + model: this.model, + type: type + }); + modalView.show(); + return false; + }, + /** + * showListModal() + * display the attachment in the list + * @param e + * @type Object(DOM event) + * @return false + * + */ + showListModal: function(e) { + var modalView = new App.ModalBoardView({ + model: this.model + }); + modalView.show(); + return false; + }, + /** + * switchListView() + * swith to the list view + * @param e + * @type Object(DOM event) + * + */ + switchListView: function() { + $('body').removeClass('modal-open'); + $('#boards-view').removeClass('col-xs-12'); + $('#switch-board-view').removeClass('calendar-view'); + $('#switch-board-view').addClass('board-viewlist col-xs-12'); + $('li.js-switch-view').removeClass('active'); + $('a.js-switch-list-view').parent().addClass('active'); + $('.js-list-form').removeClass('hide'); + var current_param = Backbone.history.fragment; + if (current_param.indexOf('/list') === -1) { + app.navigate('#/board/' + this.model.id + '/list', { + trigger: false, + trigger_function: false, + }); + } + var self = this; + $('div.js-baord-view-' + self.model.id).html(new App.SwitchToListView({ + model: self.model + }).el); + var is_card_empty = true; + var board_view = $('.js-card-list-view-' + self.model.attributes.id); + var lists = self.model.lists; + var list_length = lists.models.length; + for (var list_i = 0; list_i < list_length; list_i++) { + var list = lists.models[list_i]; + if (_.isUndefined(list.get('is_new')) && list.get('is_archived') === false) { + self.model.cards.sortByColumn('position'); + var filtered_cards = self.model.cards.where({ + list_id: list.attributes.id, + is_archived: false + }); + if (!_.isEmpty(filtered_cards)) { + is_card_empty = false; + } + var cards = new App.CardCollection(); + cards.reset(filtered_cards, { + silent: true + }); + var card_length = cards.models.length; + for (var i = 0; i < card_length; i++) { + var card = cards.models[i]; + card.list_name = _.escape(list.attributes.name); + card.list_id = list.attributes.id; + card.board_users = self.model.board_users; + card.labels.add(card.attributes.card_labels, { + silent: true + }); + card.cards.add(self.model.cards, { + silent: true + }); + card.list = list; + card.board_activities.add(self.model.activities, { + silent: true + }); + var view = new App.CardView({ + tagName: 'tr', + className: 'js-show-modal-card-view', + model: card, + template: 'list_view' + }); + board_view.append(view.render().el); + } + } + } + if (is_card_empty) { + var empty_view = new App.CardView({ + tagName: 'tr', + className: '', + model: null, + board_id: self.model.id, + template: 'list_view' + }); + empty_view.render(); + } else { + if (!_.isUndefined(card_ids) && card_ids !== null && card_ids !== '') { + _.defer(function(view) { + trigger_dockmodal = true; + var trigger_card_ids = card_ids.split(','); + for (var i = 0; i < trigger_card_ids.length; i++) { + $('#js-card-' + trigger_card_ids[i]).trigger('click'); + } + card_ids = null; + trigger_dockmodal = false; + }, this); + + } + } + return false; + }, + /** + * switchCalendarView() + * swith to the calendar view + * @param e + * @type Object(DOM event) + * + */ + switchCalendarView: function() { + $('body').removeClass('modal-open'); + $('#boards-view').addClass('col-xs-12'); + $('#switch-board-view').addClass('calendar-view'); + $('#switch-board-view').removeClass('board-viewlist col-xs-12'); + $('li.js-switch-view').removeClass('active'); + $('a.js-switch-calendar-view').parent().addClass('active'); + $('.js-list-form').removeClass('hide'); + var current_param = Backbone.history.fragment; + if (current_param.indexOf('/calendar') === -1) { + app.navigate('#/board/' + this.model.id + '/calendar', { + trigger: false, + trigger_function: false, + }); + } + $('div.js-baord-view-' + this.model.id).html(''); + $('div.js-baord-view-' + this.model.id).fullCalendar({ + header: { + left: 'prev,next today', + center: 'title', + right: 'month,basicWeek,basicDay' + }, + selectable: true, + selectHelper: true, + editable: false, + ignoreTimezone: false, + aspectRatio: 3.35 + }); + if (!_.isEmpty(this.model.cards)) { + $('div.js-baord-view-' + this.model.id).fullCalendar('addEventSource', this.model.cards.invoke('pick', ['title', 'start'])); + } + return false; + }, + /** + * showSyncGoogleCalendar() + * get sync google calender URL and display + * @param e + * @type Object(DOM event) + * + */ + showSyncGoogleCalendar: function(e) { + e.preventDefault(); + var el = this.$el; + el.find('.js-setting-response').html(new App.ShowSyncGoogleCalendarView({ + model: this.model + }).el); + }, + /** + * showCopyBoard() + * get copy board view + * @param e + * @type Object(DOM event) + * + */ + showCopyBoard: function(e) { + e.preventDefault(); + var el = this.$el; + el.find('.js-setting-response').html(new App.ShowCopyBoardView({ + model: this.model + }).el); + }, + /** + * selectGoogleSyncUrl() + * select google sync URL + * @param e + * @type Object(DOM event) + * + */ + selectGoogleSyncUrl: function(e) { + $(e.target).select(); + }, + addBoardMemberDropdown: function(e) { + e.preventDefault(); + $('.js-add-board-member-dropdown').addClass('open'); + return false; + }, + /** + * closeBoardMemberDropdown() + * copy the existing card + * @param e + * @type Object(DOM event) + * @return false + * + */ + closeBoardMemberDropdown: function(e) { + e.preventDefault(); + $('.js-add-board-member-dropdown').removeClass('open'); + return false; + }, + renderAdminBoardUsers: function() { + var admins = this.model.board_users.filter(function(normal_user) { + return normal_user.attributes.is_admin === true || normal_user.attributes.is_admin === 't'; + }); + this.model.admin_board_users = admins; + var normal_users = this.model.board_users.filter(function(normal_user) { + return normal_user.attributes.is_admin === false || normal_user.attributes.is_admin === 'f'; + }); + this.model.normal_board_users = normal_users; + this.render(); + }, + backToSidebar: function(e) { + e.preventDefault(); + $('.js-side-bar-' + this.model.id).removeClass('side-bar-large'); + var el = this.$el; + el.find('.js-back-setting-response').next().remove(); + el.find('.js-back-setting-response').after(new App.BoardSidebarView({ + model: this.model, + is_admin: this.is_admin + }).el); + this.renderBoardUsers(); + return false; + }, + renderBoardUsers: function() { + $('.js-get-board-member-lists-response').html(''); + var is_admin = false; + if (!_.isUndefined(this.authuser)) { + var admin = this.model.board_users.find(function(normal_user) { + return normal_user.attributes.user_id === authuser.user.id && normal_user.attributes.is_admin === true || normal_user.attributes.is_admin === 't'; + }); + is_admin = (!_.isEmpty(admin) || authuser.user.role_id === 1) ? true : false; + } + + var self = this; + this.model.board_users.sortBy('is_admin'); + this.model.board_users.each(function(board_user) { + is_admin = (board_user.attributes.is_admin === true || board_user.attributes.is_admin === 't') ? true : false; + self.$('.js-get-board-member-lists-response').append(new App.BoardUsersView({ + model: board_user, + is_admin: is_admin + }).el); + }); + }, + /** + * boardUserAvatarDropdown() + * copy the existing card + * @param e + * @type Object(DOM event) + * @return false + * + */ + boardUserAvatarDropdown: function(e) { + e.preventDefault(); + $(e.currentTarget).addClass('open'); + $(e.currentTarget).siblings('.dropdown').removeClass('open'); + return false; + }, + /** + * ClearBackground() + * Clear the board background image + * @param e + * @type Object(DOM event) + * @return false + * + */ + ClearBackground: function(e) { + var image = $(e.currentTarget).data('background'); + $('body').removeAttr('style'); + $('body').css('background-image', 'none'); + this.model.url = api_url + 'boards/' + this.model.id + '.json'; + this.model.set('background_picture_url', ''); + this.model.set('background_pattern_url', ''); + this.model.set('custom_background_url', ''); + this.model.set('background_color', ''); + + App.boards.get(this.model.id).set('background_picture_url', ''); + App.boards.get(this.model.id).set('background_pattern_url', ''); + App.boards.get(this.model.id).set('custom_background_url', ''); + App.boards.get(this.model.id).set('background_color', ''); + + data = { + background_color: 'NULL', + background_picture_url: 'NULL', + background_pattern_url: 'NULL', + custom_background_url: 'NULL' + }; + this.model.save(data, { + patch: true + }); + var view_my_board = $('.js-myboard-list'); + view_my_board.html(''); + if (!_.isEmpty(App.boards.models)) { + _.each(App.boards.models, function(board) { + view_my_board.append(new App.MyBoardsListingView({ + model: board, + authuser: authuser, + attributes: { + class: 'js-show-board-star' + } + }).el); + }); + } + return false; + }, + /** + * ClearMusic() + * Clear the board Music + * @param e + * @type Object(DOM event) + * @return false + * + */ + ClearMusic: function(e) { + this.model.url = api_url + 'boards/' + this.model.id + '.json'; + this.model.set('music_name', ''); + this.model.set('music_content', ''); + data = { + music_name: '', + music_content: '' + }; + this.model.save(data, { + patch: true + }); + App.music.music_content = ''; + if (!_.isUndefined(App.music.inst)) { + App.music.inst.silence(); + } + this.footerView = new App.FooterView({ + model: authuser, + }).render(); + $('#footer').html(this.footerView.el); + return false; + }, + + /** + * changeBackgroundImage() + * display the board background image + * @param e + * @type Object(DOM event) + * @return false + * + */ + changeBackgroundImage: function(e) { + var image = $(e.currentTarget).data('background'); + var image_path = 'img/board_background/medium/' + image; + $('body').removeAttr('style').css({ + 'background': 'url(' + image_path + ') 25% 25% no-repeat fixed', + 'background-size': 'cover' + }).addClass('board-view-pattern board-view'); + this.model.url = api_url + 'boards/' + this.model.id + '.json'; + this.model.set('background_picture_url', image_path); + this.model.set('background_pattern_url', ''); + this.model.set('custom_background_url', ''); + this.model.set('background_color', ''); + data = { + background_color: 'NULL', + background_picture_url: image_path, + background_pattern_url: 'NULL' + }; + this.model.save(data, { + patch: true + }); + return false; + }, + /** + * changeBackgroundPattern() + * display the board background pattern + * @param e + * @type Object(DOM event) + * @return false + * + */ + changeBackgroundPattern: function(e) { + var image = $(e.currentTarget).data('background'); + var image_path = 'img/board_background/patterns/' + image; + $('body').removeAttr('style').css({ + 'background': 'url(' + image_path + ')', + }).addClass('board-view-pattern board-view'); + this.model.url = api_url + 'boards/' + this.model.id + '.json'; + this.model.set('background_pattern_url', image_path); + this.model.set('custom_background_url', ''); + this.model.set('background_picture_url', ''); + this.model.set('background_color', ''); + data = { + background_color: 'NULL', + background_picture_url: 'NULL', + background_pattern_url: image_path + }; + this.model.save(data, { + patch: true + }); + return false; + }, + /** + * showArchivedListLists() + * display the archived lists list + * @return false + * + */ + showArchivedListLists: function() { + if (!_.isEmpty(role_links.where({ + slug: 'view_archived_lists' + }))) { + var el = this.$el; + + el.find('.js-archived-items-container').html(new App.ArchivedListsView({ + model: this.model + }).el); + var filtered_lists = this.model.lists.where({ + is_archived: true + }); + if (!_.isEmpty(filtered_lists)) { + _.each(filtered_lists, function(list) { + el.find('.js-archived-cards-container').append(new App.ArchivedListView({ + model: list + }).el); + }); + } else { + el.find('.js-archived-cards-container').append(new App.ArchivedListView({ + model: null + }).el); + } + } + return false; + }, + /** + * showFilteredArchivedListLists() + * display the filtered archived lists list + * @return false + * + */ + showFilteredArchivedListLists: function(e) { + if (!_.isEmpty(role_links.where({ + slug: 'view_archived_lists' + }))) { + var el = this.$el; + var search_q = $(e.currentTarget).val(); + var filtered_lists = ''; + if (!_.isEmpty(search_q)) { + filtered_lists = this.model.lists.filter(function(model) { + return ~model.get('name').indexOf(search_q); + }); + } else { + filtered_lists = this.model.lists.where({ + is_archived: true + }); + } + el.find('.js-archived-cards-container').html(""); + if (!_.isEmpty(filtered_lists)) { + var _i = 0; + _.each(filtered_lists, function(list) { + if (list.attributes.is_archived === true) { + el.find('.js-archived-cards-container').append(new App.ArchivedListView({ + model: list + }).el); + _i++; + } + }); + if (_i === 0) { + el.find('.js-archived-cards-container').append(new App.ArchivedListView({ + model: null + }).el); + } + } else { + el.find('.js-archived-cards-container').append(new App.ArchivedListView({ + model: null + }).el); + } + } + return false; + }, + /** + * sendCardTobard() + * send back to archived card to board + * @param e + * @type Object(DOM event) + * @return false + * + */ + sendCardTobard: function(e) { + e.preventDefault(); + var card_id = $(e.currentTarget).data('card_id'); + this.model.cards.findWhere({ + id: parseInt(card_id) + }).set('is_archived', false); + var find_card = this.model.cards.findWhere({ + id: parseInt(card_id) + }); + this.model.cards.get(find_card.id).set('is_archived', false); + $(e.currentTarget).parents('li').remove(); + var card = new App.Card(); + card.set('id', card_id); + card.set('is_archived', false); + card.url = api_url + 'boards/' + this.model.attributes.id + '/lists/' + find_card.attributes.list_id + '/cards/' + card_id + '.json'; + card.save({ + success: function(model, response) {} + }); + return false; + }, + /** + * showSearchBoardMembers() + * display searched member list + */ + showSearchBoardMembers: function(e) { + if (e.which === 13) { + return false; + } + var self = this; + var q = $('#inputBoardUserSearch').val(); + var users = new App.UserCollection(); + users.url = api_url + 'users/search.json'; + users.fetch({ + data: { + q: q + }, + success: function() { + $('.js-board-member-search-response').html(''); + var is_user_empty = true; + _.each(users.models, function(user) { + var is_already_added = self.model.board_users.where({ + user_id: parseInt(user.id) + }); + if (_.isEmpty(is_already_added)) { + is_user_empty = false; + $('.js-board-member-search-response').append(new App.BoardMemberAddSearchResultView({ + model: user, + board: self.model + }).el); + } + + + }); + if (users.models.length === 0 || is_user_empty) { + $('.js-board-member-search-response').html(new App.BoardMemberAddSearchResultView({ + model: null, + className: 'small', + }).el); + } + + } + }); + + }, + /** + * switchGridView() + * switch to grid view + * @param e + * @type Object(DOM event) + * + */ + switchGridView: function(e) { + $('body').addClass('modal-open'); + e.preventDefault(); + app.navigate('#/board/' + this.model.id, { + trigger: false, + trigger_function: false, + }); + $('#content').html(new App.BoardView({ + model: this.model + }).el); + this.board_view_height(); + }, + /** + * boardRename() + * Edit the board name + * @param e + * @type Object(DOM event) + * @return false + * + */ + boardRename: function(e) { + e.preventDefault(); + var el = this.$el; + var data = el.find('form#BoardRenameForm').serializeObject(); + var board = new App.Board(); + this.model.set(data); + App.boards.get(this.model.attributes.id).set(data); + board.set(data); + board.set('id', this.model.id); + board.url = api_url + 'boards/' + this.model.id + '.json'; + board.save(data, { + patch: true + }); + $('.js-rename-board').html("" + _.escape(data.name) + ""); + this.closeSpanPopover(e); + return false; + }, + /** + * closeSpanPopover() + * close popup + * @param e + * @type Object(DOM event) + * @return false + * + */ + closeSpanPopover: function(e) { + var el = this.$el; + var target = el.find(e.target); + target.parents('span.dropdown').removeClass('open'); + return false; + }, + /** + * saveBoardVisibility() + * change the board visibility + * @param e + * @type Object(DOM event) + * @return false + * + */ + saveBoardVisibility: function(e) { + e.preventDefault(); + var target = $(e.target); + data = target.serializeObject(); + data.board_visibility = 1; + + var organizations = authuser.user.organizations; + var org = organizations.findWhere({ + id: parseInt(data.organization_id) + }); + this.model.set('organization_name', _.escape(org.attributes.name)); + this.model.set('organization_logo_url', _.escape(org.attributes.organization_logo_url)); + this.model.set('board_visibility', 1); + this.model.set('organization_id', parseInt(data.organization_id)); + + + $('.js-sidebar-board-visibility').html('Change Visibility'); + var board = new App.Board(); + this.model.url = api_url + 'boards/' + this.model.attributes.id + '.json'; + + this.closePopup(e); + this.model.save(data, { + patch: true + }); + target.parents('div.dropdown').removeClass('open'); + return false; + }, + /** + * showChangeOrganizationForm() + * show board organiztion change form + * @param e + * @type Object(DOM event) + * + */ + showChangeOrganizationForm: function(e) { + var target = $(e.target); + var parent = target.parents('.js-visibility-list-dropdown'); + var visibility = this.model.attributes.board_visibility; + var insert = $('.js-visibility-list', parent); + insert.nextAll().remove(); + $(new App.BoardOrganizationFormView({ + model: authuser.user.organizations, + board: this.model + }).el).insertAfter(insert); + }, + /** + * changeBackgroundColor() + * display the board background color + * @param e + * @type Object(DOM event) + * @return false + * + */ + changeBackgroundColor: function(e) { + var color = $(e.currentTarget).data('color'); + $('body').removeAttr('style').css('background', color).addClass('board-view'); + this.model.url = api_url + 'boards/' + this.model.id + '.json'; + this.model.set('background_color', color); + this.model.set('background_picture_url', ''); + this.model.set('background_pattern_url', ''); + this.model.set('custom_background_url', ''); + data = { + background_color: color, + background_picture_url: null, + background_pattern_url: null + }; + this.model.save(data, { + patch: true, + }); + return false; + }, + /** + * sendListToboard() + * send back to archived list to board + * @param e + * @type Object(DOM event) + * @return false + * + */ + sendListToboard: function(e) { + e.preventDefault(); + var list_id = $(e.currentTarget).data('list_id'); + var view_list = this.$('#js-board-lists'); + this.model.lists.findWhere({ + id: parseInt(list_id) + }).set('is_archived', false); + var find_list = this.model.lists.findWhere({ + id: parseInt(list_id) + }); + this.model.lists.get(find_list.id).set('is_archived', false); + App.boards.get(find_list.attributes.board_id).lists.get(find_list.id).set('is_archived', false); + $(e.currentTarget).parents('li').remove(); + var list = new App.List(); + list.set('id', list_id); + list.set('is_archived', false); + list.url = api_url + 'boards/' + this.model.attributes.id + '/lists/' + list_id + '.json'; + list.save({ + success: function(model, response) {} + }); + return false; + }, + /** + * toggleAdditionalSettings() + * toggle thr label filter list + * @param e + * @type Object(DOM event) + * + */ + toggleAdditionalSettings: function(e) { + var target = $(e.currentTarget); + if (target.hasClass('js-AdditionalSettings-enabled')) { + data = { + 'is_show_image_front_of_card': false + }; + $('div.js-card-attachment-image').addClass('hide'); + $('.js-AdditionalSettings-enabled').addClass('hide'); + $('.js-AdditionalSettings-enable').removeClass('hide'); + this.model.set('is_show_image_front_of_card', false); + } else { + data = { + 'is_show_image_front_of_card': true + }; + $('div.js-card-attachment-image').removeClass('hide'); + $('.js-AdditionalSettings-enabled').removeClass('hide'); + $('.js-AdditionalSettings-enable').addClass('hide'); + this.model.set('is_show_image_front_of_card', true); + } + var board = new App.Board(); + board.url = api_url + 'boards/' + this.model.attributes.id + '.json'; + board.set('id', this.model.attributes.id); + board.save(data); + return false; + }, + /** + * toggleLabelFilter() + * toggle thr label filter list + * @param e + * @type Object(DOM event) + * + */ + toggleLabelFilter: function(e) { + var target = $(e.currentTarget); + target.toggleClass('selected', !target.hasClass('selected')); + this.labelFilter(); + return false; + }, + /** + * labelFilter() + * toggle thr label filter list + * @param e + * @type Object(DOM event) + * + */ + labelFilter: function(e) { + var contain = ''; + var not_contain = ''; + var selected_label = ''; + $('i.js-filter-icon').remove(); + $('.js-due-filte').removeClass('selected'); + $('.js-toggle-member-filter').removeClass('selected'); + //$('i', $('li:not(.selected)', $('ul.js-board-labels'))).remove(); + if ($('li.selected', $('ul.js-board-labels')).length === 0) { + $('ul.js-card-labels').parents('div.js-board-list-card').show(); + $('ul.js-card-labels').parents('tr.js-show-modal-card-view').show(); + } + $('li.selected > div.js-label', $('ul.js-board-labels')).each(function() { + not_contain += ':not(:contains(' + $(this).html() + '))'; + contain += 'ul.js-card-labels:contains(' + $(this).html() + '), '; + if ($(this).next('i').length === 0) { + $(this).after(''); + } + }); + contain = contain.substring(0, contain.lastIndexOf(', ')); + if (!_.isEmpty(not_contain)) { + $(contain).parents('div.js-board-list-card').show(); + $(contain).parents('tr.js-show-modal-card-view').show(); + $('ul.js-card-labels' + not_contain).parents('div.js-board-list-card').hide(); + $('ul.js-card-labels' + not_contain).parents('tr.js-show-modal-card-view').hide(); + } + }, + /** + * toggleMemberFilter() + * toggle thr member filter list + * @param e + * @type Object(DOM event) + * + */ + toggleMemberFilter: function(e) { + var target = $(e.currentTarget); + target.toggleClass('selected', !target.hasClass('selected')); + this.memberFilter(); + return false; + }, + /** + * memberFilter() + * toggle thr member filter list + * @param e + * @type Object(DOM event) + * + */ + memberFilter: function(e) { + var contain = ''; + var not_contain = ''; + var selected_label = ''; + $('i.js-filter-icon').remove(); + $('.js-due-filte').removeClass('selected'); + $('.js-toggle-label-filter').removeClass('selected'); + //$('i', $('li:not(.selected)', $('ul.js-board-users'))).remove(); + if ($('li.selected', $('ul.js-board-users')).length === 0) { + $('ul.js-card-users').parents('div.js-board-list-card').show(); + $('ul.js-card-users').parents('tr.js-show-modal-card-view').show(); + } + $('li.selected > span.js-user', $('ul.js-board-users')).each(function() { + not_contain += ':not(:contains(' + $(this).html() + '))'; + contain += 'ul.js-card-users:contains(' + $(this).html() + '), '; + if ($(this).next('i').length === 0) { + $(this).after(''); + } + }); + contain = contain.substring(0, contain.lastIndexOf(', ')); + if (!_.isEmpty(not_contain)) { + $(contain).parents('div.js-board-list-card').show(); + $(contain).parents('tr.js-show-modal-card-view').show(); + $('ul.js-card-users' + not_contain).parents('div.js-board-list-card').hide(); + $('ul.js-card-users' + not_contain).parents('tr.js-show-modal-card-view').hide(); + } + }, + /** + * toggleDueFilter() + * filter board cards by due date + * @param e + * @type Object(DOM event) + * + */ + toggleDueFilter: function(e) { + var target = $(e.currentTarget); + target.toggleClass('selected', !target.hasClass('selected')); + this.dueFilter(); + return false; + }, + /** + * dueFilter() + * filter board cards by due date + * @param e + * @type Object(DOM event) + * + */ + dueFilter: function(e) { + var contain = ''; + var not_contain = ''; + var selected_label = ''; + $('i.js-filter-icon').remove(); + $('.js-toggle-member-filter').removeClass('selected'); + $('.js-toggle-label-filter').removeClass('selected'); + if ($('li.selected', $('ul.js-board-dues')).length === 0) { + $('ul.js-card-due').parents('div.js-board-list-card').show(); + $('ul.js-card-due').parents('tr.js-show-modal-card-view').show(); + } + $('li.selected > span.js-due', $('ul.js-board-dues')).each(function() { + not_contain += ':not(:contains(' + $(this).html() + '))'; + contain += 'ul.js-card-due:contains(' + $(this).html() + '), '; + if ($(this).next('i').length === 0) { + $(this).after(''); + } + }); + contain = contain.substring(0, contain.lastIndexOf(', ')); + if (!_.isEmpty(not_contain)) { + $(contain).parents('div.js-board-list-card').show(); + $(contain).parents('tr.js-show-modal-card-view').show(); + $('ul.js-card-due' + not_contain).parents('div.js-board-list-card').hide(); + $('ul.js-card-due' + not_contain).parents('tr.js-show-modal-card-view').hide(); + } + }, + /** + * computerOpenBoardBackground() + * trigger file upload + * @param e + * @type Object(DOM event) + * @return false + */ + computerOpenBoardBackground: function(e) { + var fileLi = $(e.target); + $('#js-custom-background-attachment').remove(); + var form = $('#js-board-custom-background-form'); + $(form).append(''); + $('#js-custom-background-attachment', form).trigger('click'); + return false; + }, + /** + * addBoardBackground() + * add card attachment + * @param e + * @type Object(DOM event) + */ + addBoardBackground: function(e) { + e.preventDefault(); + var self = this; + $('#custom-dropzone-cssloader').addClass('cssloader'); + var form = $('#js-board-custom-background-form'); + var target = $(e.target); + var fileData = new FormData(form[0]); + this.model.url = api_url + 'boards/' + this.model.id + '/custom_backgrounds.json'; + this.model.save(fileData, { + type: 'POST', + data: fileData, + processData: false, + cache: false, + contentType: false, + success: function(model, response) { + $('#custom-dropzone-cssloader').removeClass('cssloader'); + self.model.set({ + background_picture_url: '' + }, { + silent: true + }); + self.model.set({ + background_picture_url: response.background_picture_url + '?' + Date.now() + }); + } + }); + }, + noAction: function(e) { + e.preventDefault(); + return false; + }, + /** + * setPrivteBoard() + * change the board visibility as privte + * @param e + * @type Object(DOM event) + * @return false + * + */ + setPrivteBoard: function(e) { + e.preventDefault(); + this.model.url = api_url + 'boards/' + this.model.attributes.id + '.json'; + this.model.set({ + board_visibility: 0, + organization_id: 0 + }); + this.closePopup(e); + this.model.save({ + board_visibility: 0, + organization_id: 0 + }, { + patch: true + }); + var target = $(e.target); + target.parents('div.dropdown').removeClass('open'); + return false; + }, + /** + * setPublicBoard() + * change the board visibility as public + * @param e + * @type Object(DOM event) + * @return false + * + */ + setPublicBoard: function(e) { + e.preventDefault(); + this.model.url = api_url + 'boards/' + this.model.attributes.id + '.json'; + this.model.set({ + board_visibility: 2, + organization_id: 0 + }); + this.closePopup(e); + this.model.save({ + board_visibility: 2, + organization_id: 0 + }, { + patch: true + }); + var target = $(e.target); + target.parents('div.dropdown').removeClass('open'); + return false; + }, + /** + * showBoardOrganization() + * change the board visibility as organization + * @param e + * @type Object(DOM event) + * @return false + * + */ + showBoardOrganization: function(e) { + e.preventDefault(); + this.$('.js-back-to-board-visibility').removeClass('hide'); + this.showChangeOrganizationForm(e); + return false; + }, + /** + * selectBoardVisibility() + * change the board visibility + * @param e + * @type Object(DOM event) + * @return false + * + */ + selectBoardVisibility: function(e) { + var name = $(e.currentTarget).attr('name'); + var value = 0; + var content = 'Private'; + $('#js-board-add-organization').html(''); + if (name == 'org') { + value = 1; + content = 'Organization'; + $('#js-change-visible-content').html(content); + this.showBoardAddeOrganizationForm(e); + } else if (name == 'public') { + content = 'Public'; + value = 2; + } + + $('#inputBoardCopyVisibility').val(value); + $('#js-change-visible-content').html(content); + $('.js-visibility-chooser-copy-board-dropdown').removeClass('open'); + $('.js-visibility-chooser-copy-board').nextAll().remove(); + return false; + }, + /** + * showChangeOrganizationForm() + * show board organiztion change form + * @param e + * @type Object(DOM event) + * + */ + showBoardAddeOrganizationForm: function(e) { + e.preventDefault(); + var organizations = new App.OrganizationCollection(); + organizations = authuser.user.organizations; + if (authuser.user.organizations !== null && _.isUndefined(authuser.user.organizations.models)) { + organizations.add(JSON.parse(authuser.user.organizations)); + } + authuser.user.organizations = organizations; + $('#js-board-add-organization').html(new App.BoardAddOrganizationFormView({ + model: authuser.user.organizations + }).el); + } +}); diff --git a/client/js/views/board_index_header_view.js b/client/js/views/board_index_header_view.js new file mode 100644 index 000000000..9e909eb5e --- /dev/null +++ b/client/js/views/board_index_header_view.js @@ -0,0 +1,68 @@ +/** + * @fileOverview This file has functions related to board index header view. This view calling from apllication view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : user model + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Header View + * @class HeaderView + * @constructor + * @extends Backbone.View + */ +App.BoardIndexHeaderView = Backbone.View.extend({ + template: JST['templates/board_index_header'], + className: 'navbar navbar-default', + id: 'js-navbar-default', + attributes: { + role: 'navigation' + }, + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-close-popover': 'closePopup', + }, + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + page_title: this.model, + })); + this.showTooltip(); + return this; + }, + + /** + * closePopup() + * close opened dropdown + * @param e + * @type Object(DOM event) + * @return false + * + */ + closePopup: function(e) { + var target = $(e.target); + target.parents('li.dropdown').removeClass('open'); + return false; + } +}); diff --git a/client/js/views/board_member_add_search_result_view.js b/client/js/views/board_member_add_search_result_view.js new file mode 100644 index 000000000..83a16682c --- /dev/null +++ b/client/js/views/board_member_add_search_result_view.js @@ -0,0 +1,70 @@ +/** + * @fileOverview This file has functions related to board member add search result view. This view calling from board header view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board model. It contain all board based object @see Available Object in App.BoardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * BoardMemberAddSearchResult View + * @class BoardMemberAddSearchResultView + * @constructor + * @extends Backbone.View + */ +App.BoardMemberAddSearchResultView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.board = options.board; + this.render(); + }, + template: JST['templates/board_member_add_search_result'], + tagName: 'li', + className: 'js-add-board-member clearfix', + events: { + 'click': 'addBoardMember', + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + user: this.model + })); + this.showTooltip(); + return this; + }, + addBoardMember: function() { + var board_user = new App.BoardsUser(); + var self = this; + board_user.set('board_id', this.board.attributes.id); + board_user.set('user_id', this.model.attributes.id); + board_user.set('is_admin', false); + board_user.set(this.model.toJSON()); + delete board_user.attributes.id; + this.$el.remove(); + board_user.url = api_url + 'boards/' + this.board.attributes.id + '/users.json'; + board_user.save({ + user_id: this.model.attributes.id, + board_id: this.board.attributes.id, + is_admin: false + }, { + success: function(model, response) { + board_user.set(response.boards_users); + self.board.board_users.add(board_user); + } + }); + return false; + } +}); diff --git a/client/js/views/board_organization_form_view.js b/client/js/views/board_organization_form_view.js new file mode 100644 index 000000000..4e845e92e --- /dev/null +++ b/client/js/views/board_organization_form_view.js @@ -0,0 +1,48 @@ +/** + * @fileOverview This file has functions related to board organization form view. + * This view calling from board header view, board simple view, board organization foem view and orgaiztion board view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : organizations collection + * this.board : board model. It contain all board based object @see Available Object in App.BoardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * BoardOrganizationForm View + * @class BoardOrganizationFormView + * @constructor + * @extends Backbone.View + */ +App.BoardOrganizationFormView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + this.board = options.board; + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/board_organization_form'], + tagName: 'li', + className: 'col-xs-12', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + organizations: this.model, + board: this.board + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/board_sidebar_view.js b/client/js/views/board_sidebar_view.js new file mode 100644 index 000000000..f33dc4ce4 --- /dev/null +++ b/client/js/views/board_sidebar_view.js @@ -0,0 +1,46 @@ +/** + * @fileOverview This file has functions related to board sidebar view. This view calling from apllication view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board model. It contain all board based object @see Available Object in App.BoardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * BoardSidebar View + * @class BoardSidebarView + * @constructor + * @extends Backbone.View + */ +App.BoardSidebarView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.is_admin = options.is_admin; + this.render(); + }, + template: JST['templates/board_sidebar'], + tagName: 'ul', + className: 'dropdown-menu arrow arrow-right col-xs-12 js-setting-response', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + board: this.model, + is_admin: this.is_admin + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/board_simple_view.js b/client/js/views/board_simple_view.js new file mode 100644 index 000000000..d99bd5f99 --- /dev/null +++ b/client/js/views/board_simple_view.js @@ -0,0 +1,341 @@ +/** + * @fileOverview This file has functions related to board simple view. This view calling from apllication view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : boards collection + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * BoardSimple View + * @class BoardSimpleView + * @constructor + * @extends Backbone.View + */ +App.BoardSimpleView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.message = options.message; + this.starred_boards = options.starred_boards; + _.bindAll(this, 'render'); + if (this.model !== null) { + this.model.collection.bind('change', this.render); + this.model.collection.bind('add', this.render); + this.model.collection.bind('remove', this.render); + } + App.boards.bind('change', this.render); + this.render(); + }, + template: JST['templates/board_simple_view'], + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-close-popover': 'closePopup', + 'click .js-star-board': 'starBoard', + 'click .js-board-inner-view': 'showBoard', + 'click .js-board-visibility': 'showBoardVisibility', + 'click .js-set-privte-board': 'setPrivteBoard', + 'click .js-set-public-board': 'setPublicBoard', + 'click .js-show-board-organization': 'showBoardOrganization', + 'submit .js-save-board-visibility': 'saveBoardVisibility', + 'click .js-close-span-popover': 'closeSpanPopover', + 'click .js-back-to-board-visibility': 'showBoardVisibility', + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + if (this.model !== null && this.model.attributes.users !== null) { + for (i = 0; i < this.model.attributes.users.length; i++) { + if (authuser.user.id == this.model.attributes.users[i].user_id) { + this.model.attributes.is_admin = (this.model.attributes.users[i].is_admin === true || this.model.attributes.users[i].is_admin === 't') ? true : false; + } + } + } + this.$el.html(this.template({ + board: this.model, + message: this.message, + starred_boards: this.starred_boards + })); + if (this.model !== null) { + this.model.lists.sortByColumn('position'); + var data = []; + var color_codes = ['#DB7093', '#F47564', '#EDA287', '#FAC1AD', '#FFE4E1', '#D3ABF0', '#DC9CDC', '#69BFBA', '#66CDAA', '#8FBC8F', '#CBFDCA', '#EEE8AA', '#BC8F8F', '#CD853F', '#D2B48C', '#F5DEB3', '#64BCF2', '#87CEFA', '#B0C4DE', '#D6E2F7']; + var i = 0; + this.model.lists.each(function(list) { + var _data = {}; + _data.title = list.attributes.name; + _data.value = list.attributes.card_count; + _data.color = color_codes[i]; + i++; + if (i > 20) { + i = 0; + } + if (list.attributes.card_count > 0) { + data.push(_data); + } + }); + var _this = this; + _(function() { + _this.$el.find('.js-chart').html('').drawDoughnutChart(data); + }).defer(); + } + this.showTooltip(); + return this; + }, + /** + * closeSpanPopover() + * close popup + * @param e + * @type Object(DOM event) + * @return false + * + */ + closeSpanPopover: function(e) { + $('.js-close-span-popover').parents('span.dropdown').removeClass('open'); + return false; + }, + /** + * starBoard() + * subcribe the board + * @param e + * @type Object(DOM event) + * @return false + * + */ + starBoard: function(e) { + e.preventDefault(); + var name = $(e.currentTarget).attr('name'); + var value = 'unstar'; + var is_starred = true; + var self = this; + var content = ''; + if (name == 'unstar') { + value = 'star'; + is_starred = false; + content = ''; + } + $(e.currentTarget).attr('name', value); + $(e.currentTarget).html(content); + self.boardStar = new App.BoardStar(); + var subscribe_data = {}; + self.boardStar.url = api_url + 'boards/' + this.model.id + '/boards_stars.json'; + self.boardStar.set('board_id', this.model.attributes.id); + self.boardStar.set('user_id', parseInt(authuser.user.id)); + self.boardStar.set('is_starred', is_starred); + if (!is_starred) { + this.starred_boards.splice($.inArray(parseInt(self.model.id), this.starred_boards), 1); + self.model.set('is_starred', false); + $('#js-starred-board-' + self.model.id).remove(); + if (this.starred_boards.length === 0 || $('.js-header-starred-boards > .js-board-view').length === 0) { + $('.js-header-starred-boards').append(new App.BoardSimpleView({ + model: null, + message: 'No starred boards available.', + id: 'js-starred-board-empty', + className: 'col-lg-3 col-md-3 col-sm-4 col-xs-12 media-list' + }).el); + } + + } else { + if (!_.isUndefined(this.starred_boards)) { + this.starred_boards.push(self.model.id); + } + self.model.set('is_starred', true); + if (this.starred_boards.length !== 0) { + $('#js-starred-board-empty').remove(); + } + $('.js-header-starred-boards').append(new App.BoardSimpleView({ + model: self.model, + id: 'js-starred-board-' + self.model.attributes.id, + className: 'col-lg-3 col-md-3 col-sm-4 col-xs-12 media-list js-board-view js-board-view-' + self.model.attributes.id, + starred_boards: self.starred_boards + }).el); + } + self.boardStar.save(subscribe_data, { + success: function(model, response) { + App.boards.get(self.model.attributes.id).boards_stars.reset(self.boardStar); + self.model.boards_stars.add(self.boardStar); + self.footerView = new App.FooterView({ + model: authuser + }).renderStarredBoards(); + } + }); + return false; + }, + /** + * showBoardVisibility() + * render the board visibility + * @param e + * @type Object(DOM event) + * + */ + showBoardVisibility: function(e) { + var target = $(e.currentTarget); + this.$('.js-back-to-board-visibility').addClass('hide'); + var visibility = this.model.attributes.board_visibility; + var insert = $('.js-visibility-list', target.next('.dropdown-menu')); + insert.nextAll().remove(); + $(new App.ShowBoardVisibilityView({ + model: visibility + }).el).insertAfter(insert); + }, + /** + * closePopup() + * close the opend dropdown + * @param e + * @type Object(DOM event) + * @return false + * + */ + closePopup: function(e) { + var el = this.$el; + var target = el.find(e.target); + target.parents('.dropdown').removeClass('open'); + return false; + }, + /** + * showBoard() + * render board view + * @param e + * @type Object(DOM event) + * + */ + showBoard: function(e) { + e.preventDefault(); + this.$el.removeClass('col-lg-3 col-md-3 col-sm-4').html(new App.BoardView({ + model: this.model + }).el); + }, + /** + * showChangeOrganizationForm() + * show board organiztion change form + * @param e + * @type Object(DOM event) + * + */ + showChangeOrganizationForm: function(e) { + var target = $(e.currentTarget); + var parent = target.parents('.dropdown-menu'); + var visibility = this.model.attributes.board_visibility; + var insert = $('.js-visibility-list', parent); + insert.nextAll().remove(); + $(new App.BoardOrganizationFormView({ + model: authuser.user.organizations, + board: this.model + }).el).insertAfter(insert); + }, + /** + * setPrivteBoard() + * change the board visibility as privte + * @param e + * @type Object(DOM event) + * @return false + * + */ + setPrivteBoard: function(e) { + e.preventDefault(); + this.model.url = api_url + 'boards/' + this.model.attributes.id + '.json'; + this.model.set({ + board_visibility: 0, + organization_id: 0 + }); + this.closePopup(e); + this.model.save({ + board_visibility: 0, + organization_id: 0 + }, { + patch: true + }); + var target = $(e.target); + target.parents('div.dropdown').removeClass('open'); + return false; + }, + /** + * setPublicBoard() + * change the board visibility as public + * @param e + * @type Object(DOM event) + * @return false + * + */ + setPublicBoard: function(e) { + e.preventDefault(); + this.model.url = api_url + 'boards/' + this.model.attributes.id + '.json'; + this.model.set({ + board_visibility: 2, + organization_id: 0 + }); + this.closePopup(e); + this.model.save({ + board_visibility: 2, + organization_id: 0 + }, { + patch: true + }); + var target = $(e.target); + target.parents('div.dropdown').removeClass('open'); + return false; + }, + /** + * showBoardOrganization() + * change the board visibility as organization + * @param e + * @type Object(DOM event) + * @return false + * + */ + showBoardOrganization: function(e) { + e.preventDefault(); + this.$('.js-back-to-board-visibility').removeClass('hide'); + this.showChangeOrganizationForm(e); + return false; + }, + /** + * saveBoardVisibility() + * change the board visibility + * @param e + * @type Object(DOM event) + * @return false + * + */ + saveBoardVisibility: function(e) { + e.preventDefault(); + var target = $(e.target); + data = target.serializeObject(); + data.board_visibility = 1; + + var organizations = authuser.user.organizations; + var org = organizations.findWhere({ + id: parseInt(data.organization_id) + }); + this.model.set('organization_name', _.escape(org.attributes.name)); + this.model.set('organization_logo_url', _.escape(org.attributes.organization_logo_url)); + this.model.set('board_visibility', 1); + this.model.set('organization_id', parseInt(data.organization_id)); + + + $('.js-sidebar-board-visibility').html('Change Visibility'); + var board = new App.Board(); + this.model.url = api_url + 'boards/' + this.model.attributes.id + '.json'; + + this.closePopup(e); + this.model.save(data, { + patch: true + }); + target.parents('div.dropdown').removeClass('open'); + return false; + } +}); diff --git a/client/js/views/board_user_actions_view.js b/client/js/views/board_user_actions_view.js new file mode 100644 index 000000000..2b192bd6c --- /dev/null +++ b/client/js/views/board_user_actions_view.js @@ -0,0 +1,158 @@ +/** + * @fileOverview This file has functions related to board user actions view. This view calling from board user view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board user model + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * CardSearchResult View + * @class CardSearchResultView + * @constructor + * @extends Backbone.View + */ +App.BoardUserActionsView = Backbone.View.extend({ + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-view-user-activities': 'showUserActivitiesListModal', + 'click .js-show-dropdown': 'showDropdown', + 'click .js-no-action': 'noAction', + 'click .js-edit-board-member-permission-to-admin': 'editBoardMemberPermissionToAdmin', + 'click .js-edit-board-member-permission-to-normal': 'editBoardMemberPermissionToNormal', + 'click .js-close-popup': 'closePopup', + }, + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.is_admin = options.is_admin; + this.render(); + }, + template: JST['templates/board_user_actions'], + tagName: 'ul', + className: 'list-unstyled', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + var is_admin; + if (this.model !== null && this.model.collection !== null && this.model.collection.models !== null) { + for (i = 0; i < this.model.collection.models.length; i++) { + if (this.model.collection.models[i].attributes.user_id == authuser.user.id) { + is_admin = (this.model.collection.models[i].attributes.is_admin === true || this.model.collection.models[i].attributes.is_admin === 't') ? true : false; + } + } + } + this.$el.html(this.template({ + user: this.model, + is_admin: is_admin + })); + this.showTooltip(); + return this; + }, + /** + * showUserActivitiesListModal() + * display list attachments + * @param e + * @type Object(DOM event) + * @return false + * + */ + showUserActivitiesListModal: function(e) { + var user_id = $(e.currentTarget).data('user-id'); + var modalView = new App.ModalUserActivitiesListView({ + model: this.model, + user_id: user_id + }); + modalView.show(); + return false; + }, + /** + * showDropdown() + * show Dropdown + * @param e + * @type Object(DOM event) + * @return false + * + */ + showDropdown: function(e) { + e.preventDefault(); + $(e.currentTarget).parents('.dropdown:first').addClass('open'); + return false; + }, + noAction: function(e) { + e.preventDefault(); + return false; + }, + /** + * editBoardMemberPermissionToAdmin() + * change board member permission to admin + * @param e + * @type Object(DOM event) + * @return false + */ + editBoardMemberPermissionToAdmin: function(e) { + var self = this; + var target = $(e.currentTarget); + this.model.url = api_url + 'boards_users/' + this.model.attributes.id + '.json'; + this.model.set('is_admin', true); + this.model.save({ + is_admin: true + }, { + success: function(model, response) { + + } + }); + + return false; + }, + /** + * editBoardMemberPermissionToNoraml() + * change board member permission to noraml + * @param e + * @type Object(DOM event) + * @return false + */ + editBoardMemberPermissionToNormal: function(e) { + var self = this; + var target = $(e.currentTarget); + this.model.url = api_url + 'boards_users/' + this.model.attributes.id + '.json'; + this.model.set('is_admin', false); + this.model.save({ + is_admin: false + }, { + success: function(model, response) { + + } + }); + + return false; + }, + /** + * boardRename() + * close the dropdown + * @param e + * @type Object(DOM event) + * @return false + * + */ + closePopup: function(e) { + var el = this.$el; + var target = el.find(e.target); + target.parents('div.dropdown:first, li.dropdown:first').removeClass('open'); + return false; + } +}); diff --git a/client/js/views/board_user_activity_view.js b/client/js/views/board_user_activity_view.js new file mode 100644 index 000000000..e198f7042 --- /dev/null +++ b/client/js/views/board_user_activity_view.js @@ -0,0 +1,43 @@ +/** + * @fileOverview This file has functions related to board user activity view. This view calling from board user view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board model. It contain all board based object @see Available Object in App.BoardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * BoardUserActivity View + * @class BoardUserActivityView + * @constructor + * @extends Backbone.View + */ +App.BoardUserActivityView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/board_user_activity'], + tagName: 'div', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + board: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/board_user_remove_confirm_view.js b/client/js/views/board_user_remove_confirm_view.js new file mode 100644 index 000000000..609c1bc6d --- /dev/null +++ b/client/js/views/board_user_remove_confirm_view.js @@ -0,0 +1,43 @@ +/** + * @fileOverview This file has functions related to board user delete view. This view calling from board user view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board user model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * BoardUserRemoveConfirm View + * @class BoardUserRemoveConfirmView + * @constructor + * @extends Backbone.View + */ +App.BoardUserRemoveConfirmView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/board_user_remove_confirm'], + tagName: 'div', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + board_user: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/board_view.js b/client/js/views/board_view.js new file mode 100644 index 000000000..1e1136479 --- /dev/null +++ b/client/js/views/board_view.js @@ -0,0 +1,1154 @@ +/** +/** + * @fileOverview This file has functions related to board view. This view calling from application view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board model and it's related values + * this.model.activities : activities collection(Based on board) + * this.model.attachments : attachments collection(Based on board) + * this.model.board_stars : starred board collection(Based on logged in user) + * this.model.board_users : board user collection(Based on board) + * this.model.boards_subscribers : board user collection(Based on board) + * this.model.cards : cards collection(Based on board) + * this.model.checklists : checklists collection(Based on card) + * this.model.checklist_items : checklist items collection(Based on checklist) + * this.model.custom_attachments : custom attachments collection(Based on board) + * this.model.lists : lists collection(Based on board) + * this.model.labels : labels collection(Based on board) + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Board View + * @class BoardView + * @constructor + * @extends Backbone.View + */ +App.BoardView = Backbone.View.extend({ + tagName: 'section', + className: 'clearfix', + id: 'boards-view', + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + var self = this; + this.authuser = authuser.user; + if (_.isUndefined(window.sessionStorage.getItem('music_play'))) { + window.sessionStorage.setItem('music_play', "1"); + } + this.model.attachments.add(this.model.get('attachments')); + _.bindAll(this, 'render', 'renderListsCollection', 'renderActivitiesCollection', 'setBoardBackground', 'populateChecklistItems'); + this.model.bind('change:name change:is_closed', this.render); + this.model.bind('change:board_visibility', this.render); + this.model.bind('change:background_color change:background_picture_url change:background_pattern_url', this.setBoardBackground); + this.model.bind('change:music_content', this.musical); + this.model.lists.bind('add', this.renderListsCollection); + this.model.lists.bind('change:position', this.renderListsCollection); + this.model.lists.bind('change:is_archived', this.renderListsCollection, this); + this.model.activities.bind('add', this.renderActivitiesCollection); + this.model.checklists.bind('add', this.populateChecklistItems); + this.model.board_users.bind('add', this.render); + this.model.board_users.bind('remove', this.render); + this.model.board_users.bind('change', this.render); + if (!_.isUndefined(App.music)) { + App.music.inst = new Instrument(); + } + if (this.model.attributes.music_content !== undefined && this.model.attributes.music_content !== "") { + App.music.music_content = this.model.attributes.music_content; + App.music.music_name = this.model.attributes.music_name; + } + this.populateLists(); + this.populateCards(); + this.populateChecklists(); + this.populateChecklistItems(); + this.populateLabels(); + this.populateActivities(); + this.populateUsers(); + this.populateCustomAttachments(); + this.populateAttachments(); + this.populateSubscribers(); + this.populateStars(); + this.render(); + }, + // Resets this boards lists collection + populateLists: function() { + var lists = this.model.get('lists') || []; + this.model.lists.reset(lists, { + silent: true + }); + }, + // Resets this boards activities collection + populateActivities: function() { + var activities = this.model.get('activities') || []; + this.model.activities.reset(activities, { + silent: true + }); + }, + // Resets this boards users collection + populateUsers: function() { + var board_users = this.model.get('boards_users') || []; + this.model.board_users.reset(board_users, { + silent: true + }); + }, + // Resets this boards custom attachments collection + populateCustomAttachments: function() { + var custom_attachments = this.model.get('custom_backgrounds') || []; + this.model.custom_attachments.reset(custom_attachments, { + silent: true + }); + }, + // Resets this boards attachments collection + populateAttachments: function() { + var attachments = this.model.get('attachments') || []; + this.model.attachments.reset(attachments, { + silent: true + }); + }, + // Resets this boards subscribers collection + populateSubscribers: function() { + var boards_subscribers = this.model.get('boards_subscribers') || []; + this.model.board_subscribers.add(boards_subscribers, { + silent: true + }); + }, + // Resets this boards stars collection + populateStars: function() { + var boards_stars = this.model.get('boards_stars') || []; + this.model.board_stars.add(boards_stars, { + silent: true + }); + }, + // Resets this boards cards collection + populateCards: function() { + var self = this; + self.model.lists.each(function(list) { + var cards = list.get('cards') || []; + if (!_.isEmpty(cards)) { + self.model.cards.add(cards, { + silent: true + }); + } + }); + }, + // Resets this checklists collection + populateChecklists: function() { + var self = this; + self.model.cards.each(function(card) { + var checklists = card.get('cards_checklists') || []; + if (!_.isEmpty(checklists)) { + self.model.checklists.add(checklists, { + silent: true + }); + } + }); + }, + // Resets this checklist items collection + populateChecklistItems: function() { + var self = this; + self.model.checklists.each(function(checklist) { + var checklist_itmes = checklist.get('checklists_items') || []; + if (!_.isEmpty(checklist_itmes)) { + self.model.checklist_items.add(checklist_itmes, { + silent: true + }); + } + }); + }, + // Resets this labels collection + populateLabels: function() { + var self = this; + self.model.cards.each(function(card) { + var labels = card.get('cards_labels') || []; + if (!_.isEmpty(labels)) { + self.model.labels.add(labels, { + silent: true + }); + } + }); + }, + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-close-popover': 'closePopup', + 'click .js-close-sub-popover': 'closeSubPopup', + 'click .js-board-visibility': 'showBoardVisibility', + 'click .js-subscribe-board': 'subcribeBoard', + 'click .js-star-board': 'starredBoard', + 'click .js-close-board': 'closeBoard', + 'submit #BoardReopenForm': 'reopenBoard', + 'click .js-change-visibility': 'showAllVisibility', + 'click .js-select': 'copyBoardVisibility', + 'click .js-hide-sidebar': 'hideSidebar', + 'click .js-show-sidebar-menu': 'showSidebarMenu', + 'click .js-hide-sidebar-menu': 'hideSidebarMenu', + 'click .js-show-board-member-permission-form': 'showBoardMemberPermissionForm', + 'click .js-edit-board-member-permission-to-admin': 'editBoardMemberPermissionToAdmin', + 'click .js-edit-board-member-permission-to-normal': 'editBoardMemberPermissionToNormal', + 'click #js-add-board-member': 'addBoardMember', + 'click .js-board-user-activity': 'showMemberActivities', + 'change .js-add-custom-background': 'addCustomBackground', + 'click .js-show-board-modal': 'showListModal', + 'click #js-select-google-sync-url': 'selectGoogleSyncUrl', + 'keyup .js-search-archived-cards': 'searchArchivedCards', + 'keyup .js-search-archived-lists': 'searchArchivedLists', + 'click .js-board-commenting-permissions': 'showCBoardCommentingPermissions', + 'click .js-select-commenting-permission': 'selectCommentingPermission', + 'click .js-show-board-member-remove-form': 'showBoardMemberRemoveForm', + 'click .js-show-add-list-form': 'showAddListForm', + 'click .js-hide-add-list-form': 'hideAddListForm', + 'submit form.js-add-list': 'addList', + 'click .js-syn-google-calendar': 'syncGoogleCalendar', + 'click .js-open-dropdown': 'openDropdown', + 'click .js-sync-google-dropdown': 'syncGoogleDropdown', + }, + template: JST['templates/board'], + /** + * openDropdown() + * copy the existing card + * @param e + * @type Object(DOM event) + * @return false + * + */ + openDropdown: function(e) { + e.preventDefault(); + $(e.currentTarget).addClass('open'); + return false; + }, + + + /** + * syncGoogleDropdown() + * show the sync the board cards duedate to google calander URL + * @param e + * @type Object(DOM event) + * @return false + * + */ + syncGoogleDropdown: function(e) { + e.preventDefault(); + $('.js-sync-google-dropdown').addClass('open'); + return false; + }, + /** + * boardRename() + * close the dropdown + * @param e + * @type Object(DOM event) + * @return false + * + */ + closePopup: function(e) { + var el = this.$el; + var target = el.find(e.target); + target.parents('div.dropdown').removeClass('open'); + return false; + }, + /** + * closeSubPopup() + * close the sub dropdown + * @param e + * @type Object(DOM event) + * @return false + * + */ + closeSubPopup: function(e) { + var el = this.$el; + var target = el.find(e.target); + target.parents('li.dropdown').removeClass('open'); + return false; + }, + /** + * showBoardVisibility() + * display the board visibility + * @param e + * @type Object(DOM event) + * + */ + showBoardVisibility: function(e) { + var target = $(e.target); + var parent = target.parents('.js-visibility-list-dropdown'); + var visibility = this.model.attributes.board_visibility; + $('.js-visibility-list', parent).html(new App.ShowBoardVisibilityView({ + model: visibility + }).el); + parent.addClass('open'); + return false; + }, + /** + * subcribeBoard() + * subcribe the board + * @param e + * @type Object(DOM event) + * @return false + * + */ + subcribeBoard: function(e) { + e.preventDefault(); + var name = $(e.currentTarget).attr('name'); + var value = 'unsubscribe'; + var content = 'Unsubscribe'; + if (name == 'unsubscribe') { + value = 'subscribe'; + content = 'Subscribe'; + } + $(e.currentTarget).attr('name', value); + $(e.currentTarget).attr('title', value); + $(e.currentTarget).html(content); + var boardSubscriber = new App.BoardSubscriber(); + if (!_.isEmpty(this.model.board_subscriber) && this.model.board_subscriber.attributes.id) { + value = ''; + if ($('#inputBoardSubscribe').val() == 'false') { + value = 'true'; + $('#inputBoardSubscribe').val(value); + } else { + value = 'false'; + $('#inputBoardSubscribe').val(value); + } + var data = $('form#BoardSubscribeForm').serializeObject(); + boardSubscriber.url = api_url + 'boards/' + this.model.board.board_id + '/board_subscribers/' + this.model.subscriber.attributes.id + '.json'; + boardSubscriber.set('id', this.model.subscriber.attributes.id); + boardSubscriber.save(data, { + success: function(model, response) {} + }); + } else { + var subscribe_data = {}; + var self = this; + boardSubscriber.url = api_url + 'boards/' + this.model.id + '/board_subscribers.json'; + boardSubscriber.save(subscribe_data, { + success: function(model, response) { + self.model.board_subscribers.add(response); + } + }); + } + return false; + }, + /** + * starredBoard() + * subcribe the board + * @param e + * @type Object(DOM event) + * @return false + * + */ + starredBoard: function(e) { + e.preventDefault(); + $('.js-star-board').addClass('hide'); + $('.js-star-load').removeClass('hide'); + var name = $(e.currentTarget).attr('name'); + var value = 'unstar'; + var is_starred = true; + var content = ''; + if (name == 'unstar') { + value = 'star'; + is_starred = false; + content = ''; + } + $(e.currentTarget).attr('name', value); + $('.js-star-load').addClass('hide'); + $('.js-star-board').removeClass('hide'); + $(e.currentTarget).html(content); + var boardStar = new App.BoardStar(); + if (!_.isEmpty(this.model.board_star) && this.model.board_star.attributes.id) { + value = ''; + if ($('#inputBoardStar').val() == 'false') { + value = 'true'; + is_starred = true; + $('#inputBoardStar').val(value); + } else { + value = 'false'; + is_starred = false; + $('#inputBoardStar').val(value); + } + var data = $('form#BoardStarForm').serializeObject(); + boardStar.url = api_url + 'boards/' + this.model.board.board_id + '/boards_stars/' + this.model.star.attributes.id + '.json'; + boardStar.set('id', this.model.star.attributes.id); + boardStar.save(data, { + success: function(model, response) { + App.boards.get(self.model.attributes.id).boards_stars.get(parseInt(response.id)).set('is_starred', is_starred); + } + }); + } else { + var subscribe_data = {}; + var self = this; + boardStar.url = api_url + 'boards/' + this.model.id + '/boards_stars.json'; + boardStar.set('board_id', this.model.attributes.id); + boardStar.set('user_id', parseInt(authuser.user.id)); + boardStar.set('is_starred', is_starred); + boardStar.save(subscribe_data, { + success: function(model, response) { + boardStar.set('id', parseInt(response.id)); + App.boards.get(self.model.attributes.id).boards_stars.reset(boardStar); + self.model.boards_stars.add(response); + self.footerView = new App.FooterView({ + model: authuser, + board_id: self.model.attributes.id + }).renderStarredBoards(); + } + }); + } + return false; + }, + /** + * closeBoard() + * close the board + * @param e + * @type Object(DOM event) + * @return false + * + */ + closeBoard: function(e) { + e.preventDefault(); + this.model.url = api_url + 'boards/' + this.model.id + '.json'; + App.boards.get(this.model.id).set('is_closed', true); + this.model.set('is_closed', true); + this.footerView = new App.FooterView({ + model: authuser, + board_id: this.model.id + }).renderClosedBoards(); + this.model.save({ + is_closed: true + }, { + patch: true, + success: function(model, response) { + + } + }); + return false; + }, + /** + * reopenBoard() + * reopen closed the board + * @return false + * + */ + reopenBoard: function(e) { + var data = $(e.target).serializeObject(); + this.model.url = api_url + 'boards/' + this.model.id + '.json'; + this.model.set('is_closed', false); + this.model.save({ + is_closed: false + }, { + patch: true, + success: function(model, response) {} + }); + return false; + }, + showAllVisibility: function() { + $('.js-visibility-container').html(''); + var visibility = $('#inputBoardVisibility').val(); + $('.js-visibility-chooser').html(new App.ShowAllVisibilityView({ + model: visibility + }).el); + return false; + }, + copyBoardVisibility: function(e) { + e.preventDefault(); + var name = $(e.currentTarget).attr('name'); + var value = 0; + if (name == 'org') { + value = 1; + } else if (name == 'public') { + value = 2; + } + $('#inputBoardVisibility').val(value); + $('.js-visibility-container').html(new App.CopyBoardVisibilityView({ + name: name + }).el); + $('.js-visibility-chooser').html(''); + return false; + }, + hideSidebar: function() { + var el = this.$el; + el.find('.side-bar').addClass('disabled'); + return false; + }, + showSidebarMenu: function(e) { + var el = this.$el; + el.find('.js-sidebar-menu-container').removeClass('hide'); + el.find('.js-sidebar-menu').addClass('js-hide-sidebar-menu').removeClass('js-show-sidebar-menu'); + }, + hideSidebarMenu: function(e) { + var el = this.$el; + el.find('.js-sidebar-menu-container').addClass('hide'); + el.find('.js-sidebar-menu').addClass('js-show-sidebar-menu').removeClass('js-hide-sidebar-menu'); + }, + /** + * showBoardMemberPermissionForm() + * show the board member permission list + * @param e + * @type Object(DOM event) + * @return false + * + */ + showBoardMemberPermissionForm: function(e) { + var target = $(e.currentTarget); + var board_user_id = target.data('board_user_id'); + $('.js-board-member-settings').html(new App.showBoardMemberPermissionFormView({ + board_user_id: board_user_id + }).el); + $('.js-board-member-profile').addClass('hide'); + return false; + }, + /** + * editBoardMemberPermissionToAdmin() + * change the board member permission as admin + * @param e + * @type Object(DOM event) + * @return false + * + */ + editBoardMemberPermissionToAdmin: function(e) { + var self = this; + var target = $(e.currentTarget); + var board_user_id = target.data('board_user_id'); + $('.js-board-member-settings').html(new App.EditBoardMemberPermissionToAdmin({ + board_user_id: board_user_id + }).el); + $('.js-board-member-profile').removeClass('hide'); + var boardUser = new App.BoardUsers(); + boardUser.url = api_url + 'boards_users/' + board_user_id + '.json'; + boardUser.set('id', board_user_id); + boardUser.set('is_admin', 1); + boardUser.save(); + return false; + }, + /** + * editBoardMemberPermissionToNormal() + * change the board member permission as noraml + * @param e + * @type Object(DOM event) + * @return false + * + */ + editBoardMemberPermissionToNormal: function(e) { + var self = this; + var target = $(e.currentTarget); + var board_user_id = target.data('board_user_id'); + $('.js-board-member-settings').html(new App.EditBoardMemberPermissionToNormal({ + board_user_id: board_user_id + }).el); + $('.js-board-member-profile').removeClass('hide'); + var boardUser = new App.BoardUsers(); + boardUser.url = api_url + 'boards_users/' + board_user_id + '.json'; + boardUser.set('id', board_user_id); + boardUser.set('is_admin', 0); + boardUser.save(); + return false; + }, + /** + * showMemberActivities() + * display the board member activities + * @return false + * + */ + showMemberActivities: function() { + $('.js-board-member-settings').html(new App.BoardUserActivityView({ + model: this.model + }).el); + $('.js-board-member-profile').addClass('hide'); + return false; + }, + /** + * showChangeBackground() + * display the board background change form + * @return false + * + */ + showChangeBackground: function() { + $('.js-side-bar-' + this.model.id).addClass('side-bar-large'); + var el = this.$el; + el.find('.js-setting-response').html(new App.BoardBackgroundView({ + model: this.model + }).el); + var self = this; + _(function() { + Backbone.TemplateManager.baseUrl = '{name}'; + var uploadManager = new Backbone.UploadManager({ + uploadUrl: api_url + 'boards/' + self.model.id + '/custom_backgrounds.json?token=' + api_token, + autoUpload: true, + dropZone: $('#dropzone'), + singleFileUploads: true, + formData: $('form.js-user-profile-edit').serialize(), + fileUploadHTML: '', + }); + uploadManager.on('fileadd', function(file) { + $('#dropzone').addClass('cssloader'); + }); + uploadManager.on('filedone', function(file, data) { + $('#dropzone').removeClass('cssloader'); + }); + uploadManager.renderTo($('#manager-area')); + }).defer(); + return false; + }, + musical: function() { + self = this; + var temp = new App.MusicRepeatView(); + temp.continueMusic(); + }, + + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + var self = this; + $('body').addClass('modal-open'); + $('#header').html(new App.BoardHeaderView({ + model: this.model, + }).el); + this.musical(); + changeTitle('Board - ' + _.escape(this.model.attributes.name)); + if (!_.isUndefined(this.authuser)) { + this.model.board_subscriber = this.model.board_subscribers.findWhere({ + user_id: parseInt(this.authuser.id) + }); + this.model.board_star = this.model.board_stars.findWhere({ + user_id: parseInt(this.authuser.id) + }); + } + + this.$el.html(this.template({ + board: this.model, + subscriber: this.model.board_subscriber, + star: this.model.board_star + })); + this.renderListsCollection(); + this.setBoardBackground(); + if (!_.isUndefined(authuser.user)) { + $('#js-board-lists', this.$el).sortable({ + containment: 'window', + axis: 'x', + items: 'div.js-board-list', + placeholder: 'col-lg-3 col-md-3 col-sm-4 col-xs-12 board-list-placeholder list ', + forcePlaceholderSize: true, + cursor: 'grab', + scrollSensitivity: 100, + scrollSpeed: 50, + handle: '.js-list-head', + tolerance: 'pointer', + update: function(ev, ui) { + ui.item.trigger('listSort', ev, ui); + }, + start: function(ev, ui) { + ui.placeholder.height(ui.item.outerHeight()); + $(ev.target).find('.js-list-head').removeClass('cur-grab'); + $(ev.target).find('.js-list-head').children('div.dropdown').removeClass('open'); + }, + stop: function(ev, ui) { + $(ev.target).find('.js-list-head').addClass('cur-grab'); + }, + over: function(ev, ui) { + var scrollLeft = 0; + if ((($(window).width() - ui.offset.left) < 520) || (ui.offset.left > $(window).width())) { + scrollLeft = $('#js-board-lists').scrollLeft() + ($(window).width() - ui.offset.left); + $('#js-board-lists').stop().animate({ + scrollLeft: scrollLeft + }, 800); + } else if (ui.offset.left <= 260) { + scrollLeft = $('#js-board-lists').scrollLeft() - ($(window).width() - ui.offset.left); + $('#js-board-lists').stop().animate({ + scrollLeft: scrollLeft + }, 800); + } + } + }); + } + $('a.js-switch-grid-view').parent().addClass('active'); + if (!_.isUndefined(authuser.user)) { + if (parseInt(self.model.attributes.id) !== 0) { + var board_activities = new App.FooterView({ + model: authuser, + board_id: self.model.attributes.id, + board: self.model + }); + clearInterval(set_interval_id); + set_interval_id = setInterval(function() { + board_activities.userActivities(true, 1); + }, 10000); + } + } + self.board_view_height(); + this.showTooltip(); + return this; + }, + /** + * renderListsCollection() + * display the lists in the board + * + */ + renderListsCollection: function() { + var self = this; + var view_list = self.$('#js-add-list-block'); + var list_content = ''; + $('.js-board-list').remove(); + var postion = this.model.lists.max(function(list) { + return list.get('position'); + }); + var new_position = 1; + if (_.isObject(postion)) { + new_position += postion.get('position'); + } + self.model.lists.sortByColumn('position'); + self.model.lists.each(function(list) { + list.board_users = self.model.board_users; + list.labels = self.model.labels; + _.map(list.get('lists_cards'), function(num) { + _.map(num.card_labels, function(label) { + var data = { + id: label.label_id, + name: label.label_name + }; + var _match = _.matches(data); + if (_.isEmpty(_.filter(self.model.labels, _match))) { + self.model.labels.push(data, { + silent: true + }); + } + }); + }); + var view; + if (!_.isUndefined(list.get('is_new')) && list.get('is_new') === true) { + list.set('board_id', self.model.id); + } else { + if (list.get('is_archived') === false || list.get('is_archived') === 'f') { + var subscribers = new App.ListSubscriberCollection(); + subscribers.add(list.get('lists_subscribers'), { + silent: true + }); + if (!_.isUndefined(self.authuser)) { + var subscribe = subscribers.findWhere({ + user_id: parseInt(self.authuser.id) + }); + if (!_.isUndefined(subscribe)) { + list.subscriber.set(subscribe.attributes, { + silent: true + }); + } + } + list.activities.add(self.model.activities, { + silent: true + }); + list.attachments = self.model.attachments; + view = new App.ListView({ + model: list, + attributes: { + 'data-list_id': list.attributes.id + }, + }); + if (view_list.length > 0) { + view_list.before(view.render().el); + } else { + self.$('#js-board-lists').append(view.render().el); + } + } + } + }); + _.defer(function(view) { + if (!_.isUndefined(card_ids) && card_ids !== null && card_ids !== '') { + trigger_dockmodal = true; + var trigger_card_ids = card_ids.split(','); + for (var i = 0; i < trigger_card_ids.length; i++) { + var card_view = $('#js-card-' + trigger_card_ids[i]); + if (card_view.length === 0) { + var card = self.model.cards.findWhere({ + id: parseInt(trigger_card_ids[i]) + }); + if (!_.isUndefined(card)) { + card.list = self.model.lists.findWhere({ + id: card.attributes.list_id + }); + new App.CardView({ + model: card + }).showCardModal(); + } + } else { + card_view.trigger('click'); + } + } + card_ids = null; + trigger_dockmodal = false; + } + }, this); + }, + /** + * renderActivitiesCollection() + * display board activities + * + */ + renderActivitiesCollection: function() { + var self = this; + var view_activity = this.$('#js-board-activities'); + this.model.activities.sortBy(); + this.model.activities.each(function(activity) { + activity.set('board_name', _.escape(self.model.attributes.name)); + activity.cards.add(self.model.cards); + activity.lists.add(self.model.lists); + activity.boards.add(self.model.attributes.lists); + var view = new App.ActivityView({ + model: activity + }); + }); + }, + /** + * setBoardBackground() + * change board background + * + */ + setBoardBackground: function() { + var background_color = this.model.attributes.background_color; + var background_picture_url = this.model.attributes.background_picture_url; + var background_pattern_url = this.model.attributes.background_pattern_url; + if (!_.isEmpty(background_picture_url) && background_picture_url != 'NULL') { + background_picture_url = background_picture_url.replace('_XXXX.jpg', '_b.jpg'); + $('body').css({ + 'background': 'url(' + background_picture_url + ') 25% 25% no-repeat fixed', + 'background-size': 'cover' + }).addClass('board-view'); + } else if (!_.isEmpty(background_pattern_url) && background_pattern_url != 'NULL') { + background_pattern_url = background_pattern_url.replace('_XXXX.jpg', '_s.jpg'); + $('body').css({ + 'background': 'url(' + background_pattern_url + ')', + }).addClass('board-view board-view-pattern'); + } else if (!_.isEmpty(background_color) && background_color != 'NULL') { + $('body').css({ + 'background': background_color, + }).addClass('board-view'); + } + }, + /** + * addCustomBackground() + * add board custom background image + * @param e + * @type Object(DOM event) + * + */ + addCustomBackground: function(e) { + var self = this; + var form = $('form.js-add-custom-background-form'); + var target = $(e.target); + var board_background = new App.CardAttachment(); + board_background.url = api_url + 'boards/' + self.model.id + '/custom_backgrounds.json'; + board_background.save({}, { + data: {}, + files: $('input.js-add-custom-background', form), + iframe: true, + success: function(model, response) { + self.model.custom_attachments.push(board_background); + $('.js-board-background-custom-lists').append(new App.BoardCustomBackgroundView({ + attributes: { + 'data-background': model.attributes.custom_attachments.path, + }, + model: board_background + }).el); + } + }); + }, + /** + * changeCustomBackground() + * change board custom background image + * @param e + * @type Object(DOM event) + * @return false + * + */ + changeCustomBackground: function(e) { + var image_path = $(e.currentTarget).data('background'); + $('body').removeAttr('style').css({ + 'background': 'url(' + image_path + ') left top', + 'background-size': 'cover' + }).addClass('board-view-pattern board-view'); + this.model.url = api_url + 'boards/' + this.model.id + '.json'; + this.model.set('custom_background_url', image_path); + this.model.set('background_pattern_url', ''); + this.model.set('background_picture_url', ''); + this.model.set('background_color', ''); + data = { + background_color: 'NULL', + background_picture_url: 'NULL', + background_pattern_url: image_path + }; + this.model.save(data, { + patch: true + }); + return false; + }, + /** + * showListModal() + * display the attachment in the list + * @param e + * @type Object(DOM event) + * @return false + * + */ + showListModal: function(e) { + var modalView = new App.ModalBoardView({ + model: this.model + }); + modalView.show(); + return false; + }, + showAddListForm: function(e) { + e.preventDefault(); + var toggle = $(e.target); + toggle.addClass('hide').next('.js-add-list').removeClass('hide').find('#inputListName').focus(); + }, + /** + * hideAddListForm() + * hide the list add form + * @param e + * @type Object(DOM event) + * @return false + * + */ + hideAddListForm: function(e) { + e.preventDefault(); + var toggle = $(e.target); + toggle.parents('form').addClass('hide').prev('.js-show-add-list-form').removeClass('hide'); + return false; + }, + /** + * addList() + * add list into the board + * @param e + * @type Object(DOM event) + * @return false + * + */ + addList: function(e) { + e.preventDefault(); + var self = this; + var el = this.$el; + var target = $(e.target); + var view_list = el.find('#js-board-lists'); + var data = target.serializeObject(); + target[0].reset(); + target.parents('.dropdown').removeClass('open'); + target.prev().prev('.js-show-add-list-form').removeClass('hide').addClass('toggle-show').next('.js-show-list-actions').removeClass('hide'); + var list = new App.List(); + data.uuid = new Date().getTime(); + list.set('uuid', data.uuid); + var postion = self.model.lists.max(function(list) { + return (!_.isUndefined(list)) ? list.get('position') : 1; + }); + if (_.isUndefined(data.position)) { + data.position = (!_.isUndefined(postion) && !_.isEmpty(postion)) ? postion.get('position') + 1 : 1; + } else { + var before = this.model.lists.get(data.clone_list_id); + var after = this.model.lists.next(before); + if (typeof after == 'undefined') { + afterPosition = before.position() + 2; + } else { + afterPosition = after.position(); + } + var difference = (afterPosition - before.position()) / 2; + var newPosition = difference + before.position(); + data.position = newPosition; + } + data.is_archived = false; + data.board_id = self.model.id; + if (_.isUndefined(data.clone_list_id)) { + list.set(data, { + silent: true + }); + } + list.url = api_url + 'boards/' + self.model.id + '/lists.json'; + list.save(data, { + success: function(model, response, options) { + if (!_.isUndefined(data.clone_list_id)) { + if (!_.isUndefined(response.list.attachments) && response.list.attachments.length > 0) { + self.model.attachments.add(response.list.attachments, { + silent: true + }); + list.attachments.add(response.list.attachments, { + silent: true + }); + } + if (!_.isUndefined(response.list.checklists) && response.list.checklists.length > 0) { + self.model.checklists.add(response.list.checklists, { + silent: true + }); + } + if (!_.isUndefined(response.list.checklists_items) && response.list.checklists_items.length > 0) { + self.model.checklist_items.add(response.list.checklists_items, { + silent: true + }); + } + if (!_.isUndefined(response.list.labels) && response.list.labels.length > 0) { + self.model.labels.add(response.list.labels, { + silent: true + }); + list.labels.add(response.list.labels, { + silent: true + }); + } + if (!_.isUndefined(response.list.activities) && response.list.activities.length > 0) { + self.model.activities.add(response.list.activities, { + silent: true + }); + list.activities.add(response.list.activities, { + silent: true + }); + } + list.set(response.list); + list.set('cards', response.list.cards); + self.model.cards.add(response.list.cards); + var i = 1; + _.each(response.list.attachments, function(attachment) { + var options = { + silent: true + }; + if (i === response.list.attachments.length) { + options.silent = false; + } + var new_attachment = new App.CardAttachment(); + new_attachment.set(attachment); + new_attachment.set('id', parseInt(attachment.id)); + new_attachment.set('board_id', parseInt(attachment.board_id)); + new_attachment.set('list_id', parseInt(attachment.list_id)); + new_attachment.set('card_id', parseInt(attachment.card_id)); + self.model.cards.get(parseInt(attachment.card_id)).attachments.add(new_attachment, options); + i++; + }); + } else { + list.set('lists_cards', []); + } + if (_.isUndefined(options.temp_id)) { + list.set('id', parseInt(response.id)); + } else { + global_uuid[data.uuid] = options.temp_id; + list.set('id', data.uuid); + } + list.set('board_id', self.model.id); + if (list.attributes.is_archived === 'f') { + list.attributes.is_archived = false; + } + self.model.lists.add(list, { + silent: true + }); + list = self.model.lists.findWhere({ + uuid: data.uuid + }); + App.boards.get(list.attributes.board_id).lists.add(list); + var view = new App.ListView({ + model: list, + attributes: { + 'data-list_id': list.attributes.id, + }, + }); + if (!_.isUndefined(data.clone_list_id)) { + $(view.render().el).insertAfter($(e.target).parents('.js-board-list')); + } else { + $(view.render().el).insertBefore($('#js-add-list-block')); + } + } + }); + return false; + }, + /** + * syncGoogleCalendar() + * get sync google calender URL and display + * @param e + * @type Object(DOM event) + * + */ + syncGoogleCalendar: function(e) { + e.preventDefault(); + this.$el.find('input.js-syn-calendar-response').val(this.model.attributes.google_syn_url); + }, + /** + * selectGoogleSyncUrl() + * select google sync URL + * @param e + * @type Object(DOM event) + * + */ + selectGoogleSyncUrl: function(e) { + $(e.target).select(); + }, + /** + * searchArchivedCards() + * search show show archived cards + * @param e + * @type Object(DOM event) + * + */ + searchArchivedCards: function(e) { + var self = this; + var el = this.$el; + var search_q = $(e.currentTarget).val(); + + var filtered_cards = self.model.cards.search(search_q); + var cards = new App.CardCollection(); + if (!_.isEmpty(search_q)) { + cards.add(filtered_cards._wrapped); + } else { + cards = self.model.cards; + } + el.find('.js-archived-cards-container').html(''); + cards.each(function(card) { + if (card.attributes.is_archived === true) { + el.find('.js-archived-cards-container').append(new App.ArchivedCardView({ + model: card + }).el); + } + }); + }, + /** + * searchArchivedLists() + * search show show archived lists + * @param e + * @type Object(DOM event) + * + */ + searchArchivedLists: function(e) { + var self = this; + var el = this.$el; + var search_q = $(e.currentTarget).val(); + var filtered_lists = this.model.lists.search(search_q); + var lists = new App.ListCollection(); + if (!_.isEmpty(search_q)) { + lists.add(filtered_lists._wrapped); + } else { + lists = self.model.lists; + } + el.find('.js-archived-cards-container').html(''); + lists.each(function(list) { + if (list.attributes.is_archived === true) { + el.find('.js-archived-cards-container').append(new App.ArchivedListView({ + model: list + }).el); + } + }); + }, + /** + * closeSpanPopover() + * close popup + * @param e + * @type Object(DOM event) + * @return false + * + */ + closeSpanPopover: function(e) { + var el = this.$el; + var target = el.find(e.target); + target.parents('span.dropdown').removeClass('open'); + return false; + }, + showBoardMemberRemoveForm: function(e) { + e.preventDefault(); + } +}); diff --git a/client/js/views/board_visibility_view.js b/client/js/views/board_visibility_view.js new file mode 100644 index 000000000..eedd3778d --- /dev/null +++ b/client/js/views/board_visibility_view.js @@ -0,0 +1,44 @@ +/** + * @fileOverview This file has functions related to board visibility view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board model. It contain all board based object @see Available Object in App.BoardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * BoardVisibility View + * @class BoardVisibilityView + * @constructor + * @extends Backbone.View + */ +App.BoardVisibilityView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/board_visibility'], + tagName: 'ul', + className: 'list-unstyled', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + board: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/boards_index_view.js b/client/js/views/boards_index_view.js new file mode 100644 index 000000000..c54a86efb --- /dev/null +++ b/client/js/views/boards_index_view.js @@ -0,0 +1,74 @@ +/** + * @fileOverview This file has functions related to board index view. This view calling from apllication view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : undefined + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * BoardsIndex View + * @class BoardsIndexView + * @constructor + * @extends Backbone.View + */ +App.BoardsIndexView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/board_index'], + tagName: 'section', + className: 'clearfix', + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-delete-board': 'deleteBoard', + }, + /** + * deleteBoard() + * delete board + * @param e + * @type Object(DOM event) + * @return false + * + */ + deleteBoard: function(e) { + if (confirm($(e.currentTarget).data('confirm'))) { + var dataUrl = $(e.currentTarget).attr('href'); + + var board = new App.Board(); + board.url = api_url + dataUrl + '.json'; + board.set('id', '-1'); + board.destroy({ + success: function() { + Backbone.history.loadUrl(); + } + }); + } + return false; + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + 'board': this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/boards_user_view.js b/client/js/views/boards_user_view.js new file mode 100644 index 000000000..6e395fe72 --- /dev/null +++ b/client/js/views/boards_user_view.js @@ -0,0 +1,85 @@ +/** + * @fileOverview This file has functions related to board user view. This view calling from board header view. + * Available Object: + * this.model : board user model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * BoardUsers View + * @class BoardUsersView + * @constructor + * @extends Backbone.View + */ +App.BoardUsersView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.is_admin = options.is_admin; + this.model.bind('change:is_admin', this.showBoardUserActions, this); + this.render(); + }, + tagName: 'li', + className: 'form-inline navbar-btn btn-xs pull-left js-board-user-avatar-click dropdown nav', + template: JST['templates/board_users_view'], + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-show-board-user-action': 'showBoardUserActions', + 'click .js-show-confirm-delete-board-member': 'showConfirmDeleteBoardUser', + 'click .js-delete-board-member': 'deleteBoardMember', + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + user: this.model, + is_admin: this.is_admin + })); + this.showTooltip(); + return this; + }, + showBoardUserActions: function() { + this.$el.find('.js-show-board-user-action-response').html(new App.BoardUserActionsView({ + model: this.model, + is_admin: this.is_admin + }).el); + }, + showConfirmDeleteBoardUser: function(e) { + e.preventDefault(); + this.$el.find('.js-show-board-user-action-response').html(new App.BoardUserRemoveConfirmView({ + model: this.model, + authuser: authuser.user, + }).el); + return false; + }, + /** + * deleteBoardMember() + * delete board member + * @param e + * @type Object(DOM event) + * @return false + * + */ + deleteBoardMember: function(e) { + var self = this; + var target = $(e.currentTarget); + this.$el.remove(); + this.model.url = api_url + 'boards_users/' + this.model.attributes.id + '.json'; + this.model.destroy(); + return false; + } +}); diff --git a/client/js/views/card_actions_view.js b/client/js/views/card_actions_view.js new file mode 100644 index 000000000..835e04085 --- /dev/null +++ b/client/js/views/card_actions_view.js @@ -0,0 +1,44 @@ +/** + * @fileOverview This file has functions related to card action view. This view calling from card view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : card model. It contain all card based object @see Available Object in App.CardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * CardActions View + * @class CardActionsView + * @constructor + * @extends Backbone.View + */ +App.CardActionsView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/card_actions'], + tagName: 'ul', + className: 'dropdown-menu dropdown-menu-right arrow arrow-right js-card-action-list-response js-dropdown-popup', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + card: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/card_attachment_view.js b/client/js/views/card_attachment_view.js new file mode 100644 index 000000000..7f97bbf01 --- /dev/null +++ b/client/js/views/card_attachment_view.js @@ -0,0 +1,93 @@ +/** + * @fileOverview This file has functions related to card attachment view. This view calling from modal card view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : card attachment model. It contain all card based object @see Available Object in App.CardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * CardAttachment View + * @class CardAttachmentView + * @constructor + * @extends Backbone.View + */ +App.CardAttachmentView = Backbone.View.extend({ + template: JST['templates/card_attachment'], + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + this.model.downloadLink = this.downloadLink; + } + }, + tagName: 'li', + className: 'clearfix navbar-btn', + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click a.js-show-confirm-delete-attachment': 'showConfirmAttachmentDelete', + 'click .js-close-popup': 'closePopup', + 'click .js-span-close-popup': 'closeSpanPopup', + 'click .js-delete-attachment': 'deleteAttachment' + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + attachment: this.model + })); + this.showTooltip(); + return this; + }, + /** + * deleteAttachment() + * delete the attachment in card + * @return false + * + */ + deleteAttachment: function() { + this.$el.remove(); + this.model.url = api_url + 'boards/' + this.model.attributes.board_id + '/lists/' + this.model.attributes.list_id + '/cards/' + this.model.attributes.card_id + '/attachments/' + this.model.id + '.json'; + this.model.destroy(); + return false; + }, + /** + * showConfirmAttachmentDelete() + * display attachment delete confirmation form + * @param e + * @type Object(DOM event) + * + */ + showConfirmAttachmentDelete: function(e) { + e.preventDefault(); + $('.js-attachment-confirm-respons-' + this.model.id, this.$el).html(new App.AttachmentDeleteConfirmView({ + model: this.model + }).el); + }, + /** + * closePopup() + * hide opend dropdown + * @param e + * @type Object(DOM event) + * @return false + * + */ + closePopup: function(e) { + var target = $(e.target); + target.parents('li.dropdown').removeClass('open'); + return false; + }, + closeSpanPopup: function(e) { + var target = $(e.target); + target.parents('.dropdown').removeClass('open'); + return false; + } +}); diff --git a/client/js/views/card_checklist_item_view.js b/client/js/views/card_checklist_item_view.js new file mode 100644 index 000000000..3ab7740d5 --- /dev/null +++ b/client/js/views/card_checklist_item_view.js @@ -0,0 +1,431 @@ +/** + * @fileOverview This file has functions related to card checklist item view. This view calling from card checklist view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : checklist item model. + * this.model.checklist : checklist model. @see Available Object in CardCheckListView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * CardCheckListItem View + * @class CardCheckListItemView + * @constructor + * @extends Backbone.View + */ +App.CardCheckListItemView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + _.bindAll(this, 'render', 'renderProgress'); + this.model.checklist.card.list.collection.board.checklist_items.bind('add', this.render); + this.model.checklist.card.list.collection.board.checklist_items.bind('change', this.render); + this.model.checklist.card.list.collection.board.checklist_items.bind('change', this.renderProgress); + this.model.checklist.card.list.collection.board.checklist_items.bind('remove', this.renderProgress); + }, + template: JST['templates/card_checklist_item'], + className: function() { + var class_name = 'js-checklist-item btn-block pull-left'; + if (!_.isUndefined(authuser.user)) { + class_name += ' cur-grab'; + } + return class_name; + + }, + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-show-item-edit-form': 'showItemEditForm', + 'submit form.js-item-edit-form': 'updateItem', + 'click a.js-hide-item-edit-form': 'hideChecklistEditForm', + 'click .js-show-confirm-item-delete': 'showConfirmItemDelete', + 'click .js-delete-item': 'deleteItem', + 'click .js-markas-completed': 'markAsCompleted', + 'click .js-markas-incomplete': 'markAsIncomplete', + 'click .js-show-item-options': 'showItemOptions', + 'click .js-show-mention-member-form': 'showMentionMemberForm', + 'click .js-convert-to-card': 'convertToCard', + 'keyup .js-item-search-member': 'showSearchItemMembers', + 'click .js-back-to-item-options': 'backToItemOptions', + 'click .js-edit-item-member': 'editItemMember', + 'click .js-no-action': 'noAction', + 'itemSort': 'itemSort' + }, + /** + * listSort() + * save the list moved position + * @param e + * @type Object(DOM event) + * @param data + * @type Object + * + */ + itemSort: function(ev, ui) { + var target = $(ev.target); + var data = {}; + var checklist_id = parseInt(target.parents('.js-checklist-items-sorting:first').data('checklist_id')); + + var previous_item_id = target.prev('.js-checklist-item').data('item_id'); + var next_item_id = target.next('.js-checklist-item').data('item_id'); + + this.model.url = api_url + 'boards/' + this.model.card.get('board_id') + '/lists/' + this.model.card.get('list_id') + '/cards/' + this.model.card.id + '/checklists/' + this.model.attributes.checklist_id + '/items/' + this.model.id + '.json'; + if ((typeof previous_item_id == 'undefined' && typeof next_item_id == 'undefined') || checklist_id != this.model.attributes.checklist_id) { + data.checklist_id = checklist_id; + } + if (typeof previous_item_id != 'undefined') { + this.model.moveAfter(previous_item_id); + } else if (typeof next_item_id != 'undefined') { + this.model.moveBefore(next_item_id); + } + this.model.set('checklist_id', checklist_id); + data.position = this.model.attributes.position; + + this.model.save(data, { + patch: true + }); + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + checklist_item: this.model + })); + this.showTooltip(); + return this; + }, + + /** + * renderProgress() + * display checklist item completed progress bar + * return object + * + */ + renderProgress: function() { + var checklist_id = this.model.get('checklist_id'); + var checklist_items = this.model.collection.where({ + checklist_id: checklist_id + }); + items = new App.CheckListItemCollection(); + items.add(checklist_items); + var completed_count = items.filter(function(checklist_item) { + return checklist_item.get('is_completed') === true || checklist_item.get('is_completed') == 'true' || checklist_item.get('is_completed') == 't'; + }).length; + var total_count = items.models.length; + completed_count = 0 < total_count ? Math.round(100 * completed_count / total_count) : 0; + $('#js-checklist-progress-percent-' + checklist_id).text(completed_count + '%'); + $('#js-checklist-progress-bar-' + checklist_id).stop().animate({ + 'width': completed_count + '%', + '-moz-transition': 'all .6s ease', + '-ms-transition': 'all .6s ease', + '-o-transition': 'all .6s ease', + '-webkit-transition': 'all .6s ease', + 'transition': 'all .6s ease' + }, 100); + return this; + }, + /** + * showItemEditForm() + * show checklist item edit form + * @param e + * @type Object(DOM event) + * @return false + * + */ + showItemEditForm: function(e) { + var prev_form = $('form.js-item-edit-form'); + prev_form.parent().addClass('js-show-item-edit-form').html($('textarea', prev_form).val()); + prev_form.remove(); + $(e.target).addClass('hide').html(''); + $(e.target).after(new App.ChecklistItemEditFormView({ + model: this.model + }).el); + return false; + }, + /** + * hideChecklistEditForm() + * hide checklist item edit form + * @param e + * @type Object(DOM event) + * + */ + hideChecklistEditForm: function(e) { + e.preventDefault(); + var form = $('form.js-item-edit-form'); + form.prev('.js-show-item-edit-form').removeClass('hide').html($('textarea', form).val()); + form.remove(); + }, + /** + * updateItem() + * update checklist item edit + * @param e + * @type Object(DOM event) + * @return false + * + */ + updateItem: function(e) { + e.preventDefault(); + var data = $(e.target).serializeObject(); + this.model.url = api_url + 'boards/' + this.model.card.get('board_id') + '/lists/' + this.model.card.get('list_id') + '/cards/' + this.model.card.id + '/checklists/' + this.model.attributes.checklist_id + '/items/' + this.model.id + '.json'; + this.model.set(data); + this.render(); + this.model.save(data, { + patch: true + }); + return false; + }, + /** + * showConfirmItemDelete() + * display checklist item delete confirmation form + * @param e + * @type Object(DOM event) + * + */ + showConfirmItemDelete: function(e) { + e.preventDefault(); + $('#js-item-actions-response-' + this.model.id).html(new App.ChecklistItemDeleteConfirmFormView({ + model: this.model + }).el); + }, + /** + * deleteItem() + * delete checklist item + * @param e + * @type Object(DOM event) + * @return false + * + */ + deleteItem: function() { + this.$el.remove(); + this.model.url = api_url + 'boards/' + this.model.card.get('board_id') + '/lists/' + this.model.card.get('list_id') + '/cards/' + this.model.card.id + '/checklists/' + this.model.attributes.checklist_id + '/items/' + this.model.id + '.json'; + var checkList_item = this.model.card.list.collection.board.checklist_items.get(this.model.id); + var bool = checkList_item.attributes.is_completed; + if (bool) { + this.model.set('is_completed', 'true'); + this.model.checklist.set('checklist_item_completed_count', parseInt(this.model.checklist.get('checklist_item_completed_count')) - 1); + this.model.checklist.card.set('checklist_item_completed_count', parseInt(this.model.checklist.card.attributes.checklist_item_completed_count) - 1); + this.model.checklist.card.list.collection.board.cards.get(this.model.checklist.card).set('checklist_item_completed_count', this.model.checklist.card.attributes.checklist_item_completed_count, { + silent: true + }); + } + this.model.checklist.set('checklist_item_count', parseInt(this.model.checklist.get('checklist_item_count')) - 1); + this.model.checklist.card.set('checklist_item_count', parseInt(this.model.checklist.card.attributes.checklist_item_count) - 1); + this.model.checklist.card.list.collection.board.cards.get(this.model.checklist.card).set('checklist_item_count', this.model.checklist.card.attributes.checklist_item_count, { + silent: true + }); + this.model.destroy(); + return false; + }, + /** + * markAsCompleted() + * mark checklist item as completed + * @param e + * @type Object(DOM event) + * + */ + markAsCompleted: function(e) { + e.preventDefault(); + var self = this; + this.model.url = api_url + 'boards/' + this.model.card.get('board_id') + '/lists/' + this.model.card.get('list_id') + '/cards/' + this.model.card.id + '/checklists/' + this.model.attributes.checklist_id + '/items/' + this.model.id + '.json'; + this.model.set('is_completed', true); + this.model.checklist.checklist_item_completed_count = parseInt(this.model.checklist.get('checklist_item_completed_count')) + 1; + this.model.checklist.card.list.collection.board.cards.get(this.model.checklist.card).checklist_item_completed_count = this.model.checklist.card.attributes.checklist_item_completed_count; + this.model.checklist.card.set('checklist_item_completed_count', parseInt(this.model.checklist.card.attributes.checklist_item_completed_count) + 1); + this.model.save({ + is_completed: 'true' + }, { + silent: true, + patch: true, + success: function(model, response) { + var activity = new App.Activity(); + activity.set(response.activity); + var view = new App.ActivityView({ + model: activity + }); + model.set('activities', activity, { + silent: true + }); + var view_activity = $('#js-card-activities-' + self.model.card.id); + view_activity.prepend(view.render().el).find('.timeago').timeago(); + } + }); + }, + /** + * markAsIncomplete() + * mark checklist item as incompleted + * @param e + * @type Object(DOM event) + * + */ + markAsIncomplete: function(e) { + e.preventDefault(); + var self = this; + this.model.url = api_url + 'boards/' + this.model.card.get('board_id') + '/lists/' + this.model.card.get('list_id') + '/cards/' + this.model.card.id + '/checklists/' + this.model.attributes.checklist_id + '/items/' + this.model.id + '.json'; + this.model.set('is_completed', false); + this.model.checklist.checklist_item_completed_count = parseInt(this.model.checklist.get('checklist_item_completed_count')) - 1; + this.model.checklist.card.set('checklist_item_completed_count', parseInt(this.model.checklist.card.attributes.checklist_item_completed_count) - 1); + this.model.checklist.card.list.collection.board.cards.get(this.model.checklist.card).checklist_item_completed_count = this.model.checklist.card.attributes.checklist_item_completed_count; + this.model.save({ + is_completed: 'false' + }, { + silent: true, + patch: true, + success: function(model, response) { + var activity = new App.Activity(); + activity.set(response.activity); + var view = new App.ActivityView({ + model: activity + }); + model.set('activities', activity, { + silent: true + }); + var view_activity = $('#js-card-activities-' + self.model.card.id); + view_activity.prepend(view.render().el).find('.timeago').timeago(); + } + }); + }, + /** + * showItemOptions() + * show checklist item actions + * @param e + * @type Object(DOM event) + * + */ + showItemOptions: function(e) { + e.preventDefault(); + $('#js-item-option-response-' + this.model.id).html(new App.ChecklistItemActionsView({ + model: this.model + }).el); + }, + /** + * backToItemOptions() + * show checklist item actions + * @param e + * @type Object(DOM event) + * + */ + backToItemOptions: function(e) { + e.preventDefault(); + $('#js-item-option-response-' + this.model.id).html(new App.ChecklistItemActionsView({ + model: this.model + }).el); + return false; + }, + /** + * showMentionMemberForm() + * show board member list form + * @param e + * @type Object(DOM event) + * return false + * + */ + showMentionMemberForm: function(e) { + e.preventDefault(); + $('#js-item-option-response-' + this.model.id).html(new App.ChecklistItemMentionMemberSerachFormView().el); + this.$el.find('.js-item-member-search-response').html(''); + this.renderBoardUsers(); + return false; + }, + /** + * convertToCard() + * convert checklist item to card + * + */ + convertToCard: function(e) { + var self = this; + e.preventDefault(); + var card = new App.Card(); + card.url = api_url + 'boards/' + self.model.card.get('board_id') + '/lists/' + self.model.card.get('list_id') + '/cards/' + self.model.card.id + '/checklists/' + self.model.attributes.id + '/items/' + self.model.id + '/convert_to_card.json'; + card.save({}, { + success: function(model, response) { + self.deleteItem(); + card.set(response.cards); + card.set('id', parseInt(response.cards.id)); + card.set('list_id', parseInt(response.cards.list_id)); + card.set('board_id', parseInt(response.cards.board_id)); + self.model.card.list.collection.board.cards.add(card); + } + }); + }, + /** + * showSearchMembers() + * display searched member list + */ + showSearchItemMembers: function(e) { + var self = this; + var q = $(e.target).val(); + var view = this.$el.find('.js-item-member-search-response'); + if (q !== '') { + var filtered_users = this.model.card.list.collection.board.board_users.search(q); + var users = new App.UserCollection(); + if (!_.isEmpty(filtered_users._wrapped)) { + $.unique(filtered_users._wrapped); + } + users.add(filtered_users._wrapped); + $('.js-item-member-search-response').html(''); + if (!_.isEmpty(users.models)) { + users.each(function(board_user) { + view.append(new App.ChecklistItemMentionMemberView({ + model: board_user + }).el); + }); + } else { + view.html(new App.ChecklistItemMentionMemberView({ + model: null + }).el); + } + } else { + view.html(''); + this.renderBoardUsers(); + } + }, + renderBoardUsers: function() { + var view = this.$el.find('.js-item-member-search-response'); + if (!_.isEmpty(this.model.card.list.collection.board.board_users.models)) { + this.model.card.list.collection.board.board_users.each(function(board_user) { + view.append(new App.ChecklistItemMentionMemberView({ + model: board_user, + class_name: 'js-edit-item-member' + }).el); + }); + } else { + view.html(new App.ChecklistItemMentionMemberView({ + model: null + }).el); + } + }, + /** + * editCardMember() + * show board member in checklist item + * @param e + * @type Object(DOM event) + * return false + * + */ + editItemMember: function(e) { + e.preventDefault(); + var member_id = $(e.currentTarget).data('member-id'); + var selected_user = this.model.board_users.get({ + id: member_id + }); + var target = $('form.js-item-edit-form textarea'); + target.val(target.val() + ' @' + selected_user.attributes.username); + return false; + }, + noAction: function(e) { + e.preventDefault(); + return false; + } +}); diff --git a/client/js/views/card_checklist_view.js b/client/js/views/card_checklist_view.js new file mode 100644 index 000000000..16cc7e260 --- /dev/null +++ b/client/js/views/card_checklist_view.js @@ -0,0 +1,586 @@ +/** + * @fileOverview This file has functions related to card checklist view. This view calling from modal card view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : checklist model. It contain all card based object @see Available Object in App.CardView + * this.model.card : card model. @see Available Object in CardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * CardCheckList View + * @class CardCheckListView + * @constructor + * @extends Backbone.View + */ +App.CardCheckListView = Backbone.View.extend({ + template: JST['templates/card_checklist'], + tagName: 'div', + className: 'js-card-checklist', + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-show-checklist-edit-form': 'showChecklistEditForm', + 'submit form.checklist-edit-form': 'updateChecklist', + 'click a.js-hide-checklist-edit-form': 'hideChecklistEditForm', + 'click .js-show-confirm-checklist-delete': 'showConfirmChecklistDelete', + 'click .js-delete-checklist': 'deleteChecklist', + 'click .js-show-checklist-actions': 'showChecklistActions', + 'click .js-back-to-checklist-actions': 'backToChecklistActions', + 'click .js-close-popup': 'closePopup', + 'click .js-show-checklist-item-add-form': 'showChecklistItemAddForm', + 'click .js-hide-checklist-item-add-form': 'hideChecklistItemAddForm', + 'submit form.js-add-item': 'addChecklistItem', + 'click .js-show-item-options': 'showItemOptions', + 'click .js-back-to-item-options': 'backToItemOptions', + 'click .js-show-mention-member-form': 'showMentionMemberForm', + 'click .js-add-item-member': 'addItemMember', + 'keyup .js-item-search-member': 'showSearchItemMembers', + 'click .js-no-action': 'noAction', + 'checklistSort': 'checklistSort', + 'keypress textarea': 'onEnter', + }, + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + _.bindAll(this, 'render', 'renderItemsCollection'); + this.model.checklist_items.bind('remove', this.renderItemsCollection); + }, + /** + * checklistSort() + * save the checklist moved position + * @param e + * @type Object(DOM event) + * @param data + * @type Object + * + */ + checklistSort: function(ev, ui) { + var target = $(ev.target); + var previous_checklist_id = target.prev('.js-card-checklist').data('checklist_id'); + var next_checklist_id = target.next('.js-card-checklist').data('checklist_id'); + if (typeof previous_checklist_id == 'undefined' && typeof next_checklist_id == 'undefined') { + previous_checklist_id = 1; + next_checklist_id = 1; + } + if (typeof previous_checklist_id != 'undefined') { + this.model.moveAfter(previous_checklist_id); + } else if (typeof next_checklist_id != 'undefined') { + this.model.moveBefore(next_checklist_id); + } else { + if (this.model.collection.length != 1) { + throw 'Unable to determine position'; + } + } + this.model.url = api_url + 'boards/' + this.model.card.get('board_id') + '/lists/' + this.model.card.get('list_id') + '/cards/' + this.model.card.id + '/checklists/' + this.model.id + '.json'; + this.model.save({ + position: this.model.attributes.position + }, { + patch: true + }); + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + checklist: this.model + })); + if (!_.isUndefined(authuser.user)) { + $('.js-checklist-items-sorting', this.$el).sortable({ + items: 'div.js-checklist-item', + helper: 'clone', + connectWith: '.js-checklist-items-sorting', + dropOnEmpty: true, + placeholder: 'form-group card-list-placeholder col-xs-12', + cursor: 'grab', + axis: 'y', + scroll: true, + tolerance: 'pointer', + handle: '.list-group-item-text', + update: function(ev, ui) { + ui.item.trigger('itemSort', ev, ui); + }, + start: function(ev, ui) { + ui.placeholder.height(ui.item.outerHeight()); + $(ev.target).find('.js-checklist-item').removeClass('cur-grab'); + $(ev.target).find('.js-checklist-item').addClass('cur-grabbing'); + }, + stop: function(ev, ui) { + $(ev.target).find('.js-checklist-item').addClass('cur-grab'); + $(ev.target).find('.js-checklist-item').removeClass('cur-grabbing'); + } + }).disableSelection(); + } + this.renderItemsCollection(); + this.renderProgress(); + this.showTooltip(); + return this; + }, + /** + * deleteChecklist() + * delete check list + * @return false + * + */ + deleteChecklist: function() { + this.$el.remove(); + this.model.url = api_url + 'boards/' + this.model.card.attributes.board_id + '/lists/' + this.model.card.attributes.list_id + '/cards/' + this.model.card.attributes.id + '/checklists/' + this.model.id + '.json'; + var cnt = 0; + var completed_cnt = 0; + var checkLists = this.model.card.list.collection.board.cards.get(this.model.card.attributes.id).checklists; + checkLists.each(function(list) { + completed_cnt += parseInt(list.attributes.checklist_item_completed_count); + cnt += parseInt(list.checklist_items.length); + }); + var this_complete_count = completed_cnt - parseInt(this.model.attributes.checklist_item_completed_count); + var this_count = cnt - parseInt(this.model.checklist_items.length); + if (!this_complete_count.isNan) { + this.model.set({ + checklist_item_completed_count: this_complete_count + }, { + silent: true + }); + this.model.card.set('checklist_item_completed_count', this_complete_count); + this.model.card.list.collection.board.cards.get(this.model.card.attributes.id).set({ + checklist_item_completed_count: this_complete_count + }, { + silent: true + }); + } + if (!this_count.isNan) { + this.model.set({ + checklist_item_count: this_count + }, { + silent: true + }); + this.model.card.set('checklist_item_count', this_count); + this.model.card.list.collection.board.cards.get(this.model.card.attributes.id).set({ + checklist_item_count: this_count + }, { + silent: true + }); + } + this.model.destroy(); + return false; + }, + /** + * renderItemsCollection() + * display items in checklist + * + */ + renderItemsCollection: function(is_show_link) { + var self = this; + var view_item = this.$('#js-checklist-items-' + this.model.id); + view_item.html(''); + this.model.checklist_items.sortByColumn('position'); + this.model.checklist_items.each(function(checklist_item) { + checklist_item.card = self.model.card; + checklist_item.cards = self.model.cards; + checklist_item.checklist = new App.CheckList(); + checklist_item.checklist = self.model; + checklist_item.board_users = self.model.board_users; + var view = new App.CardCheckListItemView({ + model: checklist_item, + attributes: { + 'data-item_id': checklist_item.attributes.id, + }, + }); + view_item.append(view.render().el); + }); + if (!_.isEmpty(role_links.where({ + slug: 'add_checklist_item' + })) && is_show_link !== false) { + view_item.after(new App.ChecklistItemAddLinkView().el); + } + }, + /** + * showChecklistEditForm() + * display checklist edit form + * @param e + * @type Object(DOM event) + * @return false + * + */ + showChecklistEditForm: function(e) { + var prev_form = $('form.checklist-edit-form'); + prev_form.parent().html($('textarea', prev_form).val()); + prev_form.remove(); + $(e.currentTarget).addClass('hide'); + $(e.currentTarget).after(new App.ChecklistEditFormView({ + model: this.model + }).el); + return false; + }, + /** + * updateChecklist() + * update checklist + * @param e + * @type Object(DOM event) + * @return false + * + */ + updateChecklist: function(e) { + e.preventDefault(); + var data = $(e.target).serializeObject(); + this.model.url = api_url + 'boards/' + this.model.card.get('board_id') + '/lists/' + this.model.card.get('list_id') + '/cards/' + this.model.card.id + '/checklists/' + this.model.id + '.json'; + this.model.set('card_id', this.model.card.id); + this.model.set('list_id', this.model.card.attributes.list_id); + this.model.set('board_id', this.model.card.attributes.board_id); + this.model.set(data); + this.render(); + this.model.save(data, { + patch: true + }); + return false; + }, + /** + * hideChecklistEditForm() + * hide checklist edit form + * @param e + * @type Object(DOM event) + * @return false + * + */ + hideChecklistEditForm: function(e) { + e.preventDefault(); + var form = $('form.checklist-edit-form'); + form.prev().removeClass('hide'); + $('a', form.prev()).html($('textarea', form).val()); + form.remove(); + }, + /** + * showConfirmChecklistDelete() + * display checklist delete form + * @param e + * @type Object(DOM event) + * @return false + * + */ + showConfirmChecklistDelete: function(e) { + $('#js-checklist-confirm-response-' + this.model.id).html(new App.ChecklistDeleteConfirmFormView({ + model: this.model + }).el); + return false; + }, + /** + * showChecklistActions() + * display checklist action lists + * @param e + * @type Object(DOM event) + * + */ + showChecklistActions: function(e) { + $('#js-checklist-actions-response-' + this.model.id).html(new App.ChecklistActionsView({ + model: this.model + }).el); + }, + /** + * backToChecklistActions() + * back to checklist action lists + * @param e + * @type Object(DOM event) + * @return false + * + */ + backToChecklistActions: function(e) { + $('.js-back-to-checklist-actions').remove(); + this.showChecklistActions(e); + return false; + }, + /** + * closePopup() + * hide checklist action lists + * @param e + * @type Object(DOM event) + * @return false + * + */ + closePopup: function(e) { + var target = $(e.target); + target.parents('li.dropdown, div.dropdown').removeClass('open'); + return false; + }, + /** + * showChecklistItemAddForm() + * display checklist item add form + * @param e + * @type Object(DOM event) + * + */ + showChecklistItemAddForm: function(e) { + e.preventDefault(); + $(e.target).removeClass('js-show-checklist-item-add-form').addClass('hide').next('.js-checklist-item-add-form-view').html(new App.ChecklistItemAddFormView({ + model: this.model + }).el); + $('.js-add-item').find('textarea').focus(); + }, + /** + * hideChecklistItemAddForm() + * hide checklist item add form + * @param e + * @type Object(DOM event) + * + */ + hideChecklistItemAddForm: function(e) { + e.preventDefault(); + $(e.target).parents('div.js-checklist-item-add-form-view').prev('a.js-add-item-view').removeClass('hide').addClass('js-show-checklist-item-add-form').html('Add Item'); + $(e.target).parents('div.js-checklist-item-add-form-view').html(''); + }, + /** + * addChecklistItem() + * add item in checklist + * @param e + * @type Object(DOM event) + * + */ + addChecklistItem: function(e) { + e.preventDefault(); + var self = this; + var target = $(e.target); + var data = target.serializeObject(); + var checklist_item = new App.CheckListItem(); + checklist_item.set('card_id', self.model.card.attributes.id); + checklist_item.set('list_id', self.model.card.attributes.list_id); + checklist_item.set('board_id', self.model.card.attributes.board_id); + checklist_item.set('checklist_id', self.model.attributes.id); + checklist_item.url = api_url + 'boards/' + self.model.card.attributes.board_id + '/lists/' + self.model.card.attributes.list_id + '/cards/' + self.model.card.attributes.id + '/checklists/' + self.model.id + '/items.json'; + $(e.target).find('textarea').val('').focus(); + checklist_item.save(data, { + success: function(model, response) { + if (!_.isUndefined(response.checklist_items)) { + _.each(response.checklist_items, function(item) { + checklist_item = new App.CheckListItem(); + checklist_item.set('id', parseInt(item.id)); + checklist_item.set('card_id', parseInt(item.card_id)); + checklist_item.set('user_id', parseInt(item.user_id)); + checklist_item.set('checklist_id', parseInt(item.checklist_id)); + checklist_item.set('position', parseFloat(item.position)); + checklist_item.set('name', item.name); + checklist_item.set('is_completed', item.is_completed); + checklist_item.card = self.model.card; + checklist_item.checklist = new App.CheckList(); + checklist_item.checklist = self.model; + self.model.card.list.collection.board.checklist_items.add(checklist_item, { + silent: true + }); + self.model.checklist_items.add(checklist_item, { + silent: true + }); + }); + self.model.set({ + checklist_item_count: parseInt(self.model.attributes.checklist_item_count) + response.checklist_items.length + }, { + silent: true + }); + self.model.card.set('checklist_item_count', parseInt(self.model.card.attributes.checklist_item_count) + response.checklist_items.length); + } else { + var items = data.name.split('\n'); + var i = 1; + _.each(items, function(item) { + checklist_item = new App.CheckListItem(); + checklist_item.set('id', new Date().getTime()); + checklist_item.set('card_id', self.model.card.attributes.id); + checklist_item.set('user_id', parseInt(authuser.user.id)); + checklist_item.set('checklist_id', self.model.id); + checklist_item.set('position', i); + checklist_item.set('name', _.escape(item.replace('\r', ''))); + checklist_item.set('is_completed', false); + checklist_item.card = self.model.card; + checklist_item.checklist = new App.CheckList(); + checklist_item.checklist = self.model; + self.model.card.list.collection.board.checklist_items.add(checklist_item, { + silent: true + }); + self.model.checklist_items.add(checklist_item, { + silent: true + }); + i++; + }); + self.model.set({ + checklist_item_count: parseInt(self.model.attributes.checklist_item_count) + i + }, { + silent: true + }); + self.model.card.set({ + checklist_item_count: parseInt(self.model.card.attributes.checklist_item_count) + i + }); + } + self.model.card.list.collection.board.cards.get(self.model.card).set({ + checklist_item_count: self.model.card.attributes.checklist_item_count + }, { + silent: true + }); + self.renderItemsCollection(false); + self.renderProgress(); + if (!_.isUndefined(response.activities)) { + _.each(response.activities, function(_activity) { + var activity = new App.Activity(); + activity.set(_activity); + var view = new App.ActivityView({ + model: activity + }); + self.model.set('activities', activity); + var view_activity = $('#js-card-activities-' + self.model.card.attributes.id); + view_activity.prepend(view.render().el).find('.timeago').timeago(); + }); + } + } + }); + }, + /** + * renderProgress() + * display checklist item completed progress bar + * return false + * + */ + renderProgress: function() { + var completed_count = this.model.checklist_items.filter(function(checklist_item) { + return checklist_item.get('is_completed') === true || checklist_item.get('is_completed') == 'true'; + }).length; + var total_count = this.model.checklist_items.length; + completed_count = 0 < total_count ? Math.round(100 * completed_count / total_count) : 0; + $('#js-checklist-progress-bar-' + this.model.id).stop().animate({ + 'width': completed_count + '%', + '-moz-transition': 'all .6s ease', + '-ms-transition': 'all .6s ease', + '-o-transition': 'all .6s ease', + '-webkit-transition': 'all .6s ease', + 'transition': 'all .6s ease' + }, 100); + $('#js-checklist-progress-percent-' + this.model.id).text(completed_count + '%'); + return false; + }, + /** + * showItemOptions() + * show checklist item actions + * @param e + * @type Object(DOM event) + * + */ + showItemOptions: function(e) { + e.preventDefault(); + $('#js-item-add-option-response-' + this.model.id).html(new App.ChecklistItemActionsView({ + model: this.model + }).el); + }, + /** + * backToItemOptions() + * show checklist item actions + * @param e + * @type Object(DOM event) + * + */ + backToItemOptions: function(e) { + e.preventDefault(); + $('#js-item-add-option-response-' + this.model.id).html(new App.ChecklistItemActionsView({ + model: this.model + }).el); + return false; + }, + /** + * showMentionMemberForm() + * show board member list form + * @param e + * @type Object(DOM event) + * return false + * + */ + showMentionMemberForm: function(e) { + e.preventDefault(); + $('#js-item-add-option-response-' + this.model.id).html(new App.ChecklistItemMentionMemberSerachFormView().el); + this.$el.find('.js-item-member-search-response').html(''); + this.renderBoardUsers(); + return false; + }, + /** + * addCardMember() + * show board member in card + * @param e + * @type Object(DOM event) + * return false + * + */ + addItemMember: function(e) { + e.preventDefault(); + var member_id = $(e.currentTarget).data('member-id'); + var selected_user = this.model.card.list.collection.board.board_users.get({ + id: parseInt(member_id) + }); + var target = $('form.js-add-item textarea'); + target.val(target.val() + ' @' + selected_user.attributes.username); + return false; + }, + /** + * showSearchMembers() + * display searched member list + */ + showSearchItemMembers: function(e) { + var self = this; + var q = $(e.target).val(); + if (q !== '') { + var filtered_users = this.model.card.list.collection.board.board_users.search(q); + var users = new App.UserCollection(); + if (!_.isEmpty(filtered_users._wrapped)) { + $.unique(filtered_users._wrapped); + } + users.add(filtered_users._wrapped); + $('.js-item-member-search-response').html(''); + if (!_.isEmpty(users.models)) { + users.each(function(board_user) { + self.$el.find('.js-item-member-search-response').append(new App.ChecklistItemMentionMemberView({ + model: board_user + }).el); + }); + } else { + $('.js-item-member-search-response').html(new App.ChecklistItemMentionMemberView({ + model: null + }).el); + } + } else { + this.$el.find('.js-item-member-search-response').html(''); + this.renderBoardUsers(); + } + }, + renderBoardUsers: function() { + var view = this.$el.find('.js-item-member-search-response'); + if (!_.isEmpty(this.model.card.list.collection.board.board_users.models)) { + this.model.card.list.collection.board.board_users.each(function(board_user) { + view.append(new App.ChecklistItemMentionMemberView({ + model: board_user + }).el); + }); + } else { + view.html(new App.ChecklistItemMentionMemberView({ + model: null + }).el); + } + }, + noAction: function(e) { + e.preventDefault(); + return false; + }, + onEnter: function(e) { + if (e.which === 13) { + e.preventDefault(); + var form = $(e.target).closest('form'); + if (form.attr('name') === 'checklistItemAddForm') { + $('input[type=submit]', form).trigger('click'); + } else { + return false; + } + } + } +}); diff --git a/client/js/views/card_copy_view.js b/client/js/views/card_copy_view.js new file mode 100644 index 000000000..ca388d3ed --- /dev/null +++ b/client/js/views/card_copy_view.js @@ -0,0 +1,44 @@ +/** + * @fileOverview This file has functions related to card copy view. This view calling from list view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : list model. @see Available Object in ListView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * CardCopy View + * @class CardCopyView + * @constructor + * @extends Backbone.View + */ +App.CardCopyView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/card_copy'], + tagName: 'ul', + className: 'dropdown-menu dropdown-menu-right arrow arrow-right js-card-action-list-response js-dropdown-popup', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + card: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/card_duedate_from_view.js b/client/js/views/card_duedate_from_view.js new file mode 100644 index 000000000..390985602 --- /dev/null +++ b/client/js/views/card_duedate_from_view.js @@ -0,0 +1,45 @@ +/** + * @fileOverview This file has functions related to card duedate form view. This view calling from modal card view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : card model. @see Available Object in CardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * CardDuedateFrom View + * @class CardDuedateFromView + * @constructor + * @extends Backbone.View + */ +App.CardDuedateFromView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/card_duedate_from'], + tagName: 'form', + className: 'form-horizontal clearfix js-card-edit-form', + id: 'cardDueDateEditForm', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + card: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/card_label_form_view.js b/client/js/views/card_label_form_view.js new file mode 100644 index 000000000..6df84a382 --- /dev/null +++ b/client/js/views/card_label_form_view.js @@ -0,0 +1,43 @@ +/** + * @fileOverview This file has functions related to card label form view. This view calling from modal card view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : labels collection. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * CardLabelForm View + * @class CardLabelFormView + * @constructor + * @extends Backbone.View + */ +App.CardLabelFormView = Backbone.View.extend({ + template: JST['templates/card_label_form'], + tagName: 'li', + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + labels: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/card_label_view.js b/client/js/views/card_label_view.js new file mode 100644 index 000000000..dd7adf032 --- /dev/null +++ b/client/js/views/card_label_view.js @@ -0,0 +1,63 @@ +/** + * @fileOverview This file has functions related to card label view. This view calling from modal card view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : label model. It contain all card based object @see Available Object in App.CardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * CardLabel View + * @class CardLabelView + * @constructor + * @extends Backbone.View + */ +App.CardLabelView = Backbone.View.extend({ + template: JST['templates/card_label'], + tagName: 'li', + className: 'js-card-label-show card-label-show navbar-btn', + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: {}, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + label: this.model, + background: this.getLabelcolor('' + this.model.attributes.name).substring(0, 6) + })); + this.showTooltip(); + return this; + }, + /** + * getLabelcolor() + * generate color code + * @param string + * @type string + * @return color code + * @type string + */ + getLabelcolor: function(string) { + return calcMD5(string).slice(0, 6); + }, + /** + * deleteLabel() + * delete card label + * @return false + * + */ + deleteLabel: function() { + this.model.destroy(); + this.model.url = api_url + 'boards/' + this.model.collection.card.get('board_id') + '/lists/' + this.model.collection.card.get('list_id') + '/cards/' + this.model.collection.card.id + '/labels/' + this.model.id + '.json'; + this.$el.remove(); + return false; + } +}); diff --git a/client/js/views/card_labels_form_view.js b/client/js/views/card_labels_form_view.js new file mode 100644 index 000000000..549fa8655 --- /dev/null +++ b/client/js/views/card_labels_form_view.js @@ -0,0 +1,43 @@ +/** + * @fileOverview This file has functions related to card label form view. This view calling from card view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : card model. It contain all card based object @see Available Object in App.CardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * CardLabelsForm View + * @class CardLabelsFormView + * @constructor + * @extends Backbone.View + */ +App.CardLabelsFormView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/card_labels_form'], + tagName: 'div', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + card: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/card_member_form_view.js b/client/js/views/card_member_form_view.js new file mode 100644 index 000000000..a189a4122 --- /dev/null +++ b/client/js/views/card_member_form_view.js @@ -0,0 +1,43 @@ +/** + * @fileOverview This file has functions related to card member form view. This view calling from card view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : card model. It contain all card based object @see Available Object in App.CardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * CardMemberForm View + * @class CardMemberFormView + * @constructor + * @extends Backbone.View + */ +App.CardMemberFormView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/card_member_form'], + tagName: 'div', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.el = this.template({ + card: this.model + }); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/card_positions_form_view.js b/client/js/views/card_positions_form_view.js new file mode 100644 index 000000000..0c53db739 --- /dev/null +++ b/client/js/views/card_positions_form_view.js @@ -0,0 +1,45 @@ +/** + * @fileOverview This file has functions related to card position form view. This view calling from card view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : card model. It contain all card based object @see Available Object in App.CardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * CardLabelsForm View + * @class CardPositionsFormView + * @constructor + * @extends Backbone.View + */ +App.CardPositionsFormView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.boards = options.boards; + this.render(); + }, + template: JST['templates/card_positions_form'], + tagName: 'li', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + card: this.model, + boards: this.boards + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/card_search_result_view.js b/client/js/views/card_search_result_view.js new file mode 100644 index 000000000..b8d07d0b5 --- /dev/null +++ b/client/js/views/card_search_result_view.js @@ -0,0 +1,44 @@ +/** + * @fileOverview This file has functions related to card search view. This view calling from list view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : card model. It contain all board based object @see Available Object in App.CardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * CardSearchResult View + * @class CardSearchResultView + * @constructor + * @extends Backbone.View + */ +App.CardSearchResultView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/card_search_result'], + tagName: 'li', + className: 'js-select-card', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + card: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/card_search_users_result_view.js b/client/js/views/card_search_users_result_view.js new file mode 100644 index 000000000..ab35fdbbe --- /dev/null +++ b/client/js/views/card_search_users_result_view.js @@ -0,0 +1,46 @@ +/** + * @fileOverview This file has functions related to card search user list view. This view calling from instanr card add view and modal card view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : user model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * CardSearchUsersResult View + * @class CardSearchUsersResultView + * @constructor + * @extends Backbone.View + */ +App.CardSearchUsersResultView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.is_added_user = options.is_added_user; + this.added_user = options.added_user; + this.render(); + }, + template: JST['templates/card_search_users_result'], + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.el = this.template({ + user: this.model, + is_added_user: this.is_added_user, + added_user: this.added_user + }); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/card_view.js b/client/js/views/card_view.js new file mode 100644 index 000000000..59d193e74 --- /dev/null +++ b/client/js/views/card_view.js @@ -0,0 +1,563 @@ +/** + * @fileOverview This file has functions related to card view. This view calling from list view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : card model and it's related values + * this.model.attachments : attachments collection(Based on card) + * this.model.board_users : board users collection(Based on board) + * this.model.card_voters : card users collection(Based on card) + * this.model.cards_subscribers : card subscribers collection(Based on card) + * this.model.checklists : checklists collection(Based on card) + * this.model.labels : labels collection(Based on card) + * this.model.list : list model(Based on card). It contain all list based object @see Available Object in App.ListView + * this.model.users : card users collection(Based on card) + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Card View + * @class CardView + * @constructor + * @extends Backbone.View + */ +App.CardView = Backbone.View.extend({ + template: JST['templates/card'], + converter: new Showdown.converter(), + /** + * Constructor + * initialize default values and actions + */ + initialize: function(opts) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + opts = opts || {}; + this.tmp = opts.template; + this.board_id = opts.board_id; + this.card_users = []; + _.bindAll(this, 'render', 'renderListChange'); + if (!_.isUndefined(opts.template)) { + this.template = JST['templates/card_list_view']; + } + if (!_.isEmpty(this.model)) { + this.model.bind('change:id change:name change:description change:board_id change:cards_checklists change:cards_labels change:cards_subscribers change:is_archived change:due_date change:list_id change:title change:checklist_item_completed_count change:checklist_item_count change:is_offline', this.render); + this.model.bind('change:list_id', this.renderListChange); + if (this.model.has('list')) { + this.list = this.model.get('list'); + this.model.unset('list'); + } + if (this.model.has('card_attachments')) { + this.model.attachments.add(this.model.get('card_attachments'), { + silent: true + }); + } + if (this.model.has('cards_checklists')) { + this.model.checklists.add(this.model.get('cards_checklists'), { + silent: true + }); + } + if (this.model.has('cards_users')) { + this.model.users.add(this.model.get('cards_users'), { + silent: true + }); + } + if (this.model.has('board_users')) { + this.model.board_users.add(this.model.get('board_users'), { + silent: true + }); + } + if (this.model.has('cards_voters')) { + this.model.card_voters.add(this.model.get('cards_voters'), { + silent: true + }); + } + if (this.model.has('cards_subscribers')) { + this.model.cards_subscribers.add(this.model.get('cards_subscribers'), { + silent: true + }); + } + this.model.attachments.bind('add', this.render); + this.model.bind('change:id', this.render); + this.model.attachments.bind('remove', this.render); + this.model.labels.bind('remove', this.render); + this.model.labels.bind('add', this.render); + this.model.labels.bind('change', this.render); + this.model.users.bind('add', this.render); + this.model.users.bind('add:id', this.render); + this.model.users.bind('change:id', this.render); + this.model.users.bind('remove', this.render); + if (!_.isUndefined(this.model.list)) { + this.model.list.collection.board.labels.bind('add', this.render); + this.model.list.collection.board.labels.bind('remove', this.render); + this.model.list.collection.board.activities.bind('add remove', this.render); + } + this.model.cards_subscribers.bind('add', this.render); + this.model.cards_subscribers.bind('remove', this.render); + this.model.card_voters.bind('add', this.render); + this.model.card_voters.bind('remove', this.render); + } + }, + className: 'panel js-show-modal-card-view js-board-list-card cur', + attributes: { + 'data-toggle': 'modal', + 'data-target': '#myModal', + 'href': '#' + }, + templateAdd: JST['templates/card_add'], + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-cancel-card-add': 'hideAddCardFrom', + 'click': 'showCardModal', + 'click .js-show-modal-card-view': 'showCardModal', + 'click .js-show-card-action-list': 'showCardActionList', + 'click .js-back-to-card-actions': 'backToCardActions', + 'click .js-show-add-member-form': 'showAddMemberForm', + 'click .js-show-card-label-form': 'showCardLabelForm', + 'click .js-show-card-position-form': 'showCardPositionForm', + 'change .js-change-card-position': 'selectCardPosition', + 'change .js-position': 'changeCardPosition', + 'click .js-add-card-member': 'addCardMember', + 'click .js-remove-card-member': 'removeCardMember', + 'cardSort': 'cardSort' + }, + /** + * cardSort() + * save the card moved position + * @param e + * @type Object(DOM event) + * @param data + * @type Object + * + */ + cardSort: function(ev, ui) { + var target = $(ev.target); + var data = {}; + var list_id = parseInt(target.parents('.js-board-list:first').data('list_id')); + var previous_card_id = target.prev('.js-board-list-card').data('card_id'); + var next_card_id = target.next('.js-board-list-card').data('card_id'); + this.model.url = api_url + 'boards/' + this.model.attributes.board_id + '/lists/' + this.model.attributes.list_id + '/cards/' + this.model.attributes.id + '.json'; + if ((typeof previous_card_id == 'undefined' && typeof next_card_id == 'undefined') || list_id != this.model.attributes.list_id) { + data.list_id = list_id; + } + if (typeof previous_card_id != 'undefined') { + this.model.moveAfter(previous_card_id); + } else if (typeof next_card_id != 'undefined') { + this.model.moveBefore(next_card_id); + } + this.model.set({ + list_id: list_id + }, { + silent: true + }); + data.position = this.model.attributes.position; + this.model.save(data, { + patch: true, + silent: true + }); + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function(ops) { + var content = ''; + var self = this; + if (_.isUndefined(this.tmp) && self.model !== null && !_.isEmpty(self.model) && !_.isUndefined(this.model.list) && !_.isUndefined(this.model.list.collection)) { + content += '
            '; + var filtered_labels = this.model.list.collection.board.labels.where({ + card_id: self.model.id + }); + var labels = new App.CardLabelCollection(); + labels.add(filtered_labels); + labels.each(function(label) { + if (_.escape(label.attributes.name) !== "") { + content += '
          • ' + _.escape(label.attributes.name) + '
          • '; + } + }); + content += '
          '; + this.$el.html(content); + content = '
            '; + this.model.users.each(function(user) { + content += '
          • user-filter-' + user.get('user_id') + '
          • '; + }); + content += '
          '; + this.$el.append(content); + content = '
            '; + content += '
          • ' + this.getDue(this.model.get('to_date')) + '
          • '; + content += '
          '; + this.$el.append(content); + this.$el.append(this.template({ + card: this.model, + converter: this.converter + })); + if (!_.isUndefined(this.model.attributes.name) && this.model.attributes.name !== '') { + this.$el.addClass('panel js-show-modal-card-view js-board-list-card cur').removeAttr('id').attr('data-toggle', 'modal').attr('data-target', '#myModal').attr('data-card_id', this.model.id).attr('id', 'js-card-' + this.model.id); + } + } else if (self.model !== null && !_.isEmpty(self.model) && !_.isUndefined(self.model.attributes.id) && !_.isUndefined(this.model.list) && !_.isUndefined(this.model.list.collection)) { + content += '
            '; + var filtered_card_labels = this.model.list.collection.board.labels.where({ + card_id: self.model.id + }); + var card_labels = new App.CardLabelCollection(); + card_labels.add(filtered_card_labels); + var card_labels_length = card_labels.models.length; + for (var card_labels_i = 0; card_labels_i < card_labels_length; card_labels_i++) { + var label = card_labels.models[card_labels_i]; + if (_.escape(label.attributes.name) !== "") { + content += '
          • ' + _.escape(label.attributes.name) + '
          • '; + } + } + content += '
          '; + this.$el.html(content); + content = '
            '; + var users = this.model.users; + var card_users_length = users.models.length; + for (var card_users_i = 0; card_users_i < card_users_length; card_users_i++) { + var user = users.models[card_users_i]; + content += '
          • user-filter-' + user.attributes.user_id + '
          • '; + } + content += '
          '; + this.$el.append(content); + content = '
            '; + content += '
          • ' + this.getDue(this.model.get('to_date')) + '
          • '; + content += '
          '; + this.$el.append(content); + this.$el.append(self.template({ + card: self.model + })); + } else if (self.model === null) { + $('.js-card-list-view-' + this.board_id).html(this.$el.append(self.template({ + card: self.model + }))); + } + return this; + }, + /** + * renderAdd() + * render card add form + * @return object + * + */ + renderAdd: function() { + this.$el.html(this.templateAdd({ + model: this.model + })); + return this; + }, + /** + * hideAddCardFrom() + * hide card add form + * @param e + * @type Object(DOM event) + * @return false + * + */ + hideAddCardFrom: function(e) { + $('#js-list-card-add-form-' + this.model.attributes.list_id).remove(); + $('.js-show-add-card-form', $('#js-card-listing-' + this.model.attributes.list_id).next()).removeClass('hide'); + return false; + }, + /** + * addCard() + * add card to list + * @param e + * @type Object(DOM event) + * + */ + addCard: function(e) { + e.preventDefault(); + var self = this; + var data = $(e.target).parents('form.js-cardAddForm').serializeObject(); + if (data.selected_list_id !== undefined) { + data.list_id = data.selected_list_id; + } + $('.js-show-add-card-form', $('#js-card-listing-' + this.model.attributes.list_id).next()).removeClass('hide'); + self.model.unset('list'); + self.model.set(data); + this.list.cards.push(self.model); + self.model.collection.add(self.model); + self.model.url = api_url + 'boards/' + this.model.attributes.board_id + '/lists/' + this.model.attributes.list_id + '/cards.json'; + self.model.save({}, { + success: function(model, response) { + self.model.set('id', parseInt(response.id)); + self.list.collection.board.activities.unshift(response.activity); + } + }); + }, + /** + * selectCardPosition() + * change position based on selected list + * @param string + * @type string + */ + selectCardPosition: function(e) { + var target = $(e.currentTarget); + var self = this; + var list_id = target.val(); + var board_id = $(e.target).parents('form.js-cardAddForm').find('.js-selected-board').val(); + var content_position = ''; + self.$el.find('.js-card-add-list').val(list_id); + var list = this.model.list.collection.board.lists.findWhere({ + id: parseInt(list_id) + + }); + var filtered_cards_count = this.model.list.collection.board.cards.where({ + list_id: list.attributes.id, + is_archived: false + }).length; + + var current_position = filtered_cards_count + 1; + for (var i = 1; i <= filtered_cards_count; i++) { + + content_position += ''; + + } + self.$el.find('.js-card-add-position').val(i); + content_position += ''; + self.$el.find('.js-position').html(content_position); + + }, + /** + * changeCardPosition() + * change position based on selected list + * @param string + * @type string + */ + changeCardPosition: function(e) { + var target = $(e.currentTarget); + var posoition = target.val(); + this.$el.find('.js-card-add-position').val(posoition); + + }, + /** + * showCardModal() + * show card detail in docmodal + * @param e + * @type Object(DOM event) + * @return false + * + */ + showCardModal: function() { + if (this.model === null || _.isEmpty(this.model)) { + return false; + } + if (!_.isEmpty(this.model.attributes.name)) { + changeTitle('Card - ' + _.escape(this.model.attributes.name) + ' on ' + _.escape(this.model.list.collection.board.attributes.name)); + } + var current_param = Backbone.history.fragment; + if (!_.isUndefined(this.model.id) && (card_ids_ref[0] === 0 || _.indexOf(card_ids_ref, this.model.id) === -1)) { + if (!card_ids_ref) { + card_ids_ref = []; + } + card_ids_ref.push(this.model.id); + if (current_param.indexOf('/card/') != -1) { + current_param += ',' + this.model.id; + } else { + current_param += '/card/' + this.model.id; + } + current_param = current_param.replace('/board', 'board'); + app.navigate('#/' + current_param, { + trigger: false, + trigger_function: false, + }); + } + if (!_.isUndefined(this.model.id)) { + var modalView = new App.ModalCardView({ + model: this.model + }); + var view_card = this.$('#js-card-listing-' + this.model.id); + view_card.html(' '); + modalView.show(); + return false; + } + }, + /** + * getDue() + * show card due date + * @param card_due_date + * @type String + * @return message + * @type String + * + */ + getDue: function(card_due_date) { + if (card_due_date === null) { + return ''; + } + var today = new Date(); + var last_day = new Date(today.getFullYear(), today.getMonth() + 1, 0); + var next_month_last_day = new Date(today.getFullYear(), today.getMonth() + 2, 0); + var due_date = new Date(card_due_date); + var diff = Math.floor(due_date.getTime() - today.getTime()); + + var day = 1000 * 60 * 60 * 24; + var days = Math.floor(diff / day); + var months = Math.floor((days + (today.getDate() + 1)) / next_month_last_day.getDate()); + + var years = Math.floor(months / 12); + var week = days - (6 - (today.getDay())); + var message = 'feature'; + if (years > 0) { + message = 'year'; + } else if (months == 1) { + message = 'month'; + } else if (days === 0) { + message = 'day'; + } else if (week >= 0 && week <= 6) { + message = 'week'; + } else if (years < 0 || months < 0 || days < 0) { + message = 'overdue'; + } + return message; + }, + /** + * showCardActionList() + * render card actions + * @param e + * @type Object(DOM event) + * + */ + showCardActionList: function(e) { + e.preventDefault(); + $(e.currentTarget).next().remove(); + $(e.currentTarget).after(new App.CardActionsView({ + model: this.model + }).el); + }, + /** + * showAddMemberForm() + * show card member add form + * @param e + * @type Object(DOM event) + * return false + * + */ + showAddMemberForm: function(e) { + e.preventDefault(); + $('.js-card-action-list-response').html(new App.CardMemberFormView({ + model: this.model + }).el); + return false; + }, + /** + * showCardLabelForm() + * show card label add form + * @param e + * @type Object(DOM event) + * return false + * + */ + showCardLabelForm: function(e) { + var self = this; + $('.js-card-action-list-response').html(new App.CardLabelsFormView({ + model: this.model + }).el); + $('.js-card-label').select2({ + tags: this.model.list.collection.board.labels.pluck('name'), + tokenSeparators: [',', ' '] + }).on('select2-selecting', function(e) { + var labels = _.pluck(self.$('.js-card-label').select2('data'), 'text'); + labels.push(e.choice.text); + self.$el.find('.js-card-add-labels').val(labels.join(',')); + }).on('select2-removed', function(e) { + var _labels = _.pluck(self.$('.js-card-label').select2('data'), 'text'); + self.$el.find('.js-card-add-labels').val(_labels.join(',')); + }); + return false; + }, + /** + * showCardPositionForm() + * show card position add form + * @param e + * @type Object(DOM event) + * return false + * + */ + showCardPositionForm: function(e) { + $('.js-card-action-list-response').html(new App.CardPositionsFormView({ + model: this.model, + boards: App.boards, + }).el); + + return false; + }, + /** + * backToCardActions() + * display card actions list + * @param e + * @type Object(DOM event) + * return false + * + */ + backToCardActions: function(e) { + $('a.js-show-card-action-list').next().remove(); + $('a.js-show-card-action-list').after(new App.CardActionsView({ + model: this.model + }).el); + return false; + }, + /** + * getLabelcolor() + * generate color code + * @param string + * @type string + * @return color code + * @type string + */ + getLabelcolor: function(string) { + return calcMD5(string).slice(0, 6); + }, + /** + * showCardPositionForm() + * show card position add form + * @param e + * @type Object(DOM event) + * return false + * + */ + addCardMember: function(e) { + var target = $(e.currentTarget); + target.removeClass('js-add-card-member').addClass('js-remove-card-member'); + target.append(''); + var user_id = target.data('user-id'); + this.card_users.push(parseInt(user_id)); + $.unique(this.card_users); + this.$el.find('.js-card-user-ids').val(this.card_users.join(',')); + return false; + }, + /** + * removeCardMember() + * remove card member + * @param e + * @type Object(DOM event) + * + */ + removeCardMember: function(e) { + e.preventDefault(); + var target = $(e.currentTarget); + target.removeClass('js-remove-card-member').addClass('js-add-card-member'); + target.find('i.icon-ok').remove(); + var user_id = target.data('user-id'); + $.unique(this.card_users); + this.card_users.splice($.inArray(parseInt(user_id), this.card_users), 1); + this.$el.find('.js-card-user-ids').val(this.card_users.join(',')); + }, + /** + * renderListChange() + * renderListChange + * @param e + * @type Object(DOM event) + * + */ + renderListChange: function() { + this.$el.remove(); + this.render(); + } +}); diff --git a/client/js/views/card_voters_list_view.js b/client/js/views/card_voters_list_view.js new file mode 100644 index 000000000..c621bdcdf --- /dev/null +++ b/client/js/views/card_voters_list_view.js @@ -0,0 +1,43 @@ +/** + * @fileOverview This file has functions related to card voter list view. This view calling from modal card view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : card model. It contain all board based object @see Available Object in App.CardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * CardVotersList View + * @class CardVotersListView + * @constructor + * @extends Backbone.View + */ +App.CardVotersListView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/card_voters_list'], + tagName: 'div', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.el = this.template({ + card: this.model + }); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/chat_view.js b/client/js/views/chat_view.js new file mode 100644 index 000000000..efc252251 --- /dev/null +++ b/client/js/views/chat_view.js @@ -0,0 +1,42 @@ +/** + * @fileOverview This file has functions related to chat view. This view calling from footer view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : undefined. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Chat View + * @class ChatView + * @constructor + * @extends Backbone.View + */ +App.ChatView = Backbone.View.extend({ + template: JST['templates/chat'], + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + this.render(); + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template()).attr('title', 'Chat'); + this.$el.dockmodal({ + initialState: 'docked', + height: 300, + width: 400 + }); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/checklist_actions_view.js b/client/js/views/checklist_actions_view.js new file mode 100644 index 000000000..8a6deaa77 --- /dev/null +++ b/client/js/views/checklist_actions_view.js @@ -0,0 +1,48 @@ +/** + * @fileOverview This file has functions related to checklist action view. This view calling from card checklist view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : checklist model. It contain all checklist based object @see Available Object in App.CardCheckListView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ChecklistActions View + * @class ChecklistActionsView + * @constructor + * @extends Backbone.View + */ +App.ChecklistActionsView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/checklist_actions'], + tagName: 'a', + className: 'js-show-confirm-checklist-delete', + attributes: { + 'title': 'Delete This Checklist', + 'href': '#' + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + checklist: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/checklist_add_form_view.js b/client/js/views/checklist_add_form_view.js new file mode 100644 index 000000000..e9dd1bde0 --- /dev/null +++ b/client/js/views/checklist_add_form_view.js @@ -0,0 +1,47 @@ +/** + * @fileOverview This file has functions related to checklist add form view. This view calling from modal card view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : card model. It contain all card based object @see Available Object in App.CardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ChecklistAddForm View + * @class ChecklistAddFormView + * @constructor + * @extends Backbone.View + */ +App.ChecklistAddFormView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/checklist_add_form'], + tagName: 'form', + className: 'js-add-checklist', + attributes: { + 'method': 'POST' + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + card: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/checklist_delete_confirm_form_view.js b/client/js/views/checklist_delete_confirm_form_view.js new file mode 100644 index 000000000..944343116 --- /dev/null +++ b/client/js/views/checklist_delete_confirm_form_view.js @@ -0,0 +1,43 @@ +/** + * @fileOverview This file has functions related to checklist delete confirm form view. This view calling from card checklist view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : checklist model. It contain all card checklist object @see Available Object in App.CardCheckListView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ChecklistDeleteConfirmForm View + * @class ChecklistDeleteConfirmFormView + * @constructor + * @extends Backbone.View + */ +App.ChecklistDeleteConfirmFormView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/checklist_delete_confirm_form'], + tagName: 'li', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + checklist: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/checklist_edit_form_view.js b/client/js/views/checklist_edit_form_view.js new file mode 100644 index 000000000..50a2eac8b --- /dev/null +++ b/client/js/views/checklist_edit_form_view.js @@ -0,0 +1,47 @@ +/** + * @fileOverview This file has functions related to checklist edit form view. This view calling from card checklist view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : checklist model. It contain all card checklist object @see Available Object in App.CardCheckListView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ChecklistAddForm View + * @class ChecklistAddFormView + * @constructor + * @extends Backbone.View + */ +App.ChecklistEditFormView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/checklist_edit_form'], + tagName: 'form', + className: 'col-xs-12 checklist-edit-form form-horizontal', + attributes: { + 'method': 'POST' + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + checklist: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/checklist_item_actions_view.js b/client/js/views/checklist_item_actions_view.js new file mode 100644 index 000000000..3b9169395 --- /dev/null +++ b/client/js/views/checklist_item_actions_view.js @@ -0,0 +1,43 @@ +/** + * @fileOverview This file has functions related to checklist action view. This view calling from card checklist view and card checklist item view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : checklist model. It contain all card checklist object @see Available Object in App.CardCheckListView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ChecklistItemActions View + * @class ChecklistItemActionsView + * @constructor + * @extends Backbone.View + */ +App.ChecklistItemActionsView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/checklist_item_actions'], + tagName: 'div', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + checklist_item: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/checklist_item_add_form_view.js b/client/js/views/checklist_item_add_form_view.js new file mode 100644 index 000000000..5a46eb8dd --- /dev/null +++ b/client/js/views/checklist_item_add_form_view.js @@ -0,0 +1,48 @@ +/** + * @fileOverview This file has functions related to checklist iten add view. This view calling from card checklist view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : checklist model. It contain all card checklist object @see Available Object in App.CardCheckListView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ChecklistItemAddForm View + * @class ChecklistItemAddFormView + * @constructor + * @extends Backbone.View + */ +App.ChecklistItemAddFormView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/checklist_item_add_form'], + tagName: 'form', + className: 'js-add-item no-mar col-xs-12', + attributes: { + 'name': 'checklistItemAddForm', + 'method': 'POST' + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + checklist: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/checklist_item_add_link_view.js b/client/js/views/checklist_item_add_link_view.js new file mode 100644 index 000000000..df22727e7 --- /dev/null +++ b/client/js/views/checklist_item_add_link_view.js @@ -0,0 +1,42 @@ +/** + * @fileOverview This file has functions related to checklist item add link view. This view calling from card checklist view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : undefinded + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * BoardSidebar View + * @class BoardSidebarView + * @constructor + * @extends Backbone.View + */ +App.ChecklistItemAddLinkView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/checklist_item_add_link'], + tagName: 'div', + className: 'col-xs-12 h4 btn-link', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template()); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/checklist_item_delete_confirm_form_view.js b/client/js/views/checklist_item_delete_confirm_form_view.js new file mode 100644 index 000000000..2eb08785e --- /dev/null +++ b/client/js/views/checklist_item_delete_confirm_form_view.js @@ -0,0 +1,43 @@ +/** + * @fileOverview This file has functions related to checklist item delete confirm form view. This view calling from card checklist item view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : checklist item model. It contain all card checklist item object @see Available Object in App.CardCheckListItemView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ChecklistItemDeleteConfirmForm View + * @class ChecklistItemDeleteConfirmFormView + * @constructor + * @extends Backbone.View + */ +App.ChecklistItemDeleteConfirmFormView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/checklist_item_delete_confirm_form'], + tagName: 'div', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + checklist_item: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/checklist_item_edit_form_view.js b/client/js/views/checklist_item_edit_form_view.js new file mode 100644 index 000000000..b9a1a47a3 --- /dev/null +++ b/client/js/views/checklist_item_edit_form_view.js @@ -0,0 +1,47 @@ +/** + * @fileOverview This file has functions related to checklist item edit form view. This view calling from card checklist item view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board user collection + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ChecklistItemEditForm View + * @class ChecklistItemEditFormView + * @constructor + * @extends Backbone.View + */ +App.ChecklistItemEditFormView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/checklist_item_edit_form'], + tagName: 'form', + className: 'js-item-edit-form form-horizontal', + attributes: { + 'method': 'POST' + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + checklist_item: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/checklist_item_mention_member_search_form_view.js b/client/js/views/checklist_item_mention_member_search_form_view.js new file mode 100644 index 000000000..bcde03183 --- /dev/null +++ b/client/js/views/checklist_item_mention_member_search_form_view.js @@ -0,0 +1,41 @@ +/** + * @fileOverview This file has functions related to checklist item mention member search form view. This view calling from card checklist item view and card checklist view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : undefined. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ChecklistItemMentionMember View + * @class ChecklistItemMentionMemberView + * @constructor + * @extends Backbone.View + */ +App.ChecklistItemMentionMemberSerachFormView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/checklist_item_mention_member_search_form'], + tagName: 'div', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template()); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/checklist_item_mention_member_view.js b/client/js/views/checklist_item_mention_member_view.js new file mode 100644 index 000000000..4993c1b27 --- /dev/null +++ b/client/js/views/checklist_item_mention_member_view.js @@ -0,0 +1,44 @@ +/** + * @fileOverview This file has functions related to checklist item mention member view. This view calling from card checklist item view and card checklist view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : checklist item model. It contain all card checklist item object @see Available Object in App.CardCheckListItemView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ChecklistItemMentionMember View + * @class ChecklistItemMentionMemberView + * @constructor + * @extends Backbone.View + */ +App.ChecklistItemMentionMemberView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + this.class_name = options.class_name; + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/checklist_item_mention_member'], + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + board_user: this.model, + class_name: this.class_name + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/closed_boards_index_view.js b/client/js/views/closed_boards_index_view.js new file mode 100644 index 000000000..57691c378 --- /dev/null +++ b/client/js/views/closed_boards_index_view.js @@ -0,0 +1,74 @@ +/** + * @fileOverview This file has functions related to closed boards index view. This view calling from application view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : undefined. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * BoardsIndex View + * @class BoardsIndexView + * @constructor + * @extends Backbone.View + */ +App.ClosedBoardsIndexView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/closed_boards_index'], + tagName: 'section', + className: 'clearfix', + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-delete-board': 'deleteBoard', + }, + /** + * deleteBoard() + * delete board + * @param e + * @type Object(DOM event) + * @return false + * + */ + deleteBoard: function(e) { + if (confirm($(e.currentTarget).data('confirm'))) { + var dataUrl = $(e.currentTarget).attr('href'); + + var board = new App.Board(); + board.url = api_url + dataUrl + '.json'; + board.set('id', '-1'); + board.destroy({ + success: function() { + Backbone.history.loadUrl(); + } + }); + } + return false; + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + 'board': this.model + })); + this.showTooltip(); + return this; + }, +}); diff --git a/client/js/views/closed_boards_listing_view.js b/client/js/views/closed_boards_listing_view.js new file mode 100644 index 000000000..dc71b8b63 --- /dev/null +++ b/client/js/views/closed_boards_listing_view.js @@ -0,0 +1,100 @@ +/** + * @fileOverview This file has functions related to closed boardsL listing view. This view calling from footer view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board item model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ShowAllVisibility View + * @class ShowAllVisibilityView + * @constructor + * @extends Backbone.View + */ +App.ClosedBoardsListingView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.authuser = options.authuser; + this.render(); + }, + template: JST['templates/closed_boards_listing'], + tagName: 'li', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + events: { + 'click .js-close-popover': 'closePopup', + 'click .js-reopen-dropdown': 'openDropdown', + 'click .js-board-reopen': 'reopenBoard' + }, + + /** + * reopenBoard() + * reopen closed the board + * @return false + * + */ + reopenBoard: function(e) { + var board = new App.Board(); + board.url = api_url + 'boards/' + this.model.id + '.json'; + board.set('id', this.model.id); + App.boards.get(this.model.id).set('is_closed', false); + board.save({ + is_closed: false + }, { + success: function(model, response) { + app.navigate('#/board/' + board.get('id'), { + trigger: true, + replace: true + }); + } + }); + return false; + }, + /** + * openDropdown() + * copy the existing card + * @param e + * @type Object(DOM event) + * @return false + * + */ + openDropdown: function(e) { + e.preventDefault(); + $(e.currentTarget).addClass('open'); + return false; + }, + /** + * boardRename() + * close the dropdown + * @param e + * @type Object(DOM event) + * @return false + * + */ + closePopup: function(e) { + var el = this.$el; + var target = el.find(e.target); + $('.js-reopen-dropdown').removeClass('open'); + return false; + }, + render: function() { + this.$el.html(this.template({ + board: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/copy_board_visibility_view.js b/client/js/views/copy_board_visibility_view.js new file mode 100644 index 000000000..0c2201e48 --- /dev/null +++ b/client/js/views/copy_board_visibility_view.js @@ -0,0 +1,43 @@ +/** + * @fileOverview This file has functions related to copy board visibility view. This view calling from footer view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board name. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * CopyBoardVisibility View + * @class CopyBoardVisibilityView + * @constructor + * @extends Backbone.View + */ +App.CopyBoardVisibilityView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/show_all_visibility'], + tagName: 'div', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + name: this.model, + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/copy_card_view.js b/client/js/views/copy_card_view.js new file mode 100644 index 000000000..314309c8e --- /dev/null +++ b/client/js/views/copy_card_view.js @@ -0,0 +1,45 @@ +/** + * @fileOverview This file has functions related to copy card view. This view calling from modal card view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : card model. @see Available Object in App.CardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * CopyCard View + * @class CopyCardView + * @constructor + * @extends Backbone.View + */ +App.CopyCardView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.boards = options.boards; + this.render(); + }, + template: JST['templates/copy_card'], + tagName: 'div', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + card: this.model, + boards: this.boards + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/copy_from_existing_card_view.js b/client/js/views/copy_from_existing_card_view.js new file mode 100644 index 000000000..86c891d76 --- /dev/null +++ b/client/js/views/copy_from_existing_card_view.js @@ -0,0 +1,46 @@ +/** + * @fileOverview This file has functions related to copy from existing card view. This view calling from list view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : card model. @see Available Object in App.CardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * CopyFromExistingCard View + * @class CopyFromExistingCardView + * @constructor + * @extends Backbone.View + */ +App.CopyFromExistingCardView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.boards = options.boards; + this.render(); + }, + template: JST['templates/copy_from_existing_card'], + tagName: 'ul', + className: 'dropdown-menu dropdown-menu-right arrow arrow-right js-card-action-list-response js-dropdown-popup', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + card: this.model, + boards: this.boards, + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/copy_list_view.js b/client/js/views/copy_list_view.js new file mode 100644 index 000000000..bc3cae2b1 --- /dev/null +++ b/client/js/views/copy_list_view.js @@ -0,0 +1,43 @@ +/** + * @fileOverview This file has functions related to copy list view. This view calling from list view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : list model. @see Available Object in App.ListView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * CopyList View + * @class CopyListView + * @constructor + * @extends Backbone.View + */ +App.CopyListView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/copy_list'], + tagName: 'li', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + list: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/edit_activity_form_view.js b/client/js/views/edit_activity_form_view.js new file mode 100644 index 000000000..a61f7113f --- /dev/null +++ b/client/js/views/edit_activity_form_view.js @@ -0,0 +1,44 @@ +/** + * @fileOverview This file has functions related to edit activity form view. This view calling from modal card view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : activitiy model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * EditActivityForm View + * @class EditActivityFormView + * @constructor + * @extends Backbone.View + */ +App.EditActivityFormView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/edit_activity_form'], + tagName: 'form', + className: 'js-edit-comment', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + activity: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/edit_board_member_permission_to_adminl_view.js b/client/js/views/edit_board_member_permission_to_adminl_view.js new file mode 100644 index 000000000..6555b218b --- /dev/null +++ b/client/js/views/edit_board_member_permission_to_adminl_view.js @@ -0,0 +1,44 @@ +/** + * @fileOverview This file has functions related to edit board member permission to admin view. This view calling from board view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board user ID. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * UserCards View + * @class UserCardsView + * @constructor + * @extends Backbone.View + */ +App.EditBoardMemberPermissionToAdmin = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/edit_board_member_permission_to_admin'], + tagName: 'div', + className: 'clearfix', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + board_user_id: this.model, + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/edit_board_member_permission_to_normal_view.js b/client/js/views/edit_board_member_permission_to_normal_view.js new file mode 100644 index 000000000..96931181e --- /dev/null +++ b/client/js/views/edit_board_member_permission_to_normal_view.js @@ -0,0 +1,44 @@ +/** + * @fileOverview This file has functions related to edit board member permission to normal view. This view calling from board view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board user ID. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * UserCards View + * @class UserCardsView + * @constructor + * @extends Backbone.View + */ +App.EditBoardMemberPermissionToNormal = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/edit_board_member_permission_to_normal'], + tagName: 'div', + className: 'clearfix', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + board_user_id: this.model, + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/email_template_view.js b/client/js/views/email_template_view.js new file mode 100644 index 000000000..fde9c364a --- /dev/null +++ b/client/js/views/email_template_view.js @@ -0,0 +1,88 @@ +/** + * @fileOverview This file has functions related to email template view. This view calling from application view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : undefined. + */ +if (typeof App == 'undefined') { + App = {}; +} +App.EmailTemplateView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.id = options.id; + this.getListing(); + }, + template: JST['templates/email_templates'], + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'submit form#js-email-templates-form': 'updateTemplate', + }, + /** + * updateTemplate() + * @return false + */ + updateTemplate: function(e) { + var target = $(e.currentTarget); + var data = target.serializeObject(); + var self = this; + var email_template = new App.EmailTemplate(); + email_template.set('id', this.id); + email_template.url = api_url + 'email_templates/' + this.id + '.json'; + email_template.save(data, { + success: function(model, response) { + if (!_.isEmpty(response.success)) { + self.flash('success', response.success); + } else { + self.flash('danger', 'Email Template not updated properly.'); + } + } + }); + return false; + }, + /** + * getListing() + * get settings + * @return false + */ + getListing: function() { + self = this; + if (_.isUndefined(this.id)) { + this.id = 1; + } + email_templates = new App.EmailTemplateCollection(); + email_templates.url = api_url + 'email_templates/' + this.id + '.json'; + email_templates.fetch({ + cache: false, + abortPending: true, + success: function(collections, response) { + self.render(collections); + } + }); + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function(collections) { + this.$el.html(this.template({ + list: collections, + id: this.id + })); + $('.js-admin-email-menu').addClass('active'); + $('.js-admin-activity-menu, .js-admin-user-menu, .js-admin-role-menu, .js-admin-setting-menu').removeClass('active'); + return this; + } +}); diff --git a/client/js/views/error_404_view.js b/client/js/views/error_404_view.js new file mode 100644 index 000000000..30f6fdaf6 --- /dev/null +++ b/client/js/views/error_404_view.js @@ -0,0 +1,42 @@ +/** + * @fileOverview This file has functions related to error 404 view. This view calling from application view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : undefined. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Error404 View + * @class Error404View + * @constructor + * @extends Backbone.View + */ +App.Error404View = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/error_404'], + tagName: 'section', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + message: this.model + })); + return this; + } +}); diff --git a/client/js/views/flickr_view.js b/client/js/views/flickr_view.js new file mode 100644 index 000000000..019431c86 --- /dev/null +++ b/client/js/views/flickr_view.js @@ -0,0 +1,83 @@ +/** + * @fileOverview This file has functions related to attachment delete confirm view. This view calling from modal board and modal list view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : attachment model and it's related values + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Flickr View + * @class FlickrView + * @constructor + * @extends Backbone.View + */ +App.FlickrView = Backbone.View.extend({ + template: JST['templates/flickr'], + tagName: 'li', + className: 'clearfix navbar-btn', + initialize: function() { + this.render(); + }, + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click a.js-show-confirm-delete-attachment': 'showConfirmAttachmentDelete', + 'click .js-close-popup': 'closePopup', + 'click .js-delete-attachment': 'deleteAttachment' + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + photo: this.model + })); + this.showTooltip(); + return this; + }, + /** + * deleteAttachment() + * delete the attachment + * @param NULL + * @return false + * + */ + deleteAttachment: function() { + this.$el.remove(); + this.model.url = api_url + 'boards/' + this.model.attributes.board_id + '/lists/' + this.model.attributes.list_id + '/cards/' + this.model.attributes.card_id + '/attachments/' + this.model.id + '.json'; + this.model.destroy(); + return false; + }, + /** + * showConfirmAttachmentDelete() + * show the confirm attachment delete + * @param e + * @type Object(DOM event) + */ + showConfirmAttachmentDelete: function(e) { + e.preventDefault(); + $('.js-attachment-confirm-respons-' + this.model.id, this.$el).html(new App.AttachmentDeleteConfirmFormView({ + model: this.model + }).el); + }, + /** + * closePopup() + * close the opened dropdown + * @param e + * @type Object(DOM event) + * @return false + */ + closePopup: function(e) { + var target = $(e.target); + target.parents('li.dropdown').removeClass('open'); + return false; + } +}); diff --git a/client/js/views/footer_view.js b/client/js/views/footer_view.js new file mode 100644 index 000000000..ae4c15d71 --- /dev/null +++ b/client/js/views/footer_view.js @@ -0,0 +1,1513 @@ +/** + * @fileOverview This file has functions related to list view. This view calling from application view and board view. + * boardActivities() used for sync @see boardActivities function below + * Available Object: + * this.model : board model. It contain all board based object @see Available Object in App.BoardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Footer View + * @class FooterView + * @constructor + * @extends Backbone.View + */ +App.FooterView = Backbone.View.extend({ + template: JST['templates/footer'], + className: 'row', + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-open-popover': 'openPopup', + 'click .js-show-organizations-board-from': 'showOrganizationsBoardFrom', + 'click .js-show-board-add-form': 'showBoardAddForm', + 'click .js-show-organizations-add-form': 'showOrganizationsAddForm', + 'click .js-show-instant-card-from': 'showInstantCardFrom', + 'click .js-show-chat': 'showChat', + 'click .js-show-boards-list': 'showBoardsList', + 'click .js-collapse-myboards': 'collapseMyBoards', + 'click .js-collapse-closedboards': 'collapseClosedBoards', + 'click .js-collapse-starred-boards': 'collapseStarredBoards', + 'click .js-expand-myboards': 'expandMyBoards', + 'click .js-expand-closedboards': 'expandClosedBoards', + 'click .js-expand-starred-boards': 'expandStarredBoards', + 'keyup .js-search-boards': 'showSearchBoards', + 'click .js-all-activities': function() { + $('.js-all-activity-list').removeClass('hide'); + $('.js-boards-activity-list').addClass('hide'); + $('#js-load-link2').removeClass('hide'); + $('#js-load-link1').addClass('hide'); + this.userActivities(false, 2); + }, + 'click .js-all-user-activities': 'showUserActivities', + 'click .js-product-beat-action': 'actionBeat', + 'click .js-board-activities': function() { + is_append_activities = true; + $('#js-notification-load-more').removeClass('js-all-load-more').addClass('js-board-load-more'); + $('#js-notification-load-more-all').removeClass('js-all-load-more-all').addClass('js-board-load-more-all'); + $('.js-all-activity-list').addClass('hide'); + $('.js-boards-activity-list').removeClass('hide'); + $('#js-load-link2').addClass('hide'); + $('#js-load-link1').removeClass('hide'); + this.boardActivities(); + }, + 'click .js-all-board-activities': 'showBoardActivities', + 'click .js-notification-menu': 'notificationMenu', + 'keyup .js-search': 'qSearch', + 'click .js-close-popover': 'closePopup', + 'click .js-search': 'showSearchMsg', + 'focusout .js-search': 'searchClose', + 'submit form.js-instantCardAddForm': 'addInstantCard', + 'click .js-show-notification': 'showNotification', + 'click .js-back-boards-list': 'showBackBoardsList', + 'click .js-board-load-more': function(e) { + e.preventDefault(); + this.loadMore('board', '1'); + return false; + }, + 'click .js-all-load-more': function(e) { + e.preventDefault(); + this.loadMore('user', '1'); + return false; + }, + 'click .js-board-load-more-all': function(e) { + e.preventDefault(); + this.loadMore('board', '0'); + return false; + }, + 'click .js-all-load-more-all': function(e) { + e.preventDefault(); + this.loadMore('user', '0'); + return false; + }, + 'click .js-enable-desktop-notification': 'enabledesktopNotification', + 'click .js-show-board-import-form': 'showBoardImportForm', + 'change .js-board-import-file': 'importBoard', + }, + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.music_content = ''; + if (!_.isUndefined(App.music) && App.music.music_content !== null) { + this.music_content = App.music.music_content; + } + this.music_name = ''; + if (!_.isUndefined(App.music) && App.music.music_name !== null) { + this.music_name = App.music.music_name; + } + this.board_id = options.board_id; + this.board = options.board; + _.bindAll(this, 'renderClosedBoards', 'renderStarredBoards'); + if (!_.isUndefined(App.boards)) { + App.boards.bind('add remove change', this.renderClosedBoards); + } + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.model.is_show_enable_notification = false; + var current_param = Backbone.history.fragment; + var current_param_split = current_param.split('/'); + this.model.current_param = (current_param.indexOf('changepassword') === -1 && current_param.indexOf('login') === -1 && current_param.indexOf('forgotpassword') === -1 && current_param.indexOf('register') === -1 && current_param.indexOf('activation') === -1) ? current_param_split[0] : ''; + if (typeof Notification != 'undefined') { + this.model.is_show_enable_notification = (Notification.permission == 'default') ? true : false; + } + if (typeof Notification != 'undefined' && Notification.permission !== 'granted') { + Notification.requestPermission(function(permission) { + if (!('permission' in Notification)) { + Notification.permission = permission; + } + if (permission === 'granted') { + var notification = new Notification('Desktop notification enabled.'); + } + }); + } + this.$el.html(this.template({ + model: this.model, + board_id: this.board_id, + })); + var board_activities = new App.FooterView({ + model: authuser + }); + if (_.isEmpty(this.board_id)) { + if (!_.isUndefined(authuser.user)) { + if (authuser.user.notify_count > 0) { + $('#js-notification-count').removeClass('hide').html(authuser.user.notify_count); + favicon.badge(authuser.user.notify_count); + } else { + favicon.badge(0); + $('#js-notification-count').addClass('hide'); + } + clearInterval(set_interval_id); + set_interval_id = setInterval(function() { + board_activities.userActivities(false, 1); + }, 10000); + } + $('#js-load-link1').addClass('hide'); + $('#js-load-link2').addClass('hide'); + } else { + $('#js-load-link2').addClass('hide'); + } + + this.showTooltip(); + return this; + }, + /** + * actionBeat() + * show instant card add form + * @param e + * @type Object(DOM event) + * @return false + * + */ + actionBeat: function(e) { + var type = $(e.currentTarget).data('type'); + var set_type = ''; + var set_icon = ''; + var set_animation = ''; + var remove_animation = ''; + var remove_icon = ''; + var volume = false; + if (type === 'off') { + volume = true; + set_type = 'on'; + set_icon = 'icon-volume-up'; + set_animation = 'tada-animation'; + remove_icon = 'icon-volume-off text-muted'; + } else { + set_type = 'off'; + set_icon = 'icon-volume-off text-muted'; + remove_icon = 'icon-volume-up'; + remove_animation = 'tada-animation'; + } + var music_content = ''; + if (!_.isUndefined(authuser.user) && !_.isUndefined(authuser.user.is_productivity_beats)) { + var user = new App.User(); + user.url = api_url + 'users/' + authuser.user.id + '.json'; + user.set('id', parseInt(authuser.user.id)); + user.save({ + 'is_productivity_beats': volume + }); + authuser.user.is_productivity_beats = volume; + var Auth = JSON.parse(window.sessionStorage.getItem('auth')); + Auth.user.is_productivity_beats = volume; + window.sessionStorage.setItem('auth', JSON.stringify(Auth)); + $(e.currentTarget).data('type', set_type); + $(e.currentTarget).find('i').removeClass(remove_icon).addClass(set_icon); + $(e.currentTarget).find('i').removeClass(remove_animation).addClass(set_animation); + if (!_.isEmpty(App.music.music_content) && App.music.music_content != 'NULL') { + var repeatMusic = new App.MusicRepeatView(); + repeatMusic.continueMusic(); + } + } else { + if (volume === true) { + window.sessionStorage.setItem('music_play', "1"); + } else { + window.sessionStorage.setItem('music_play', "0"); + } + $(e.currentTarget).data('type', set_type); + $(e.currentTarget).find('i').removeClass(remove_icon).addClass(set_icon); + $(e.currentTarget).find('i').removeClass(remove_animation).addClass(set_animation); + if (window.sessionStorage.getItem('music_play') !== undefined && window.sessionStorage.getItem('music_play') === "1") { + var repeatMus = new App.MusicRepeatView(); + repeatMus.continueMusic(); + } + } + return false; + }, + /** + * showInstantCardFrom() + * show instant card add form + * @param e + * @type Object(DOM event) + * @return false + * + */ + showInstantCardFrom: function(e) { + e.preventDefault(); + var instantCardAdd = new App.InstantCardAdd({ + board_id: '', + list_id: '' + }); + var cardAddView = new App.InstantCardAddView({ + model: instantCardAdd, + board: this.board + }); + return false; + }, + /** + * showChat() + * show chat form + * @param e + * @type Object(DOM event) + * @return false + * + */ + showChat: function(e) { + e.preventDefault(); + var chat = new App.ChatView({ + model: chat, + }); + return false; + }, + /** + * showBackBoardsList() + * Back to Boards list form + * @param e + * @type Object(DOM event) + * @return false + * + */ + showBackBoardsList: function(e) { + var target = $(e.target); + target.parents('li.dropdown').addClass('open'); + $('li.js-back').remove(); + this.showBoardsList(e); + return false; + }, + /** + * openPopup() + * show dropdown + * @param e + * @type Object(DOM event) + * @return false + */ + openPopup: function(e) { + var target = $(e.target).parents('.dropdown:first'); + target.addClass('open').siblings('.dropdown.open').removeClass('open'); + var headerH = $('header').height(); + var footerH = $('footer').height(); + var windowH = $(window).height(); + var boardH = windowH - headerH - footerH - 14; + var boardlistH = $(e.target).next('.sidebar-boards-list').height(); + if (boardlistH > boardH) { + $(e.target).next('.sidebar-boards-list').css({ + 'max-height': boardH - 35 + }); + $(e.target).next('.sidebar-boards-list').addClass('vertical-scrollbar'); + } + return false; + }, + /** + * showOrganizationsBoardFrom() + * show organizations and board add link + * @param e + * @type Object(DOM event) + * @return false + * + */ + showOrganizationsBoardFrom: function(e) { + e.preventDefault(); + $('.js-show-boards-list-response').removeClass('hide'); + $('.js-boards-list-container-search').addClass('hide'); + $('.js-boards-list-container').addClass('hide'); + $('.js-qsearch-container').addClass('hide'); + var target = $(e.target); + var parent = target.parents('.js-show-add-boards-list'); + var insert = $('.js-show-boards-list-response', parent); + $(new App.OrganizationsBoardFormView({}).el).insertAfter(insert); + $('.js-show-boards-list-response').html(''); + return false; + }, + /** + * showBoardAddForm() + * show board add form + * @param e + * @type Object(DOM event) + * @return false + * + */ + showBoardAddForm: function(e) { + var workflow_template = new App.WorkFlowTemplateCollection(); + workflow_template.url = api_url + 'workflow_templates.json'; + workflow_template.fetch({ + success: function(model, response) { + var templates = ''; + $('li.js-back').remove(); + var target = $(e.target); + var parent = target.parents('.js-show-add-boards-list'); + var insert = $('.js-show-boards-list-response', parent); + $('.js-show-boards-list-response').html(new App.BoardAddView({ + model: workflow_template + }).el).find('#inputtemplatelist').select2({ + formatResult: function(repo) { + markup = '
          ' + repo.text + '' + repo.id + '
          '; + return markup; + } + }); + } + }); + return false; + }, + /** + * showOrganizationsAddForm() + * show board add form + * @param e + * @type Object(DOM event) + * @return false + * + */ + showOrganizationsAddForm: function(e) { + $('li.js-back').remove(); + var target = $(e.target); + var parent = target.parents('.js-show-add-boards-list'); + var insert = $('.js-show-boards-list-response', parent); + $('.js-show-boards-list-response').html(new App.OrganizationAddView().el); + return false; + }, + /** + * searchClose() + * close + * @param e + * @type Object(DOM event) + * @return false + * + */ + searchClose: function() { + $('.search-container').removeClass('search-tab'); + return false; + }, + /** + * closePopup() + * hide opened dropdown + * @param e + * @type Object(DOM event) + * @return false + * + */ + closePopup: function(e) { + var target = $(e.target); + target.parents('li.dropdown').removeClass('open'); + $('li.js-back').remove(); + return false; + }, + /** + * showBoardsList() + * display board lists + * @param e + * @type Object(DOM event) + * + */ + showBoardsList: function() { + $('.js-boards-list-container-search').removeClass('hide'); + $('.js-boards-list-container').addClass('hide'); + $('.js-qsearch-container').removeClass('hide'); + $('.js-show-boards-list-response').addClass('hide'); + $('li.js-back').remove(); + var recent_boards = ''; + var my_boards = ''; + var self = this; + self.boards = App.boards; + if (!_.isUndefined(App.boards)) { + App.boards.bind('add remove change', this.renderClosedBoards); + } + if (!_.isEmpty(role_links.where({ + slug: 'view_my_boards' + }))) { + var view_my_board = $('.js-myboard-list'); + view_my_board.html(''); + var is_displayed = false; + if (!_.isEmpty(App.boards.models)) { + _.each(App.boards.models, function(board) { + if (board.attributes.is_closed === false || board.attributes.is_closed === 'f') { + is_displayed = true; + view_my_board.append(new App.MyBoardsListingView({ + model: board, + authuser: authuser, + attributes: { + class: 'js-show-board-star' + } + }).el); + } + }); + } + if (!is_displayed) { + view_my_board.html(new App.MyBoardsListingView({ + model: null, + authuser: authuser, + attributes: { + class: 'clearfix text-center' + } + }).el); + } + } + this.renderStarredBoards(); + this.renderClosedBoards(); + + }, + /** + * renderStarredBoards() + * collapse my board lists + * @return false + * + */ + renderStarredBoards: function() { + this.boards = App.boards; + if (!_.isEmpty(this.boards) && !_.isEmpty(role_links.where({ + slug: 'view_stared_boards' + }))) { + var view_starred_board = $('.js-board-starred-list'); + view_starred_board.html(''); + var is_displayed = false; + if (!_.isEmpty(App.boards.models)) { + _.each(App.boards.models, function(board) { + var starred = board.boards_stars.findWhere({ + board_id: parseInt(board.id), + user_id: parseInt(authuser.user.id), + is_starred: true + }); + if (!_.isUndefined(starred) && !_.isEmpty(starred) && (board.attributes.is_closed !== true && board.attributes.is_closed !== 't')) { + is_displayed = true; + view_starred_board.append(new App.StartedBoardsListingView({ + model: board, + authuser: authuser, + attributes: { + class: 'js-show-board-star' + } + }).el); + } + }); + } + if (!is_displayed) { + view_starred_board.html(new App.StartedBoardsListingView({ + model: null, + authuser: authuser, + attributes: { + class: 'clearfix text-center' + } + }).el); + } + } + }, + /** + * renderClosedBoards() + * collapse my board lists + * @return false + * + */ + renderClosedBoards: function() { + if (!_.isEmpty(role_links.where({ + slug: 'view_closed_boards' + }))) { + var view_closed_board = $('.js-closedboard-list'); + view_closed_board.html(''); + var is_displayed = false; + if (!_.isEmpty(App.boards.models)) { + _.each(App.boards.models, function(board) { + if (board.attributes.is_closed === true) { + is_displayed = true; + view_closed_board.append(new App.ClosedBoardsListingView({ + model: board, + attributes: { + class: 'js-show-board-closed panel-default well-sm clearfix' + } + }).el); + } + }); + } + if (!is_displayed) { + view_closed_board.html(new App.ClosedBoardsListingView({ + model: null, + attributes: { + class: 'clearfix text-center' + } + }).el); + } + } + }, + /** + * collapseMyBoards() + * collapse my board lists + * @return false + * + */ + collapseMyBoards: function() { + $('.js-myboard-list').addClass('hide'); + $('.js-collapse-myboards').addClass('hide'); + $('.js-expand-myboards').removeClass('hide'); + return false; + }, + /** + * expandMyBoards() + * expand my board lists + * @return false + * + */ + expandMyBoards: function() { + $('.js-myboard-list').removeClass('hide'); + $('.js-collapse-myboards').removeClass('hide'); + $('.js-expand-myboards').addClass('hide'); + return false; + }, + /** + * collapseMyClosedBoards() + * collapse my closed board lists + * @return false + * + */ + collapseClosedBoards: function() { + $('.js-closedboard-list').addClass('hide'); + $('.js-collapse-closedboards').addClass('hide'); + $('.js-expand-closedboards').removeClass('hide'); + return false; + }, + /** + * expandMyClosedBoards() + * expand my closed board lists + * @return false + * + */ + expandClosedBoards: function() { + $('.js-closedboard-list').removeClass('hide'); + $('.js-collapse-closedboards').removeClass('hide'); + $('.js-expand-closedboards').addClass('hide'); + return false; + }, + /** + * collapseStarredBoards() + * collapse starred board lists + * @return false + * + */ + collapseStarredBoards: function() { + $('.js-board-starred-list').addClass('hide'); + $('.js-collapse-starred-boards').addClass('hide'); + $('.js-expand-starred-boards').removeClass('hide'); + return false; + }, + /** + * expandStarredBoards() + * expand starred board lists + * @return false + * + */ + expandStarredBoards: function() { + $('.js-board-starred-list').removeClass('hide'); + $('.js-collapse-starred-boards').removeClass('hide'); + $('.js-expand-starred-boards').addClass('hide'); + return false; + }, + /** + * showSearchBoards() + * display board search form + * @param e + * @type Object(DOM event) + * + */ + showSearchBoards: function(b) { + sd = [16, 17, 18, 27, 20]; + nc = [37, 39, 38, 40]; + var self = this; + if (!(e = b.keyCode, 0 <= checkKeycode(nc, e)) && !(c = b.keyCode, 0 <= checkKeycode(sd, c))) { + var q = $('#inputBoardSearch').val(); + if (q === '') { + $('.js-show-add-boards-list').removeClass("pre-scrollable"); + $('js-show-add-boards-list').removeClass("vertical-scrollbar"); + this.$el.find('.js-boards-list-container').nextAll('.js-board-search-result').remove(); + this.showBoardsList(b); + } else { + $('.js-show-add-boards-list').addClass("pre-scrollable"); + $('js-show-add-boards-list').addClass("vertical-scrollbar"); + var filtered_boards = App.boards.search(q); + var boards = new App.BoardCollection(); + if (!_.isEmpty(filtered_boards._wrapped)) { + $.unique(filtered_boards._wrapped); + } + boards.add(filtered_boards._wrapped); + var style = ''; + this.$el.find('.js-boards-list-container').nextAll('.js-board-search-result').remove(); + _.each(boards.models, function(board) { + if (board.attributes.background_picture_url) { + background_picture_url = board.attributes.background_picture_url.replace('_XXXX.jpg', '_b.jpg'); + style = 'background-image:url(' + board.attributes.background_picture_url + ');'; + } else if (board.attributes.background_pattern_url) { + background_pattern_url = board.attributes.background_pattern_url.replace('_XXXX.jpg', '_n.jpg'); + style = 'background-image:url(' + background_pattern_url + ');'; + } else if (board.attributes.background_color) { + style = 'background-color:' + board.attributes.background_color + ';'; + } else { + style = ''; + } + + $(new App.ShowSearchBoardsView({ + model: board, + style: style, + className: 'clearfix js-board-search-result', + attributes: { + style: style + }, + }).el).insertAfter(self.$el.find('.js-boards-list-container')); + + $('.js-boards-list-container').append(); + $('.js-boards-list-container-search').addClass('hide'); + $('.js-boards-list-container').removeClass('hide'); + + }); + if (boards.models.length === 0) { + $(new App.ShowSearchBoardsView({ + model: null, + style: style, + className: 'clearfix js-board-search-result', + }).el).insertAfter(self.$el.find('.js-boards-list-container')); + } + } + } + }, + /** + * userActivities() + * display user activiteis + * @param e + * @type Object(DOM event) + * + */ + userActivities: function(bool, mode) { + var self = this; + var activities = new App.ActivityCollection(); + var view_activity = $('#js-all-activities'); + var Auth = JSON.parse(window.sessionStorage.getItem('auth')); + if (_.isUndefined(authuser.user.last_activity_id)) { + authuser.user.last_activity_id = Auth.user.last_activity_id; + } + favCount = parseInt(Auth.user.notify_count); + if (mode == 1) { + query_string = '&last_activity_id=' + authuser.user.last_activity_id; + activities.url = api_url + 'users/' + authuser.user.id + '/activities.json?type=all' + query_string; + } else { + $('#js-activity-loader').remove(); + view_activity.append('
        • '); + activities.url = api_url + 'users/' + authuser.user.id + '/activities.json'; + } + activities.fetch({ + cache: false, + success: function() { + $('#js-activity-loader').remove(); + if (!_.isEmpty(activities.models)) { + var last_activity = _.min(activities.models, function(activity) { + return activity.id; + }); + last_user_activity_id = last_activity.id; + $('.notification-list').removeClass('notification-empty'); + $('#js-notification-load-more').removeClass('hide'); + var count = 0; + activities.each(function(activity) { + if (parseInt(activity.attributes.user_id) !== parseInt(authuser.user.id)) { + count += 1; + } + }); + var update_last_activity = _.max(activities.models, function(activity) { + return activity.id; + }); + if (mode == 1) { + favCount += parseInt(count); + favicon.badge(favCount); + if (parseInt(favCount) > 0) { + $('#js-notification-count').removeClass('hide').html(favCount); + } else { + $('#js-notification-count').addClass('hide'); + } + Auth = JSON.parse(window.sessionStorage.getItem('auth')); + Auth.user.last_activity_id = update_last_activity.id; + Auth.user.notify_count = favCount; + window.sessionStorage.setItem('auth', JSON.stringify(Auth)); + authuser.user.last_activity_id = update_last_activity.id; + } else if (mode == 2) { + if (favCount > 0) { + Auth = JSON.parse(window.sessionStorage.getItem('auth')); + var user = new App.User(); + user.url = api_url + 'users/' + authuser.user.id + '.json'; + user.set('id', parseInt(authuser.user.id)); + user.save({ + 'last_activity_id': update_last_activity.id + }); + authuser.user.notify_count = 0; + Auth.user.notify_count = 0; + favicon.badge(0); + $('#js-notification-count').addClass('hide'); + window.sessionStorage.setItem('auth', JSON.stringify(Auth)); + } + } + activities.each(function(activity) { + activity.from_footer = true; + + if (mode == 1 && parseInt(activity.attributes.user_id) !== parseInt(authuser.user.id) && Notification.permission === 'granted') { + var icon = window.location.pathname + 'img/logo-icon.png'; + if (activity.attributes.type != 'add_comment' && activity.attributes.type != 'edit_comment') { + var cardLink = activity.attributes.card_name; + activity.attributes.comment = activity.attributes.comment.replace('##CARD_LINK##', cardLink); + activity.attributes.comment = activity.attributes.comment.replace('##LABEL_NAME##', activity.attributes.label_name); + activity.attributes.comment = activity.attributes.comment.replace('##CARD_NAME##', activity.attributes.card_name); + activity.attributes.comment = activity.attributes.comment.replace('##DESCRIPTION##', activity.attributes.card_description); + activity.attributes.comment = activity.attributes.comment.replace('##LIST_NAME##', activity.attributes.list_name); + activity.attributes.comment = activity.attributes.comment.replace('##BOARD_NAME##', activity.attributes.card_description); + } else if (activity.attributes.type === 'add_comment') { + activity.attributes.comment = activity.attributes.username + ' commented in card ' + activity.attributes.card_name + ' ' + activity.attributes.comment; + } + new Notification(activity.attributes.comment, { + icon: icon + }); + } + var view = new App.ActivityView({ + model: activity, + type: 'all' + }); + if (mode == 1) { + view_activity.prepend(view.render().el).find('.timeago').timeago(); + } else { + if ($('.js-list-activity-' + activity.id, view_activity).length === 0) { + view_activity.append(view.render().el).find('.timeago').timeago(); + } + } + if (bool && parseInt(activity.attributes.user_id) !== parseInt(authuser.user.id)) { + // Update board view code starting + if (!_.isUndefined(activity.attributes.card_id) && activity.attributes.card_id !== 0 && !_.isUndefined(activity.attributes.board_id) && parseInt(activity.attributes.board_id) === parseInt(self.board_id)) { // Update Card + var card = self.board.cards.findWhere({ + id: parseInt(activity.attributes.card_id) + }); + if (activity.attributes.type === 'add_card' || activity.attributes.type === 'copy_card') { + var new_card = new App.Card(); + new_card.set(activity.attributes.card); + new_card.set('id', parseInt(activity.attributes.card.id)); + new_card.set('board_id', parseInt(activity.attributes.card.board_id)); + new_card.set('list_id', parseInt(activity.attributes.card.list_id)); + new_card.set('user_id', parseInt(activity.attributes.card.user_id)); + var card_list = self.board.lists.findWhere({ + id: parseInt(activity.attributes.list_id) + }); + new_card.list = card_list; + self.board.cards.add(new_card); + if (!_.isUndefined(card_list)) { + card_list.set('cards', activity.attributes.card); + } + } + if (!_.isUndefined(card)) { + if (activity.attributes.type === 'add_card_duedate') { + card.set('start', activity.attributes.revisions.new_value.due_date); + } else if (activity.attributes.type === 'delete_card_duedate') { + card.set('start', activity.attributes.revisions.new_value.due_date); + } + if (!_.isEmpty(activity.attributes.revisions)) { + card.set(activity.attributes.revisions.new_value); + } + if (activity.attributes.type === 'add_card_checklist') { + var new_checklist = new App.CheckList(); + new_checklist.set(activity.attributes.checklist); + new_checklist.set('id', parseInt(activity.attributes.checklist.id)); + new_checklist.set('user_id', parseInt(activity.attributes.checklist.user_id)); + new_checklist.set('card_id', parseInt(activity.attributes.checklist.card_id)); + if (activity.attributes.checklist.checklists_items !== null) { + _.each(activity.attributes.checklist.checklists_items, function(checklists_item) { + var new_item = new App.CheckListItem(); + new_item.set(checklists_item); + new_item.set('id', parseInt(checklists_item.id)); + new_item.set('user_id', parseInt(checklists_item.user_id)); + new_item.set('card_id', parseInt(checklists_item.card_id)); + new_item.set('checklist_id', parseInt(checklists_item.checklist_id)); + new_item.set('position', parseFloat(checklists_item.position)); + self.board.checklist_items.add(new_item); + }); + checklist_items = self.board.checklist_items.where({ + card_id: parseInt(activity.attributes.checklist.card_id) + }); + items = new App.CheckListItemCollection(); + items.add(checklist_items); + completed_count = items.filter(function(checklist_item) { + return checklist_item.get('is_completed') === true || checklist_item.get('is_completed') == 'true' || checklist_item.get('is_completed') == 't'; + }).length; + total_count = items.models.length; + card.set('checklist_item_completed_count', completed_count); + card.set('checklist_item_count', total_count); + } + self.board.checklists.add(new_checklist); + } else if (activity.attributes.type === 'add_card_label') { + var filtered_labels = self.board.labels.where({ + card_id: activity.attributes.card_id + }); + self.board.labels.remove(filtered_labels, { + silent: true + }); + var i = 1; + _.each(activity.attributes.labels, function(label) { + var new_label = new App.Label(); + new_label.set(label); + new_label.set('id', parseInt(label.id)); + new_label.set('label_id', parseInt(label.label_id)); + new_label.set('card_id', parseInt(label.card_id)); + new_label.set('list_id', parseInt(label.list_id)); + new_label.set('board_id', parseInt(label.board_id)); + var options = { + silent: true + }; + if (i === activity.attributes.labels.length) { + options.silent = false; + } + self.board.labels.add(new_label, options); + i++; + }); + card.set('cards_labels', activity.attributes.labels); + } else if (activity.attributes.type === 'add_card_voter') { + if (_.isUndefined(card.attributes.cards_voters) || card.attributes.cards_voters === null) { + card.set('cards_voters', []); + } + card.attributes.cards_voters.push(activity.attributes.voter); + var new_voter = new App.CardVoter(); + new_voter.set(activity.attributes.voter); + new_voter.set('id', parseInt(activity.attributes.voter.id)); + new_voter.set('user_id', parseInt(activity.attributes.voter.user_id)); + new_voter.set('card_id', parseInt(activity.attributes.voter.card_id)); + card.card_voters.add(new_voter); + } else if (activity.attributes.type === 'add_card_voter') { + card.set('cards_users', activity.attributes.user); + } else if (activity.attributes.type === 'add_checklist_item') { + var new_item = new App.CheckListItem(); + new_item.set(activity.attributes.item); + new_item.set('id', parseInt(activity.attributes.item.id)); + new_item.set('user_id', parseInt(activity.attributes.item.user_id)); + new_item.set('card_id', parseInt(activity.attributes.item.card_id)); + new_item.set('checklist_id', parseInt(activity.attributes.item.checklist_id)); + new_item.set('position', parseFloat(activity.attributes.item.position)); + self.board.checklist_items.add(new_item); + var added_checklist_items = self.board.checklist_items.where({ + card_id: parseInt(activity.attributes.card_id) + }); + items = new App.CheckListItemCollection(); + items.add(added_checklist_items); + var added_completed_count = items.filter(function(checklist_item) { + return checklist_item.get('is_completed') === true || checklist_item.get('is_completed') == 'true' || checklist_item.get('is_completed') == 't'; + }).length; + var added_total_count = items.models.length; + card.set('checklist_item_completed_count', added_completed_count); + card.set('checklist_item_count', added_total_count); + } else if (activity.attributes.type === 'update_card_checklist') { + checklist = self.board.checklists.findWhere({ + id: parseInt(activity.attributes.checklist.id) + }); + if (!_.isUndefined(checklist)) { + checklist.set(activity.attributes.checklist); + checklist.set('id', parseInt(activity.attributes.checklist.id)); + checklist.set('user_id', parseInt(activity.attributes.checklist.user_id)); + checklist.set('card_id', parseInt(activity.attributes.checklist.card_id)); + checklist.set('position', parseFloat(activity.attributes.checklist.position)); + } + } else if (activity.attributes.type === 'update_card_checklist_item' || activity.attributes.type === 'moved_card_checklist_item') { + var checklist_item = self.board.checklist_items.findWhere({ + id: parseInt(activity.attributes.item.id) + }); + if (!_.isUndefined(checklist_item)) { + checklist_item.set(activity.attributes.item); + checklist_item.set('id', parseInt(activity.attributes.item.id)); + checklist_item.set('user_id', parseInt(activity.attributes.item.user_id)); + checklist_item.set('card_id', parseInt(activity.attributes.item.card_id)); + checklist_item.set('checklist_id', parseInt(activity.attributes.item.checklist_id)); + checklist_item.set('position', parseFloat(activity.attributes.item.position)); + checklist_items = self.board.checklist_items.where({ + card_id: parseInt(activity.attributes.card_id) + }); + items = new App.CheckListItemCollection(); + items.add(checklist_items); + completed_count = items.filter(function(checklist_item) { + return checklist_item.get('is_completed') === true || checklist_item.get('is_completed') == 'true' || checklist_item.get('is_completed') == 't'; + }).length; + total_count = items.models.length; + card.set('checklist_item_completed_count', completed_count); + card.set('checklist_item_count', total_count); + } + } else if (activity.attributes.type === 'add_card_user') { + var new_user = new App.CardUser(); + new_user.set(activity.attributes.user); + new_user.set('id', parseInt(activity.attributes.user.id)); + new_user.set('user_id', parseInt(activity.attributes.user.user_id)); + new_user.set('card_id', parseInt(activity.attributes.user.card_id)); + card.users.add(new_user); + } else if (activity.attributes.type === 'add_comment') { + card.list.collection.board.activities.add(activity); + } else if (activity.attributes.type === 'add_card_attachment') { + var new_attachment = new App.CardAttachment(); + new_attachment.set(activity.attributes.attachment); + new_attachment.set('id', parseInt(activity.attributes.attachment.id)); + new_attachment.set('board_id', parseInt(activity.attributes.attachment.board_id)); + new_attachment.set('list_id', parseInt(activity.attributes.attachment.list_id)); + new_attachment.set('card_id', parseInt(activity.attributes.attachment.card_id)); + self.board.attachments.add(new_attachment); + } else if (activity.attributes.type === 'change_card_position') { + card.set('position', activity.attributes.card.position); + var card_new_list = self.board.lists.findWhere({ + id: parseInt(activity.attributes.list_id) + }); + if (!_.isUndefined(App.boards) && !_.isUndefined(App.boards.get(parseInt(activity.attributes.board_id)))) { + var updated_card_list_cards = self.board.cards.where({ + list_id: parseInt(activity.attributes.list_id) + }); + App.boards.get(parseInt(activity.attributes.board_id)).lists.get(parseInt(activity.attributes.list_id)).set('card_count', (updated_card_list_cards.length === 0) ? 0 : updated_card_list_cards.length - 1); + } + card_new_list.set('card_count', card_new_list.attributes.card_count + 1); + card.list = card_new_list; + card.set('list_id', parseInt(activity.attributes.list_id)); + var cards_attachments = self.board.attachments.where({ + card_id: parseInt(activity.attributes.card_id) + }); + var k = 1; + if (!_.isUndefined(cards_attachments) && cards_attachments.length > 0) { + _.each(cards_attachments, function(cards_attachment) { + var options = { + silent: true + }; + if (k === cards_attachments.length) { + options.silent = false; + } + self.board.attachments.findWhere({ + id: parseInt(cards_attachment.attributes.id) + }).set({ + list_id: parseInt(activity.attributes.list_id) + }, options); + k++; + }); + } + } else if (activity.attributes.type === 'delete_card_attachment') { + self.board.attachments.remove(self.board.attachments.findWhere({ + id: parseInt(activity.attributes.foreign_id) + })); + } else if (activity.attributes.type === 'delete_card_comment') { + self.board.activities.remove(self.board.activities.findWhere({ + id: parseInt(activity.attributes.foreign_id) + })); + } else if (activity.attributes.type === 'delete_checklist') { + self.board.checklists.remove(self.board.checklists.findWhere({ + id: parseInt(activity.attributes.foreign_id) + })); + self.board.checklist_items.remove(self.board.checklist_items.where({ + checklist_id: parseInt(activity.attributes.foreign_id) + })); + var checklist_items = self.board.checklist_items.where({ + card_id: parseInt(activity.attributes.card_id) + }); + items = new App.CheckListItemCollection(); + items.add(checklist_items); + var completed_count = items.filter(function(checklist_item) { + return checklist_item.get('is_completed') === true || checklist_item.get('is_completed') == 'true' || checklist_item.get('is_completed') == 't'; + }).length; + var total_count = items.models.length; + card.set('checklist_item_completed_count', completed_count); + card.set('checklist_item_count', total_count); + } else if (activity.attributes.type === 'delete_checklist_item') { + self.board.checklist_items.remove(self.board.checklist_items.findWhere({ + id: parseInt(activity.attributes.foreign_id) + })); + var update_checklist_items = self.board.checklist_items.where({ + card_id: parseInt(activity.attributes.card_id) + }); + items = new App.CheckListItemCollection(); + items.add(update_checklist_items); + var update_completed_count = items.filter(function(checklist_item) { + return checklist_item.get('is_completed') === true || checklist_item.get('is_completed') == 'true' || checklist_item.get('is_completed') == 't'; + }).length; + var update_total_count = items.models.length; + card.set('checklist_item_completed_count', update_completed_count); + card.set('checklist_item_count', update_total_count); + } else if (activity.attributes.type === 'delete_card_users') { + card.users.remove(card.users.findWhere({ + id: parseInt(activity.attributes.foreign_id) + })); + } else if (activity.attributes.type === 'unvote_card') { + card.card_voters.remove(card.card_voters.findWhere({ + id: parseInt(activity.attributes.foreign_id) + })); + } else if (activity.attributes.type === 'delete_card') { + self.board.cards.remove(card); + } + } + } else if (!_.isUndefined(activity.attributes.list_id) && activity.attributes.list_id !== 0 && !_.isUndefined(activity.attributes.board_id) && parseInt(activity.attributes.board_id) === parseInt(self.board_id)) { // Update List + var list = self.board.lists.findWhere({ + id: parseInt(activity.attributes.list_id) + }); + if (activity.attributes.type === 'add_list') { + var new_list = new App.List(); + new_list.set(activity.attributes.list); + new_list.set('id', parseInt(activity.attributes.list.id)); + new_list.set('board_id', parseInt(activity.attributes.list.board_id)); + new_list.set('lists_cards', []); + self.board.lists.add(new_list); + self.board.set('lists', activity.attributes.list); + + if (!_.isUndefined(App.boards) && !_.isUndefined(App.boards.get(new_list.attributes.board_id))) { + App.boards.get(new_list.attributes.board_id).lists.add(new_list); + } + + } + if (!_.isUndefined(list)) { + list.set(activity.attributes.revisions.new_value); + if (activity.attributes.type === 'delete_list') { + var removed_list_cards = self.board.cards.where({ + list_id: parseInt(list.attributes.id) + }); + self.board.cards.remove(removed_list_cards, { + silent: true + }); + list.collection.board.lists.remove(list); + self.board.lists.remove(list); + } else if (activity.attributes.type === 'change_list_position') { + if (activity.attributes.list.board_id !== list.attributes.board_id) { + self.board.lists.remove(list); + } else { + list.set('position', parseFloat(activity.attributes.list.position)); + } + } else if (activity.attributes.type === 'moved_list_card') { + var cards = self.board.cards.where({ + list_id: parseInt(activity.attributes.list_id) + }); + var j = 1; + if (!_.isUndefined(cards) && cards.length > 0) { + _.each(cards, function(card) { + var options = { + silent: true + }; + if (j === cards.length) { + options.silent = false; + } + self.board.cards.findWhere({ + id: parseInt(card.attributes.id) + }).set({ + list_id: parseInt(activity.attributes.foreign_id) + }, options); + j++; + }); + } + } else if (activity.attributes.type === 'archived_card') { + var list_cards = self.board.cards.where({ + list_id: parseInt(activity.attributes.list_id) + }); + var l = 1; + if (!_.isUndefined(list_cards) && list_cards.length > 0) { + _.each(list_cards, function(card) { + var options = { + silent: true + }; + if (l === list_cards.length) { + options.silent = false; + } + self.board.cards.findWhere({ + id: parseInt(card.attributes.id) + }).set({ + is_archived: true + }, options); + l++; + }); + } + } + } + } else if (!_.isUndefined(self.board) && !_.isUndefined(activity.attributes.board_id) && !_.isUndefined(activity.attributes.board_id) && parseInt(activity.attributes.board_id) === parseInt(self.board_id)) { // Update Board + self.board.set(activity.attributes.revisions.new_value); + if (activity.attributes.type === 'add_board_user') { + self.board.board_users.add(activity.attributes.board_user); + } else if (activity.attributes.type === 'delete_board_user') { + self.board.board_users.remove(self.board.board_users.findWhere({ + id: activity.attributes.foreign_id + })); + } + } + } + }); + } + var headerH = $('header').height(); + var windowH = $(window).height(); + var footerH = $('footer').height(); + var notificationH = windowH - footerH; + var boardH = windowH - headerH - footerH - 14; + $('.notification-list').css({ + 'height': notificationH - 100, + 'overflow-y': 'scroll' + }); + } + }); + }, + /** + * boardActivities() + * display board activiteis, sync every 10 sec and update the board view. + * Update the board view based on activity type. + * @param e + * @type Object(DOM event) + * + */ + boardActivities: function() { + var view_activity = $('#js-board-activities'); + var self = this; + var Auth = JSON.parse(window.sessionStorage.getItem('auth')); + var clicked_notification_count = 0, + clicked_all_notification_count = 0; + var activities = new App.ActivityCollection(); + activities.url = api_url + 'boards/' + authuser.board_id + '/activities.json'; + activities.storeName = 'activity'; + $('#js-activity-loader').remove(); + view_activity.append('
        • '); + if (!_.isUndefined(authuser.user) && _.isUndefined(authuser.user.last_activity_id)) { + authuser.user.last_activity_id = 0; + } + activities.fetch({ + success: function(models, response, options) { + $('#js-activity-loader').remove(); + if (!_.isEmpty(activities.models)) { + activities.each(function(activity) { + activity.from_footer = true; + var all_activity = $('#js-all-activities'); + var view = new App.ActivityView({ + model: activity, + board: self.board + }); + if ($('.js-list-activity-' + activity.id, view_activity).length === 0) { + view_activity.append(view.render().el).find('.timeago').timeago(); + } + }); + var last_board_activity = _.min(activities.models, function(activity) { + return activity.id; + }); + load_more_last_board_activity_id = last_board_activity.id; + if ($('#js-notification-count').html() > 0) { + var max_last_user_activity = _.max(activities.models, function(activity) { + return activity.id; + }); + var user = new App.User(); + user.url = api_url + 'users/' + authuser.user.id + '.json'; + user.set('id', parseInt(authuser.user.id)); + user.save({ + 'last_activity_id': last_board_activity.id + }); + authuser.user.notify_count = 0; + Auth.user.notify_count = 0; + favicon.badge(0); + $('#js-notification-count').addClass('hide'); + window.sessionStorage.setItem('auth', JSON.stringify(Auth)); + } + } + var headerH = $('header').height(); + var windowH = $(window).height(); + var footerH = $('footer').height(); + var notificationH = windowH - footerH; + var boardH = windowH - headerH - footerH - 14; + $('.notification-list').css({ + 'height': notificationH - 100, + 'overflow-y': 'scroll' + }); + } + }); + }, + /** + * notificationMenu() + * display activiteis + * @param e + * @type Object(DOM event) + * + */ + notificationMenu: function(e) { + e.preventDefault(); + if (!_.isUndefined(authuser) && !_.isEmpty(authuser) && authuser.board_id !== 0) { + + $('.js-notification-response-container').html(new App.NotificationMenuView({ + user: authuser + }).el); + } else { + this.userActivities(); + } + }, + /** + * qSearch() + * search board, list, card + * @param e + * @type Object(DOM event) + * + */ + qSearch: function(e) { + e.preventDefault(); + $('.search-container').addClass('search-tab'); + $('.js-search').autocomplete({ + minLength: 2, + source: function(request, _response) { + var elastic_search = new App.ElasticSearchCollection(); + elastic_search.url = api_url + 'search.json'; + var q = $(e.target).val(); + if (q !== '' && e.which !== 38 && e.which !== 40 && e.which !== 39 && e.which !== 47) { + elastic_search.fetch({ + data: { + q: q, + token: api_token + }, + success: function(model, response) { + var self = this; + self.result = response; + var content = ''; + var cards = new App.CardCollection(); + cards.add(response.result); + var card_names = cards.pluck('name'); + var res_suggestion = response.suggestion; + if (!_.isEmpty(response.hits) && !_.isEmpty(response.hits.hits)) { + content = new App.SearchResultView({ + model: response.hits.hits + }).el; + } else { + content = 'No results.'; + } + $('.js-show-search-result').html(content); + $('.js-boards-list-container-search').addClass('hide'); + + mappedItems = $.map(self.result.result, function(item) { + item.url = '#/board/' + item.board_id; + if (item.type == 'cards') { + item.url = '#/board/' + item.board_id + '/card/' + item.id; + } + var result = {}; + result.label = item.name; + result.value = item.name; + result.id = item.id; + result.board_name = item.board_name; + result.list_name = item.list_name; + result.type = item.type; + result.board_id = item.board_id; + result.url = item.url; + return result; + }); + return _response(mappedItems); + } + }); + } + }, + select: function(event, ui) { + var board_id = ui.item.board_id; + var card_id = ui.item.card_id; + var current_path = window.location.hash; + var current_items = current_path.split('/'); + if (current_items !== undefined && current_items[2] !== undefined && current_items[2] === board_id) { + if (card_id !== undefined && card_id !== null && ($('#js-card-' + card_id).length !== 0)) { + $('#js-card-' + card_id).trigger('click'); + } + } else { + window.location.hash = ui.item.url; + window.location.reload(); + } + return false; + }, + position: { + my: 'right top', + at: 'right bottom' + } + }).data('autocomplete')._renderItem = function(ul, item) { + var inner_text = ''; + if (item.type == 'cards') { + inner_text = '#' + item.id + '' + item.label + '' + item.board_name + ' » ' + item.list_name + ' » Card'; + } + if (item.type == 'lists') { + inner_text = '#' + item.id + '' + item.label + '' + item.board_name + ' » List'; + } + if (item.type == 'boards') { + inner_text = '#' + item.id + '' + item.label + 'Board'; + } + return $('
        • ') + .data('item.autocomplete', item) + .append($('' + inner_text + '')) + .appendTo(ul); + }; + }, + /** + * showSearchMsg() + * display search result + * @param e + * @type Object(DOM event) + * + */ + showSearchMsg: function(e) { + e.preventDefault(); + $('.js-show-search-result').html(new App.ShowSearchMessageView({ + model: this.model + }).el); + }, + addInstantCard: function(e) { + e.preventDefault(); + $('li.dropdown').removeClass('open'); + var self = this; + var data = $(e.target).serializeObject(); + var card = new App.Card(); + card.url = api_url + 'boards/' + data.board_id + '/lists/' + data.list_id + '/cards.json'; + card.save(data); + }, + showNotification: function(e) { + e.preventDefault(); + + if (!_.isEmpty(this.board_id)) { + this.$el.find('.js-board-activities').click(); + } else { + this.$el.find('.js-all-activities').click(); + } + }, + showBoardActivities: function(e) { + e.preventDefault(); + var modalView = new App.ModalActivityView({ + model: this.model, + type: 'board' + }); + modalView.show(); + return false; + }, + showUserActivities: function(e) { + e.preventDefault(); + var modalView = new App.ModalActivityView({ + model: this.model, + type: 'user' + }); + modalView.show(); + return false; + }, + /** + * loadMore() + * load more + * @param e + * @type Object(DOM event) + * @mode Object(DOM event) + * @return false + */ + loadMore: function(type, mode) { + var view_activity, query_string = ''; + var self = this; + var activities = new App.ActivityCollection(); + if (type === 'user') { + view_activity = $('#js-all-activities'); + query_string = (last_user_activity_id !== 0 && !_.isUndefined(last_user_activity_id)) ? '&last_activity_id=' + last_user_activity_id : ''; + activities.url = api_url + 'users/' + authuser.user.id + '/activities.json?type=all' + query_string; + } else { + view_activity = $('#js-board-activities'); + query_string = (load_more_last_board_activity_id !== 0 && !_.isUndefined(load_more_last_board_activity_id)) ? '&last_activity_id=' + load_more_last_board_activity_id : ''; + activities.url = api_url + 'boards/' + authuser.board_id + '/activities.json?type=all' + query_string; + } + self.$('#js-empty', view_activity).remove(); + $('#js-activity-loader').remove(); + view_activity.append('
        • '); + activities.fetch({ + success: function() { + $('#js-activity-loader').remove(); + var last_activity_id = _.min(activities.models, function(activity) { + return activity.id; + }); + if (type === 'user') { + last_user_activity_id = last_activity_id.id; + } else { + load_more_last_board_activity_id = last_activity_id.id; + } + if (type == 'user') { + $('#js-load-link2').removeClass('hide'); + $('#js-load-link1').addClass('hide'); + } else if (type == 'board') { + $('#js-load-link1').removeClass('hide'); + $('#js-load-link2').addClass('hide'); + } + if (!_.isEmpty(activities.models)) { + $('.notification-list').removeClass('notification-empty'); + $('.js-empty').remove(); + modeType = 'all'; + if (mode == 1) { + modeType = ''; + } + activities.each(function(activity) { + var view = new App.ActivityView({ + model: activity, + board: self.board, + type: modeType + }); + view_activity.append(view.render().el).find('.timeago').timeago(); + }); + } else { + if (type == 'user') { + $('#js-load-link2').addClass('hide'); + } else if (type == 'board') { + $('#js-load-link1').addClass('hide'); + } + } + } + }); + return false; + }, + /** + * enabledesktopNotification() + * enable desktop notification + * @param e + * @type Object(DOM event) + * + */ + enabledesktopNotification: function(e) { + e.preventDefault(); + var self = this; + Notification.requestPermission(function(permission) { + // Whatever the user answers, we make sure we store the information + if (!('permission' in Notification)) { + Notification.permission = permission; + } + // If the user is okay, let's create a notification + if (permission === 'granted') { + var notification = new Notification('Desktop notification enabled.'); + } + }); + }, + /** + * showBoardImportForm() + * show Board Import Form + * @param e + * @type Object(DOM event) + * + */ + showBoardImportForm: function(e) { + e.preventDefault(); + var form = $('#js-board-import'); + $('.js-board-import-file', form).trigger('click'); + return false; + }, + /** + * importBoard() + * import Board + * @param e + * @type Object(DOM event) + * + */ + importBoard: function(e) { + e.preventDefault(); + $('#js-board-import-loader').removeClass('hide'); + var self = this; + var form = $('form#js-board-import'); + var fileData = new FormData(form[0]); + var board = new App.Board(); + board.url = api_url + 'boards.json'; + board.save(fileData, { + type: 'POST', + data: fileData, + processData: false, + cache: false, + contentType: false, + error: function(e, s) { + $('#js-board-import-loader').addClass('hide'); + }, + success: function(model, response) { + $('#js-board-import-loader').addClass('hide'); + if (!_.isUndefined(response.id)) { + app.navigate('#/board/' + response.id, { + trigger: true, + replace: true + }); + } else { + var error = 'Unable to import. please try again.'; + if (!_.isUndefined(response.error)) { + error = response.error; + } + self.flash('danger', error); + + } + } + }); + } +}); diff --git a/client/js/views/header_view.js b/client/js/views/header_view.js new file mode 100644 index 000000000..51771cff0 --- /dev/null +++ b/client/js/views/header_view.js @@ -0,0 +1,121 @@ +/** + * @fileOverview This file has functions related to header view. This view calling from application view and login view. + * Available Object: + * this.model : user model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Header View + * @class HeaderView + * @constructor + * @extends Backbone.View + */ +App.HeaderView = Backbone.View.extend({ + template: JST['templates/header'], + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-close-popover': 'closePopup', + 'click .js-open-popover': 'openPopup', + 'click .js-star-board': 'sidebarSubcribeBoard', + 'click .js-show-change-avatar-form': 'showChangeAvatarForm', + 'click .js-sort-by': 'sortBy' + }, + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (options.users) { + this.users = options.users; + } + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + if (!_.isUndefined(this.users)) { + var admins = this.users.filter(function(normal_user) { + return normal_user.attributes.role_id === 1; + }); + var admin_board_users = admins; + this.admin_board_users = admin_board_users.length; + var normal_users = this.users.filter(function(normal_user) { + return normal_user.attributes.role_id !== 1; + }); + var normal_board_users = normal_users; + this.normal_board_users = normal_board_users.length; + } + this.render(); + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.model.is_show_enable_notification = false; + var current_param = Backbone.history.fragment; + var current_param_split = current_param.split('/'); + this.model.current_param = (current_param.indexOf('changepassword') === -1 && current_param.indexOf('login') === -1 && current_param.indexOf('forgotpassword') === -1 && current_param.indexOf('register') === -1 && current_param.indexOf('activation') === -1) ? current_param_split[0] : ''; + if (typeof Notification != 'undefined') { + this.model.is_show_enable_notification = (Notification.permission == 'default') ? true : false; + } + this.$el.html(this.template(this.model)); + this.showTooltip(); + return this; + }, + renderList: function() { + $('#content').html(new App.UserIndexContainerView({ + model: this.users, + sortField: this.sortField, + sortDirection: this.sortDirection + }).el); + $('.timeago', $('#content')).timeago(); + }, + sortBy: function(e) { + e.preventDefault(); + var sortField = $(e.currentTarget).data('field'); + if (_.isUndefined(this.sortDirection)) { + this.sortDirection = 'desc'; + } else { + if (this.sortDirection === 'desc') { + this.sortDirection = 'asc'; + } else { + this.sortDirection = 'desc'; + } + } + this.sortField = sortField; + this.renderList(); + }, + /** + * openPopup() + * show dropdown + * @param e + * @type Object(DOM event) + * @return false + */ + openPopup: function(e) { + var target = $(e.target).parents('.dropdown:first'); + target.addClass('open').prev('.dropdown').removeClass('open'); + target.next('.dropdown').removeClass('open'); + return false; + }, + /** + * closePopup() + * close opened dropdown + * @param e + * @type Object(DOM event) + * @return false + * + */ + closePopup: function(e) { + var target = $(e.target); + target.parents('li.dropdown').removeClass('open'); + return false; + } +}); diff --git a/client/js/views/instant_card_add_labels_form_view.js b/client/js/views/instant_card_add_labels_form_view.js new file mode 100644 index 000000000..83a54504d --- /dev/null +++ b/client/js/views/instant_card_add_labels_form_view.js @@ -0,0 +1,44 @@ +/** + * @fileOverview This file has functions related to instant card add labels form view. This view calling from instant card add view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : instant card add model. @see Available Object in App.InstantCardAddView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * CardLabelsForm View + * @class CardLabelsFormView + * @constructor + * @extends Backbone.View + */ +App.InstantCardAddLabelsFormView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/instant_card_add_labels_form'], + tagName: 'li', + className: '', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + card: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/instant_card_add_members_form_view.js b/client/js/views/instant_card_add_members_form_view.js new file mode 100644 index 000000000..a3ae86de9 --- /dev/null +++ b/client/js/views/instant_card_add_members_form_view.js @@ -0,0 +1,42 @@ +/** + * @fileOverview This file has functions related to instant card add labels form view. This view calling from instant card add view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : instant card add model. @see Available Object in App.InstantCardAddView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * InstantCardAddMembersForm View + * @class InstantCardAddMembersFormView + * @constructor + * @extends Backbone.View + */ +App.InstantCardAddMembersFormView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/instant_card_add_members_form'], + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + user_id: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/instant_card_add_view.js b/client/js/views/instant_card_add_view.js new file mode 100644 index 000000000..d5143daa7 --- /dev/null +++ b/client/js/views/instant_card_add_view.js @@ -0,0 +1,438 @@ +/** + * @fileOverview This file has functions related to instant card add labels form view. This view calling from footer view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : instant card add model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * InstantCardAdd View + * @class InstantCardAddView + * @constructor + * @extends Backbone.View + */ +App.InstantCardAddView = Backbone.View.extend({ + template: JST['templates/instant_card_add'], + converter: new Showdown.converter(), + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'submit form.js-instantCardAddForm': 'addInstantCard', + 'keyup .js-search-users': 'showSearchUsers', + 'click .js-close-popover': 'closePopup', + 'click .js-add-card-member': 'addCardMember', + 'click .js-remove-card-member': 'removeCardMember', + 'click .js-show-card-label-form': 'showCardLabelForm', + }, + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + _.bindAll(this, 'render'); + this.board = options.board; + this.boards = new App.BoardCollection(); + this.card_users = []; + this.card_users_names = []; + this.render(); + }, + className: 'js-instant-card-from', + tagName: 'div', + attributes: { + 'title': 'Instant Add Card' + }, + bindings: { + '#board_id': { + observe: 'board_id', + initialize: function($el) { + $el.select2({ + width: 175, + allowClear: true + }); + }, + selectOptions: { + collection: function() { + var board_arr = []; + _.each(this.boards.models, function(board) { + if (!_.isEmpty(board.attributes.lists) && board.attributes.lists.length > 0 && board.attributes.is_closed === false) { + var temp = {}; + temp.id = board.id; + temp.name = _.escape(board.attributes.name); + board_arr.push(temp); + } + }); + return board_arr; + }, + valuePath: 'id', + labelPath: 'name', + defaultOption: { + label: '', + value: null + } + }, + onSet: function(val) { + this.model.set('list_id', null); + $('#list_id').select2('val', null); + this.showCardLabelForm(); + return val; + } + }, + '#list_id': { + observe: ['list_id', 'board_id'], + initialize: function($el) { + $el.select2({ + width: 175, + allowClear: true + }); + }, + selectOptions: { + collection: function() { + var boardId = this.model.get('board_id'); + var board = this.boards.findWhere({ + id: parseInt(boardId) + }); + this.$('.js-instant-card-user-ids').val(''); + this.$('.js-instant-card-member-search-response').nextAll().remove(); + this.$('#inputInstantCardAddUserSearch').val(''); + $('
        • Search for a person in ' + SITE_NAME + ' by name or email address.
        • ').insertAfter(this.$('.js-instant-card-member-search-response')); + var list_arr = []; + if (!_.isUndefined(board)) { + _.each(board.attributes.lists, function(list) { + if (list.is_archived === false) { + var list_temp = {}; + list_temp.id = list.id; + list_temp.name = _.escape(list.name); + list_arr.push(list_temp); + } + }); + } + return list_arr; + }, + valuePath: 'id', + labelPath: 'name', + defaultOption: { + label: '', + value: null + } + }, + onSet: function(val) { + this.model.set('list_id', val); + $('#list_id').select2('val', val); + return val; + } + } + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.boards = App.boards; + var _id = 'js-instant-card-from-' + $('.js-instant-card-from').length; + this.id = _id; + var self = this; + $('.js-hidden-blocks').append(this.$el.html(this.template({ + boards: this.boards + })).attr('title', 'Instant Add Card')); + this.$el.dockmodal({ + initialState: 'docked', + height: 300, + width: 400, + open: function(event, dialog) { + $('.dockmodal').removeClass('active'); + event.parent().parent().addClass('active'); + $('.dockmodal').click(function(e) { + $('.dockmodal').removeClass('active'); + $(this).addClass('active'); + }); + $(window).bind('keydown', function(e) { + if (e.keyCode === 27) { + $('.action-close', $('.dockmodal.active')).trigger('click'); + } + }); + + } + }).find('.js-chosen-select').select2(); + this.$el.find('.duedate').datetimepicker({ + format: 'yyyy-mm-dd hh:ii:ss', + autoclose: true, + todayBtn: true, + pickerPosition: 'top-right', + todayHighlight: 1, + bootcssVer: 3, + pickTime: true + }).on('changeDate', function(ev) { + var date = new Date(ev.date); + var year = date.getFullYear(); + var month = date.getMonth(); + var day = date.getDate(); + var hour = date.getHours(); + var minute = date.getMinutes(); + var second = date.getSeconds(); + var formattedTime = year + '-' + (month + 1) + '-' + day + ' ' + hour + ':' + minute + ':' + second; + var formattedTimeToolTip = dateFormat(formattedTime, 'mmm dd yyyy h:MM TT'); + self.$('.js-instant-date').addClass('text-primary').parent().addClass('js-tooltip').attr('title', formattedTimeToolTip); + $('.js-tooltip').tooltip(); + self.$('.js-instant-duedate').val(formattedTime); + }); + this.stickit(this.model, this.bindings); + this.showTooltip(); + return this; + }, + /** + * addInstantCard() + * add card + * @param e + * @type Object(DOM event) + * + */ + addInstantCard: function(e) { + e.preventDefault(); + this.card_users = []; + this.card_users_names = []; + $('li.dropdown').removeClass('open'); + var self = this; + var data = $(e.target).serializeObject(); + if (data.board_id === 'undefined' || data.board_id === '') { + $('#board_id').select2('open'); + } else if (data.list_id === 'undefined' || data.list_id === '') { + $('#list_id').select2('open'); + } + if (data.board_id && data.list_id) { + $('input[type!="submit"], textarea, select', $(e.currentTarget)).val('').removeAttr('checked').removeAttr('selected'); + $('#board_id, #list_id').select2('val', null); + var card = new App.Card(); + var board_id = parseInt(data.board_id); + data.board_id = board_id; + var list_id = parseInt(data.list_id); + data.list_id = list_id; + card.url = api_url + 'boards/' + data.board_id + '/lists/' + data.list_id + '/cards.json'; + card.set('is_archived', false); + card.set(data); + card.set('board_id', board_id); + card.set('due_date', null); + card.set('checklist_item_count', 0); + card.set('checklist_item_completed_count', 0); + if (!_.isUndefined(self.board) && board_id === self.board.id) { + card.list = self.board.lists.findWhere({ + id: list_id + }); + var view = new App.CardView({ + tagName: 'div', + model: card, + converter: this.converter + }); + $('#js-card-listing-' + list_id).append(view.render().el); + } + this.$('.js-instant-user').removeClass('text-primary').parent().removeClass('js-tooltip').attr('data-original-title', '').attr('title', ''); + this.$('.js-instant-label').removeClass('text-primary').parent().removeClass('js-tooltip').attr('data-original-title', '').attr('title', ''); + this.$('.js-instant-date').removeClass('text-primary').parent().removeClass('js-tooltip').attr('data-original-title', '').attr('title', ''); + $(e.target)[0].reset(); + card.save(data, { + success: function(model, response) { + card.set('list_id', parseInt(data.list_id)); + card.set('id', parseInt(response.id)); + if (!_.isUndefined(self.board) && board_id === self.board.id) { + self.board.cards.add(card); + self.board.lists.get(parseInt(data.list_id)).cards.add(card); + if (!_.isUndefined(response.cards_labels)) { + var _i = 1; + _.each(response.cards_labels, function(label) { + var new_label = new App.Label(); + new_label.set(label); + new_label.set('id', parseInt(label.id)); + new_label.set('label_id', parseInt(label.label_id)); + new_label.set('card_id', parseInt(label.card_id)); + new_label.set('list_id', parseInt(label.list_id)); + new_label.set('board_id', parseInt(label.board_id)); + var options = { + silent: true + }; + if (_i === response.cards_labels.length) { + options.silent = false; + } + self.board.labels.add(new_label, options); + _i++; + }); + } + } else { + var card_count = App.boards.get(board_id).lists.get(parseInt(data.list_id)).get('card_count'); + App.boards.get(board_id).lists.get(parseInt(data.list_id)).set('card_count', card_count + 1); + if (this.model !== null) { + App.boards.get(board_id).lists.sortByColumn('position'); + data = []; + var color_codes = ['#DB7093', '#F47564', '#EDA287', '#FAC1AD', '#FFE4E1', '#D3ABF0', '#DC9CDC', '#69BFBA', '#66CDAA', '#8FBC8F', '#CBFDCA', '#EEE8AA', '#BC8F8F', '#CD853F', '#D2B48C', '#F5DEB3', '#64BCF2', '#87CEFA', '#B0C4DE', '#D6E2F7']; + var i = 0; + App.boards.get(board_id).lists.each(function(list) { + var _data = {}; + _data.title = list.attributes.name; + _data.value = list.attributes.card_count; + _data.color = color_codes[i]; + i++; + if (i > 20) { + i = 0; + } + if (list.attributes.card_count > 0) { + data.push(_data); + } + }); + var _this = this; + _(function() { + var starred_board = $('#js-starred-board-' + board_id); + var my_board = $('#js-my-board-' + board_id); + if (starred_board.length > 0) { + starred_board.find('.js-chart').html('').drawDoughnutChart(data); + } + if (my_board.length > 0) { + my_board.find('.js-chart').html('').drawDoughnutChart(data); + } + }).defer(); + } + } + } + }); + } + }, + /** + * showSearchUsers() + * display searched users + * @param e + * @type Object(DOM event) + * + */ + showSearchUsers: function(e) { + var self = this; + var q = $(e.target).val(); + var users = new App.UserCollection(); + var board_id = $('#board_id').val(); + if (board_id !== 0) { + users.url = api_url + 'users/search.json?board_id=' + board_id; + } else { + users.url = api_url + 'users/search.json'; + } + users.fetch({ + data: { + q: q + }, + success: function() { + self.$el.find('.js-instant-card-member-search-response').nextAll().remove(); + _.each(users.models, function(user) { + var is_added_user = ($.inArray(user.id, self.card_users) !== -1) ? true : false; + $('
        • ' + new App.CardSearchUsersResultView({ + model: user, + is_added_user: is_added_user + }).el + '
        • ').insertAfter(self.$el.find('.js-instant-card-member-search-response')); + }); + if (users.models.length === 0) { + $('
        • ' + new App.CardSearchUsersResultView({ + model: null, + }).el + '
        • ').insertAfter(self.$el.find('.js-instant-card-member-search-response')); + } + } + }); + }, + /** + * closePopup() + * hide opened dropdown + * @param e + * @type Object(DOM event) + * + */ + closePopup: function(e) { + var target = $(e.target); + target.parents('li.dropdown').removeClass('open'); + target.parents('li.dropup').removeClass('open'); + return false; + }, + /** + * addCardMember() + * add card member + * @param e + * @type Object(DOM event) + * + */ + addCardMember: function(e) { + e.preventDefault(); + var target = $(e.currentTarget); + target.removeClass('js-add-card-member').addClass('js-remove-card-member'); + target.append(''); + var user_id = target.data('user-id'); + var user_name = target.data('user-name'); + this.card_users.push(parseInt(user_id)); + this.card_users_names.push(user_name); + $.unique(this.card_users); + $.unique(this.card_users_names); + this.$('.js-instant-user').addClass('text-primary').parent().addClass('js-tooltip').attr('data-original-title', this.card_users_names.join(',')).attr('title', this.card_users_names.join(',')); + $('.js-tooltip').tooltip(); + this.$('.js-instant-card-user-ids').val(this.card_users.join(',')); + }, + /** + * removeCardMember() + * remove card member + * @param e + * @type Object(DOM event) + * + */ + removeCardMember: function(e) { + e.preventDefault(); + var target = $(e.currentTarget); + target.removeClass('js-remove-card-member').addClass('js-add-card-member'); + target.find('i.icon-ok').remove(); + var user_id = target.data('user-id'); + var user_name = target.data('user-name'); + $.unique(this.card_users); + this.card_users.splice($.inArray(parseInt(user_id), this.card_users), 1); + this.card_users_names.splice($.inArray(user_name, this.card_users_names), 1); + this.$('.js-instant-user').parent().addClass('js-tooltip').attr('data-original-title', this.card_users_names.join(',')).attr('title', this.card_users_names.join(',')); + $('.js-tooltip').tooltip(); + if (this.card_users.length === 0) { + this.$('.js-instant-user').removeClass('text-primary').parent().removeClass('js-tooltip').attr('data-original-title', '').attr('title', 'Users'); + } + this.$('.js-card-user-ids').val(this.card_users.join(',')); + }, + /** + * showCardLabelForm() + * display card label add form + * @param e + * @type Object(DOM event) + * + */ + showCardLabelForm: function() { + var board = App.boards.findWhere({ + id: parseInt(this.$el.find('#board_id').val()) + }); + var labels = new App.CardLabelCollection(); + if (!_.isEmpty(board) && !_.isEmpty(board.attributes.labels)) { + labels.set(board.attributes.labels); + } + $('.js-show-instant-card-label-form-response').html(new App.InstantCardAddLabelsFormView({ + model: this.model + }).el); + var self = this; + this.$('.js-card-label').select2({ + tags: labels.pluck('name'), + tokenSeparators: [',', ' '] + }).on('select2-selecting', function(e) { + var labels = _.pluck(self.$('.js-card-label').select2('data'), 'text'); + labels.push(e.choice.text); + self.$('.js-instant-label').addClass('text-primary').parent().addClass('js-tooltip').attr('data-original-title', labels.join(',')).attr('title', labels.join(',')); + $('.js-tooltip').tooltip(); + }).on('select2-removed', function(e) { + var _labels = _.pluck(self.$('.js-card-label').select2('data'), 'text'); + self.$('.js-instant-label').parent().attr('data-original-title', _labels.join(',')).attr('title', _labels.join(',')); + if (self.$('.js-card-label').select2('data').length === 0) { + self.$('.js-instant-label').removeClass('text-primary').parent().removeClass('js-tooltip').attr('data-original-title', '').attr('title', 'Labels'); + } + }); + } +}); diff --git a/client/js/views/list_actions_view.js b/client/js/views/list_actions_view.js new file mode 100644 index 000000000..3c92846ff --- /dev/null +++ b/client/js/views/list_actions_view.js @@ -0,0 +1,48 @@ +/** + * @fileOverview This file has functions related to list action view. This view calling from list view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : list model. It contain all list based object @see Available Object in App.ListView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ListActions View + * @class ListActionsView + * @constructor + * @extends Backbone.View + */ +App.ListActionsView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.subscribers = options.subscribers; + this.authuser = options.authuser; + this.render(); + }, + template: JST['templates/list_actions'], + tagName: 'ul', + className: 'dropdown-menu dropdown-menu-right arrow arrow-right js-list-actions-response', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + list: this.model, + subscribers: this.subscribers, + authuser: this.authuser + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/list_archive_confirm_view.js b/client/js/views/list_archive_confirm_view.js new file mode 100644 index 000000000..9d1c20d55 --- /dev/null +++ b/client/js/views/list_archive_confirm_view.js @@ -0,0 +1,43 @@ +/** + * @fileOverview This file has functions related to list archive confirm view. This view calling from list view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : list model. It contain all list based object @see Available Object in App.ListView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ListArchiveConfirm View + * @class ListArchiveConfirmView + * @constructor + * @extends Backbone.View + */ +App.ListArchiveConfirmView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/list_archive_confirm'], + tagName: 'div', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + list: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/list_cards_archive_confirm_view.js b/client/js/views/list_cards_archive_confirm_view.js new file mode 100644 index 000000000..598354420 --- /dev/null +++ b/client/js/views/list_cards_archive_confirm_view.js @@ -0,0 +1,43 @@ +/** + * @fileOverview This file has functions related to list cards archive confirm view. This view calling from list view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : list model. It contain all list based object @see Available Object in App.ListView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ListCardsArchiveConfirm View + * @class ListCardsArchiveConfirmView + * @constructor + * @extends Backbone.View + */ +App.ListCardsArchiveConfirmView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/list_cards_archive_confirm'], + tagName: 'div', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + list: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/list_delete_confirm_view.js b/client/js/views/list_delete_confirm_view.js new file mode 100644 index 000000000..cd9a2f30d --- /dev/null +++ b/client/js/views/list_delete_confirm_view.js @@ -0,0 +1,43 @@ +/** + * @fileOverview This file has functions related to list delete confirm view. This view calling from list view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : list model. It contain all list based object @see Available Object in App.ListView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ListDeleteConfirm View + * @class ListDeleteConfirmView + * @constructor + * @extends Backbone.View + */ +App.ListDeleteConfirmView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/list_delete_confirm'], + tagName: 'div', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + list: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/list_view.js b/client/js/views/list_view.js new file mode 100644 index 000000000..3b8083926 --- /dev/null +++ b/client/js/views/list_view.js @@ -0,0 +1,1271 @@ +/** + * @fileOverview This file has functions related to list view. This view calling from board view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : list model and it's related values + * this.model.attachments : attachments collection(Based on list) + * this.model.cards : cards collection(Based on list) + * this.model.collection.board : board model. It contain all board based object @see Available Object in App.BoardView + * this.model.lists_subscribers : list user collection(Based on list) + * this.model.boards_subscribers : board user collection(Based on board) + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * List View + * @class ListView + * @constructor + * @extends Backbone.View + */ +App.ListView = Backbone.View.extend({ + tagName: 'div', + className: 'col-lg-3 col-md-3 col-sm-4 col-xs-12 js-board-list list', + converter: new Showdown.converter(), + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.authuser = authuser.user; + this.mov_boards = new App.BoardCollection(); + if (this.model.has('lists_subscribers')) { + this.model.lists_subscribers.add(this.model.get('lists_subscribers'), { + silent: true + }); + } + if (this.model.has('list_attachments')) { + this.model.attachments.add(this.model.attributes.list_attachments, { + silent: true + }); + } + if (!_.isUndefined(this.model.collection)) { + this.total_board_list = this.model.collection.board.attributes.lists; + this.total_board_list_length = 0; + if (!_.isEmpty(this.model.collection.board.attributes.lists)) { + this.total_board_list_length = this.model.collection.board.attributes.lists.length; + } + this.board = this.model.collection.board; + } + if (this.model.has('board_activities')) { + this.board_activites = this.model.collection.board.get('board_activities'); + } + _.bindAll(this, 'render', 'renderCardsCollection', 'removeRender'); + this.model.bind('change:name', this.render); + this.model.collection.board.labels.bind('add', this.renderCardsCollection); + this.model.collection.board.attachments.bind('add', this.renderCardsCollection); + this.model.collection.board.attachments.bind('remove', this.renderCardsCollection); + this.model.collection.board.cards.bind('add', this.renderCardsCollection); + this.model.collection.board.cards.bind('add:name', this.renderCardsCollection); + this.model.collection.board.cards.bind('add:id', this.renderCardsCollection); + this.model.collection.board.cards.bind('remove', this.renderCardsCollection); + this.model.collection.board.cards.bind('change:position', this.renderCardsCollection); + this.model.collection.board.cards.bind('change:is_archived', this.renderCardsCollection); + this.model.collection.board.cards.bind('change:list_id', this.renderCardsCollection); + this.model.bind('remove', this.removeRender); + }, + template: JST['templates/list'], + templateAdd: JST['templates/list_add'], + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'submit form.js-edit-list': 'editList', + 'click .js-show-list-actions': 'showListActions', + 'click .js-close-popup': 'closePopup', + 'click .js-back-to-list-actions': 'backToListActions', + 'click .js-show-confirm-archive-list': 'showConfirmArchiveList', + 'click .js-archive-list': 'archiveList', + 'click .js-show-confirm-list-delete': 'showConfirmListDelete', + 'click .js-delete-list': 'deleteList', + 'click .js-show-copy-list-form': 'showCopyListForm', + 'click .js-show-move-list-form': 'showMoveListForm', + 'submit form.js-move-list': 'moveList', + 'click .js-list-subscribe': 'listSubscribe', + 'click .js-list-unsubscribe': 'listUnsubscribe', + 'click .js-show-move-card-list-form': 'showMoveCardListForm', + 'click .js-move-cards': 'moveCards', + 'click .js-show-confirm-archive-cards': 'showConfirmArchiveCards', + 'click .js-archive-card': 'archiveCard', + 'click .js-show-add-card-form': 'showAddCardForm', + 'click .js-show-list-modal': 'showListModal', + 'click .js-no-action': 'noAction', + 'click .js-show-edit-list-form': 'showListEditForm', + 'submit form.js-cardAddForm': 'addCard', + 'keypress textarea': 'onEnter', + 'click .js-copy-from-existing-card': 'copyFromExistingCard', + 'keyup .js-card-add-search': 'cardSearch', + 'click .js-select-card': 'selectCard', + 'change .js-move-change-list': 'moveChangeList', + 'change .js-move-change-position': 'moveChangePosition', + 'change .js-change-list': 'changeList', + 'change .js-change-position': 'changePosition', + 'click .js-copy-existing-card': 'copyExistingCard', + 'click .js-hide-edit-list-form': 'hideListEditForm', + 'listSort': 'listSort' + }, + /** + * listSort() + * save the list moved position + * @param e + * @type Object(DOM event) + * @param data + * @type Object + * + */ + listSort: function(ev, ui) { + var target = $(ev.target); + var previous_list_id = target.prev('.js-board-list').data('list_id'); + var next_list_id = target.next('.js-board-list').data('list_id'); + if (typeof previous_list_id == 'undefined' && typeof next_list_id == 'undefined') { + previous_list_id = 1; + next_list_id = 1; + } + if (typeof previous_list_id != 'undefined') { + this.model.moveAfter(previous_list_id); + } else if (typeof next_list_id != 'undefined') { + this.model.moveBefore(next_list_id); + } else { + if (this.model.collection.length != 1) { + throw 'Unable to determine position'; + } + } + App.boards.get(this.model.attributes.board_id).lists.get(this.model.attributes.id).set('position', this.model.attributes.position); + this.model.url = api_url + 'boards/' + this.model.attributes.board_id + '/lists/' + this.model.attributes.id + '.json'; + this.model.save({ + position: this.model.attributes.position + }, { + patch: true + }); + }, + /** + * drop() + * handle image upload + * @param event + * @type Object(DOM event) + * @param data + * @type Object + * + */ + drop: function(event, data) { + event.stopPropagation(); + event.preventDefault(); + var files = event.originalEvent.dataTransfer.files; + this.processFiles(files, event.currentTarget.dataset.card_id); + return false; + }, + /** + * dragover() + * prevent default event action + * @param e + * @type Object(DOM event) + */ + dragover: function(e) { + e.stopPropagation(); + e.preventDefault(); + }, + /** + * processFiles() + * handle dropped image + * @param files + * @type Object(DOM event) + * @param card_id + * @type integer + */ + processFiles: function(files, card_id) { + _.each(files, function(file) { + this.processFile(file, card_id); + }, this); + }, + /** + * processFile() + * saved dropped images + * @param file + * @type Object(DOM event) + * @param card_id + * @type integer + * + */ + processFile: function(file, card_id) { + var fileData = new FormData(); + fileData.append('attachment', file); + var card_attachment = new App.CardAttachment(); + card_attachment.url = api_url + 'boards/' + this.model.attributes.board_id + '/lists/' + this.model.attributes.id + '/cards/' + card_id + '/attachments.json'; + card_attachment.save(fileData, { + type: 'POST', + data: fileData, + processData: false, + cache: false, + contentType: false + }); + }, + /** + * editList() + * update list + * @param e + * @type Object(DOM event) + * @return false + * + */ + editList: function(e) { + e.preventDefault(); + var temp_data = {}; + var list_id = this.model.id; + var bool = $('.js-list-subscribed-' + list_id).hasClass('hide'); + var data = $(e.target).serializeObject(); + temp_data.name = data.name + 0; + this.model.set(temp_data); + this.model.url = api_url + 'boards/' + this.model.attributes.board_id + '/lists/' + list_id + '.json'; + this.model.save(data, { + patch: true + }); + if (bool) { + $('.js-list-subscribed-' + list_id).addClass('hide'); + } else { + $('.js-list-subscribed-' + list_id).removeClass('hide'); + } + return false; + }, + /** + * showListActions() + * display list actions + * @param e + * @type Object(DOM event) + * + */ + showListActions: function(e) { + $('.js-list-actions-response').remove(); + $(e.currentTarget).after(new App.ListActionsView({ + model: this.model, + subscribers: this.model.lists_subscribers, + authuser: this.authuser + }).el); + }, + /** + * backToListActions() + * display list actions + * @param e + * @type Object(DOM event) + * @return false + * + */ + backToListActions: function(e) { + var list_id = $(e.target).parents().find('.js-show-list-actions').attr('data-list-id'); + var list_action = $(e.target).parents().find('#js-show-list-actions-' + list_id); + $('.js-list-actions-response').remove(); + var subscribers = new App.ListSubscriberCollection(); + subscribers.add(this.model.attributes.lists_subscribers); + $(list_action).after(new App.ListActionsView({ + model: this.model, + subscribers: subscribers, + authuser: this.authuser + }).el); + return false; + }, + /** + * closePopup() + * close opened dropdwon + * @param e + * @type Object(DOM event) + * @return false + * + */ + closePopup: function(e) { + var target = $(e.target); + target.parents('li.dropdown:first, div.dropdown:first').removeClass('open'); + return false; + }, + /** + * showConfirmArchiveList() + * display confirmation box for list archive + * @param e + * @type Object(DOM event) + * @return false + * + */ + showConfirmArchiveList: function(e) { + $('.js-list-actions-response').html(new App.ListArchiveConfirmView({ + model: this.model + }).el); + return false; + }, + /** + * archiveList() + * archive list + * @param e + * @type Object(DOM event) + * @return false + * + */ + archiveList: function(e) { + var self = this; + self.$el.remove(); + var list_id = self.model.id; + this.model.set('is_archived', true); + this.model.url = api_url + 'boards/' + self.board.id + '/lists/' + list_id + '.json'; + App.boards.get(this.model.attributes.board_id).lists.get(this.model.attributes.id).set('is_archived', true); + this.model.save({ + is_archived: true + }, { + patch: true, + success: function(model, response) { + self.board.activities.unshift(response.activity); + } + }); + return false; + }, + /** + * showConfirmListDelete() + * display delete confirmation + * @param e + * @type Object(DOM event) + * @return false + * + */ + showConfirmListDelete: function(e) { + $('.js-list-actions-response').html(new App.ListDeleteConfirmView({ + model: this.model, + }).el); + return false; + }, + /** + * showConfirmListDelete() + * delete list + * @param e + * @type Object(DOM event) + * @return false + * + */ + deleteList: function(e) { + var self = this; + self.$el.remove(); + var list_id = self.model.id; + var removed_list_cards = self.board.cards.where({ + list_id: self.model.attributes.id + }); + this.model.cards.remove(removed_list_cards, { + silent: true + }); + this.model.collection.board.cards.remove(removed_list_cards, { + silent: true + }); + this.model.collection.board.lists.remove(self.model); + App.boards.get(self.model.attributes.board_id).lists.remove(self.model); + this.board.lists.remove(self.model); + this.model.url = api_url + 'boards/' + self.board.id + '/lists/' + list_id + '.json'; + this.model.destroy({ + success: function(model, response) { + self.board.activities.unshift(response.activity); + } + }); + return false; + }, + /** + * showCopyListForm() + * display copy list form + * @param e + * @type Object(DOM event) + * @return false + * + */ + showCopyListForm: function(e) { + $('.js-list-actions-response').html(new App.CopyListView({ + model: this.model, + }).el); + return false; + }, + /** + * showMoveListForm() + * display move list form + * @param e + * @type Object(DOM event) + * @return false + * + */ + showMoveListForm: function(e) { + e.preventDefault(); + var list_id = this.model.id; + $('.js-list-actions-response').html(new App.MoveListFromView({ + model: this.model, + boards: App.boards, + total_board_list_length: this.total_board_list_length + }).el); + return false; + }, + /** + * moveList() + * save moved list + * @param e + * @type Object(DOM event) + * @return false + * + */ + moveList: function(e) { + e.preventDefault(); + var list_id = this.model.id; + var current_list = this.model.attributes; + var data = $(e.target).serializeObject(); + var position = parseInt(data.position); + if (_.isEmpty(position)) { + position = $(e.target).find('#list_position').val(); + } + var board_id = parseInt(data.board_id); + this.model.collection.sortByColumn('position'); + if (board_id !== this.model.attributes.board_id) { + this.model.collection.remove({ + id: list_id + }); + this.$el.remove(); + } else { + var previous_position = position - 1; + var current_index = this.$el.index() + 1; + if (position !== current_index) { + if ($('.js-board-list').length === position || current_index < position) { + this.$el.insertAfter('.js-board-list:nth-child(' + position + ')'); + } else { + this.$el.insertBefore('.js-board-list:nth-child(' + position + ')'); + } + var target = $('.js-board-list:nth-child(' + position + ')'); + var previous_list_id = target.prev('.js-board-list').data('list_id'); + var next_list_id = target.next('.js-board-list').data('list_id'); + if (typeof previous_list_id == 'undefined' && typeof next_list_id == 'undefined') { + previous_list_id = 1; + next_list_id = 1; + } + if (typeof previous_list_id != 'undefined') { + this.model.moveAfter(previous_list_id); + } else if (typeof next_list_id != 'undefined') { + this.model.moveBefore(next_list_id); + } else { + if (this.model.collection.length != 1) { + throw 'Unable to determine position'; + } + } + } else { + this.closePopup(e); + } + } + App.boards.get(this.model.attributes.board_id).lists.get(this.model.attributes.id).set('position', this.model.attributes.position); + data.position = this.model.attributes.position; + data.board_id = board_id; + this.model.url = api_url + 'boards/' + this.model.attributes.board_id + '/lists/' + list_id + '.json'; + this.model.save(data, { + patch: true + }); + this.closePopup(e); + return false; + }, + /** + * listSubscribe() + * subscribe list + * @param e + * @type Object(DOM event) + * @return false + * + */ + listSubscribe: function(e) { + var list_id = this.model.id; + var subscribe_id = $(e.currentTarget).data('subscribe-id'); + $(e.currentTarget).removeClass('js-list-subscribe').addClass('js-list-unsubscribe'); + $('i.icon-ok', e.currentTarget).removeClass('hide'); + $('.js-subscribe-text', e.currentTarget).html('Subscribed'); + $('.js-list-subscribed-' + list_id).removeClass('hide'); + var list_subscribe = new App.ListSubscriber(); + list_subscribe.set('user_id', parseInt(authuser.user.id)); + list_subscribe.set('is_subscribed', true); + this.model.lists_subscribers.add(list_subscribe); + if (typeof subscribe_id === 'undefined' || subscribe_id === 'undefined' || subscribe_id === '') { + var subscribe = { + subscribe: { + is_subscribed: 't' + } + }; + list_subscribe.url = api_url + 'boards/' + this.model.attributes.board_id + '/lists/' + list_id + '/list_subscribers.json'; + list_subscribe.save({ + is_subscribed: true + }); + } else { + list_subscribe.url = api_url + 'boards/' + this.model.attributes.board_id + '/lists/' + list_id + '/list_subscribers/' + subscribe_id + '.json'; + list_subscribe.save({ + id: parseInt(subscribe_id), + is_subscribed: true + }); + } + return false; + }, + /** + * listUnsubscribe() + * unsubscribe list + * @param e + * @type Object(DOM event) + * @return false + * + */ + listUnsubscribe: function(e) { + $(e.currentTarget).removeClass('js-list-unsubscribe').addClass('js-list-subscribe'); + $('i.icon-ok', e.currentTarget).addClass('hide'); + $('.js-subscribe-text', e.currentTarget).html('Subscribe'); + var subscribe_id = $(e.currentTarget).data('subscribe-id'); + var list_id = this.model.id; + $('.js-list-subscribed-' + list_id).addClass('hide'); + var list_subscribe = new App.ListSubscriber(); + this.model.lists_subscribers.remove(this.model.lists_subscribers.findWhere({ + user_id: parseInt(authuser.user.id), + is_subscribed: true + })); + if (typeof subscribe_id === 'undefined' || subscribe_id === 'undefined' || subscribe_id === '') { + list_subscribe.url = api_url + 'boards/' + this.model.attributes.board_id + '/lists/' + list_id + '/list_subscribers.json'; + list_subscribe.save({ + is_subscribed: false + }); + } else { + list_subscribe.url = api_url + 'boards/' + this.model.attributes.board_id + '/lists/' + list_id + '/list_subscribers/' + subscribe_id + '.json'; + list_subscribe.save({ + id: parseInt(subscribe_id), + is_subscribed: false + }); + } + return false; + }, + /** + * showMoveCardListForm() + * display move card form + * @param e + * @type Object(DOM event) + * @return false + * + */ + showMoveCardListForm: function(e) { + var list_id = this.model.id; + var current_list = this.model; + var board_list = new App.ListCollection(); + board_list.add(this.model.collection.models); + var filtered_lists = board_list.where({ + is_archived: false + }); + this.$('.js-list-actions-response').html(new App.MoveCardsFromListView({ + filtered_lists: filtered_lists, + model: this.model, + + }).el); + return false; + }, + /** + * moveCards() + * save moved card + * @param e + * @type Object(DOM event) + * @return false + * + */ + moveCards: function(e) { + $('li.dropdown').removeClass('open'); + var list_id = this.model.id; + var self = this; + var move_list_id = parseInt($(e.currentTarget).data('move-list-id')); + var copied_cards = this.model.collection.board.cards.where({ + list_id: list_id + }); + this.model.cards.set(copied_cards); + var view_card = $('#js-card-listing-' + move_list_id); + var i = 1; + _.each(copied_cards, function(copied_card) { + var options = { + silent: true + }; + if (i === copied_cards.length) { + options.silent = false; + } + self.model.collection.board.cards.get(copied_card.id).set({ + list_id: move_list_id + }, options); + i++; + }); + var attachments = this.model.collection.board.attachments.where({ + list_id: this.model.id + }); + var j = 1; + _.each(attachments, function(attachment) { + var options = { + silent: true + }; + if (j === attachments.length) { + options.silent = false; + } + self.model.collection.board.attachments.get(attachment.id).set({ + list_id: move_list_id + }, options); + j++; + }); + this.model.id = list_id; + this.model.url = api_url + 'boards/' + this.board.id + '/lists/' + list_id + '/cards.json'; + this.model.save({ + list_id: move_list_id + }, { + patch: true + }); + return false; + }, + /** + * showConfirmArchiveCards() + * display card archive confirmation + * @param e + * @type Object(DOM event) + * @return false + * + */ + showConfirmArchiveCards: function(e) { + $('.js-list-actions-response').html(new App.ListCardsArchiveConfirmView({ + model: this.model + }).el); + return false; + }, + /** + * archiveCard() + * save archived card + * @param e + * @type Object(DOM event) + * @return false + * + */ + archiveCard: function(e, ui) { + this.closePopup(e); + var self = this; + var list_id = this.model.id; + var archived_cards = this.model.collection.board.cards.where({ + list_id: list_id + }); + _.each(archived_cards, function(archived_card) { + self.model.collection.board.cards.get(archived_card.attributes.id).set('is_archived', true); + }); + var card = new App.Card(); + card.set('id', list_id); + card.url = api_url + 'boards/' + this.board.id + '/lists/' + list_id + '/cards.json'; + card.save({ + is_archived: true + }, { + patch: true + }); + return false; + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + list: this.model + })); + this.renderCardsCollection(); + if (!_.isUndefined(authuser.user)) { + $('.js-board-list-cards', this.$el).sortable({ + containment: 'window', + items: 'div.js-board-list-card', + connectWith: '.js-board-list-cards', + placeholder: 'card-list-placeholder', + appendTo: document.body, + dropOnEmpty: true, + cursor: 'grabbing', + helper: 'clone', + tolerance: 'pointer', + scrollSensitivity: 100, + scrollSpeed: 50, + update: function(ev, ui) { + if (this === ui.item.parent()[0]) { + ui.item.trigger('cardSort', ev, ui); + } + }, + start: function(ev, ui) { + ui.placeholder.height(ui.item.outerHeight()); + $('.js-show-modal-card-view ').removeClass('cur'); + }, + stop: function(ev, ui) { + $('.js-show-modal-card-view ').addClass('cur'); + }, + over: function(ev, ui) { + var scrollLeft = 0; + if ((($(window).width() - ui.offset.left) < 520) || (ui.offset.left > $(window).width())) { + scrollLeft = $('#js-board-lists').stop().scrollLeft() + ($(window).width() - ui.offset.left); + $('#js-board-lists').animate({ + scrollLeft: scrollLeft + }, 800); + } else if (ui.offset.left <= 260) { + scrollLeft = $('#js-board-lists').stop().scrollLeft() - ($(window).width() - ui.offset.left); + $('#js-board-lists').animate({ + scrollLeft: scrollLeft + }, 800); + } + + }, + change: function(event, ui) { + var scrollTop = 0; + if ((($(window).height() - ui.offset.top) < 350) || (ui.offset.top > $(window).height())) { + scrollTop = $(this).scrollTop() + ($(window).height() - ui.offset.top); + $(this).stop().animate({ + scrollTop: scrollTop + }, 800); + } else if (ui.offset.top <= 230) { + scrollTop = $(this).scrollTop() - ($(window).height() - ui.offset.top); + $(this).stop().animate({ + scrollTop: scrollTop + }, 800); + } + } + }); + } + this.showTooltip(); + return this; + }, + /** + * renderListAdd() + * display list add form + * @return false + * + */ + renderListAdd: function() { + this.$el.html(this.templateAdd()); + return this; + }, + /** + * renderCardsCollection() + * display cards in list + * + */ + renderCardsCollection: function() { + var self = this; + var view_card = this.$('#js-card-listing-' + this.model.id); + view_card.html(' '); + this.model.cards.sortByColumn('position'); + if (!_.isUndefined(this.model.collection)) { + var filtered_cards = this.model.collection.board.cards.where({ + list_id: this.model.id + }); + var cards = new App.CardCollection(); + cards.reset(filtered_cards); + this.model.cards.add(cards.toJSON(), { + silent: true + }); + this.model.cards.sortByColumn('position'); + cards.sortByColumn('position'); + cards.each(function(card) { + var card_id = card.id; + if (card.get('is_archived') === false || card.get('is_archived') === 'f') { + card.board_users = self.model.board_users; + var filter_labels = self.model.labels.filter(function(model) { + return parseInt(model.get('card_id')) === parseInt(card_id); + }); + var labels = new App.CardLabelCollection(); + labels.add(filter_labels, { + silent: true + }); + card.labels = labels; + card.card_voters.add(card.get('card_voters'), { + silent: true + }); + card.cards = self.model.collection.board.cards; + card.list = self.model; + card.board_activities.add(self.model.activities, { + silent: true + }); + filter_attachments = self.model.attachments.where({ + card_id: card.id + }); + card.attachments.add(filter_attachments, { + silent: true + }); + var view = new App.CardView({ + tagName: 'div', + model: card, + converter: this.converter + }); + view_card.append(view.render().el); + } + }); + } + + }, + /** + * showAddCardForm() + * display cards add form + * @param e + * @type Object(DOM event) + * @return false + * + */ + showAddCardForm: function(e) { + e.preventDefault(); + var target = $(e.target); + var el = this.$el; + var view_card = this.$('.js-card-add-form-' + this.model.id); + el.find('.js-show-add-card-form').addClass('hide'); + target.parents('.dropdown').removeClass('open'); + el.find('#cardAddForm').remove(); + var card = new App.Card(); + card.set('list_id', (!isNaN(this.model.id)) ? this.model.id : this.model.attributes.temp_id); + card.set('board_id', this.model.get('board_id')); + card.board_users = this.model.board_users; + card.list = this.model; + card.collection = this.model.cards; + var view = new App.CardView({ + model: card, + className: 'col-xs-12', + attributes: '', + id: 'js-list-card-add-form-' + this.model.id + }); + $('#js-list-card-add-form-' + this.model.id).remove(); + if (target.hasClass('js-bottom')) { + view_card.append(view.renderAdd().el).find('textarea').focus(); + } else { + view_card.prepend(view.renderAdd().el).find('textarea').focus(); + } + $('#js-card-listing-' + this.model.id).scrollTop($('#js-card-listing-' + this.model.id)[0].scrollHeight); + return false; + }, + /** + * showListModal() + * display list attachments + * @param e + * @type Object(DOM event) + * @return false + * + */ + showListModal: function(e) { + var modalView = new App.ModalListView({ + model: this.model + }); + modalView.show(); + return false; + }, + /** + * showListEditForm() + * display list edit form + * @param e + * @type Object(DOM event) + * @return false + * + */ + showListEditForm: function(e) { + e.preventDefault(); + this.closePopup(e); + $(e.currentTarget).addClass('hide').next('form').removeClass('hide'); + this.$('#js-show-list-actions-' + this.model.attributes.id).addClass('hide'); + return false; + }, + /** + * hideEditListForm() + * hide the list edit form + * @param e + * @type Object(DOM event) + * @return false + * + */ + hideListEditForm: function(e) { + e.preventDefault(); + var toggle = $(e.currentTarget); + toggle.parents('form').addClass('hide').prev('.js-show-edit-list-form').removeClass('hide'); + this.$('#js-show-list-actions-' + this.model.attributes.id).removeClass('hide'); + return false; + }, + /** + * addCard() + * save newly added card + * @param e + * @type Object(DOM event) + * + */ + addCard: function(e) { + e.preventDefault(); + var self = this; + var data = $(e.target).serializeObject(); + $('.js-card-add-list').val(this.model.id); + $('.js-card-user-ids').val(''); + $('.js-card-add-labels').val(''); + $('.js-card-add-position').val(''); + data.uuid = new Date().getTime(); + data.list_id = parseInt(data.list_id); + data.board_id = parseInt(data.board_id); + var cards = this.model.collection.board.cards.where({ + list_id: data.list_id + }); + var list_cards = new App.CardCollection(); + list_cards.add(cards); + list_cards.sortByColumn('position'); + if (data.position === undefined || data.position === '') { + data.position = list_cards.length + 1; + } + $(e.target).find('textarea').val('').focus(); + var view_card = $('#js-card-listing-' + data.list_id); + var card = new App.Card(); + card.set('is_offline', true); + card.url = api_url + 'boards/' + self.model.attributes.board_id + '/lists/' + self.model.id + '/cards.json'; + card.set({ + name: data.name, + is_archived: false, + list_id: parseInt(data.list_id), + board_id: parseInt(data.board_id), + due_date: null, + checklist_item_count: 0, + checklist_item_completed_count: 0 + }, { + silent: true + }); + card.list = self.model; + var view = new App.CardView({ + tagName: 'div', + model: card, + converter: this.converter + }); + var next = view_card.find('.js-board-list-card:nth-child(' + data.position + ')'); + var prev = view_card.find('.js-board-list-card:nth-child(' + (data.position - 1) + ')'); + var before = ''; + var after = ''; + var difference = ''; + var newPosition = ''; + if (prev.length !== 0) { + before = list_cards.get(parseInt(prev.data('card_id'))); + after = list_cards.at(list_cards.indexOf(before) + 1); + if (typeof after == 'undefined') { + afterPosition = before.position() + 2; + } else { + afterPosition = after.position(); + } + difference = (afterPosition - before.position()) / 2; + newPosition = difference + before.position(); + card.set({ + position: newPosition + }); + data.position = newPosition; + prev.after(view.render().el); + } else if (next.length !== 0) { + after = list_cards.get(parseInt(next.data('card_id'))); + before = list_cards.at(list_cards.indexOf(after) - 1); + if (typeof before == 'undefined') { + beforePosition = 0.0; + } else { + beforePosition = before.position(); + } + if (typeof after == 'undefined') { + afterPosition = 0.0; + } else { + afterPosition = after.position(); + } + difference = (afterPosition - beforePosition) / 2; + newPosition = difference + beforePosition; + card.set({ + position: newPosition + }); + data.position = newPosition; + next.before(view.render().el); + } else { + view_card.append(view.render().el); + data.position = 1; + } + $('#js-card-listing-' + this.model.id).scrollTop($('#js-card-listing-' + this.model.id)[0].scrollHeight); + card.save(data, { + success: function(model, response, options) { + if (_.isUndefined(options.temp_id)) { + card.set('is_offline', false); + } + card.board_activities.add(self.model.activities); + if (!_.isUndefined(response.cards_users) && response.cards_users.length > 0) { + card.set('cards_users', response.cards_users); + card.users.add(response.cards_users); + } + if (!_.isUndefined(response.cards_labels) && response.cards_labels.length > 0) { + self.board.labels.add(response.cards_labels, { + silent: true + }); + card.labels.add(response.cards_labels); + } + var list = App.boards.get(card.attributes.board_id).lists.get(card.attributes.list_id); + if (!_.isUndefined(list)) { + list.set('card_count', list.attributes.card_count + 1); + } + + if (!_.isUndefined(response.id) && _.isUndefined(options.temp_id)) { + card.set({ + id: parseInt(response.id) + }); + } else { + global_uuid[data.uuid] = options.temp_id; + card.set('id', data.uuid); + } + self.model.collection.board.cards.add(card, { + silent: true + }); + self.model.cards.add(card, { + silent: true + }); + } + }); + }, + /** + * copyFromExistingCard() + * save copied card + * @param e + * @type Object(DOM event) + * @return false + * + */ + copyFromExistingCard: function(e) { + e.preventDefault(); + this.$el.find('.js-card-action-list-response').remove(); + this.$el.find('.js-show-card-action-list').after(new App.CardCopyView({ + model: this.model + }).el); + return false; + }, + /** + * cardSearch() + * search cards + * @param e + * @type Object(DOM event) + * + */ + cardSearch: function(e) { + var self = this; + var search_q = $(e.currentTarget).val(); + var filtered_cards = self.board.cards.search(search_q); + var cards = new App.CardCollection(); + cards.add(filtered_cards._wrapped); + cards.each(function(card) { + self.$el.find('.js-card-add-search-response').html(new App.CardSearchResultView({ + model: card, + attributes: { + 'data-card-id': card.id + } + }).el); + + }); + + }, + /** + * selectCard() + * add selected card in lis + * @param e + * @type Object(DOM event) + * @return false + * + */ + selectCard: function(e) { + var self = this; + self.boards = new App.BoardCollection(); + self.boards.url = api_url + 'users/' + authuser.user.id + '/boards.json?type=simple'; + self.boards.fetch({ + success: function() { + self.$el.find('.js-show-card-action-list').next().remove(); + var card_id = $(e.currentTarget).attr('data-card-id'); + var card = self.board.cards.findWhere({ + id: parseInt(card_id) + }); + self.$el.find('.js-show-card-action-list').after(new App.CopyFromExistingCardView({ + model: card, + boards: self.boards + }).el); + + } + + }); + return false; + }, + /** + * moveChangeList() + * change list based on selected board + * @param string + * @type string + */ + moveChangeList: function(e) { + var target = $(e.currentTarget); + var self = this; + var board_id = parseInt(target.val()); + var content_position = ''; + if (board_id == this.model.attributes.board_id) { + this.showMoveListForm(e); + } else { + var board = App.boards.findWhere({ + id: parseInt(board_id), + is_closed: false + }); + board.lists.add(board.attributes.lists); + var board_lists = board.lists.where({ + is_archived: false, + is_deleted: false + }); + var current_position = this.model.collection.indexOf(this.model) + 1; + for (var i = 1; i <= board_lists.length; i++) { + if (self.model.attributes.board_id == board.attributes.id && i == current_position) { + content_position += ''; + } else { + content_position += ''; + } + } + if (self.model.attributes.board_id != board.attributes.id) { + var next_position = parseInt(board_lists.length) + 1; + content_position += ''; + } + self.$el.find('.js-move-change-position').html(content_position); + } + }, + /** + * moveChangePosition() + * change position based on selected list + * @param string + * @type string + */ + moveChangePosition: function(e) { + var target = $(e.currentTarget); + var self = this; + var list_id = target.val(); + var board_id = target.parent().prev().find('.js-change-list').val(); + var content_position = ''; + var board = App.boards.findWhere({ + id: parseInt(board_id) + }); + if (!_.isUndefined(board)) { + var list = board.lists.findWhere({ + id: parseInt(list_id) + }); + var current_position = this.model.collection.indexOf(this.model) + 1; + for (var i = 1; i <= list.attributes.card_count; i++) { + if (self.model.attributes.list_id == list.attributes.id && i == current_position) { + content_position += ''; + } else { + content_position += ''; + } + } + if (this.model.attributes.list_id != list.attributes.id) { + var next_position = parseInt(list.attributes.card_count) + 1; + content_position += ''; + } + self.$el.find('.js-position').html(content_position); + } + }, + /** + * changeList() + * change list based on selected board + * @param e + * @type Object(DOM event) + * + */ + changeList: function(e) { + var target = $(e.currentTarget); + var self = this; + var board_id = target.val(); + var content_list = ''; + var content_position = ''; + var board = self.boards.findWhere({ + id: parseInt(board_id) + }); + board.lists.add(board.attributes.lists); + var is_first_list = true; + board.lists.each(function(list) { + if (self.model.attributes.list_id == self.model.attributes.id) { + content_list += ''; + is_first_list = true; + } else { + content_list += ''; + } + if (is_first_list) { + is_first_list = false; + for (var i = 1; i <= list.attributes.card_count; i++) { + content_position += ''; + } + } + }); + self.$el.find('.js-change-position').html(content_list); + self.$el.find('.js-position').html(content_position); + }, + /** + * changePosition() + * change position based on list + * @param e + * @type Object(DOM event) + * + */ + changePosition: function(e) { + var target = $(e.currentTarget); + var self = this; + var list_id = target.val(); + var board_id = target.parent().prev().find('.js-change-list').val(); + var content_position = ''; + var board = self.boards.findWhere({ + id: parseInt(board_id) + }); + board.cards.add(board.attributes.cards); + var filtered_cards = board.cards.where({ + board_id: parseInt(board_id), + list_id: parseInt(list_id) + }); + var cards = new App.CardCollection(); + cards.add(filtered_cards); + cards.each(function(card) { + content_position += ''; + }); + if (content_position === '') { + content_position += ''; + } + self.$el.find('.js-position').html(content_position); + }, + /** + * copyExistingCard() + * save copied card + * @param e + * @type Object(DOM event) + * + */ + copyExistingCard: function(e) { + var self = this; + self.closePopup(e); + var data = $(e.currentTarget).parents('form.js-copy-existing-card-form').serializeObject(); + var card = new App.Card(); + data.keep_attachments = 1; + data.keep_activities = 1; + data.keep_labels = 1; + data.keep_users = 1; + card.url = api_url + 'boards/' + this.model.attributes.board_id + '/lists/' + this.model.attributes.id + '/cards/' + data.copied_card_id + '/copy.json'; + card.save(data, { + patch: true, + success: function(model, response) { + var view_card = self.$el.find('#js-card-listing-' + self.model.id); + var card_view = new App.CardView({ + tagName: 'div', + model: card + }); + card_view.hideAddCardFrom(e); + view_card.append(card_view.render().el); + var activity = new App.Activity(); + activity.set(response.activity); + var view = new App.ActivityView({ + model: activity + }); + var view_activity = $('#js-card-activities-' + data.copied_card_id); + view_activity.prepend(view.render().el).find('.timeago').timeago(); + } + }); + }, + noAction: function(e) { + e.preventDefault(); + return false; + }, + removeRender: function() { + this.$el.remove(); + }, + onEnter: function(e) { + if (e.which === 13) { + e.preventDefault(); + var form = $(e.target).closest('form'); + if (form.attr('name') === 'cardAddForm') { + $('input[type=submit]', form).trigger('click'); + } else { + return false; + } + } + } +}); diff --git a/client/js/views/login_view.js b/client/js/views/login_view.js new file mode 100644 index 000000000..9ca4ef93d --- /dev/null +++ b/client/js/views/login_view.js @@ -0,0 +1,108 @@ +/** + * @fileOverview This file has functions related to login view. This view calling from application view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : user model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Login View + * @class LoginView + * @constructor + * @extends Backbone.View + */ +App.LoginView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + this.changeFavicon(); + }, + template: JST['templates/login'], + tagName: 'section', + className: 'clearfix', + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'submit': 'login' + }, + /** + * login() + * user login + * @return false + * + */ + login: function(e) { + var self = this; + var target = $(e.target); + var data = target.serializeObject(); + var user = new App.User(); + user.url = api_url + 'users/login.json'; + user.save(data, { + success: function(model, response) { + authuser = response; + if (!_.isUndefined(response.access_token)) { + window.sessionStorage.setItem('auth', JSON.stringify(response)); + api_token = response.access_token; + var links = JSON.parse(response.links); + window.sessionStorage.setItem('links', response.links); + role_links.reset(); + if (!_.isEmpty(links)) { + role_links.add(links); + } + var organizations = authuser.user.organizations; + authuser.user.organizations = new App.OrganizationCollection(); + authuser.user.organizations.add(organizations); + self.changeFavicon(response.user.notify_count); + this.headerView = new App.HeaderView({ + model: model + }); + $('#header').html(this.headerView.el); + app.navigate('#/boards', { + trigger: true, + replace: true + }); + //self.flash('success', 'Wellcome ' + authuser.user.username); + } else { + $('input#inputPassword', target).val(''); + self.flash('danger', response.error); + } + + } + }); + return false; + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template()); + this.showTooltip(); + return this; + }, + /** + * changeFavicon() + * update notification count in favicon + * @param count + * @type number + * + */ + changeFavicon: function(count) { + if (!_.isUndefined(count) && count !== '0') { + favicon.badge(count); + } + } +}); diff --git a/client/js/views/modal_activity_view.js b/client/js/views/modal_activity_view.js new file mode 100644 index 000000000..59d2df457 --- /dev/null +++ b/client/js/views/modal_activity_view.js @@ -0,0 +1,162 @@ +/** + * @fileOverview This file has functions related to modal activity view. This view calling from footer view, organization and user index view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : activity model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ModalActivity View + * @class ModalActivityView + * @constructor + * @extends Backbone.View + */ +App.ModalActivityView = Backbone.View.extend({ + id: 'base-modal', + className: '', + template: JST['templates/modal_activity_view'], + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-close-popover': 'closePopup', + 'click .js-load-more': 'loadMore', + }, + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.type = options.type; + this.organization_id = options.organization_id; + this.last_activity_id = 0; + _(this).bindAll('show', 'teardown'); + this.activities = new App.ActivityCollection(); + var query_string = ''; + if (!_.isUndefined(this.organization_id)) { + query_string += '&organization_id=' + this.organization_id; + } + if (this.type === 'board') { + this.activities.url = api_url + 'boards/' + authuser.board_id + '/activities.json?type=all'; + } else if (this.type === 'user_listing') { + this.activities.url = api_url + 'users/' + this.model.attributes.id + '/activities.json?type=all' + query_string; + } else if (this.type === 'org_user_listing') { + this.activities.url = api_url + 'users/' + this.model + '/activities.json?type=all' + query_string; + } else { + this.activities.url = api_url + 'users/' + authuser.user.id + '/activities.json?type=all' + query_string; + } + }, + teardown: function() {}, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + list: this.model, + type: this.type + })); + + this.$el.modal({ + show: true, + backdrop: false + }); + this.showTooltip(); + return this; + }, + /** + * show() + * display list attachment + */ + show: function() { + var self = this; + self.render(); + this.activities.fetch({ + success: function() { + self.renderActivitiesCollection(false); + if (self.activities.models.length > 0) { + self.$el.find('.js-load-more').removeClass('hide'); + } else { + self.$el.find('.js-load-more').addClass('hide'); + } + } + }); + this.$el.find('#modalActivityView').modal('show'); + }, + /** + * closePopup() + * close opened dropdown + * @param e + * @type Object(DOM event) + * @return false + */ + closePopup: function(e) { + var target = $(e.target); + target.parents('li.dropdown').removeClass('open'); + return false; + }, + /** + * renderActivitiesCollection() + * render Activity + */ + renderActivitiesCollection: function(is_load_more) { + var self = this; + var last_activity = _.min(this.activities.models, function(activity) { + return activity.id; + }); + this.last_activity_id = last_activity.id; + this.activities.each(function(activity) { + var view = new App.ActivityView({ + model: activity, + type: 'all' + }); + self.$el.find('#js-activities-list').append(view.render().el).find('.timeago').timeago(); + }); + if (!is_load_more && this.activities.models.length === 0) { + var view = new App.ActivityView({ + model: null, + type: 'all' + }); + self.$el.find('#js-activities-list').html(view.render().el); + } + }, + /** + * loadMore() + * load more + * @param e + * @type Object(DOM event) + * @return false + */ + loadMore: function(e) { + var self = this; + var query_string = '&last_activity_id=' + this.last_activity_id; + if (!_.isUndefined(this.organization_id)) { + query_string += '&organization_id=' + this.organization_id; + } + if (this.type === 'board') { + this.activities.url = api_url + 'boards/' + authuser.board_id + '/activities.json?type=all' + query_string; + } else if (this.type === 'user_listing') { + this.activities.url = api_url + 'users/' + this.model.attributes.id + '/activities.json?type=profile' + query_string; + } else if (this.type === 'org_user_listing') { + this.activities.url = api_url + 'users/' + this.model + '/activities.json?type=profile' + query_string; + } else { + this.activities.url = api_url + 'users/' + authuser.user.id + '/activities.json?type=profile' + query_string; + } + + this.activities.fetch({ + success: function() { + self.renderActivitiesCollection(true); + } + }); + return false; + } +}); diff --git a/client/js/views/modal_board_view.js b/client/js/views/modal_board_view.js new file mode 100644 index 000000000..a693ee58f --- /dev/null +++ b/client/js/views/modal_board_view.js @@ -0,0 +1,114 @@ +/** + * @fileOverview This file has functions related to modal board view. This view calling from board header view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board model. @see Available Object in App.BoardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ModalBoard View + * @class ModalBoardView + * @constructor + * @extends Backbone.View + */ +App.ModalBoardView = Backbone.View.extend({ + id: 'base-modal', + className: '', + template: JST['templates/modal_list_view'], + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-close-popover': 'closePopup', + }, + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.model.attachments.bind('remove', this.displayEmptyMessage, this); + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + list: this.model + })); + this.renderAttachmentsCollection(); + this.$el.modal({ + show: true, + backdrop: false + }); + this.showTooltip(); + return this; + }, + /** + * show() + * display list attachment in modal box + * + */ + show: function() { + this.render(); + this.$el.find('#modalListView').modal('show'); + }, + /** + * closePopup() + * hide displayed dropdown + * @param e + * @type Object(DOM event) + * @return false + */ + closePopup: function(e) { + var target = $(e.target); + target.parents('li.dropdown').removeClass('open'); + return false; + }, + /** + * renderAttachmentsCollection() + * display attachments in list + * @param e + * @type Object(DOM event) + * @return false + */ + renderAttachmentsCollection: function() { + var view_attachment = this.$('#js-list-attachments-list'); + view_attachment.html(''); + var attachments = this.model.attachments; + var attachments_length = attachments.models.length; + if (attachments_length > 0) { + for (var attachments_i = 0; attachments_i < attachments_length; attachments_i++) { + var attachment = attachments.models[attachments_i]; + var view = new App.AttachmentView({ + model: attachment + }); + view_attachment.append(view.render().el); + } + view_attachment.find('.timeago').timeago(); + } else { + var empty_view = new App.AttachmentView({ + model: null + }); + view_attachment.append(empty_view.render().el); + } + }, + displayEmptyMessage: function() { + if (this.model.attachments.length === 0) { + var view_attachment = this.$('#js-list-attachments-list'); + var view = new App.AttachmentView({ + model: null + }); + view_attachment.html(view.render().el); + } + } +}); diff --git a/client/js/views/modal_card_member_form_view.js b/client/js/views/modal_card_member_form_view.js new file mode 100644 index 000000000..6dbf9b8ff --- /dev/null +++ b/client/js/views/modal_card_member_form_view.js @@ -0,0 +1,48 @@ +/** + * @fileOverview This file has functions related to modal card member form view. This view calling from modal card view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board users collection. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ModalCardMemberForm View + * @class ModalCardMemberFormView + * @constructor + * @extends Backbone.View + */ +App.ModalCardMemberFormView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.users = options.users; + this.card = options.card; + this.render(); + }, + template: JST['templates/modal_card_member_form'], + tagName: 'div', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.el = this.template({ + board_users: this.model, + users: this.users, + card: this.card, + }); + + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/modal_card_view.js b/client/js/views/modal_card_view.js new file mode 100644 index 000000000..268996b2f --- /dev/null +++ b/client/js/views/modal_card_view.js @@ -0,0 +1,2593 @@ +/** + * @fileOverview This file has functions related to dockmodal card view. This view calling from card view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : card model and it's related values + * this.model.attachments : attachments collection(Based on card) + * this.model.board_users : board users collection(Based on board) + * this.model.card_voters : card users collection(Based on card) + * this.model.cards_subscribers : card subscribers collection(Based on card) + * this.model.checklists : checklists collection(Based on card) + * this.model.labels : labels collection(Based on card) + * this.model.list : list model(Based on card). It contain all list based object @see Available Object in App.ListView + * this.model.users : card users collection(Based on card) + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ModalCard View + * @class ModalCardView + * @constructor + * @extends Backbone.View + */ +App.ModalCardView = Backbone.View.extend({ + id: 'base-modal', + className: '', + converter: new Showdown.converter(), + template: JST['templates/modal_card_view'], + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-close-popover': 'closePopup', + 'click .js-open-popover': 'openPopup', + 'click .js-show-card-title-edit-form': 'showCardTitleEditForm', + 'click .js-cancel-card-title-edit': 'cancelCardTitleEditForm', + 'submit form.js-card-edit-form': 'editCard', + 'click .js-show-card-desc-edit-form': 'showCardDescEditForm', + 'click .js-cancel-card-description-edit': 'cancelCardDescEditForm', + 'click .js-show-card-due-date-form': 'showCardDueDateForm', + 'click .js-edit-card-due-date-form': 'editCardDueDateForm', + 'submit form.js-card-label-add-form': 'addCardLabel', + 'click .js-show-card-label-form': 'showCardLabelForm', + 'click .js-add-card-vote': 'addCardVote', + 'click .js-delete-card-vote': 'deleteCardVote', + 'click .js-card-subscribe': 'cardSubscribe', + 'click .js-card-unsubscribe': 'cardUnsubscribe', + 'click .js-show-move-card-form': 'showMoveCardForm', + 'submit form.js-move-card': 'moveCard', + 'click .js-archive-card': 'archiveCard', + 'click .js-card-send-to-board': 'cardSendToBoard', + 'click .js-delete-card': 'deleteCard', + 'click a.js-attachment-dropbox-open': 'dropboxChooser', + 'click a.js-attachment-computer-open': 'computerOpen', + 'change .js-card-attachment': 'addCardAttachment', + 'click .js-show-checklist-add-form': 'showChecklistAddForm', + 'submit form.js-add-checklist': 'addChecklist', + 'click .js-show-add-member-form': 'showAddMemberForm', + 'click .js-add-card-member': 'addCardMember', + 'click .js-remove-card-member': 'removeCardMember', + 'click .js-remove-due-date': 'removeDueDate', + 'change .js-change-list': 'changeList', + 'change .js-change-position': 'changePosition', + 'click .js-show-copy-card-form': 'showCopyCardForm', + 'submit .js-copy-card': 'copyCard', + 'click .js-more-dropdown': 'showMoreForm', + 'click .js-select-card-url': 'selectCardURL', + 'click .js-show-add-comment-form': 'showAddCommentForm', + 'submit form.js-add-comment': 'addComment', + 'click .js-show-edit-activity': 'showEditCommentForm', + 'submit form.js-edit-comment': 'editComment', + 'click .js-hide-edit-comment-form': 'hideEditCommentForm', + 'click .js-show-confirm-comment-delete': 'showConfirmCommentDelete', + 'click .js-delete-comment': 'deleteComment', + 'click .js-show-reply-activity-form': 'showReplyCommentForm', + 'click .js-hide-reply-comment-form': 'hideReplyCommentForm', + 'submit .js-card-attachment-link-form': 'addCardAttachmentLink', + 'click .js-show-card-voters-list': 'showCardVotersList', + 'keyup .js-search-card': 'showSearchCards', + 'click .js-add-comment-card': 'AddCommentCard', + 'keyup .js-search-member': 'showSearchMembers', + 'click .js-add-comment-member': 'AddCommentMember', + 'focus .js-comment': 'showActions', + 'keyup .js-search-users': 'showSearchUsers', + 'click .js-load-dropbox': 'loadDropbox', + 'click .js-no-action': 'noAction', + 'click .js-show-side-card-title-edit-form': 'showSideCardTitleEditForm', + 'click .js-open-dropdown': 'openDropdown', + 'keypress input[type=text]': 'onEnter' + }, + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + var self = this; + _.bindAll(this, 'render', 'renderChecklistsCollection', 'renderUsersCollection', 'refreshdock'); + this.model.bind('change:name change:description change:board_id change:cards_checklists change:cards_labels change:cards_subscribers change:is_archived change:due_date change:list_id change:title', this.refreshdock); + this.model.cards_subscribers.bind('add remove', this.refreshdock); + this.model.checklists.bind('remove', this.renderChecklistsCollection); + this.model.checklists.bind('add', this.renderChecklistsCollection); + this.model.list.collection.board.checklist_items.bind('add', this.renderChecklistsCollection); + this.model.list.collection.board.checklist_items.bind('remove', this.renderChecklistsCollection); + this.model.list.collection.board.cards.bind('change:list_id', this.refreshdock); + this.model.users.bind('add', this.renderUsersCollection); + this.model.users.bind('remove', this.renderUsersCollection); + self.authuser = authuser.user; + this.model.card_voters.bind('add', this.refreshdock); + this.model.card_voters.bind('remove', this.refreshdock); + this.model.attachments.bind('add', this.refreshdock); + this.model.attachments.bind('remove', this.refreshdock); + this.board = self.model.list.collection.board; + _(this).bindAll('show'); + this.boards = new App.BoardCollection(); + if (!_.isUndefined(authuser.user)) { + this.boards.url = api_url + 'users/' + authuser.user.id + '/boards.json?type=simple'; + this.boards.fetch(); + + } + this.boards = App.boards; + }, + /** + * drop() + * handle card sorting(save card position) and image upload + * @param event + * @type Object(DOM event) + * @param data + * @type Object + * + */ + drop: function(event, data) { + event.stopPropagation(); + event.preventDefault(); + if (!_.isUndefined(event.originalEvent)) { + var files = event.originalEvent.dataTransfer.files; + this.processFiles(files, event.currentTarget.dataset.card_id); + } + return false; + }, + /** + * dragover() + * prevent default event action + * @param e + * @type Object(DOM event) + */ + dragover: function(e) { + e.stopPropagation(); + e.preventDefault(); + }, + /** + * processFiles() + * handle dropped image + * @param files + * @type Object(DOM event) + * @param card_id + * @type integer + */ + processFiles: function(files, card_id) { + _.each(files, function(file) { + this.processFile(file, card_id); + }, this); + }, + /** + * processFile() + * saved dropped images + * @param file + * @type Object(DOM event) + * @param card_id + * @type integer + * + */ + processFile: function(file, card_id) { + var fileData = new FormData(); + fileData.append('attachment', file); + var card_attachment = new App.CardAttachment(); + card_attachment.url = api_url + 'boards/' + this.model.attributes.board_id + '/lists/' + this.model.attributes.list_id + '/cards/' + this.model.id + '/attachments.json'; + card_attachment.save(fileData, { + type: 'POST', + data: fileData, + processData: false, + cache: false, + contentType: false + }); + }, + /** + * addCardLabel() + * save card labels + * @param e + * @type Object(DOM event) + * @return false + */ + addCardLabel: function(e) { + e.preventDefault(); + var self = this; + var target = $(e.target); + var data = target.serializeObject(); + data.uuid = new Date().getTime(); + target.parents('li.dropdown').removeClass('open'); + var filtered_labels = self.model.list.collection.board.labels.where({ + card_id: self.model.id + }); + var labels = new App.CardLabelCollection(); + labels.add(filtered_labels); + labels.each(function(label) { + self.model.list.collection.board.labels.remove(label, { + silent: true + }); + self.model.labels.remove(label, { + silent: true + }); + }); + var card_label = new App.Label(); + card_label.set('is_offline', true); + card_label.set('board_id', self.model.attributes.board_id); + card_label.set('list_id', self.model.attributes.list_id); + card_label.set('card_id', self.model.id); + var newLabelList = data.name; + var oldLabelList = data.hiddenName; + var compareList = oldLabelList.localeCompare(newLabelList); + if (compareList !== 0) { + card_label.url = api_url + 'boards/' + self.model.attributes.board_id + '/lists/' + self.model.attributes.list_id + '/cards/' + self.model.id + '/labels.json'; + card_label.save(data, { + success: function(model, response, options) { + if (_.isUndefined(options.temp_id)) { + card_label.set('is_offline', false); + } + if (!_.isUndefined(response.id) && _.isUndefined(options.temp_id)) { + card_label.set({ + id: parseInt(response.id) + }); + } else { + global_uuid[data.uuid] = options.temp_id; + card_label.set('id', data.uuid); + } + card_label.set('card_name', self.model.get('name')); + var labels = data.name.split(','); + var view_label = self.$el.find('.js-card-labels-list'); + $('li.js-card-label-show').remove(); + self.model.labels.reset(); + if (!_.isUndefined(response.cards_labels)) { + labels = response.cards_labels; + } + if (labels.length > 0) { + _.each(labels, function(label) { + var new_label = new App.Label(); + new_label.set(label); + if (!_.isUndefined(label.id)) { + new_label.set('id', parseInt(label.id)); + new_label.set('label_id', parseInt(label.label_id)); + } else { + new_label.set('name', label); + } + new_label.set('board_id', self.model.attributes.board_id); + new_label.set('list_id', self.model.attributes.list_id); + new_label.set('card_id', self.model.id); + self.model.list.collection.board.labels.add(new_label); + self.model.labels.add(new_label); + var view = new App.CardLabelView({ + model: new_label, + background: self.getLabelcolor('' + new_label.attributes.name).substring(0, 6) + }); + view_label.prepend(view.render().el); + $('#js-card-' + self.model.id).addClass('active'); + $('.js-label-dropdown').removeClass('open'); + }); + } else { + $('.js-card-label-section-' + self.model.id).html(""); + } + if (!_.isUndefined(response.activity) && response.activity !== false) { + var activity = new App.Activity(); + activity.set(response.activity); + var view = new App.ActivityView({ + model: activity + }); + self.model.activities.unshift(activity); + var view_activity = $('#js-card-activities-' + self.model.id); + view_activity.prepend(view.render().el).find('.timeago').timeago(); + } + } + }); + } + return false; + }, + /** + * showCardLabelForm() + * show card label add form + * @param e + * @type Object(DOM event) + */ + showCardLabelForm: function(e) { + var self = this; + var doc = $('#js-card-modal-' + this.model.id); + var slected_labels = new App.CardLabelCollection(); + var labels = ''; + var card_labels = this.model.list.collection.board.labels.where({ + card_id: this.model.id + }); + this.model.labels.add(card_labels, { + silent: true + }); + this.model.labels.each(function(label) { + if (_.escape(label.attributes.name) !== "") { + labels += _.escape(label.attributes.name) + ','; + } + }); + labels = labels.substr(0, labels.length - 1); + $('.js-show-card-label-form-response').html(new App.CardLabelFormView({ + model: labels + }).el); + $('.inputCardLabel', doc).select2({ + tags: _.uniq(self.model.list.collection.board.labels.pluck('name')), + tokenSeparators: [',', ' '] + }); + var target = $(e.target); + $('li.dropdown').removeClass('open'); + target.parents('li.dropdown').addClass('open'); + if (target.hasClass('js-card-header-action')) { + return false; + } + }, + /** + * editCardDueDateForm() + * update card due date + * @param e + * @type Object(DOM event) + */ + editCardDueDateForm: function(e) { + var date_time = this.model.attributes.due_date.split('T'); + date_time = date_time[0].split(' '); + $('.js-edit-card-due-date-form-response').html(new App.CardDuedateFromView({ + model: this.model + }).el); + $('.js-card-duedate-edit-' + this.model.id).datetimepicker({ + defaultDate: date_time[0], + format: 'yyyy-mm-dd', + autoclose: true, + todayBtn: true, + pickerPosition: 'bottom-right', + todayHighlight: 1, + startView: 2, + minView: 2, + bootcssVer: 3, + pickTime: false + }); + $('.js-card-duetime-edit-' + this.model.id).datetimepicker({ + format: 'hh:ii', + autoclose: true, + showMeridian: false, + startView: 1, + maxView: 1, + pickDate: false, + use24hours: true + }); + }, + /** + * showCardDueDateForm() + * display card due date form + * @param e + * @type Object(DOM event) + */ + showCardDueDateForm: function(e) { + var date = ''; + var time = ''; + if (!_.isUndefined(this.model.attributes.due_date) && this.model.attributes.due_date !== null && this.model.attributes.due_date !== 'NULL') { + var date_time = this.model.attributes.due_date.split('T'); + date_time = date_time[0].split(' '); + date = date_time[0]; + time = date_time[1]; + } + + $('.js-show-card-due-date-form-response').html('').html(new App.CardDuedateFromView({ + model: this.model + }).el); + $('.js-card-duedate-edit-' + this.model.id).datetimepicker({ + defaultDate: date, + format: 'yyyy-mm-dd', + autoclose: true, + todayBtn: true, + pickerPosition: 'bottom-right', + todayHighlight: 1, + startView: 2, + minView: 2, + bootcssVer: 2, + pickTime: false + }).on('changeDate', function(ev) { + $(this).datetimepicker('hide'); + $(this).blur(); + }); + $('.js-card-duetime-edit-' + this.model.id).datetimepicker({ + format: 'hh:ii', + autoclose: true, + showMeridian: false, + startView: 1, + maxView: 1, + pickDate: false, + use24hours: true + }).on('changeDate', function(ev) { + $(this).datetimepicker('hide'); + $(this).blur(); + }); + var target = $(e.target); + $('li.dropdown').removeClass('open'); + target.parents('li.dropdown').addClass('open'); + return false; + }, + /** + * cancelCardDescEditForm() + * hide card description edit form + * @param e + * @type Object(DOM event) + * @return false + */ + cancelCardDescEditForm: function(e) { + this.$el.find('.js-show-card-desc').next('p').show(); + this.$el.find('#cardDescriptionEditForm').hide(); + return false; + }, + /** + * showCardDescEditForm() + * display card description edit form + * @param e + * @type Object(DOM event) + * @return false + */ + showCardDescEditForm: function(e) { + e.preventDefault(); + this.$el.find('.js-show-card-desc').next('p').hide(); + this.$el.find('#cardDescriptionEditForm').removeClass('hide').show(); + return false; + }, + /** + * editCard() + * update card + * @param e + * @type Object(DOM event) + * @return false + */ + editCard: function(e) { + e.preventDefault(); + var self = this; + var data = $(e.target).serializeObject(); + if (!_.isUndefined(data.due_date) || !_.isUndefined(data.due_time)) { + data = { + to_date: data.due_date, + due_date: data.due_date + 'T' + data.due_time, + start: data.due_date + 'T' + data.due_time + }; + } + this.model.set(data); + var target = $(e.currentTarget); + $('.js-show-side-card-title-edit-form').parents().find('.dropdown').removeClass('open'); + if (!_.isUndefined(data.name)) { + target.prev('h4').html(_.escape(data.name)).removeClass('hide'); + } + if (!_.isUndefined(data.description)) { + $('.js-show-card-desc').show(); + $('#cardDescriptionEditForm').hide(); + } + this.model.url = api_url + 'boards/' + this.model.attributes.board_id + '/lists/' + this.model.attributes.list_id + '/cards/' + this.model.id + '.json'; + this.model.save(data, { + patch: true, + success: function(model, response, options) { + if (_.isUndefined(options.temp_id)) { + self.model.set('is_offline', false); + } else { + this.model.set('is_offline', true); + } + if (!_.isEmpty(response)) { + if (!_.isUndefined(response.activity)) { + var activity = new App.Activity(); + activity.set(response.activity); + var view = new App.ActivityView({ + model: activity, + board: self.model.list.collection.board + }); + self.model.activities.unshift(activity, { + silent: true + }); + var view_activity = $('#js-card-activities-' + self.model.id); + view_activity.prepend(view.render().el).find('.timeago').timeago(); + } + self.model.cards.add(self.model); + } + } + }); + return false; + }, + /** + * cancelCardTitleEditForm() + * hide card title edit form + * @param e + * @type Object(DOM event) + * @return false + */ + cancelCardTitleEditForm: function(e) { + var target = $(e.currentTarget); + target.parents('form').addClass('hide'); + target.parents('form').prev('h4').removeClass('hide'); + return false; + }, + /** + * showCardTitleEditForm() + * display card title edit form + * @param e + * @type Object(DOM event) + * @return false + */ + showCardTitleEditForm: function(e) { + var target = $(e.currentTarget); + target.parent('h4').addClass('hide'); + $('form#cardTitleEditForm').removeClass('hide'); + return false; + }, + /** + * show() + * display card details in docmodal + */ + show: function() { + $('#js-card-' + this.model.id).addClass('active'); + this.render(); + var self = this; + self.model.activities = new App.ActivityCollection(); + self.model.activities.url = api_url + 'boards/' + self.model.attributes.board_id + '/lists/' + self.model.attributes.list_id + '/cards/' + self.model.id + '/activities.json'; + self.model.activities.fetch({ + success: function(model, response) { + self.renderActivitiesCollection(); + } + }); + }, + /** + * refreshdock() + * update dock modal view + */ + refreshdock: function() { + var doc = $('#js-card-modal-' + this.model.id); + var self = this; + if (doc.length !== 0) { + var cards_subscribers = this.model.cards_subscribers.where({ + is_subscribed: true, + user_id: parseInt(authuser.user.id) + }); + var subscribed = ''; + if (!_.isEmpty(cards_subscribers)) { + subscribed = ' '; + } + var class_name = ''; + var text = _.escape(this.model.attributes.name) + ' in list ' + _.escape(this.model.list.attributes.name) + subscribed; + if (this.model.attributes.is_archived === true) { + class_name = ' label label-warning'; + text = _.escape('This card is archived.'); + } + $('.title-text', doc.parent().prev('.dockmodal-header')).html('
          #' + this.model.id + '
          ' + text + ''); + doc.html(this.template({ + card: this.model, + checklist_lists: this.checklist_list, + converter: this.converter, + list: this.model.list + })).dockmodal('refreshLayout'); + _(function() { + Backbone.TemplateManager.baseUrl = '{name}'; + var uploadManager = new Backbone.UploadManager({ + uploadUrl: api_url + 'boards/' + self.model.attributes.board_id + '/lists/' + self.model.attributes.list_id + '/cards/' + self.model.id + '/attachments.json?token=' + api_token, + autoUpload: true, + singleFileUploads: false, + formData: $('form.js-user-profile-edit').serialize(), + dropZone: $('#dropzone' + self.model.id), + + }); + var loader_id = ''; + uploadManager.on('fileadd', function(file) { + loader_id = new Date().getTime(); + $('#js-card-modal-' + self.model.id).parent('.dockmodal-body').prev('.dockmodal-header').find('.cssloader').remove(); + $('#js-card-modal-' + self.model.id).parent('.dockmodal-body').prev('.dockmodal-header').append(''); + self.$('.js_card_image_upload').addClass('cssloader'); + }); + uploadManager.on('fileuploaddragover', function(e) { + $('#js-card-modal-' + self.model.id).addClass('drophover'); + }); + var dragging = 0; + $('#dropzone' + self.model.id).on('dragenter', function(e) { + dragging++; + + }); + $('#dropzone' + self.model.id).on('dragleave', function(e) { + dragging--; + if (dragging === 0 || !$.browser.chrome) { + $('#js-card-modal-' + self.model.id).removeClass('drophover'); + } + }); + uploadManager.on('fileuploaddrop', function(e) { + dragging--; + $('#js-card-modal-' + self.model.id).removeClass('drophover'); + }); + uploadManager.on('filedone', function(file, data) { + $('#js-card-modal-' + self.model.id).parent('.dockmodal-body').prev('.dockmodal-header').find('.cssloader').remove(); + var response = {}; + response = data.result; + var card_attachments = new App.CardAttachmentCollection(); + var i = 1; + card_attachments.add(response.card_attachments); + card_attachments.each(function(attachment) { + var options = { + silent: true + }; + if (i === card_attachments.models.length) { + options.silent = false; + } + attachment.set('id', parseInt(attachment.attributes.id)); + attachment.set('board_id', parseInt(attachment.attributes.board_id)); + attachment.set('list_id', parseInt(attachment.attributes.list_id)); + attachment.set('card_id', parseInt(attachment.attributes.card_id)); + self.model.attachments.unshift(attachment, options); + self.model.list.collection.board.attachments.unshift(attachment, options); + i++; + }); + }); + }).defer(); + this.resizeSplitter(); + this.renderAttachmentsCollection(); + this.renderLabelsCollection(); + this.renderUsersCollection(); + this.renderActivitiesCollection(); + this.renderChecklistsCollection(); + } + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + var self = this; + var subscribed = ''; + if (!_.isUndefined(authuser.user)) { + var cards_subscribers = this.model.cards_subscribers.where({ + is_subscribed: true, + user_id: parseInt(authuser.user.id) + }); + if (!_.isEmpty(cards_subscribers)) { + subscribed = ' '; + } + } + var initialState = 'docked'; + if (trigger_dockmodal) { + initialState = 'minimized'; + } + var doc = $('#js-card-modal-' + this.model.id); + if (doc.length === 0) { + $('.js-hidden-blocks').append(this.$el.html(this.template({ + card: this.model, + checklist_lists: this.checklist_list, + converter: this.converter, + list: this.model.list + })).attr('id', 'js-card-modal-' + this.model.id)); + this.renderAttachmentsCollection(); + this.renderLabelsCollection(); + this.renderUsersCollection(); + this.renderChecklistsCollection(); + var title = _.escape(this.model.attributes.name) + ' in list ' + _.escape(this.model.list.attributes.name) + subscribed; + var class_name = ''; + if (this.model.attributes.is_archived === true) { + class_name = ' label label-warning'; + title = _.escape('This card is archived.'); + } + this.$el.dockmodal({ + initialState: initialState, + height: 450, + width: 600, + title: '
          #' + this.model.id + '
          ' + title + '', + beforePopout: function(event) { + if (!_.isUndefined(authuser.user)) { + $('#js-title-color-' + self.model.id).parent('.title-text').css('margin-left', '34px'); + } + $('.editor').resizable({ + maxWidth: 900 + }); + }, + beforeRestore: function(event) { + if (!_.isUndefined(authuser.user)) { + $('#js-title-color-' + self.model.id).parent('.title-text').css('margin-left', '34px'); + } + $('.editor').resizable({ + maxWidth: 490 + }); + }, + beforeMinimize: function(event) { + $('#js-title-color-' + self.model.id).parent('.title-text').removeAttr('style'); + }, + minimize: function(event) { + $('#js-title-color-' + self.model.id).parent('.title-text').removeAttr('style'); + }, + open: function(event, dialog) { + $('.dockmodal').removeClass('active'); + event.parent().parent().addClass('active'); + $('.dockmodal').click(function(e) { + $('.dockmodal').removeClass('active'); + $(this).addClass('active'); + }); + $(window).bind('keydown', function(e) { + if (e.keyCode === 27) { + $('.action-close', $('.dockmodal.active')).trigger('click'); + } + }); + }, + close: function(event, dialog) { + $('#js-card-' + self.model.id).removeClass('active'); + var current_param = Backbone.history.fragment; + if (current_param.indexOf('board/') != -1) { + card_ids_ref = _.without(card_ids_ref, self.model.id); + if (current_param.indexOf(',' + self.model.id) != -1) { + current_param = current_param.replace(',' + self.model.id, ''); + } else if (current_param.indexOf(self.model.id + ',') != -1) { + current_param = current_param.replace(self.model.id + ',', ''); + } else if (current_param.indexOf('/card/' + self.model.id) != -1) { + current_param = current_param.replace('/card/' + self.model.id, ''); + } else { + current_param = 'board/' + self.model.attributes.board_id; + } + app.navigate('#/' + current_param, { + trigger: false, + trigger_function: false, + }); + event.remove(); + } + } + }); + } else { + doc.dockmodal('restore'); + } + this.$el.find('.js-organization-member-search-response').html(''); + this.renderBoardUsers(); + this.$el.find('.js-comment-member-search-response').nextAll().remove(); + this.renderActivityBoardUsers(); + _(function() { + Backbone.TemplateManager.baseUrl = '{name}'; + var uploadManager = new Backbone.UploadManager({ + uploadUrl: api_url + 'boards/' + self.model.attributes.board_id + '/lists/' + self.model.attributes.list_id + '/cards/' + self.model.id + '/attachments.json?token=' + api_token, + autoUpload: true, + singleFileUploads: false, + formData: $('form.js-user-profile-edit').serialize(), + dropZone: $('#dropzone' + self.model.id), + + }); + var loader_id = ''; + uploadManager.on('fileadd', function(file) { + loader_id = new Date().getTime(); + $('.js-attachment-loader', $('#js-card-modal-' + self.model.id)).html(''); + self.$('.js_card_image_upload').addClass('cssloader'); + }); + uploadManager.on('fileuploaddragover', function(e) { + $('#js-card-modal-' + self.model.id).addClass('drophover'); + }); + var dragging = 0; + $('#dropzone' + self.model.id).on('dragenter', function(e) { + dragging++; + }); + $('#dropzone' + self.model.id).on('dragleave', function(e) { + dragging--; + if (dragging === 0 || !$.browser.chrome) { + $('#js-card-modal-' + self.model.id).removeClass('drophover'); + } + }); + uploadManager.on('fileuploaddrop', function(e) { + dragging--; + $('#js-card-modal-' + self.model.id).removeClass('drophover'); + }); + uploadManager.on('filedone', function(file, data) { + $('#js-card-modal-' + self.model.id).parent('.dockmodal-body').prev('.dockmodal-header').find('.cssloader').remove(); + $('.js-attachment-loader', $('#js-card-modal-' + self.model.id)).html(''); + var response = {}; + response = data.result; + var card_attachments = new App.CardAttachmentCollection(); + var i = 1; + card_attachments.add(response.card_attachments); + card_attachments.each(function(attachment) { + var options = { + silent: true + }; + if (i === card_attachments.models.length) { + options.silent = false; + } + attachment.set('id', parseInt(attachment.attributes.id)); + attachment.set('board_id', parseInt(attachment.attributes.board_id)); + attachment.set('list_id', parseInt(attachment.attributes.list_id)); + attachment.set('card_id', parseInt(attachment.attributes.card_id)); + self.model.attachments.unshift(attachment, options); + self.model.list.collection.board.attachments.unshift(attachment, options); + i++; + }); + }); + }).defer(); + if (!_.isUndefined(authuser.user)) { + $('#js-card-checklists', this.$el).sortable({ + items: 'div.js-card-checklist', + axis: 'y', + placeholder: 'form-group card-list-placeholder col-xs-12', + cursor: 'grab', + scroll: true, + helper: 'clone', + handle: '.js-checklist-head', + tolerance: 'pointer', + update: function(ev, ui) { + ui.item.trigger('checklistSort', ev, ui); + }, + start: function(ev, ui) { + ui.placeholder.height(ui.item.outerHeight()); + $(ev.target).find('.js-checklist-head').removeClass('cur-grab'); + }, + stop: function(ev, ui) { + $(ev.target).find('.js-checklist-head').addClass('cur-grab'); + } + }); + } + this.resizeSplitter(); + this.showTooltip(); + return this; + }, + resizeSplitter: function() { + $('.editor').each(function() { + var $this = $(this); + var factor1 = window.sessionStorage.getItem('factor1'); + if (factor1 === null) { + factor1 = '20'; + factor2 = '80'; + } else { + factor2 = 100 - factor1; + } + $this.resizable({ + handles: 'e', + minWidth: 110, + maxWidth: 490, + resize: function(event, ui) { + var x = ui.element.outerWidth(); + var ele = ui.element; + var factor = x * 100 / $(this).parent().width(); + var f1 = factor; + var f2 = 100 - factor; + window.sessionStorage.setItem('factor1', f1); + $this.css('width', f1 + '%'); + $this.next().css('width', f2 + '%'); + } + }).css({ + width: factor1 + '%' + }).next().css({ + width: factor2 + '%' + }); + }); + }, + /** + * closePopup() + * hide opened dropdown + * @param e + * @type Object(DOM event) + * @return false + */ + closePopup: function(e) { + var target = $(e.target); + target.parents('.dropdown:first').removeClass('open'); + return false; + }, + /** + * openPopup() + * show dropdown + * @param e + * @type Object(DOM event) + * @return false + */ + openPopup: function(e) { + var target = $(e.target).parents('.dropdown:first'); + target.addClass('open').prev('.dropdown').removeClass('open'); + target.next('.dropdown').removeClass('open'); + return false; + }, + /** + * addCardVote() + * add vote for card + * @param e + * @type Object(DOM event) + * @return false + */ + addCardVote: function(e, ui) { + var self = this; + var card_id = this.model.id; + var list_id = this.model.attributes.list_id; + var board_id = this.model.attributes.board_id; + var uuid = new Date().getTime(); + $(e.currentTarget).removeClass('js-add-card-vote'); + $('.panel-title', e.currentTarget).html(' Unvote'); + var card_voter = new App.CardVoter(); + card_voter.set('is_offline', true); + card_voter.set('card_id', parseInt(card_id)); + card_voter.set('user_id', parseInt(authuser.user.id)); + card_voter.set('board_id', board_id); + card_voter.set('list_id', list_id); + card_voter.set('username', authuser.user.username); + card_voter.set('role_id', authuser.user.role_id); + card_voter.set('profile_picture_path', authuser.user.profile_picture_path); + card_voter.set('initials', authuser.user.initials); + self.model.card_voters.add(card_voter); + card_voter.url = api_url + 'boards/' + board_id + '/lists/' + list_id + '/cards/' + card_id + '/card_voters.json'; + card_voter.save({}, { + success: function(model, response, options) { + if (_.isUndefined(options.temp_id)) { + card_voter.set('is_offline', false); + } + if (!_.isUndefined(response.id) && _.isUndefined(options.temp_id)) { + card_voter.set({ + id: parseInt(response.id) + }); + } else { + global_uuid[uuid] = options.temp_id; + card_voter.set('id', uuid); + } + if (!_.isUndefined(response.id)) { + self.model.card_voters.findWhere({ + card_id: card_id, + user_id: parseInt(authuser.user.id) + }).set('id', parseInt(response.id)); + $(e.currentTarget).addClass('js-delete-card-vote').data('id', response.id); + } + self.model.list.collection.board.cards.get(self.model.id).card_voters.add(card_voter); + self.model.set('card_voter_count', parseInt(self.model.attributes.card_voter_count) + 1); + if (!_.isUndefined(response.activity)) { + var activity = new App.Activity(); + activity.set(response.activity); + var view = new App.ActivityView({ + model: activity + }); + self.model.activities.unshift(activity); + var view_activity = $('#js-card-activities-' + self.model.id); + view_activity.prepend(view.render().el).find('.timeago').timeago(); + } + } + }); + return false; + }, + /** + * deleteCardVote() + * unvote card + * @param e + * @type Object(DOM event) + * @return false + */ + deleteCardVote: function(e, ui) { + var self = this; + var card_id = this.model.id; + var list_id = this.model.attributes.list_id; + var board_id = this.model.attributes.board_id; + var voted_user = this.model.card_voters.findWhere({ + card_id: card_id, + user_id: parseInt(authuser.user.id) + }); + var voter_id = voted_user.id; + $(e.currentTarget).removeClass('js-delete-card-vote').addClass('js-add-card-vote'); + $('.panel-title', e.currentTarget).html(' Vote'); + + $('i.icon-ok', e.currentTarget).remove(); + var card_voter = new App.CardVoter(); + card_voter.set('id', voter_id); + this.model.card_voters.remove(card_voter); + this.model.list.collection.board.cards.get(self.model.id).card_voters.remove(card_voter); + card_voter.url = api_url + 'boards/' + board_id + '/lists/' + list_id + '/cards/' + card_id + '/card_voters/' + voter_id + '.json'; + self.model.card_voters.remove(card_voter); + card_voter.destroy({ + success: function(model, response) { + if (!_.isUndefined(response.activity)) { + var activity = new App.Activity(); + activity.set(response.activity); + var view = new App.ActivityView({ + model: activity + }); + self.model.activities.unshift(activity); + var view_activity = $('#js-card-activities-' + self.model.id); + view_activity.prepend(view.render().el).find('.timeago').timeago(); + } + } + }); + return false; + }, + /** + * cardSubscribe() + * subscribe card + * @param e + * @type Object(DOM event) + * @return false + */ + cardSubscribe: function(e) { + var card_id = this.model.id; + var list_id = this.model.attributes.list_id; + var board_id = this.model.attributes.board_id; + var card_subscribe_id = 'undefined'; + var self = this; + var uuid = new Date().getTime(); + $(e.currentTarget).removeClass('js-card-subscribe').addClass('js-card-unsubscribe'); + $('.js-subscribe-change-icon').append(''); + $('.js-card-subscribed-' + card_id).removeClass('hide'); + var card_subscribe = new App.CardSubscriber(); + card_subscribe.set('is_offline', true); + card_subscribe.set('card_id', card_id); + card_subscribe.set('board_id', board_id); + card_subscribe.set('list_id', list_id); + card_subscribe.set('user_id', parseInt(authuser.user.id)); + card_subscribe.set('is_subscribed', true); + self.model.cards_subscribers.add(card_subscribe); + if (typeof card_subscribe_id == 'undefined' || card_subscribe_id == 'undefined') { + var subscribe = { + subscribe: { + is_subscribed: 't' + } + }; + card_subscribe.url = api_url + 'boards/' + board_id + '/lists/' + list_id + '/cards/' + card_id + '/card_subscribers.json'; + } else { + card_subscribe.id = parseInt(subscribe_id); + card_subscribe.url = api_url + 'boards/' + board_id + '/lists/' + list_id + '/cards/' + card_id + '/card_subscribers/' + subscribe_id + '.json'; + } + card_subscribe.save({ + is_subscribed: true + }, { + success: function(model, response, options) { + if (_.isUndefined(options.temp_id)) { + self.model.cards_subscribers.findWhere({ + user_id: parseInt(authuser.user.id), + card_id: card_id + }).set('is_offline', false); + } + if (!_.isUndefined(self.model.cards_subscribers.findWhere({ + user_id: parseInt(authuser.user.id), + card_id: card_id + }))) { + self.model.cards_subscribers.findWhere({ + user_id: parseInt(authuser.user.id), + card_id: card_id + }).set('id', parseInt(response.id)); + if (!_.isUndefined(response.id) && _.isUndefined(options.temp_id)) { + self.model.cards_subscribers.findWhere({ + user_id: parseInt(authuser.user.id), + card_id: card_id + }).set('id', parseInt(response.id)); + } else { + global_uuid[uuid] = options.temp_id; + self.model.cards_subscribers.findWhere({ + user_id: parseInt(authuser.user.id), + card_id: card_id + }).set('id', uuid); + } + } + } + }); + return false; + }, + /** + * cardUnsubscribe() + * unsubscribe card + * @param e + * @type Object(DOM event) + * @return false + */ + cardUnsubscribe: function(e) { + $(e.currentTarget).removeClass('js-card-unsubscribe').addClass('js-card-subscribe'); + $('i.icon-ok', e.currentTarget).remove(); + var card_id = this.model.id; + var list_id = this.model.attributes.list_id; + var board_id = this.model.attributes.board_id; + var self = this; + var subscribe_id = 'undefined'; + var uuid = new Date().getTime(); + self.model.cards_subscribers.remove(self.model.cards_subscribers.findWhere({ + card_id: card_id, + user_id: parseInt(authuser.user.id) + })); + $('.js-card-subscribed-' + card_id).addClass('hide'); + var card_subscribe = new App.CardSubscriber(); + card_subscribe.set('is_offline', true); + if (typeof subscribe_id == 'undefined' || subscribe_id == 'undefined') { + card_subscribe.url = api_url + 'boards/' + board_id + '/lists/' + list_id + '/cards/' + card_id + '/card_subscribers.json'; + } else { + card_subscribe.id = parseInt(subscribe_id); + card_subscribe.url = api_url + 'boards/' + board_id + '/lists/' + list_id + '/cards/' + card_id + '/card_subscribers/' + subscribe_id + '.json'; + } + card_subscribe.save({ + is_subscribed: false + }, { + success: function(model, response, options) { + if (_.isUndefined(options.temp_id)) { + card_subscribe.set('is_offline', false); + } + if (!_.isUndefined(response.id) && _.isUndefined(options.temp_id)) { + card_subscribe.set({ + id: parseInt(response.id) + }); + } else { + global_uuid[uuid] = options.temp_id; + card_subscribe.set('id', uuid); + } + } + }); + return false; + }, + /** + * showMoveCardForm() + * display card move form + * @param e + * @type Object(DOM event) + */ + showMoveCardForm: function(e) { + $('.js-show-move-card-form-response').html(new App.MoveCardView({ + model: this.model, + boards: this.boards, + }).el); + var target = $(e.target); + $('li.dropdown').removeClass('open'); + target.parents('li.dropdown').addClass('open'); + if (target.hasClass('js-card-header-action')) { + return false; + } + }, + /** + * moveCard() + * update moved card move + * @param e + * @type Object(DOM event) + * @return false + */ + moveCard: function(e) { + e.preventDefault(); + var card_id = this.model.id; + var current_card = this.model.attributes; + var data = $(e.target).serializeObject(); + data.list_id = parseInt(data.list_id); + data.board_id = parseInt(data.board_id); + var position = parseInt(data.position); + var prev_list_id = this.model.attributes.list_id; + var prev_board_id = this.model.attributes.board_id; + var view_card = $('#js-card-listing-' + data.list_id); + if (data.list_id !== this.model.attributes.list_id) { + var card_count = parseInt(this.boards.get(this.model.attributes.board_id).lists.get(this.model.attributes.list_id).get('card_count')); + this.boards.get(this.model.attributes.board_id).lists.get(this.model.attributes.list_id).set('card_count', card_count - 1); + } + var cards = this.model.list.collection.board.cards.where({ + list_id: data.list_id + }); + this.model.url = api_url + 'boards/' + this.model.attributes.board_id + '/lists/' + this.model.attributes.list_id + '/cards/' + this.model.id + '.json'; + var list_cards = new App.CardCollection(); + list_cards.add(cards); + list_cards.sortByColumn('position'); + var next = view_card.find('.js-board-list-card:nth-child(' + data.position + ')'); + var prev = view_card.find('.js-board-list-card:nth-child(' + (data.position - 1) + ')'); + var before = ''; + var after = ''; + var difference = ''; + var newPosition = ''; + var view = ''; + var list = this.board.lists.findWhere({ + id: data.list_id + }); + this.model.set(data, { + silent: true + }); + if (!_.isUndefined(list)) { + this.model.list = list; + } + if (prev.length !== 0) { + before = list_cards.get(parseInt(prev.data('card_id'))); + after = list_cards.at(list_cards.indexOf(before) + 1); + if (typeof after == 'undefined') { + afterPosition = before.position() + 2; + } else { + afterPosition = after.position(); + } + difference = (afterPosition - before.position()) / 2; + newPosition = difference + before.position(); + this.model.set({ + position: newPosition + }); + data.position = newPosition; + view = new App.CardView({ + tagName: 'div', + model: this.model, + converter: this.converter + }); + prev.after(view.render().el); + } else if (next.length !== 0) { + after = list_cards.get(parseInt(next.data('card_id'))); + before = list_cards.at(list_cards.indexOf(after) - 1); + if (typeof before == 'undefined') { + beforePosition = 0.0; + } else { + beforePosition = before.position(); + } + difference = (after.position() - beforePosition) / 2; + newPosition = difference + beforePosition; + this.model.set({ + position: newPosition + }); + data.position = newPosition; + view = new App.CardView({ + tagName: 'div', + model: this.model, + converter: this.converter + }); + next.before(view.render().el); + } else { + view = new App.CardView({ + tagName: 'div', + model: this.model, + converter: this.converter + }); + view_card.append(view.render().el); + data.position = position; + } + if (data.board_id !== prev_board_id) { + this.boards.get(data.board_id).lists.get(data.list_id).set('card_count', list_cards.length + 1); + this.model.collection.remove(this.model, { + silent: true + }); + var close_doc = $('#js-card-modal-' + this.model.id); + close_doc.dockmodal('close'); + $('#js-card-' + this.model.id).remove(); + } else { + if (data.list_id !== prev_list_id) { + this.boards.get(list.attributes.board_id).lists.get(list.attributes.id).set('card_count', list_cards.length + 1); + } else { + this.boards.get(list.attributes.board_id).lists.get(list.attributes.id).set('card_count', list_cards.length); + } + list.set('card_count', list_cards.length + 1); + this.model.set(data); + this.refreshdock(); + } + if (data.list_id !== current_card.list_id) { + this.$el.modal('hide'); + this.model.collection.remove(this.model); + data.board_id = parseInt(data.board_id); + if (data.board_id === current_card.board_id) { + this.model.list.collection.board.cards.remove(this.model); + this.model.list.collection.board.lists.get(parseInt(data.list_id)).cards.add(this.model); + this.model.list = this.model.list.collection.get(parseInt(data.list_id)); + var doc = $('#js-card-modal-' + this.model.id); + doc.dockmodal('close'); + } + this.model.set(data); + } + this.model.list.cards.sortByColumn('position'); + + this.model.save(data, { + patch: true + }); + return false; + }, + /** + * archiveCard() + * save archived card + * @param e + * @type Object(DOM event) + * @return false + */ + archiveCard: function(e) { + var uuid = new Date().getTime(); + var self = this; + this.model.set('is_offline', true); + this.model.set('is_archived', true); + this.model.url = api_url + 'boards/' + this.model.attributes.board_id + '/lists/' + this.model.attributes.list_id + '/cards/' + this.model.id + '.json'; + this.model.list.collection.board.cards.get(this.model.id).set('is_archived', true); + this.model.save({ + is_archived: true + }, { + patch: true, + success: function(model, response, options) { + if (_.isUndefined(options.temp_id)) { + self.model.set('is_offline', false); + } + if (!_.isUndefined(self.model.id) && _.isUndefined(options.temp_id)) { + self.model.set({ + id: parseInt(self.model.id) + }); + } else { + global_uuid[uuid] = options.temp_id; + self.model.set('id', uuid); + } + var activity = new App.Activity(); + activity.set(response.activity); + var view = new App.ActivityView({ + model: activity + }); + model.set('activities', activity); + var view_activity = $('#js-card-activities-' + self.model.id); + view_activity.prepend(view.render().el).find('.timeago').timeago(); + } + }); + return false; + }, + /** + * cardSendToBoard() + * sent back to archived cards to board + * @param e + * @type Object(DOM event) + * @return false + */ + cardSendToBoard: function(e) { + var uuid = new Date().getTime(); + var self = this; + this.model.url = api_url + 'boards/' + this.model.attributes.board_id + '/lists/' + this.model.attributes.list_id + '/cards/' + this.model.id + '.json'; + this.model.set('is_offline', true); + this.model.set('is_archived', false); + this.model.save({ + is_archived: false + }, { + patch: true, + success: function(model, response, options) { + if (_.isUndefined(options.temp_id)) { + self.model.set('is_offline', false); + } + if (!_.isUndefined(self.model.id) && _.isUndefined(options.temp_id)) { + self.model.set({ + id: parseInt(self.model.id) + }); + } else { + global_uuid[uuid] = options.temp_id; + self.model.set('id', uuid); + } + var activity = new App.Activity(); + activity.set(response.activity); + var view = new App.ActivityView({ + model: activity + }); + model.set('activities', activity); + var view_activity = $('#js-card-activities-' + self.model.id); + view_activity.prepend(view.render().el).find('.timeago').timeago(); + } + }); + return false; + }, + /** + * deleteCard() + * delete card + * @param e + * @type Object(DOM event) + * @return false + */ + deleteCard: function(e) { + var self = this; + this.$el.modal('hide'); + var card_id = self.model.id; + self.model.collection.remove(self.model); + self.model.list.collection.board.cards.remove(self.model); + self.model.url = api_url + 'boards/' + self.model.attributes.board_id + '/lists/' + self.model.attributes.list_id + '/cards/' + card_id + '.json'; + var doc = $('#js-card-modal-' + this.model.id); + $('.action-close', doc.parent().prev('.dockmodal-header')).trigger('click'); + self.model.destroy({ + success: function(model, response) { + var activity = new App.Activity(); + activity.set(response.activity); + self.model.activities.unshift(activity); + self.board.activities.unshift(response.activity); + + } + }); + return false; + }, + /** + * computerOpen() + * trigger file upload + * @param e + * @type Object(DOM event) + * @return false + */ + computerOpen: function(e) { + var fileLi = $(e.target); + $('.js-card-attachment-form').remove(); + var form = $('
          '); + $(form).append(''); + $(form).append(''); + $(fileLi).after($(form)); + $('.js-card-attachment', form).trigger('click'); + return false; + }, + /** + * dropboxChooser() + * image upload from dropbox + * @param e + * @type Object(DOM event) + * @return false + */ + dropboxChooser: function(e) { + var self = this; + var attachmentUrl = api_url + 'boards/' + this.model.attributes.board_id + '/lists/' + this.model.attributes.list_id + '/cards/' + this.model.id + '/attachments.json?token=' + api_token; + var options = { + success: function(files) { + var image_link = ''; + _.map(files, function(file) { + var thumbnails = _(file.thumbnails).toArray(); + image_link = thumbnails[1]; + }); + $.ajax({ + type: 'POST', + url: attachmentUrl, + data: JSON.stringify({ + image_link: image_link + }), + success: function(response) { + self.closePopup(e); + var card_attachment = new App.CardAttachment(); + card_attachment.set(response.card_attachments); + self.model.attachments.unshift(card_attachment); + var view = new App.CardAttachmentView({ + model: card_attachment + }); + var view_attachment = self.$('#js-card-attachments-list'); + view_attachment.find('.timeago').timeago(); + + }, + contentType: 'application/json', + dataType: 'json' + }); + }, + cancel: function() {}, + linkType: 'preview', + multiselect: true + }; + Dropbox.init({ + appKey: DROPBOX_APPKEY + }); + Dropbox.choose(options); + return false; + }, + /** + * addCardAttachment() + * add card attachment + * @param e + * @type Object(DOM event) + */ + addCardAttachment: function(e) { + e.preventDefault(); + var self = this; + $('.js-attachment-loader', $('#js-card-modal-' + self.model.id)).html(''); + self.$('.js_card_image_upload').addClass('cssloader'); + var form = $('form.js-card-attachment-form'); + var target = $(e.target); + target.parents('li.dropdown').removeClass('open'); + var fileData = new FormData(form[0]); + var card_attachment = new App.CardAttachment(); + card_attachment.url = api_url + 'boards/' + self.model.attributes.board_id + '/lists/' + self.model.attributes.list_id + '/cards/' + self.model.id + '/attachments.json'; + self.closePopup(e); + card_attachment.save(fileData, { + type: 'POST', + data: fileData, + processData: false, + cache: false, + contentType: false, + success: function(model, response) { + $('#js-card-modal-' + self.model.id).parent('.dockmodal-body').prev('.dockmodal-header').find('.cssloader').remove(); + $('.js-attachment-loader', $('#js-card-modal-' + self.model.id)).html(''); + var card_attachments = new App.CardAttachmentCollection(); + var i = 1; + card_attachments.add(response.card_attachments); + card_attachments.each(function(attachment) { + var options = { + silent: true + }; + if (i === card_attachments.models.length) { + options.silent = false; + } + attachment.set('id', parseInt(attachment.attributes.id)); + attachment.set('board_id', parseInt(attachment.attributes.board_id)); + attachment.set('list_id', parseInt(attachment.attributes.list_id)); + attachment.set('card_id', parseInt(attachment.attributes.card_id)); + self.model.attachments.unshift(attachment, options); + self.model.list.collection.board.attachments.unshift(attachment, options); + i++; + }); + var view_attachment = this.$('#js-card-attachments-list'); + var activity = new App.Activity(); + activity.set(response.activity); + var view_act = new App.ActivityView({ + model: activity + }); + self.model.activities.unshift(activity); + var view_activity = $('#js-card-activities-' + self.model.id); + view_activity.prepend(view_act.render().el).find('.timeago').timeago(); + } + }); + }, + /** + * renderAttachmentsCollection() + * display attachments in card + */ + renderAttachmentsCollection: function() { + var view_attachment = this.$('#js-card-attachments-list'); + view_attachment.html(''); + this.model.attachments.each(function(attachment) { + var view = new App.CardAttachmentView({ + model: attachment + }); + view_attachment.append(view.render().el).find('.timeago').timeago(); + }); + }, + /** + * renderLabelsCollection() + * display labels in card + */ + renderLabelsCollection: function() { + var view_label = this.$el.find('.js-card-labels-list'); + // view_label.html(''); + var self = this; + this.model.labels.each(function(label) { + var slabel = self.model.labels.findWhere({ + label_id: label.attributes.label_id + }); + if (!_.isUndefined(slabel)) { + var view = new App.CardLabelView({ + model: slabel, + background: self.getLabelcolor('' + slabel.attributes.name).substring(0, 6) + }); + view_label.prepend(view.render().el); + } + }); + }, + /** + * renderActivitiesCollection() + * display card activities + */ + renderActivitiesCollection: function() { + if (!_.isEmpty(role_links.where({ + slug: 'view_card_activities' + }))) { + var self = this; + var view_activity = this.$('#js-card-activities-' + self.model.id); + view_activity.html(''); + if (!_.isEmpty(this.model.activities)) { + this.model.activities.each(function(activity) { + $('#js-loader-img').removeClass('hide'); + if (!_.isEmpty(self.model.collection)) { + activity.cards.add(self.model.collection.models); + } + var view = new App.ActivityView({ + model: activity, + board: self.model.list.collection.board + }); + view_activity.append(view.render().el).find('.timeago').timeago(); + $('#js-loader-img').addClass('hide'); + }); + } + } + }, + /** + * renderChecklistsCollection() + * display card checklists + */ + renderChecklistsCollection: function() { + if (!_.isEmpty(role_links.where({ + slug: 'view_checklist_listing' + }))) { + var self = this; + var view_checklist = this.$('#js-card-checklists'); + view_checklist.html(''); + var checklists = this.model.list.collection.board.checklists.where({ + card_id: parseInt(this.model.attributes.id) + }); + this.model.checklists.reset(checklists); + this.model.checklists.sortByColumn('position'); + this.model.checklists.each(function(checklist) { + var checklist_items = self.model.list.collection.board.checklist_items.where({ + card_id: parseInt(self.model.attributes.id), + checklist_id: parseInt(checklist.attributes.id) + }); + checklist.checklist_items.reset(checklist_items); + checklist.card = self.model; + checklist.board_users = self.model.board_users; + var view = new App.CardCheckListView({ + model: checklist, + attributes: { + 'data-checklist_id': checklist.attributes.id + } + }); + view_checklist.append(view.render().el); + }); + } + }, + /** + * renderUsersCollection() + * display card users + */ + renderUsersCollection: function() { + var view_user = this.$('#js-card-user-add-container'); + this.$el.find('.js-organization-member-search-response').html(''); + view_user.prevAll().remove(); + this.renderBoardUsers(); + var content = ''; + var self = this; + this.model.users.each(function(user) { + var content_img = '' + user.get('initials') + ''; + var profile_picture_path = user.get('profile_picture_path'); + if (!_.isEmpty(profile_picture_path)) { + var hash = calcMD5(SecuritySalt + 'User' + user.attributes.user_id + 'jpg' + 'small_thumb' + SITE_NAME); + profile_picture_path = window.location.pathname + 'img/small_thumb/User/' + user.attributes.user_id + '.' + hash + '.jpg'; + content_img = '' + user.get('username') + ''; + } + if (!isNaN(user.attributes.user_id)) { + content += '
        • ' + content_img + '
        • '; + } + }); + if (view_user.length > 0) { + view_user.before(content); + } else { + self.$('#js-card-users-list-' + self.model.id).append(content); + } + }, + /** + * showChecklistAddForm() + * display checklist add form users + * @param e + * @type Object(DOM event) + */ + showChecklistAddForm: function(e) { + $('.js-checklist-add-form-response').html(new App.ChecklistAddFormView({ + model: this.model + }).el); + var target = $(e.target); + $('li.dropdown').removeClass('open'); + target.parents('li.dropdown').addClass('open'); + return false; + }, + /** + * addChecklist() + * add checklist in card + * @param e + * @type Object(DOM event) + * @return false + */ + addChecklist: function(e) { + e.preventDefault(); + var self = this; + var target = $(e.target); + var data = target.serializeObject(); + data.uuid = new Date().getTime(); + target.parents('div.dropdown').removeClass('open'); + var postion = self.model.checklists.max(function(checklist) { + return (!_.isUndefined(checklist)) ? checklist.attributes.position : 1; + }); + data.position = (!_.isUndefined(postion) && !_.isEmpty(postion)) ? postion.get('position') + 1 : 1; + var card_checklist = new App.CheckList(); + card_checklist.set('is_offline', true); + card_checklist.url = api_url + 'checklists.json'; + card_checklist.set('card_id', self.model.id); + card_checklist.set('list_id', self.model.attributes.list_id); + card_checklist.set('board_id', self.model.attributes.board_id); + card_checklist.url = api_url + 'boards/' + self.model.attributes.board_id + '/lists/' + self.model.attributes.list_id + '/cards/' + self.model.id + '/checklists.json'; + card_checklist.save(data, { + success: function(model, response, options) { + if (_.isUndefined(options.temp_id)) { + card_checklist.set('is_offline', false); + } + if (!_.isUndefined(response.checklist) && _.isUndefined(options.temp_id)) { + card_checklist.set({ + id: parseInt(response.checklist.id) + }); + } else { + global_uuid[data.uuid] = options.temp_id; + card_checklist.set('id', data.uuid); + } + card_checklist.set('checklist_item_completed_count', 0); + card_checklist.set('name', _.escape(data.name)); + card_checklist.set('card_id', self.model.id); + card_checklist.set('list_id', self.model.attributes.list_id); + card_checklist.set('board_id', self.model.attributes.board_id); + card_checklist.card = self.model; + if (!_.isUndefined(response.checklist)) { + var checklist_items = response.checklist.checklists_items; + card_checklist.set('checklist_items', checklist_items); + _.each(response.checklist.checklists_items, function(item) { + checklist_item = new App.CheckListItem(); + checklist_item.set('id', parseInt(item.id)); + checklist_item.set('card_id', self.model.id); + checklist_item.set('user_id', parseInt(item.user_id)); + checklist_item.set('checklist_id', card_checklist.id); + checklist_item.set('name', item.name); + checklist_item.set('is_completed', false); + checklist_item.card = self.model.card; + checklist_item.checklist = new App.CheckList(); + checklist_item.checklist = self.model; + self.model.list.collection.board.checklist_items.add(checklist_item); + }); + + } else { + if (data.checklist_id !== '0') { + var _checklist_items = self.model.list.collection.board.checklist_items.where({ + checklist_id: parseInt(data.checklist_id) + }); + _.each(_checklist_items, function(item) { + checklist_item = new App.CheckListItem(); + checklist_item.set('id', new Date().getTime()); + checklist_item.set('card_id', self.model.id); + checklist_item.set('user_id', parseInt(item.attributes.user_id)); + checklist_item.set('checklist_id', card_checklist.id); + checklist_item.set('name', item.attributes.name); + checklist_item.set('is_completed', false); + checklist_item.card = self.model.card; + checklist_item.checklist = new App.CheckList(); + checklist_item.checklist = self.model; + self.model.list.collection.board.checklist_items.add(checklist_item); + }); + } + + + } + self.model.list.collection.board.checklists.add(card_checklist); + self.model.checklists.add(card_checklist); + + var __checklist_items = self.model.list.collection.board.checklist_items.where({ + card_id: parseInt(self.model.attributes.id) + }); + items = new App.CheckListItemCollection(); + items.add(__checklist_items); + var completed_count = items.filter(function(checklist_item) { + return checklist_item.get('is_completed') === true || checklist_item.get('is_completed') == 'true' || checklist_item.get('is_completed') == 't'; + }).length; + var total_count = items.models.length; + self.model.set('checklist_item_completed_count', completed_count); + self.model.set('checklist_item_count', total_count); + + if (!_.isUndefined(response.activity)) { + var activity = new App.Activity(); + activity.set(response.activity); + var view_act = new App.ActivityView({ + model: activity + }); + self.model.activities.unshift(activity); + var view_activity = $('#js-card-activities-' + self.model.id); + view_activity.prepend(view_act.render().el).find('.timeago').timeago(); + } + } + }); + return false; + }, + /** + * showAddMemberForm() + * display card member add form + * @param e + * @type Object(DOM event) + */ + showAddMemberForm: function(e) { + e.preventDefault(); + $('.js-add-member-response').html(new App.ModalCardMemberFormView({ + model: this.model.board_users, + users: this.model.users, + card: this.model + }).el); + var target = $(e.target); + $('li.dropdown').removeClass('open'); + target.parents('li.dropdown').addClass('open'); + if (target.hasClass('js-card-header-action')) { + return false; + } + }, + /** + * addCardMember() + * add card member + * @param e + * @type Object(DOM event) + */ + addCardMember: function(e) { + e.preventDefault(); + var self = this; + var target = $(e.currentTarget); + var uuid = new Date().getTime(); + target.removeClass('js-add-card-member').addClass('js-remove-card-member').append(''); + var user_id = target.data('user-id'); + var user_name = target.data('user-name'); + var user_initial = target.data('user-initial'); + var user_profile_picture_path = target.data('user-profile-picture-path'); + var content_img = '' + user_initial + ''; + if (!_.isEmpty(user_profile_picture_path)) { + var hash = calcMD5(SecuritySalt + 'User' + user_id + 'jpg' + 'small_thumb' + SITE_NAME); + var profile_picture_path = window.location.pathname + 'img/small_thumb/User/' + user_id + '.' + hash + '.jpg'; + content_img = '' + user_name + ''; + } + var view_user = $('#js-card-users-list-' + self.model.id).prepend('
        • ' + content_img + '
        • '); + $('.js-member-dropdown').removeClass('open'); + var card_user = new App.CardUser(); + card_user.set('uuid', uuid); + card_user.set('is_offline', true); + card_user.set('user_id', user_id); + card_user.set('card_id', self.model.id); + card_user.set('board_id', self.model.attributes.board_id); + card_user.set('list_id', self.model.attributes.list_id); + card_user.set('profile_picture_path', user_profile_picture_path); + card_user.set('username', user_name); + card_user.set('initials', user_initial); + self.model.users.add(card_user, { + silent: true + }); + card_user.url = api_url + 'boards/' + self.model.attributes.board_id + '/lists/' + self.model.attributes.list_id + '/cards/' + self.model.id + '/users/' + user_id + '.json'; + card_user.save({ + user_id: user_id, + card_id: self.model.id + }, { + success: function(model, response, options) { + if (_.isUndefined(options.temp_id)) { + card_user.set('is_offline', false); + } + target.attr('data-card-user-id', response.id); + if (self.model.users.models.length > 0) { + self.model.users.findWhere({ + 'user_id': user_id, + 'card_id': self.model.id + }).set('id', parseInt(response.id)); + } + if (!_.isUndefined(response.id) && _.isUndefined(options.temp_id)) { + card_user.set({ + id: parseInt(response.id) + }); + } else { + global_uuid[uuid] = options.temp_id; + card_user.set('id', uuid); + } + if (!_.isUndefined(response.activity)) { + var activity = new App.Activity(); + activity.set(response.activity); + var view = new App.ActivityView({ + model: activity + }); + self.model.activities.unshift(activity); + var view_activity = $('#js-card-activities-' + self.model.id); + view_activity.prepend(view.render().el).find('.timeago').timeago(); + } + } + }); + return false; + }, + /** + * removeCardMember() + * remove card member + * @param e + * @type Object(DOM event) + */ + removeCardMember: function(e) { + e.preventDefault(); + var target = $(e.currentTarget); + var user_id = target.attr('data-user-id'); + var id = target.attr('data-card-user-id'); + target.removeClass('js-remove-card-member').addClass('js-add-card-member'); + $('i.icon-ok', target).remove(); + var card_user = this.model.users.findWhere({ + 'user_id': user_id, + 'card_id': this.model.id + }); + $('#js-card-users-list-' + id).find('.js-added-card-user-' + user_id).remove(); + if (!_.isUndefined(card_user) || (!_.isUndefined(id) && id !== '')) { + card_user = new App.CardUser(); + card_user.set('id', id); + card_user.url = api_url + 'boards/' + this.model.attributes.board_id + '/lists/' + this.model.attributes.list_id + '/cards/' + this.model.id + '/cards_users/' + id + '.json'; + + } else { + card_user = new App.CardUser(); + card_user.set('id', user_id); + card_user.url = api_url + 'boards/' + this.model.attributes.board_id + '/lists/' + this.model.attributes.list_id + '/cards/' + this.model.id + '/users/' + user_id + '.json'; + } + card_user.destroy(); + this.model.users.remove(card_user); + return false; + }, + /** + * showAddCommentForm() + * display card comment form + * @param e + * @type Object(DOM event) + * @return false + */ + showAddCommentForm: function(e) { + $('.js-add-comment-response').html(new App.ActivityAddFormView({ + model: this.model + }).el); + return false; + }, + /** + * showReplyCommentForm() + * display reply comment form + * @param e + * @type Object(DOM event) + * @return false + */ + showReplyCommentForm: function(e) { + var activity_id = $(e.currentTarget).data('activity-id'); + var activitiy = this.model.activities.get({ + id: activity_id + }); + $('.js-acticity-action-' + activity_id).addClass('hide'); + $('.js-activity-reply-form-response-' + activity_id).html(new App.ActivityReplyFormView({ + model: activitiy + }).el); + return false; + }, + /** + * addComment() + * save comment + * @param e + * @type Object(DOM event) + * @return false + */ + addComment: function(e) { + e.preventDefault(); + var self = this; + $('.js-add-comment-response').html('Add Comment'); + var board_id = this.model.attributes.board_id; + var data = $(e.target).serializeObject(); + var is_reply = $(e.target).hasClass('js-reply-form'); + if (!is_reply) { + $(e.target)[0].reset(); + } + // Create UUID and push into list and render immediately + data.uuid = new Date().getTime(); + data.board_id = board_id; + data.list_id = this.model.attributes.list_id; + data.card_id = this.model.id; + data.user_id = authuser.user.id; + var push_data = data; + var activity = new App.Activity(); + activity.set('is_offline', true); + activity.url = api_url + 'boards/' + this.model.attributes.board_id + '/lists/' + this.model.attributes.list_id + '/cards/' + this.model.id + '/comments.json'; + activity.save(data, { + success: function(model, response, options) { + if (_.isUndefined(options.temp_id)) { + activity.set('is_offline', false); + } + if (!_.isUndefined(response.id) && _.isUndefined(options.temp_id)) { + activity.set({ + id: parseInt(response.id) + }); + } else { + global_uuid[data.uuid] = options.temp_id; + activity.set('id', data.uuid); + } + if (!_.isUndefined(response.activities)) { + model.set('created', response.activities.created); + activity.set('depth', response.activities.depth); + } + + model.set('type', 'add_comment'); + activity.set('username', authuser.user.username); + activity.set('profile_picture_path', authuser.user.profile_picture_path); + activity.set('initials', authuser.user.initials); + self.model.activities.unshift(activity); + self.model.list.collection.board.activities.add(activity); + var view = new App.ActivityView({ + model: model + }); + self.model.activities.unshift(activity); + var view_activity = $('#js-card-activities-' + self.model.id); + if (!_.isEmpty(data.root)) { + $(view.render().el).insertAfter($('.js-list-activity-' + data.root)).find('.timeago').timeago(); + } else { + view_activity.prepend(view.render().el).find('.timeago').timeago(); + } + self.hideReplyCommentForm(); + + } + }); + return false; + }, + /** + * showEditCommentForm() + * display comment edit form + * @param e + * @type Object(DOM event) + */ + showEditCommentForm: function(e) { + e.preventDefault(); + var activity_id = $(e.target).data('activity-id'); + var temp_id = $(e.target).data('activity-temp-id'); + $('.js-acticity-action-' + activity_id).addClass('hide'); + var activitiy = this.model.activities.get({ + id: activity_id + }); + $('.js-activity-' + activity_id).html(new App.EditActivityFormView({ + model: activitiy, + attributes: { + 'data-activity-id': activity_id, + 'data-activity-temp-id': temp_id + } + }).el); + $('.js-inputComment', e.target).focus(); + }, + /** + * editComment() + * update comment + * @param e + * @type Object(DOM event) + * @return false + */ + editComment: function(e) { + e.preventDefault(); + var activity_id = $(e.currentTarget).data('activity-id'); + var temp_id = $(e.currentTarget).data('activity-temp-id'); + var current_card = this.model.activities.get({ + id: activity_id + }); + var board_id = current_card.attributes.board_id; + var list_id = current_card.attributes.list_id; + var card_id = current_card.attributes.card_id; + var data = $(e.target).serializeObject(); + $('.js-activity-' + activity_id).html(this.converter.makeHtml(data.comment)); + $('.js-acticity-action-' + activity_id).removeClass('hide'); + //Update in list table + var activity = new App.Activity(); + activity.id = parseInt(activity_id); + activity.set('is_offline', true); + activity.set('temp_id', temp_id); + activity.url = api_url + 'boards/' + board_id + '/lists/' + list_id + '/cards/' + card_id + '/comments/' + activity_id + '.json'; + activity.save(data); + return false; + }, + /** + * hideEditCommentForm() + * hide update comment form + * @param e + * @type Object(DOM event) + * @return false + */ + hideEditCommentForm: function() { + var activity_id = this.$el.find('.js-hide-edit-comment-form').data('activity-id'); + var current_card = this.model.activities.get({ + id: activity_id + }); + this.$el.find('.js-hide-edit-comment-form').parents('span.js-activity-' + activity_id).html(this.converter.makeHtml(_.escape(current_card.attributes.comment))); + $('.js-acticity-action-' + activity_id).removeClass('hide'); + }, + /** + * hideReplyCommentForm() + * hide reply comment form + * @param e + * @type Object(DOM event) + * @return false + */ + hideReplyCommentForm: function() { + var activity_id = this.$el.find('.js-hide-reply-comment-form').data('activity-id'); + $('.js-activity-reply-form-response-' + activity_id).html(''); + $('.js-acticity-action-' + activity_id).removeClass('hide'); + }, + /** + * showConfirmCommentDelete() + * display comment delete confirmation + * @param e + * @type Object(DOM event) + */ + showConfirmCommentDelete: function(e) { + e.preventDefault(); + var activity_id = $(e.currentTarget).data('activity-id'); + $(e.currentTarget).siblings('ul').find('#js-acticity-actions-response-' + activity_id).html(new App.ActivityDeleteConfirmView({ + model: activity_id + }).el); + }, + /** + * deleteComment() + * delete comment + * @param e + * @type Object(DOM event) + * @return false + */ + deleteComment: function(e) { + var activity_id = $(e.currentTarget).data('activity-id'); + var current_card = this.model.activities.get({ + id: activity_id + }); + var list_id = this.model.attributes.list_id; + var card_id = this.model.id; + var board_id = this.model.attributes.board_id; + this.model.activities.remove({ + id: activity_id + }); + this.model.list.collection.board.activities.remove({ + id: parseInt(activity_id) + }); + var activity = new App.Activity(); + activity.set('id', activity_id); + activity.url = api_url + 'boards/' + board_id + '/lists/' + list_id + '/cards/' + card_id + '/comments/' + activity_id + '.json'; + $(e.currentTarget).parents('li.js-activity').remove(); + activity.destroy(); + return false; + }, + /** + * addCardAttachmentLink() + * add attachment link in card + * @param e + * @type Object(DOM event) + */ + addCardAttachmentLink: function(e) { + e.preventDefault(); + var self = this; + var target = $(e.target); + target.parents('li.dropdown').removeClass('open'); + var data = target.serializeObject(); + target[0].reset(); + var card_attachment = new App.CardAttachment(); + card_attachment.url = api_url + 'boards/' + self.model.attributes.board_id + '/lists/' + self.model.attributes.list_id + '/cards/' + self.model.id + '/attachments.json'; + card_attachment.save(data, { + success: function(model, response) { + self.closePopup(e); + card_attachment.set(response.card_attachments); + card_attachment.set('id', parseInt(response.card_attachments.id)); + card_attachment.set('board_id', parseInt(response.card_attachments.board_id)); + card_attachment.set('list_id', parseInt(response.card_attachments.list_id)); + card_attachment.set('card_id', parseInt(response.card_attachments.card_id)); + self.model.list.collection.board.attachments.unshift(card_attachment, { + silent: true + }); + self.model.attachments.unshift(card_attachment, { + silent: true + }); + var view = new App.CardAttachmentView({ + model: card_attachment + }); + var view_attachment = self.$('#js-card-attachments-list'); + view_attachment.append(view.render().el).find('.timeago').timeago(); + } + }); + }, + /** + * showCardVotersList() + * display card voters list + * @param e + * @type Object(DOM event) + */ + showCardVotersList: function(e) { + e.preventDefault(); + $('.js-show-card-voters-list-response').html(new App.CardVotersListView({ + model: this.model + }).el); + }, + /** + * removeDueDate() + * delete card due date + * @param e + * @type Object(DOM event) + * @return false + */ + removeDueDate: function(e) { + e.preventDefault(); + var self = this; + if (!_.isEmpty(this.model.attributes.due_date) && this.model.attributes.due_date !== '') { + this.model.url = api_url + 'boards/' + this.model.attributes.board_id + '/lists/' + this.model.attributes.list_id + '/cards/' + this.model.id + '.json'; + this.model.set('due_date', null); + this.model.save({ + due_date: 'NULL' + }, { + patch: true, + success: function(model, response) { + var activity = new App.Activity(); + activity.set(response.activity); + var view = new App.ActivityView({ + model: activity, + board: self.model.list.collection.board + }); + self.model.activities.unshift(activity); + var view_activity = $('#js-card-activities-' + self.model.id); + view_activity.prepend(view.render().el).find('.timeago').timeago(); + } + }); + } + }, + /** + * showSearchUsers() + * display searched board user list + */ + showSearchUsers: function(e) { + var self = this; + var q = $(e.target).val(); + if (q !== '') { + var filtered_users = this.model.list.collection.board.board_users.search(q); + var users = new App.UserCollection(); + if (!_.isEmpty(filtered_users._wrapped)) { + $.unique(filtered_users._wrapped); + } + users.add(filtered_users._wrapped); + $('.js-organization-member-search-response').html(''); + if (!_.isEmpty(users.models)) { + _.each(users.models, function(user) { + var added_user = self.model.users.findWhere({ + card_id: self.model.id, + user_id: user.attributes.user_id + }); + var is_added_user = (!_.isUndefined(added_user)) ? true : false; + $('.js-organization-member-search-response').append(new App.CardSearchUsersResultView({ + model: user, + is_added_user: is_added_user, + added_user: added_user + }).el); + }); + } else { + $('.js-organization-member-search-response').html(new App.CardSearchUsersResultView({ + model: null + }).el); + } + } else { + this.$el.find('.js-organization-member-search-response').html(''); + this.renderBoardUsers(); + } + + }, + /** + * getLabelcolor() + * generate color code + * @param string + * @type string + * @return color code + * @type string + */ + getLabelcolor: function(string) { + return calcMD5(string).slice(0, 6); + }, + /** + * showSearchCards() + * display searched card result + * @param string + * @type string + */ + showSearchCards: function() { + var self = this; + var q = this.$el.find('.js-search-card').val(); + var cards = new App.CardCollection(); + cards.url = api_url + 'cards/search.json'; + cards.fetch({ + data: { + q: q + }, + success: function() { + self.$el.find('.js_activity_card_search_response').nextAll().remove(); + if (!_.isEmpty(cards.models)) { + _.each(cards.models, function(card) { + $(new App.ActivityCardSearchView({ + model: card + }).el).insertAfter(self.$el.find('.js_activity_card_search_response')); + }); + } else { + $(new App.ActivityCardSearchView({ + model: null + }).el).insertAfter(self.$el.find('.js_activity_card_search_response')); + } + } + }); + }, + /** + * AddCommentCard() + * add card comment + * @param string + * @type string + */ + AddCommentCard: function(e) { + e.preventDefault(); + var target = $(e.currentTarget); + var card_id = target.data('card-id'); + var card_name = target.data('card-name'); + var board_id = target.data('board-id'); + this.$el.find('.js-comment').val(this.$el.find('.js-comment').val() + '#' + card_id); + }, + /** + * changeList() + * change list based on selected board + * @param string + * @type string + */ + changeList: function(e) { + var target = $(e.currentTarget); + var self = this; + var board_id = parseInt(target.val()); + var content_list = ''; + var content_position = ''; + if (board_id == this.model.attributes.board_id) { + this.showMoveCardForm(e); + } else { + var board = self.boards.findWhere({ + id: parseInt(board_id), + is_closed: false + }); + board.lists.add(board.attributes.lists); + var board_lists = board.lists.where({ + is_archived: false + }); + var current_position = this.model.collection.indexOf(this.model) + 1; + var is_first_list = true; + _.each(board_lists, function(list) { + if (self.model.attributes.list_id == list.attributes.id) { + content_list += ''; + is_first_list = true; + } else { + content_list += ''; + } + if (is_first_list) { + is_first_list = false; + for (var i = 1; i <= list.attributes.card_count; i++) { + if (self.model.attributes.list_id == list.attributes.id && i == current_position) { + content_position += ''; + } else { + content_position += ''; + } + } + if (self.model.attributes.list_id != list.attributes.id) { + var next_position = parseInt(list.attributes.card_count) + 1; + content_position += ''; + } + } + }); + self.$el.find('.js-change-position').html(content_list); + self.$el.find('.js-position').html(content_position); + } + }, + /** + * changePosition() + * change position based on selected list + * @param string + * @type string + */ + changePosition: function(e) { + var target = $(e.currentTarget); + var self = this; + var list_id = target.val(); + var board_id = target.parent().prev().find('.js-change-list').val(); + var content_position = ''; + var board = self.boards.findWhere({ + id: parseInt(board_id) + }); + var list = board.lists.findWhere({ + id: parseInt(list_id) + }); + var current_position = this.model.collection.indexOf(this.model) + 1; + for (var i = 1; i <= list.attributes.card_count; i++) { + if (self.model.attributes.list_id == list.attributes.id && i == current_position) { + content_position += ''; + } else { + content_position += ''; + } + } + if (this.model.attributes.list_id != list.attributes.id) { + var next_position = parseInt(list.attributes.card_count) + 1; + content_position += ''; + } + self.$el.find('.js-position').html(content_position); + }, + /** + * showCopyCardForm() + * display copy card form + * @param string + * @type string + */ + showCopyCardForm: function(e) { + e.preventDefault(); + $('.js-show-move-card-form-response').html(new App.CopyCardView({ + model: this.model, + boards: this.boards + }).el); + var target = $(e.target); + $('li.dropdown').removeClass('open'); + target.parents('li.dropdown').addClass('open'); + return false; + }, + /** + * showMoreForm() + * display More + * @param string + * @type string + */ + showMoreForm: function(e) { + e.preventDefault(); + var target = $(e.target); + $('li.dropdown').removeClass('open'); + target.parents('li.dropdown').addClass('open'); + return false; + }, + /** + * copyCard() + * save copied card + * @param string + * @type string + */ + copyCard: function(e) { + e.preventDefault(); + var self = this; + var data = $(e.currentTarget).serializeObject(); + data.uuid = new Date().getTime(); + var card = new App.Card(); + card.set('is_offline', true); + this.closePopup(e); + var current_card = this.model.attributes; + data.board_id = parseInt(data.board_id); + card.url = api_url + 'boards/' + this.model.attributes.board_id + '/lists/' + this.model.attributes.list_id + '/cards/' + this.model.id + '/copy.json'; + card.save(data, { + patch: true, + success: function(model, response, options) { + if (_.isUndefined(options.temp_id)) { + card.set('is_offline', false); + } + if (data.board_id === current_card.board_id) { + card.set(response.cards); + if (response.cards.is_archived === 'f') { + card.set('is_archived', false); + } else { + card.set('is_archived', true); + } + card.set('list_id', parseInt(response.cards.list_id)); + card.set('board_id', parseInt(response.cards.board_id)); + card.set('id', parseInt(response.id)); + if (!_.isUndefined(response.id) && _.isUndefined(options.temp_id)) { + card.set({ + id: parseInt(response.id) + }); + } else { + global_uuid[data.uuid] = options.temp_id; + card.set('id', data.uuid); + } + self.model.list.collection.get(data.list_id).cards.add(card); + var i = 1; + _.each(response.cards.attachments, function(attachment) { + var options = { + silent: true + }; + if (i === response.cards.attachments.length) { + options.silent = false; + } + var new_attachment = new App.CardAttachment(); + new_attachment.set(attachment); + new_attachment.set('id', parseInt(attachment.id)); + new_attachment.set('board_id', parseInt(attachment.board_id)); + new_attachment.set('list_id', parseInt(attachment.list_id)); + new_attachment.set('card_id', parseInt(attachment.card_id)); + self.model.list.collection.get(data.list_id).cards.get(parseInt(attachment.card_id)).attachments.unshift(new_attachment, options); + self.model.list.collection.board.attachments.unshift(new_attachment, options); + i++; + }); + i = 0; + _.each(response.cards.activities, function(activity) { + var options = { + silent: true + }; + if (i === response.cards.activities.length) { + options.silent = false; + } + var new_activity = new App.Activity(); + new_activity.set(activity); + new_activity.set('id', parseInt(activity.id)); + new_activity.set('board_id', parseInt(activity.board_id)); + new_activity.set('list_id', parseInt(activity.list_id)); + new_activity.set('card_id', parseInt(activity.card_id)); + self.model.list.collection.board.activities.add(new_activity, options); + self.model.activities.unshift(new_activity, options); + i++; + }); + self.model.list.collection.board.cards.add(card); + self.model.list.collection.board.checklists.add(response.cards.cards_checklists); + self.model.list.collection.board.labels.add(response.cards.cards_labels); + } + var activity = new App.Activity(); + activity.set(response.activity); + var view = new App.ActivityView({ + model: activity + }); + self.model.activities.unshift(activity); + var view_activity = $('#js-card-activities-' + self.model.id); + view_activity.prepend(view.render().el).find('.timeago').timeago(); + } + }); + + }, + /** + * showSearchMembers() + * display searched member list + */ + showSearchMembers: function(e) { + var self = this; + var q = $(e.target).val(); + if (q !== '') { + var filtered_users = this.model.list.collection.board.board_users.search(q); + var users = new App.UserCollection(); + if (!_.isEmpty(filtered_users._wrapped)) { + $.unique(filtered_users._wrapped); + } + users.add(filtered_users._wrapped); + this.$el.find('.js-comment-member-search-response').nextAll().remove(); + if (!_.isEmpty(users.models)) { + _.each(users.models, function(user) { + $(new App.ActivityUserAddSearchResultView({ + model: user + }).el).insertAfter(self.$el.find('.js-comment-member-search-response')); + }); + if (users.models.length === 0) { + $(new App.ActivityUserAddSearchResultView({ + model: null + }).el).insertAfter(self.$el.find('.js-comment-member-search-response')); + } + } else { + $(new App.ActivityUserAddSearchResultView({ + model: null + }).el).insertAfter(self.$el.find('.js-comment-member-search-response')); + } + } else { + this.$el.find('.js-comment-member-search-response').nextAll().remove(); + this.renderActivityBoardUsers(); + } + }, + /** + * AddCommentMember() + * mention member in comment + * @param string + * @type string + */ + AddCommentMember: function(e) { + e.preventDefault(); + var target = $(e.currentTarget); + var user_name = target.data('user-name'); + this.$el.find('.js-comment').val('@' + user_name + this.$el.find('.js-comment').val()); + }, + renderBoardUsers: function() { + var self = this; + var view = this.$el.find('.js-organization-member-search-response'); + this.model.list.collection.board.board_users.each(function(board_user) { + var added_user = self.model.users.findWhere({ + card_id: self.model.id, + user_id: board_user.attributes.user_id + }); + var is_added_user = (!_.isUndefined(added_user)) ? true : false; + view.append(new App.CardSearchUsersResultView({ + model: board_user, + is_added_user: is_added_user, + added_user: added_user + }).el); + }); + }, + renderActivityBoardUsers: function() { + var view = this.$el.find('.js-comment-member-search-response'); + this.model.list.collection.board.board_users.each(function(board_user) { + $(new App.ActivityUserAddSearchResultView({ + model: board_user + }).el).insertAfter(view); + }); + }, + loadDropbox: function(e) { + var target = $(e.target); + target.parents('li.dropdown').addClass('open'); + if (typeof Dropbox == 'undefined') { + $.ajax({ + cache: true, + dataType: 'script', + url: 'https://www.dropbox.com/static/api/1/dropins.js', + success: function() {} + }); + } + if (target.hasClass('js-card-header-action')) { + return false; + } + }, + /** + * selectCardURL() + * select card URL + * @param e + * @type Object(DOM event) + * + */ + selectCardURL: function(e) { + $(e.target).select(); + }, + showActions: function(e) { + this.$el.find('.js-new-comment').removeClass('hide'); + }, + noAction: function(e) { + e.preventDefault(); + return false; + }, + showSideCardTitleEditForm: function(e) { + var target = $(e.target); + $('li.dropdown').removeClass('open'); + target.parents('li.dropdown').addClass('open'); + return false; + }, + openDropdown: function(e) { + var target = $(e.target); + $('li.dropdown').removeClass('open'); + target.parents('li.dropdown').addClass('open'); + return false; + }, + onEnter: function(e) { + if (e.which === 13) { + var form = $(e.target).closest('form'); + if (form.attr('name') === 'cardAddForm') { + $('input[type=submit]', form).trigger('click'); + } else { + return false; + } + } + } +}); diff --git a/client/js/views/modal_flickr_photo_view.js b/client/js/views/modal_flickr_photo_view.js new file mode 100644 index 000000000..83724b7cf --- /dev/null +++ b/client/js/views/modal_flickr_photo_view.js @@ -0,0 +1,251 @@ +/** + * @fileOverview This file has functions related to modal board view. This view calling from board header view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board model. @see Available Object in App.BoardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ModalBoard View + * @class ModalFlickrPhotoView + * @constructor + * @extends Backbone.View + */ +App.ModalFlickrPhotoView = Backbone.View.extend({ + id: 'flickr-modal', + className: '', + template: JST['templates/modal_flickr_photo_view'], + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-close-popover': 'closePopup', + 'click .js-flickr-loadmore': 'getPhotos', + 'click .js-flickr-search-box': 'getSearch', + 'click .js-flickr-changebackground': 'changeBackgroundImage', + 'keyup .js-flickr-search': 'keyPressEventHandler', + }, + keyPressEventHandler: function(options) { + if (event.keyCode == 13) { + this.$('.js-flickr-search-box').click(); + } + }, + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + this.page = 1; + this.total_page = 1; + this.per_page = 36; + this.search_type = 'flickr.photos.getRecent'; + this.search_text = 'Recent Photos'; + this.type = options.type; + this.FLICKR_API_KEY = FLICKR_API_KEY; + if (this.type == 'texture') { + this.search_text = 'texture background'; + this.search_type = 'flickr.photos.search'; + $('.js-flickr-search').val('texture background'); + } else { + this.type = 'image'; + } + }, + /** + * getSearch() + * Search photos + * @param NULL + * @return object + * + */ + getSearch: function() { + var search = $('.js-flickr-search').val(); + if (search.trim() !== '') { + this.search_text = search.trim(); + if (this.search_text !== 'Recent Photos') { + this.search_type = 'flickr.photos.search'; + $('#js-flickr-background-photos').html(''); + this.page = 1; + this.total_page = 1; + this.getPhotos(); + } + } + return this; + }, + /** + * getPhotos() + * populate the html to the dom + * @param NULL + * @return object + * + */ + getPhotos: function() { + var self = this; + if (this.page <= this.total_page) { + $('.js-flickr-loader-and-more').html('
        • '); + var Flickr = new App.Flickr(); + Flickr.url = 'https://api.flickr.com/services/rest/?api_key=' + FLICKR_API_KEY + '&format=json&method=' + this.search_type + '&nojsoncallback=1&page=' + this.page + '&per_page=' + this.per_page + '&media=photos&content_type=7&sort=relevance'; + if (this.search_text !== 'Recent Photos') { + Flickr.url = 'https://api.flickr.com/services/rest/?api_key=' + FLICKR_API_KEY + '&format=json&method=' + this.search_type + '&nojsoncallback=1&page=' + this.page + '&per_page=' + this.per_page + '&text=' + this.search_text + '&media=photos&content_type=7&sort=relevance'; + } + Flickr.fetch({ + cache: false, + success: function(photos, response) { + $('.js-flickr-loader-and-more').html('Load More'); + $('.js-flickr-loader-and-more').show(); + if (!_.isUndefined(photos.attributes.photos) && photos.attributes.photos !== null) { + var fl_photos = photos.attributes.photos.photo; + if (fl_photos.length > 0) { + self.total_page = parseInt(photos.attributes.photos.pages); + self.page = parseInt(photos.attributes.photos.page) + 1; + for (var i = 0; i < fl_photos.length; i++) { + var photo = fl_photos[i]; + $('#js-flickr-background-photos').append(new App.FlickrView({ + model: photo + }).el); + } + if (self.page > self.total_page) { + $('.js-flickr-loader-and-more').hide(); + } + } else { + self.total_page = parseInt(photos.attributes.photos.pages); + self.page = parseInt(photos.attributes.photos.page); + if (self.page == 1) { + $('.js-flickr-loader-and-more').html('No Photo Found....'); + $('.js-flickr-loader-and-more').show(); + } else { + self.page = parseInt(photos.attributes.photos.page) + 1; + if (self.page > self.total_page) { + $('.js-flickr-loader-and-more').hide(); + } + } + } + } + } + }); + } else { + $('.js-flickr-loader-and-more').hide(); + } + }, + + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + list: this.model + })); + this.$el.modal({ + show: true, + backdrop: false + }); + this.getPhotos(); + this.showTooltip(); + return this; + }, + /** + * show() + * display list attachment in modal box + * + */ + show: function() { + this.render(); + this.$el.find('#modalFlickrPhoto').modal('show'); + if (this.type == 'texture') { + $('.js-flickr-search').val('texture background'); + } + }, + /** + * closePopup() + * hide displayed dropdown + * @param e + * @type Object(DOM event) + * @return false + */ + closePopup: function(e) { + var target = $(e.target); + target.parents('#flickr-modal').remove(); + return false; + }, + /** + * changeBackgroundImage() + * display the board background image + * @param e + * @type Object(DOM event) + * @return false + * + */ + changeBackgroundImage: function(e) { + var farm = $(e.currentTarget).data('farm'); + var id = $(e.currentTarget).data('id'); + var server = $(e.currentTarget).data('server'); + var secret = $(e.currentTarget).data('secret'); + var title = $(e.currentTarget).data('title'); + var image_path = 'http://farm' + farm + '.static.flickr.com/' + server + '/' + id + '_' + secret + '_b.jpg'; + if (this.type == 'image') { + $('body').removeAttr('style').css({ + 'background': 'url(' + image_path + ') 25% 25% no-repeat fixed', + 'background-size': 'cover' + }).addClass('board-view-pattern board-view'); + this.model.set('background_name', title); + this.model.set('background_picture_url', image_path); + this.model.set('background_pattern_url', ''); + image_path = 'http://farm' + farm + '.static.flickr.com/' + server + '/' + id + '_' + secret + '_XXXX.jpg'; + data = { + background_name: title, + background_color: 'NULL', + background_picture_url: image_path, + background_pattern_url: 'NULL' + }; + App.boards.get(this.model.id).set('background_name', title); + App.boards.get(this.model.id).set('background_picture_url', image_path); + App.boards.get(this.model.id).set('background_pattern_url', ''); + App.boards.get(this.model.id).set('background_color', ''); + + } else { + $('body').removeAttr('style').css({ + 'background': 'url(' + image_path + ')', + }).addClass('board-view-pattern board-view'); + this.model.set('background_name', title); + this.model.set('background_picture_url', ''); + this.model.set('background_pattern_url', image_path); + App.boards.get(this.model.id).set('background_name', title); + App.boards.get(this.model.id).set('background_picture_url', ''); + App.boards.get(this.model.id).set('background_pattern_url', image_path); + App.boards.get(this.model.id).set('background_color', ''); + image_path = 'http://farm' + farm + '.static.flickr.com/' + server + '/' + id + '_' + secret + '_XXXX.jpg'; + data = { + background_name: title, + background_color: 'NULL', + background_picture_url: 'NULL', + background_pattern_url: image_path + }; + } + this.model.url = api_url + 'boards/' + this.model.id + '.json'; + this.model.set('custom_background_url', ''); + this.model.set('background_color', ''); + this.model.save(data, { + patch: true + }); + var view_my_board = $('.js-myboard-list'); + view_my_board.html(''); + if (!_.isEmpty(App.boards.models)) { + _.each(App.boards.models, function(board) { + view_my_board.append(new App.MyBoardsListingView({ + model: board, + authuser: authuser, + attributes: { + class: 'js-show-board-star' + } + }).el); + }); + } + return false; + } +}); diff --git a/client/js/views/modal_list_view.js b/client/js/views/modal_list_view.js new file mode 100644 index 000000000..cda570de3 --- /dev/null +++ b/client/js/views/modal_list_view.js @@ -0,0 +1,107 @@ +/** + * @fileOverview This file has functions related to modal list view. This view calling from list view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : list model. @see Available Object in App.ListView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ModalList View + * @class ModalListView + * @constructor + * @extends Backbone.View + */ +App.ModalListView = Backbone.View.extend({ + id: 'base-modal', + className: '', + template: JST['templates/modal_list_view'], + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'hidden': 'teardown', + 'click .js-close-popover': 'closePopup', + }, + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + _(this).bindAll('show', 'teardown'); + }, + teardown: function() { + this.$el.data('modal', null); + this.remove(); + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + list: this.model + })); + this.renderAttachmentsCollection(); + this.$el.modal({ + show: true, + backdrop: false + }); + this.showTooltip(); + return this; + }, + /** + * show() + * display list attachment + */ + show: function() { + this.render(); + this.$el.find('#modalListView').modal('show'); + }, + /** + * closePopup() + * close opened dropdown + * @param e + * @type Object(DOM event) + * @return false + */ + closePopup: function(e) { + var target = $(e.target); + target.parents('li.dropdown').removeClass('open'); + return false; + }, + /** + * renderAttachmentsCollection() + * render attachments + */ + renderAttachmentsCollection: function() { + var view_attachment = this.$('#js-list-attachments-list'); + view_attachment.html(''); + var attachments = this.model.collection.board.attachments.where({ + list_id: this.model.id + }); + var filtered_attachments = new App.AttachmentCollection(); + filtered_attachments.reset(attachments); + if (filtered_attachments.length > 0) { + filtered_attachments.each(function(attachment) { + var view = new App.AttachmentView({ + model: attachment + }); + view_attachment.append(view.render().el).find('.timeago').timeago(); + }); + } else { + var view = new App.AttachmentView({ + model: null + }); + view_attachment.append(view.render().el); + } + } +}); diff --git a/client/js/views/modal_music_view.js b/client/js/views/modal_music_view.js new file mode 100644 index 000000000..d6bf93172 --- /dev/null +++ b/client/js/views/modal_music_view.js @@ -0,0 +1,113 @@ +/** + * @fileOverview This file has functions related to modal board view. This view calling from board header view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board model. @see Available Object in App.BoardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ModalBoard View + * @class ModalMusicView + * @constructor + * @extends Backbone.View + */ +App.ModalMusicView = Backbone.View.extend({ + id: 'music-modal', + className: '', + template: JST['templates/modal_music_view'], + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'submit form#js-MusicForm': 'addMusic' + }, + /** + * Constructor + * initialize default values and actions + */ + initialize: function() {}, + /** + * addMusic() + * Update music + * @param NULL + * @return object + * + */ + addMusic: function() { + var self = this; + var music_name = $('.js-music_name').val(); + var music_content = $('.js-music_content').val(); + this.model.url = api_url + 'boards/' + this.model.id + '.json'; + this.model.set('music_name', music_name); + this.model.set('music_content', music_content); + data = { + music_name: music_name, + music_content: music_content + }; + this.model.save(data, { + patch: true, + success: function(model, response) { + var view = new Backbone.View(); + view.flash('success', 'Updated successfully.'); + if (!_.isEmpty(music_content) && music_content != 'NULL') { + App.music.music_content = music_content; + var temp = new App.MusicRepeatView(); + temp.continueMusic(); + } else { + App.music.inst.silence(); + } + self.footerView = new App.FooterView({ + model: authuser, + board_id: self.model.id + }).render(); + $('#footer').html(self.footerView.el); + } + }); + + return false; + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + list: this.model + })); + this.$el.modal({ + show: true, + backdrop: false + }); + this.showTooltip(); + return this; + }, + /** + * show() + * display list attachment in modal box + * + */ + show: function() { + this.render(); + this.$el.find('#modalMusic').modal('show'); + $('.js-music_name').val(this.model.attributes.music_name); + $('.js-music_content').val(this.model.attributes.music_content); + }, + /** + * closePopup() + * hide displayed dropdown + * @param e + * @type Object(DOM event) + * @return false + */ + closePopup: function(e) { + var target = $(e.target); + target.parents('#flickr-modal').remove(); + return false; + } +}); diff --git a/client/js/views/modal_user_activities_list_view.js b/client/js/views/modal_user_activities_list_view.js new file mode 100644 index 000000000..db46b0f13 --- /dev/null +++ b/client/js/views/modal_user_activities_list_view.js @@ -0,0 +1,109 @@ +/** + * @fileOverview This file has functions related to modal user activities list view. This view calling from modal user actions view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board user model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Modal User Activities List View + * @class ModalUserActivitiesListView + * @constructor + * @extends Backbone.View + */ +App.ModalUserActivitiesListView = Backbone.View.extend({ + id: 'base-modal', + className: '', + template: JST['templates/modal_user_activities_list_view'], + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-close-popover': 'closePopup', + }, + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + this.user_id = options.user_id; + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + _(this).bindAll('show', 'teardown'); + }, + teardown: function() { + this.$el.data('modal', null); + this.remove(); + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + list: this.model + })); + this.renderUserActivitiesCollection(); + this.$el.modal({ + show: true, + backdrop: false + }); + this.showTooltip(); + return this; + }, + /** + * show() + * display list attachment + */ + show: function() { + this.render(); + this.$el.find('#modalUserActivitiesListView').modal('show'); + }, + /** + * closePopup() + * close opened dropdown + * @param e + * @type Object(DOM event) + * @return false + */ + closePopup: function(e) { + var target = $(e.target); + target.parents('li.dropdown').removeClass('open'); + return false; + }, + /** + * renderUserActivitiesCollection() + * render activities + */ + renderUserActivitiesCollection: function() { + var view_user_activities = this.$('#js-list-user-activities-list'); + view_user_activities.html(''); + var activities = new App.ActivityCollection(); + activities.url = api_url + 'users/' + this.user_id + '/activities.json?board_id=' + this.model.attributes.board_id; + activities.fetch({ + success: function(response) { + if (activities.length > 0) { + activities.each(function(activity) { + activity.from_footer = true; + var view = new App.ActivityView({ + model: activity + }); + view_user_activities.append(view.render().el).find('.timeago').timeago(); + }); + } else { + var view = new App.ActivityView({ + model: null + }); + view_user_activities.append(view.render().el); + } + } + }); + } +}); diff --git a/client/js/views/move_card_view.js b/client/js/views/move_card_view.js new file mode 100644 index 000000000..0b145a7c1 --- /dev/null +++ b/client/js/views/move_card_view.js @@ -0,0 +1,45 @@ +/** + * @fileOverview This file has functions related to move card view. This view calling from modal card view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : card model. @see Available Object in App.CardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * MoveCard View + * @class MoveCardView + * @constructor + * @extends Backbone.View + */ +App.MoveCardView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.boards = options.boards; + this.render(); + }, + template: JST['templates/move_card'], + tagName: 'div', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + card: this.model, + boards: this.boards + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/move_cards_from_list_view.js b/client/js/views/move_cards_from_list_view.js new file mode 100644 index 000000000..2b25f897c --- /dev/null +++ b/client/js/views/move_cards_from_list_view.js @@ -0,0 +1,47 @@ +/** + * @fileOverview This file has functions related to move cards form list view. This view calling from list view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : list collection. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * MoveCardsFromList View + * @class MoveCardsFromListView + * @constructor + * @extends Backbone.View + */ +App.MoveCardsFromListView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.filtered_lists = options.filtered_lists; + this.render(); + }, + template: JST['templates/move_cards_from_list'], + tagName: 'ul', + className: 'list-unstyled', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.el = this.template({ + list: this.model, + filtered_lists: this.filtered_lists + }); + this.delegateEvents(this.events); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/move_list_view.js b/client/js/views/move_list_view.js new file mode 100644 index 000000000..f553a8778 --- /dev/null +++ b/client/js/views/move_list_view.js @@ -0,0 +1,47 @@ +/** + * @fileOverview This file has functions related to move list form view. This view calling from list view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : list model. @see Available Object in App.ListView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * MoveCardsFromList View + * @class MoveCardsFromListView + * @constructor + * @extends Backbone.View + */ +App.MoveListFromView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.boards = options.boards; + this.total_board_list_length = options.total_board_list_length; + this.render(); + }, + template: JST['templates/move_list'], + tagName: 'li', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + list: this.model, + boards: this.boards, + total_board_list_length: this.total_board_list_length + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/music_repeat_view.js b/client/js/views/music_repeat_view.js new file mode 100644 index 000000000..0b0b6eba8 --- /dev/null +++ b/client/js/views/music_repeat_view.js @@ -0,0 +1,47 @@ +/** + * @fileOverview This file has functions related to user Music Repeat View. This view calling from application view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * MusicRepeat View + * @class MusicRepeatView + * @constructor + * @extends Backbone.View + */ +App.MusicRepeatView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() {}, + continueMusic: function(e) { + var temp = new App.MusicRepeatView(); + var music_content = App.music.music_content; + if (!_.isUndefined(App.music) && music_content !== undefined && music_content !== '' && music_content !== null) { + App.music.inst.setTimbre({ + wave: 'piano' + }); + if (!_.isUndefined(authuser.user)) { + if (!_.isUndefined(authuser.user.is_productivity_beats) && (authuser.user.is_productivity_beats === true || authuser.user.is_productivity_beats === 't')) { + App.music.inst.play( + music_content, temp.continueMusic + ); + } else { + App.music.inst.silence(); + } + } else { + if (!_.isUndefined(window.sessionStorage.getItem('music_play')) && window.sessionStorage.getItem('music_play') === "1") { + App.music.inst.play( + music_content, temp.continueMusic + ); + } else { + App.music.inst.silence(); + } + } + } + } +}); diff --git a/client/js/views/my_boards_listing_view.js b/client/js/views/my_boards_listing_view.js new file mode 100644 index 000000000..9614dead3 --- /dev/null +++ b/client/js/views/my_boards_listing_view.js @@ -0,0 +1,44 @@ +/** + * @fileOverview This file has functions related to my boards listing view. This view calling from footer view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ShowAllVisibility View + * @class ShowAllVisibilityView + * @constructor + * @extends Backbone.View + */ +App.MyBoardsListingView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.authuser = options.authuser; + this.render(); + }, + template: JST['templates/my_boards_listing'], + tagName: 'li', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + board: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/notification_menu_view.js b/client/js/views/notification_menu_view.js new file mode 100644 index 000000000..b124cba72 --- /dev/null +++ b/client/js/views/notification_menu_view.js @@ -0,0 +1,44 @@ +/** + * @fileOverview This file has functions related to notification menu view. This view calling from footer view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : user model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * NotificationMenu View + * @class NotificationMenuView + * @constructor + * @extends Backbone.View + */ +App.NotificationMenuView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/notification_menu'], + tagName: 'li', + className: 'list-unstyled js-notification-response', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + user: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/organization_add_view.js b/client/js/views/organization_add_view.js new file mode 100644 index 000000000..da5a94cad --- /dev/null +++ b/client/js/views/organization_add_view.js @@ -0,0 +1,69 @@ +/** + * @fileOverview This file has functions related to organization add view. This view calling from footer view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : undefined. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * OrganizationAdd View + * @class OrganizationAddView + * @constructor + * @extends Backbone.View + */ +App.OrganizationAddView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/organization_add'], + tagName: 'div', + events: { + 'submit #OrganizationAddForm': 'addOrganization', + + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({})); + this.showTooltip(); + return this; + }, + addOrganization: function(e) { + var data = $(e.target).serializeObject(); + var organization = new App.Organization(); + organization.url = api_url + 'organizations.json'; + organization.set(data); + organization.save(data, { + success: function(model, response) { + organization.set('id', parseInt(response.id)); + authuser.user.organizations.add(organization); + data.id = parseInt(response.id); + var Auth = JSON.parse(window.sessionStorage.getItem('auth')); + if (Auth.user.organizations === null) { + Auth.user.organizations = []; + } + Auth.user.organizations.push(data); + window.sessionStorage.setItem('auth', JSON.stringify(Auth)); + app.navigate('#/organization/' + response.id, { + trigger: true, + replace: true + }); + } + }); + return false; + } +}); diff --git a/client/js/views/organization_board_view.js b/client/js/views/organization_board_view.js new file mode 100644 index 000000000..a43866066 --- /dev/null +++ b/client/js/views/organization_board_view.js @@ -0,0 +1,260 @@ +/** + * @fileOverview This file has functions related to organization board view. This view calling from organization view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * OrganizationBoard View + * @class OrganizationBoardView + * @constructor + * @extends Backbone.View + */ +App.OrganizationBoardView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.stared = options.stared; + if (!_.isUndefined(App.boards)) { + App.boards.bind('change', this.render); + } + this.render(); + }, + template: JST['templates/organization_board'], + className: 'col-lg-3 col-md-3 col-sm-4 col-xs-12 js-org-board', + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-close-popover': 'closePopup', + 'click .js-star-board': 'starBoard', + 'click .js-board-inner-view': 'showBoard', + 'click .js-board-visibility': 'showBoardVisibility', + 'click .js-set-privte-board': 'setPrivteBoard', + 'click .js-set-public-board': 'setPublicBoard', + 'click .js-show-board-organization': 'showBoardOrganization', + 'submit .js-save-board-visibility': 'saveBoardVisibility', + 'click .js-back-to-board-visibility': 'showBoardVisibility', + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + board: this.model, + stared: this.stared + })); + if (this.model !== null) { + var data = []; + var color_codes = ['#DB7093', '#F47564', '#EDA287', '#FAC1AD', '#FFE4E1', '#D3ABF0', '#DC9CDC', '#69BFBA', '#66CDAA', '#8FBC8F', '#CBFDCA', '#EEE8AA', '#BC8F8F', '#CD853F', '#D2B48C', '#F5DEB3', '#64BCF2', '#87CEFA', '#B0C4DE', '#D6E2F7']; + var i = 0; + _.each(this.model.attributes.lists, function(list) { + var _data = {}; + _data.title = list.name; + _data.value = list.card_count; + _data.color = color_codes[i]; + i++; + if (i > 20) { + i = 0; + } + if (list.card_count > 0) { + data.push(_data); + } + }); + var _this = this; + _(function() { + _this.$el.find('.js-chart').drawDoughnutChart(data); + }).defer(); + } + this.showTooltip(); + return this; + }, + /** + * starBoard() + * subcribe the board + * @param e + * @type Object(DOM event) + * @return false + * + */ + starBoard: function(e) { + e.preventDefault(); + var name = $(e.currentTarget).attr('name'); + var value = 'unstar'; + var content = ''; + if (name == 'unstar') { + value = 'star'; + content = ''; + } + $(e.currentTarget).attr('name', value); + $(e.currentTarget).html(content); + var boardStar = new App.BoardStar(); + if (!_.isEmpty(this.model.board_subscriber) && this.model.board_subscriber.attributes.id) { + value = ''; + if ($('#inputBoardSubscribe').val() == 'false') { + value = 'true'; + $('#inputBoardSubscribe').val(value); + } else { + value = 'false'; + $('#inputBoardSubscribe').val(value); + } + var data = $('form#BoardSubscribeForm').serializeObject(); + boardStar.url = api_url + 'boards/' + this.model.board.board_id + '/boards_stars/' + this.model.star.attributes.id + '.json'; + boardStar.set('id', this.model.star.attributes.id); + boardStar.save(data, { + success: function(model, response) {} + }); + } else { + var subscribe_data = {}; + var self = this; + boardStar.url = api_url + 'boards/' + this.model.id + '/boards_stars.json'; + boardStar.save(subscribe_data, { + success: function(model, response) { + self.model.board_subscribers.add(response); + } + }); + } + return false; + }, + /** + * showBoardVisibility() + * render the board visibility + * @param e + * @type Object(DOM event) + * + */ + showBoardVisibility: function(e) { + var target = $(e.currentTarget); + this.$('.js-back-to-board-visibility').addClass('hide'); + var visibility = this.model.attributes.board_visibility; + var insert = $('.js-visibility-list', target.next('.dropdown-menu')); + insert.nextAll().remove(); + $(new App.ShowBoardVisibilityView({ + model: visibility + }).el).insertAfter(insert); + }, + /** + * closePopup() + * close the opend dropdown + * @param e + * @type Object(DOM event) + * @return false + * + */ + closePopup: function(e) { + var el = this.$el; + var target = el.find(e.target); + target.parents('.dropdown').removeClass('open'); + return false; + }, + /** + * showBoard() + * render board view + * @param e + * @type Object(DOM event) + * + */ + showBoard: function(e) { + e.preventDefault(); + this.$el.removeClass('col-lg-3 col-md-3 col-sm-4').html(new App.BoardView({ + model: this.model + }).el); + }, + /** + * showChangeOrganizationForm() + * show board organiztion change form + * @param e + * @type Object(DOM event) + * + */ + showChangeOrganizationForm: function(e) { + var target = $(e.currentTarget); + var parent = target.parents('.dropdown-menu'); + var visibility = this.model.attributes.board_visibility; + var insert = $('.js-visibility-list', parent); + insert.nextAll().remove(); + $(new App.BoardOrganizationFormView({ + model: authuser.user.organizations, + board: this.model + }).el).insertAfter(insert); + }, + /** + * setPrivteBoard() + * change the board visibility as privte + * @param e + * @type Object(DOM event) + * @return false + * + */ + setPrivteBoard: function(e) { + e.preventDefault(); + this.model.url = api_url + 'boards/' + this.model.attributes.id + '.json'; + this.model.set({ + board_visibility: 0, + organization_id: 0 + }); + this.closePopup(e); + this.model.save({ + board_visibility: 0, + organization_id: 0 + }, { + patch: true + }); + var target = $(e.target); + target.parents('div.dropdown').removeClass('open'); + return false; + }, + /** + * setPublicBoard() + * change the board visibility as public + * @param e + * @type Object(DOM event) + * @return false + * + */ + setPublicBoard: function(e) { + e.preventDefault(); + this.model.url = api_url + 'boards/' + this.model.attributes.id + '.json'; + this.model.set({ + board_visibility: 2, + organization_id: 0 + }); + this.closePopup(e); + this.model.save({ + board_visibility: 2, + organization_id: 0 + }, { + patch: true + }); + var target = $(e.target); + target.parents('div.dropdown').removeClass('open'); + return false; + }, + /** + * showBoardOrganization() + * change the board visibility as organization + * @param e + * @type Object(DOM event) + * @return false + * + */ + showBoardOrganization: function(e) { + e.preventDefault(); + this.$('.js-back-to-board-visibility').removeClass('hide'); + this.showChangeOrganizationForm(e); + return false; + } +}); diff --git a/client/js/views/organization_delete_form_view.js b/client/js/views/organization_delete_form_view.js new file mode 100644 index 000000000..1f614c0e8 --- /dev/null +++ b/client/js/views/organization_delete_form_view.js @@ -0,0 +1,86 @@ +/** + * @fileOverview This file has functions related to organization delete form view. This view calling from organizations list view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : organization model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * OrganizationsUser View + * @class OrganizationsUserView + * @constructor + * @extends Backbone.View + */ +App.OrganizationDeleteFormView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/organization_delete_form'], + + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-close-popover': 'closePopup', + 'click .js-delete-organization': 'deleteOrganization', + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + organization: this.model + })); + this.showTooltip(); + return this; + }, + /** + * deleteOrganization() + * delete organization + * @param e + * @type Object(DOM event) + * @return false + */ + deleteOrganization: function(e) { + var self = this; + var target = $(e.currentTarget); + target.parents('li.dropdown').removeClass('open'); + this.model.url = api_url + 'organizations/' + this.model.id + '.json'; + this.model.set('id', this.model.id); + this.flash('success', 'Organization deleted successfully.'); + authuser.user.organizations.remove(this.model); + this.model.destroy({ + success: function(model, response) { + target.closest('tr').remove(); + } + }); + return false; + }, + /** + * closePopup() + * hide opened dropdown + * @param e + * @type Object(DOM event) + * @return false + */ + closePopup: function(e) { + var target = $(e.target); + target.parents('li.dropdown').removeClass('open'); + target.parents('div.btn-group').removeClass('open'); + return false; + } +}); diff --git a/client/js/views/organization_header_view.js b/client/js/views/organization_header_view.js new file mode 100644 index 000000000..af9127c82 --- /dev/null +++ b/client/js/views/organization_header_view.js @@ -0,0 +1,167 @@ +/** + * @fileOverview This file has functions related to organization header view. This view calling from application view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : organization model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Header View + * @class HeaderView + * @constructor + * @extends Backbone.View + */ +App.OrganizationHeaderView = Backbone.View.extend({ + template: JST['templates/organization_header'], + className: 'navbar navbar-default', + id: 'js-navbar-default', + attributes: { + role: 'navigation' + }, + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-close-popover': 'closePopup', + 'click #js-edit-organization': 'editOrganization', + 'click .js-delete-organization': 'deleteOrganization', + 'click .js-show-organization-visibility-form': 'showOrganizationVisibilityForm', + 'click .js-edit-organization-visibility-to-private': 'editOrganizationVisibilityToPrivate', + 'click .js-edit-organization-visibility-to-public': 'editOrganizationVisibilityToPublic', + }, + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.model.bind('change:name change:organization_visibility', this.render, this); + this.render(); + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + organization: this.model, + type: this.type + })); + this.showTooltip(); + return this; + }, + /** + * closePopup() + * close opened dropdown + * @param e + * @type Object(DOM event) + * @return false + * + */ + closePopup: function(e) { + var target = $(e.target); + target.parents('li.dropdown').removeClass('open'); + return false; + }, + /** + * editOrganization() + * update organization + * @return false + */ + editOrganization: function(e) { + e.preventDefault(); + var self = this.model; + var data = $('form#OrganizationEditForm').serializeObject(); + this.closePopup(e); + this.model.set(data); + this.model.url = api_url + 'organizations/' + this.model.organization_id + '.json'; + this.model.save(data, { + patch: true, + success: function(model, response) { + + } + }); + return false; + }, + /** + * deleteOrganization() + * delete organization + * @param e + * @type Object(DOM event) + * @return false + */ + deleteOrganization: function(e) { + e.preventDefault(); + var organization = new App.Organization(); + organization.url = api_url + 'organizations/' + this.model.organization_id + '.json'; + organization.set('id', this.model.organization_id); + organization.destroy(); + this.flash('success', 'Organization deleted successfully'); + app.navigate('#/organizations', { + trigger: true, + replace: true + }); + return false; + }, + /** + * showOrganizationVisibilityForm() + * display organization visibility + */ + showOrganizationVisibilityForm: function() { + $('.js-organization-visibility').html(new App.OrganizationVisibilityFormView({ + model: this.model + }).el); + }, + /** + * editOrganizationVisibilityToPublic() + * update organization visibility + * @param e + * @type Object(DOM event) + * @return false + */ + editOrganizationVisibilityToPublic: function(e) { + $('.js-org-visibility-icon').removeClass('icon-lock'); + $('.js-org-visibility-icon').addClass('icon-circle'); + $('.js-org-visibility-type').html('Public'); + var target = $(e.target); + target.parents('li.dropdown').removeClass('open'); + this.model.url = api_url + 'organizations/' + this.model.id + '.json'; + this.model.set('organization_visibility', 1); + this.model.save({ + organization_visibility: 1 + }, { + patch: true + }); + return false; + }, + /** + * editOrganizationVisibilityToPrivate() + * change organization visibility as private + * @param e + * @type Object(DOM event) + * @return false + */ + editOrganizationVisibilityToPrivate: function(e) { + $('.js-org-visibility-icon').removeClass('icon-circle'); + $('.js-org-visibility-icon').addClass('icon-lock'); + $('.js-org-visibility-type').html('Private'); + var target = $(e.target); + target.parents('li.dropdown').removeClass('open'); + this.model.url = api_url + 'organizations/' + this.model.organization_id + '.json'; + this.model.set('organization_visibility', 2); + this.model.save({ + organization_visibility: 2 + }, { + patch: true + }); + return false; + } +}); diff --git a/client/js/views/organization_member_confirm_remove_form_view.js b/client/js/views/organization_member_confirm_remove_form_view.js new file mode 100644 index 000000000..add54b31b --- /dev/null +++ b/client/js/views/organization_member_confirm_remove_form_view.js @@ -0,0 +1,43 @@ +/** + * @fileOverview This file has functions related to organization member confirm remove form view. This view calling from organization view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : organization model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * OrganizationMemberConfirmRemoveForm View + * @class OrganizationMemberConfirmRemoveFormView + * @constructor + * @extends Backbone.View + */ +App.OrganizationMemberConfirmRemoveFormView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/organization_member_confirm_remove_form'], + tagName: 'div', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + organization_users: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/organization_member_permission_form_view.js b/client/js/views/organization_member_permission_form_view.js new file mode 100644 index 000000000..3d2651dba --- /dev/null +++ b/client/js/views/organization_member_permission_form_view.js @@ -0,0 +1,43 @@ +/** + * @fileOverview This file has functions related to organization member permission form view. This view calling from organization user view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : organizations user model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * OrganizationMemberPermissionForm View + * @class OrganizationMemberPermissionFormView + * @constructor + * @extends Backbone.View + */ +App.OrganizationMemberPermissionFormView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/organization_member_permission_form'], + tagName: 'div', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.el = this.template({ + organization_user: this.model + }); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/organization_member_remove_form_view.js b/client/js/views/organization_member_remove_form_view.js new file mode 100644 index 000000000..96612b218 --- /dev/null +++ b/client/js/views/organization_member_remove_form_view.js @@ -0,0 +1,44 @@ +/** + * @fileOverview This file has functions related to organization member permission form view. This view calling from organization user view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : organizations user model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * OrganizationMemberRemoveForm View + * @class OrganizationMemberRemoveFormView + * @constructor + * @extends Backbone.View + */ +App.OrganizationMemberRemoveFormView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/organization_member_remove_form'], + tagName: 'li', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.el = this.template({ + organization_users: this.model + }); + this.delegateEvents(this.events); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/organization_view.js b/client/js/views/organization_view.js new file mode 100644 index 000000000..2e1be58fb --- /dev/null +++ b/client/js/views/organization_view.js @@ -0,0 +1,351 @@ +/** + * @fileOverview This file has functions related to organization view. This view calling from footer view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : organizations model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Organizations View + * @class OrganizationsView + * @constructor + * @extends Backbone.View + */ +App.OrganizationsView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + this.type = options.type; + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.model.organizations_users.add(this.model.attributes.organizations_users); + this.render(); + if (this.type === 'users') { + this.getOrganizationMemberLists(); + } + this.model.boards.bind('change:organization_id remove', this.renderOrganizationCollection, this); + + }, + template: JST['templates/organization_view'], + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + + 'click .js-close-popover': 'closePopup', + 'click .js-get-organization-member-lists': 'getOrganizationMemberLists', + 'click .js-edit-organization-member-permission-to-admin': 'editOrganizationMemberPermissionToAdmin', + 'click .js-edit-organization-member-permission-to-normal': 'editOrganizationMemberPermissionToNormal', + 'click .js-trigger-logo-upload': 'triggerLogoUpload', + 'change .js-edit-organization-logo': 'editOrganizationLogo', + 'click .js-show-confirm-delete-organization-member': 'showConfirmDeleteOrganizationMember', + 'click .js-show-confirm-delete-organization-current-member': 'showConfirmDeleteOrganizationCurrentMember', + 'click .js-user-board-list': 'userBoardList', + 'click .js-remove-image': 'removeImage', + 'click .js-all-user-activities': 'showUserActivities', + }, + /** + * editOrganizationLogo() + * update organization logo + * @param e + * @type Object(DOM event) + * @return false + */ + editOrganizationLogo: function(ev) { + var self = this; + var form = $('form.attachment-form-logo'); + var fileData = $('form.attachment-form-logo').serializeObject(); + var organization = new App.Organization(); + organization.url = api_url + 'organizations/' + self.model.organization_id + '/upload_logo.json'; + organization.save({}, { + data: {}, + files: $('input.js-edit-organization-logo', form), + iframe: true + }, { + success: function(model, response) { + model.set('logo_url', response.logo_url); + self.render(); + if (self.type === 'users') { + self.getOrganizationMemberLists(); + } + $('#js-organization-logo-9').attr('href', response.logo_url); + }, + error: function(model, response) {} + }); + return false; + }, + /** + * triggerLogoUpload() + * trigger organization logo upload + * @param e + * @type Object(DOM event) + * @return false + */ + triggerLogoUpload: function(e) { + $('.js-edit-organization-logo').trigger('click'); + return false; + }, + /** + * showConfirmDeleteOrganizationMember() + * display organization delete confirmation + * @param e + * @type Object(DOM event) + */ + showConfirmDeleteOrganizationMember: function(e) { + var target = $(e.currentTarget); + var parent = target.parents('.js-show-confirm-delete-organization-member-dropdown'); + var insert = $('.js-show-confirm-delete-organization-member-response', parent); + insert.nextAll().remove(); + var organizations_user_id = target.data('organizations_user_id'); + this.model.organizations_user_id = organizations_user_id; + $(new App.OrganizationMemberRemoveFormView({ + model: this.model + }).el).insertAfter(insert); + }, + /** + * showConfirmDeleteOrganizationCurrentMember() + * display organization member remove confirmation + * @param e + * @type Object(DOM event) + */ + showConfirmDeleteOrganizationCurrentMember: function(ev) { + var target = $(ev.currentTarget); + var organizations_user_id = target.data('organizations_user_id'); + this.model.organizations_user_id = organizations_user_id; + $('.js-show-confirm-delete-organization-member-response').html(new App.OrganizationMemberConfirmRemoveFormView({ + model: this.model + }).el); + }, + /** + * editOrganizationMemberPermissionToNormal() + * change organization member permission to normal + * @param e + * @type Object(DOM event) + * @return false + */ + editOrganizationMemberPermissionToNormal: function(e) { + var self = this; + var target = $(e.currentTarget); + var organizations_user_id = target.data('organizations_user_id'); + $('.js-change-permission-content-' + organizations_user_id).html('Normal'); + target.parents('li.dropdown').removeClass('open'); + var organizationsUser = new App.OrganizationsUser(); + organizationsUser.url = api_url + 'organizations_users/' + organizations_user_id + '.json'; + organizationsUser.set('id', organizations_user_id); + organizationsUser.set('is_admin', false); + this.model.organizations_users.get(parseInt(organizations_user_id)).set('is_admin', false); + self.getOrganizationMemberLists(); + organizationsUser.save({ + is_admin: false + }); + return false; + }, + /** + * editOrganizationMemberPermissionToAdmin() + * change organization member permission to admin + * @param e + * @type Object(DOM event) + * @return false + */ + editOrganizationMemberPermissionToAdmin: function(e) { + var self = this; + var target = $(e.currentTarget); + var organizations_user_id = target.data('organizations_user_id'); + $('.js-change-permission-content-' + organizations_user_id).html('Admin'); + target.parents('li.dropdown').removeClass('open'); + var organizationsUser = new App.OrganizationsUser(); + organizationsUser.url = api_url + 'organizations_users/' + organizations_user_id + '.json'; + organizationsUser.set('id', organizations_user_id); + organizationsUser.set('is_admin', true); + this.model.organizations_users.get(parseInt(organizations_user_id)).set('is_admin', true); + self.getOrganizationMemberLists(); + organizationsUser.save({ + is_admin: true + }); + + return false; + }, + + /** + * getOrganizationMemberLists() + * display organization members list + */ + getOrganizationMemberLists: function() { + var organizationsUserView = new App.OrganizationsUserView({ + model: this.model, + }); + this.$('#member').html(organizationsUserView.el); + this.$('#member').find('[data-toggle="tooltip"]').tooltip(); + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + organization: this.model, + type: this.type + })); + this.renderOrganizationCollection(); + + var _this = this; + _(function() { + Backbone.TemplateManager.baseUrl = '{name}'; + var uploadManager = new Backbone.UploadManager({ + uploadUrl: api_url + 'organizations/' + _this.model.organization_id + '/upload_logo.json?token=' + api_token, + autoUpload: true, + dropZone: $('#dropzone'), + singleFileUploads: true, + formData: $('form.js-user-profile-edit').serialize(), + fileUploadHTML: '', + maxNumberOfFiles: 1, + acceptFileTypes: /(\.|\/)(jpe?g|png)$/i, + }); + uploadManager.on('fileadd', function(file) { + $('#org-loader').addClass('cssloader'); + }); + uploadManager.on('fileuploaddragover', function(e) { + $('#js-org-drag').addClass('drophover'); + }); + var dragging = 0; + $('#dropzone').on('dragenter', function(e) { + dragging++; + $('#js-org-drag').addClass('drophover'); + }); + $('#dropzone').on('dragleave', function(e) { + dragging--; + if (dragging === 0 || !$.browser.chrome) { + $('#js-org-drag').removeClass('drophover'); + } + }); + uploadManager.on('fileuploaddrop', function(e) { + dragging--; + $('#js-org-drag').removeClass('drophover'); + }); + uploadManager.on('filedone', function(file, data) { + $('#org-loader').removeClass('cssloader'); + if (!_.isUndefined(data.result.logo_url)) { + _this.model.set('logo_url', data.result.logo_url); + _this.render(); + if (_this.type === 'users') { + _this.getOrganizationMemberLists(); + } + } + }); + uploadManager.renderTo($('#manager-area')); + }).defer(); + this.showTooltip(); + return this; + }, + /** + * renderOrganizationCollection() + * populate the html to the dom + * @param NULL + * @return object + * + */ + renderOrganizationCollection: function() { + var self = this; + var view_board = this.$el.find('#js-organization-board-listing'); + view_board.html(''); + if (!_.isEmpty(this.model.boards.models)) { + this.model.boards.each(function(board) { + board.board_stars.add(board.attributes.boards_stars); + self.model.board_users.add(board.attributes.boards_users); + var stared; + if (!_.isUndefined(authuser.user)) { + stared = board.board_stars.findWhere({ + user_id: parseInt(authuser.user.id) + }); + } + var view = new App.OrganizationBoardView({ + model: board, + stared: stared + }); + view_board.append(view.el); + }); + } else { + var view = new App.OrganizationBoardView({ + model: null, + stared: null, + className: 'alert alert-info', + }); + view_board.append(view.render().el); + } + }, + /** + * editOrganization() + * hide opened dropdown + * @param e + * @type Object(DOM event) + * @return false + */ + closePopup: function(e) { + var target = $(e.target); + target.parents('li.dropdown').removeClass('open'); + target.parents('div.btn-group').removeClass('open'); + return false; + }, + /** + * userBoardList() + * display organizationboard list + * @param e + * @type Object(DOM event) + */ + userBoardList: function(e) { + e.preventDefault(); + var user_id = $(e.currentTarget).data('user-id'); + var user_name = $(e.currentTarget).data('user-name'); + var user_boards = this.model.board_users.where({ + user_id: parseInt(user_id) + }); + var board_users = new App.BoardsUserCollection(); + board_users.add(user_boards); + board_users.username = user_name; + $(e.currentTarget).parents('.js-user-board-list-response').html(new App.UserBoardslistingMenuView({ + model: board_users + }).el); + return false; + }, + showUserActivities: function(e) { + e.preventDefault(); + var user_id = $(e.target).data('user_id'); + var modalView = new App.ModalActivityView({ + model: user_id, + organization_id: this.model.id, + type: 'org_user_listing' + }); + modalView.show(); + return false; + }, + /** + * removeImage() + * remive image + * @param e + * @type Object(DOM event) + */ + removeImage: function(e) { + e.preventDefault(); + this.model.set('logo_url', null); + this.render(); + if (this.type === 'users') { + this.getOrganizationMemberLists(); + } + this.model.url = api_url + 'organizations/' + this.model.id + '.json'; + this.model.save({ + logo_url: 'NULL' + }, { + patch: true + }); + return false; + } +}); diff --git a/client/js/views/organization_visibility_form_view.js b/client/js/views/organization_visibility_form_view.js new file mode 100644 index 000000000..6d4c74588 --- /dev/null +++ b/client/js/views/organization_visibility_form_view.js @@ -0,0 +1,44 @@ +/** + * @fileOverview This file has functions related to organization visibility form view. This view calling from organization header view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : organizations model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * OrganizationVisibilityForm View + * @class OrganizationVisibilityFormView + * @constructor + * @extends Backbone.View + */ +App.OrganizationVisibilityFormView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/organization_visibility_form'], + tagName: 'ul', + className: 'list-unstyled', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.el = this.template({ + organization: this.model + }); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/organizations_board_form_view.js b/client/js/views/organizations_board_form_view.js new file mode 100644 index 000000000..325277ac9 --- /dev/null +++ b/client/js/views/organizations_board_form_view.js @@ -0,0 +1,48 @@ +/** + * @fileOverview This file has functions related to organization board form view. This view calling from footer view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : undefined. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * OrganizationsBoardForm View + * @class OrganizationsBoardFormView + * @constructor + * @extends Backbone.View + */ +App.OrganizationsBoardFormView = Backbone.View.extend({ + tagName: 'ul', + className: 'list-unstyled', + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: {}, + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/organizations_board_form_view'], + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.el = this.template({}); + this.delegateEvents(this.events); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/organizations_list_view.js b/client/js/views/organizations_list_view.js new file mode 100644 index 000000000..1c9d9192d --- /dev/null +++ b/client/js/views/organizations_list_view.js @@ -0,0 +1,103 @@ +/** + * @fileOverview This file has functions related to organizations list view. This view calling from organization lis view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : organization model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * OrganizationsUser View + * @class OrganizationsUserView + * @constructor + * @extends Backbone.View + */ +App.OrganizationsListView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/organizations_list_view'], + tagName: 'tr', + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-show-confirm-delete-organization': 'showConfirmDeleteOrganization', + 'click .js-delete-organization': 'deleteOrganization', + 'click .js-no-action': 'noAction', + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + organization: this.model + })); + this.showTooltip(); + return this; + }, + /** + * showConfirmDeleteOrganization() + * display organization delete confirmation + * @param e + * @type Object(DOM event) + */ + showConfirmDeleteOrganization: function(e) { + var target = $(e.currentTarget); + var organization_id = target.data('organization_id'); + $('.js-show-confirm-delete-organization-response').html(new App.OrganizationDeleteFormView({ + model: this.model + }).el); + }, + /** + * deleteOrganization() + * delete organization + * @param e + * @type Object(DOM event) + * @return false + */ + deleteOrganization: function(e) { + e.preventDefault(); + var organization = new App.Organization(); + organization.url = api_url + 'organizations/' + this.model.organization_id + '.json'; + organization.set('id', this.model.organization_id); + this.flash('success', 'Organization deleted successfully.'); + organization.destroy({ + success: function(model, response) { + app.navigate('#/', { + trigger: true, + replace: true + }); + } + }); + return false; + }, + /** + * getOrganizationMemberLists() + * display organization members list + */ + getOrganizationMemberLists: function() { + this.model.organizations_users.add(this.model.attributes.organizations_users); + }, + /** + * noAction() + * no action + */ + noAction: function(e) { + e.preventDefault(); + return false; + } +}); diff --git a/client/js/views/organizations_lists_header_view.js b/client/js/views/organizations_lists_header_view.js new file mode 100644 index 000000000..9c1bc801b --- /dev/null +++ b/client/js/views/organizations_lists_header_view.js @@ -0,0 +1,78 @@ +/** + * @fileOverview This file has functions related to organizations list header view. This view calling from application view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : organizations collection. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Header View + * @class HeaderView + * @constructor + * @extends Backbone.View + */ +App.OrganizationsListsHeaderView = Backbone.View.extend({ + className: '', + template: JST['templates/organizations_lists_header'], + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-sort-by': 'sortBy' + }, + + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + this.sortField = 'id'; + this.sortDirection = 'desc'; + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + user: this.model, + type: this.type + })); + this.renderList(); + this.showTooltip(); + return this; + }, + renderList: function() { + $('#content').html(new App.OrganizationsListsView({ + model: this.model, + sortField: this.sortField, + sortDirection: this.sortDirection + }).el); + $('.timeago', $('#content')).timeago(); + }, + sortBy: function(e) { + e.preventDefault(); + var sortField = $(e.currentTarget).data('field'); + if (_.isUndefined(this.sortDirection)) { + this.sortDirection = 'desc'; + } else { + if (this.sortDirection === 'desc') { + this.sortDirection = 'asc'; + } else { + this.sortDirection = 'desc'; + } + } + this.sortField = sortField; + this.renderList(); + } +}); diff --git a/client/js/views/organizations_lists_view.js b/client/js/views/organizations_lists_view.js new file mode 100644 index 000000000..ebe6a54a5 --- /dev/null +++ b/client/js/views/organizations_lists_view.js @@ -0,0 +1,73 @@ +/** + * @fileOverview This file has functions related to organizations lists header view. This view calling from application view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : organizations collection. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * OrganizationsUser View + * @class OrganizationsUserView + * @constructor + * @extends Backbone.View + */ +App.OrganizationsListsView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + this.sortField = options.sortField; + this.sortDirection = options.sortDirection; + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/organizations_lists_view'], + tagName: 'section', + className: 'row', + id: 'js-organizations_lists', + attributes: { + role: 'navigation' + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template()); + this.renderOrganizationCollection(); + this.showTooltip(); + return this; + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + renderOrganizationCollection: function() { + var view = this.$el.find('#js-organizations-list'); + if (!_.isEmpty(this.model.models)) { + this.model.setSortField(this.sortField, this.sortDirection); + this.model.sort(); + this.model.each(function(organization) { + view.append(new App.OrganizationsListView({ + model: organization + }).el); + }); + } else { + view.html(new App.OrganizationsListView({ + model: null + }).el); + } + return this; + } +}); diff --git a/client/js/views/organizations_user_view.js b/client/js/views/organizations_user_view.js new file mode 100644 index 000000000..3612dff90 --- /dev/null +++ b/client/js/views/organizations_user_view.js @@ -0,0 +1,183 @@ +/** + * @fileOverview This file has functions related to organizations user view. This view calling from organization view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : organization model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * OrganizationsUser View + * @class OrganizationsUserView + * @constructor + * @extends Backbone.View + */ +App.OrganizationsUserView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.model.organizations_users.bind('change:id add', this.render, this); + this.render(); + }, + template: JST['templates/organizations_user_view'], + tagName: 'article', + id: 'organizations_user_view', + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + + 'keyup .js-organization-users-search': 'organizationUsersSearch', + 'click .js-add-organization-member': 'addOrganizationMember', + 'click .js-delete-organization-member': 'deleteOrganizationMember', + 'click .js-show-organization-member-permission-form': 'showOrganizationMemberPermissionForm', + 'click .js-no-action': 'noAction', + 'keypress input[type=text]': 'onEnter', + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.model.organizations_users.showImage = this.showImage; + this.$el.html(this.template({ + organizations_users: this.model.organizations_users, + })); + this.showTooltip(); + return this; + }, + /** + * organizationUsersSearch() + * display search user list + * @param e + * @type Object(DOM event) + * @return false + */ + organizationUsersSearch: function(e) { + var self = this; + var q = $(e.target).val(); + if (q !== '') { + var users = new App.UserCollection(); + users.url = api_url + 'users/search.json'; + users.fetch({ + data: { + organizations: this.model.id, + q: q + }, + success: function() { + self.$('.js-organization-member-search-response').nextAll().remove(); + _.each(users.models, function(user) { + var org_user = self.model.organizations_users.findWhere({ + organization_id: self.model.id, + user_id: user.id + }); + if (_.isUndefined(org_user)) { + $(new App.UserSearchResultView({ + model: user + }).el).insertAfter(self.$el.find('.js-organization-member-search-response')); + } + }); + if (users.length === 0) { + $(new App.UserSearchResultView({ + model: null + }).el).insertAfter(self.$el.find('.js-organization-member-search-response')); + } + } + }); + } + }, + /** + * addOrganizationMember() + * add member in organization + * @param e + * @type Object(DOM event) + * @return false + */ + addOrganizationMember: function(e) { + e.preventDefault(); + var self = this; + var organizations_user = new App.OrganizationsUser(); + var user_id = $(e.currentTarget).data('user-id'); + $(e.currentTarget).remove(); + organizations_user.url = api_url + 'organizations/' + self.model.id + '/users/' + user_id + '.json'; + //this.model.organizations_users.add(organizations_user); + organizations_user.save({ + organization_id: self.model.id, + user_id: user_id, + is_admin: 'FALSE' + }, { + success: function(model, response) { + organizations_user.set(response.organizations_users); + organizations_user.set('organization_id', self.model.id); + organizations_user.set('user_id', parseInt(user_id)); + organizations_user.set('is_admin', false); + organizations_user.set('id', parseInt(response.id)); + self.model.organizations_users.add(organizations_user); + } + }); + return false; + }, + /** + * deleteOrganizationMember() + * remove organization member + * @param e + * @type Object(DOM event) + * @return false + */ + deleteOrganizationMember: function(ev) { + var self = this; + var target = $(ev.currentTarget); + var organizations_user_id = target.data('organizations_user_id'); + target.parents('li.dropdown').removeClass('open'); + self.model.organizations_users.remove(self.model.organizations_users.get(parseInt(organizations_user_id))); + self.flash('success', 'User removed from this organization'); + self.render(); + var organizationsUser = new App.OrganizationsUser(); + organizationsUser.url = api_url + 'organizations_users/' + organizations_user_id + '.json'; + organizationsUser.set('id', organizations_user_id); + organizationsUser.destroy(); + return false; + }, + /** + * showOrganizationMemberPermissionForm() + * display organization member permission form + * @param e + * @type Object(DOM event) + */ + showOrganizationMemberPermissionForm: function(e) { + var self = this; + var target = $(e.currentTarget); + var organizations_user_id = target.data('organizations_user_id'); + var organizationsUser = this.model.organizations_users.findWhere({ + id: parseInt(organizations_user_id) + }); + organizationsUser.organizations_user_id = organizations_user_id; + $('.js-show-organization-member-permission-form-response').html(new App.OrganizationMemberPermissionFormView({ + model: organizationsUser + }).el); + }, + noAction: function(e) { + e.preventDefault(); + return false; + }, + onEnter: function(e) { + if (e.which === 13) { + var form = $(e.target).closest('form'); + if (form.attr('name') === 'cardAddForm') { + $('input[type=submit]', form).trigger('click'); + } else { + return false; + } + } + } +}); diff --git a/client/js/views/register_view.js b/client/js/views/register_view.js new file mode 100644 index 000000000..b4017931d --- /dev/null +++ b/client/js/views/register_view.js @@ -0,0 +1,74 @@ +/** + * @fileOverview This file has functions related to register view. This view calling from application view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : user model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Register View + * @class RegisterView + * @constructor + * @extends Backbone.View + */ +App.RegisterView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/users_register'], + tagName: 'article', + className: 'clearfix', + id: 'user-register', + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'submit form#UserRegisterForm': 'register' + }, + /** + * register() + * save user + * @return false + */ + register: function(e) { + var target = $(e.target); + var data = target.serializeObject(); + var self = this; + var user = new App.User(); + user.url = api_url + 'users/register.json'; + user.save(data, { + success: function(model, response) { + if (!_.isEmpty(response.error)) { + self.flash('danger', response.error); + $('#inputPassword').val(''); + } else { + self.flash('success', 'You have successfully registered with our site and your activation mail has been sent to your mail inbox.'); + target[0].reset(); + } + } + }); + return false; + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template()); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/role_index_view.js b/client/js/views/role_index_view.js new file mode 100644 index 000000000..db6830849 --- /dev/null +++ b/client/js/views/role_index_view.js @@ -0,0 +1,48 @@ +/** + * @fileOverview This file has functions related to role index view. This view calling from application view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * RoleIndex View + * @class RoleIndexView + * @constructor + * @extends Backbone.View + */ +App.RoleIndexView = Backbone.View.extend({ + template: JST['templates/roles'], + tagName: 'tr', + className: '', + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: {}, + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + role: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/role_settings_view.js b/client/js/views/role_settings_view.js new file mode 100644 index 000000000..dbb2d684b --- /dev/null +++ b/client/js/views/role_settings_view.js @@ -0,0 +1,72 @@ +/** + * @fileOverview This file has functions related to role settings view. This view calling from application view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : acl links collection. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * RoleSettings View + * @class RoleSettingsView + * @constructor + * @extends Backbone.View + */ +App.RoleSettingsView = Backbone.View.extend({ + template: JST['templates/role_settings'], + tagName: 'section', + className: 'clearfix row', + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-update-role': 'saveRoleSettings' + }, + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.roles = options.roles; + this.render(); + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + acl_links: this.model, + roles: this.roles + })); + $('.js-admin-role-menu').addClass('active'); + $('.js-admin-activity-menu, .js-admin-user-menu, .js-admin-email-menu, .js-admin-setting-menu').removeClass('active'); + this.showTooltip(); + return this; + }, + /** + * saveRoleSettings() + * save role alloewd acl links + * @param e + * @type Object(DOM event) + */ + saveRoleSettings: function(e) { + var self = $(e.target); + var data = { + 'acl_link_id': self.data('acl_link_id'), + 'role_id': self.data('role_id') + }; + var role_setting = new App.RoleSetting(); + role_setting.set(data); + role_setting.url = api_url + 'acl_links.json'; + role_setting.save(data); + } +}); diff --git a/client/js/views/search_board_subscribe_view.js b/client/js/views/search_board_subscribe_view.js new file mode 100644 index 000000000..69112dd86 --- /dev/null +++ b/client/js/views/search_board_subscribe_view.js @@ -0,0 +1,39 @@ +/** + * @fileOverview This file has functions related to show search board subscribe view. This view calling from footer view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * SearchResult View + * @class SearchResultView + * @constructor + * @extends Backbone.View + */ +App.ShowSearchBoardSubscribeView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + //template: JST['templates/show_search_board_subscribe'], + tagName: 'span', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/search_result_view.js b/client/js/views/search_result_view.js new file mode 100644 index 000000000..8f1707501 --- /dev/null +++ b/client/js/views/search_result_view.js @@ -0,0 +1,44 @@ +/** + * @fileOverview This file has functions related to search result view. This view calling from footer view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : JSON Object. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * SearchResult View + * @class SearchResultView + * @constructor + * @extends Backbone.View + */ +App.SearchResultView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/search_result'], + tagName: 'ul', + className: 'unstyled', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + hits: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/select_board_visibility_view.js b/client/js/views/select_board_visibility_view.js new file mode 100644 index 000000000..0d5b1e01a --- /dev/null +++ b/client/js/views/select_board_visibility_view.js @@ -0,0 +1,43 @@ +/** + * @fileOverview This file has functions related to select board visibility view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ArchivedCard View + * @class ArchivedCardView + * @constructor + * @extends Backbone.View + */ +App.SelectBoardVisibilityView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.data = options.data; + this.render(); + }, + template: JST['templates/select_board_visibility'], + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + name: this.model, + data: this.data + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/selected_board_visibility_view.js b/client/js/views/selected_board_visibility_view.js new file mode 100644 index 000000000..dcc146210 --- /dev/null +++ b/client/js/views/selected_board_visibility_view.js @@ -0,0 +1,44 @@ +/** + * @fileOverview This file has functions related to select board visibility view. This view calling from board add view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board name. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * UserIndexContainer View + * @class UserIndexContainerView + * @constructor + * @extends Backbone.View + */ +App.SelectedBoardVisibilityView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/selected_board_visibility'], + tag: 'span', + className: 'js-visibility-container', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + name: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/setting_view.js b/client/js/views/setting_view.js new file mode 100644 index 000000000..2b48ca557 --- /dev/null +++ b/client/js/views/setting_view.js @@ -0,0 +1,94 @@ +/** + * @fileOverview This file has functions related to setting view. This view calling from application view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : page view id. + */ +if (typeof App == 'undefined') { + App = {}; +} +App.SettingView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.id = options.id; + this.getListing(); + }, + template: JST['templates/setting_list'], + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'submit form#js-setting-list-form': 'updateSetting', + }, + /** + * updateSetting() + * @return false + */ + updateSetting: function(e) { + var target = $(e.currentTarget); + var data = target.serializeObject(); + if (!_.isUndefined(data.LDAP_LOGIN_ENABLED) && $('.js-checkbox').is(":checked")) { + data.LDAP_LOGIN_ENABLED = 'true'; + } else { + if (parseInt(this.id) === 2) { + data.LDAP_LOGIN_ENABLED = 'false'; + } + } + var self = this; + var settingModel = new App.SettingCategory(); + settingModel.url = api_url + 'settings.json'; + settingModel.save(data, { + success: function(model, response) { + if (!_.isEmpty(response.success)) { + self.flash('success', response.success); + } else { + self.flash('danger', 'Settings not updated properly.'); + } + } + }); + return false; + }, + /** + * getListing() + * get settings + * @return false + */ + getListing: function() { + self = this; + if (_.isUndefined(this.id)) { + this.id = 3; + } + settingsCol = new App.SettingCategoryCollection(); + settingsCol.url = api_url + 'settings/' + this.id + '.json'; + settingsCol.fetch({ + cache: false, + abortPending: true, + success: function(collections, response) { + self.render(collections); + } + }); + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function(collections) { + this.$el.html(this.template({ + list: collections, + id: this.id + })); + $('.js-admin-setting-menu').addClass('active'); + $('.js-admin-activity-menu, .js-admin-user-menu, .js-admin-email-menu, .js-admin-role-menu').removeClass('active'); + return this; + } +}); diff --git a/client/js/views/show_all_visibility_view.js b/client/js/views/show_all_visibility_view.js new file mode 100644 index 000000000..eca2096de --- /dev/null +++ b/client/js/views/show_all_visibility_view.js @@ -0,0 +1,45 @@ +/** + * @fileOverview This file has functions related to show all visibility view. This view calling from board add view, board header view and board view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : visibility(String). + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ShowAllVisibility View + * @class ShowAllVisibilityView + * @constructor + * @extends Backbone.View + */ +App.ShowAllVisibilityView = Backbone.View.extend({ + tagName: 'ul', + className: 'list-unstyled', + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/show_all_visibility'], + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.el = this.template({ + visibility: this.model + }); + this.delegateEvents(this.events); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/show_board_member_permission_form_view.js b/client/js/views/show_board_member_permission_form_view.js new file mode 100644 index 000000000..d4ed94e49 --- /dev/null +++ b/client/js/views/show_board_member_permission_form_view.js @@ -0,0 +1,43 @@ +/** + * @fileOverview This file has functions related to show board member permission form view. This view calling from board view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board user id. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * showBoardMemberPermissionForm View + * @class showBoardMemberPermissionFormView + * @constructor + * @extends Backbone.View + */ +App.showBoardMemberPermissionFormView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/show_board_member_permission_form'], + tagName: 'div', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + board_user_id: this.model, + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/show_board_visibility_view.js b/client/js/views/show_board_visibility_view.js new file mode 100644 index 000000000..55c8ff002 --- /dev/null +++ b/client/js/views/show_board_visibility_view.js @@ -0,0 +1,66 @@ +/** + * @fileOverview This file has functions related to show board visibilit view. This view calling from board header view, board simple view and organization board view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ShowBoardVisibility View + * @class ShowBoardVisibilityView + * @constructor + * @extends Backbone.View + */ +App.ShowBoardVisibilityView = Backbone.View.extend({ + tagName: 'ul', + className: 'list-unstyled', + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/show_board_visibility'], + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-close-popover': 'closePopup' + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.el = this.template({ + visibility: this.model + }); + this.delegateEvents(this.events); + this.showTooltip(); + return this; + }, + /** + * boardRename() + * close the dropdown + * @param e + * @type Object(DOM event) + * @return false + * + */ + closePopup: function(e) { + var el = this.$el; + var target = el.find(e.target); + target.parents('div.dropdown:first, li.dropdown:first').removeClass('open'); + return false; + } +}); diff --git a/client/js/views/show_boards_list_view.js b/client/js/views/show_boards_list_view.js new file mode 100644 index 000000000..fff139633 --- /dev/null +++ b/client/js/views/show_boards_list_view.js @@ -0,0 +1,40 @@ +/** + * @fileOverview This file has functions related to show board list view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ShowAllVisibility View + * @class ShowAllVisibilityView + * @constructor + * @extends Backbone.View + */ +App.ShowBoardsListView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/show_boards_list'], + tagName: 'div', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({})); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/show_copy_board_view.js b/client/js/views/show_copy_board_view.js new file mode 100644 index 000000000..3d5e2733c --- /dev/null +++ b/client/js/views/show_copy_board_view.js @@ -0,0 +1,76 @@ +/** + * @fileOverview This file has functions related to show all visibility view. This view calling from board add view, board header view and board view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : visibility(String). + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ShowCopyBoard View + * @class ShowCopyBoardView + * @constructor + * @extends Backbone.View + */ +App.ShowCopyBoardView = Backbone.View.extend({ + tagName: 'li', + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/show_copy_board'], + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'submit #BoardCopyForm': 'copyNewBoard' + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + board: this.model, + })); + $('.js-filter-parent').addClass('open'); + this.showTooltip(); + return this; + }, + /** + * copyBoard() + * copy the exisiting board + * @param e + * @type Object(DOM event) + * @return false + * + */ + copyNewBoard: function(e) { + e.preventDefault(); + $('#submitBoardCopy', $(e.target)).attr('disabled', true); + var data = $(e.target).serializeObject(); + data.user_id = authuser.user.id; + var board = new App.Board(); + board.url = api_url + 'boards/' + this.model.id + '/copy.json'; + board.save(data, { + success: function(model, response) { + app.navigate('#/board/' + board.get('id'), { + trigger: true, + replace: true, + }); + } + }); + return false; + }, +}); diff --git a/client/js/views/show_search_boards_view.js b/client/js/views/show_search_boards_view.js new file mode 100644 index 000000000..109faf571 --- /dev/null +++ b/client/js/views/show_search_boards_view.js @@ -0,0 +1,46 @@ +/** + * @fileOverview This file has functions related to show search boards view. This view calling from footer view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * SearchResult View + * @class SearchResultView + * @constructor + * @extends Backbone.View + */ +App.ShowSearchBoardsView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.style = options.style; + this.render(); + }, + template: JST['templates/show_search_boards'], + tagName: 'li', + className: '', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + board: this.model, + style: this.style + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/show_search_message_view.js b/client/js/views/show_search_message_view.js new file mode 100644 index 000000000..7cb6ffa0b --- /dev/null +++ b/client/js/views/show_search_message_view.js @@ -0,0 +1,44 @@ +/** + * @fileOverview This file has functions related to show search message view. This view calling from footer view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board model. It contain all board based object @see Available Object in App.BoardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ShowSearchMessage View + * @class ShowSearchMessageView + * @constructor + * @extends Backbone.View + */ +App.ShowSearchMessageView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/show_search_message'], + tagName: 'p', + className: 'search-tips-helper quiet js-search-tips-helper', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + board: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/show_sync_google_calendar_view.js b/client/js/views/show_sync_google_calendar_view.js new file mode 100644 index 000000000..c2b3ae5db --- /dev/null +++ b/client/js/views/show_sync_google_calendar_view.js @@ -0,0 +1,44 @@ +/** + * @fileOverview This file has functions related to show all visibility view. This view calling from board add view, board header view and board view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : visibility(String). + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * ShowSyncGoogleCalendar View + * @class ShowSyncGoogleCalendarView + * @constructor + * @extends Backbone.View + */ +App.ShowSyncGoogleCalendarView = Backbone.View.extend({ + tagName: 'li', + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/show_sync_google_calendar'], + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + url: this.model.attributes.google_syn_url, + })); + this.$el.find('input.js-syn-calendar-response').val(this.model.attributes.google_syn_url); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/starred_boards_index_view.js b/client/js/views/starred_boards_index_view.js new file mode 100644 index 000000000..66596b318 --- /dev/null +++ b/client/js/views/starred_boards_index_view.js @@ -0,0 +1,74 @@ +/** + * @fileOverview This file has functions related to starred boards index view. This view calling from footer view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : undefined + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * BoardsIndex View + * @class BoardsIndexView + * @constructor + * @extends Backbone.View + */ +App.StarredBoardsIndexView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/starred_boards_index'], + tagName: 'section', + className: 'clearfix', + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-delete-board': 'deleteBoard', + }, + /** + * deleteBoard() + * delete board + * @param e + * @type Object(DOM event) + * @return false + * + */ + deleteBoard: function(e) { + if (confirm($(e.currentTarget).data('confirm'))) { + var dataUrl = $(e.currentTarget).attr('href'); + + var board = new App.Board(); + board.url = api_url + dataUrl + '.json'; + board.set('id', '-1'); + board.destroy({ + success: function() { + Backbone.history.loadUrl(); + } + }); + } + return false; + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + 'board': this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/started_boards_listing_view.js b/client/js/views/started_boards_listing_view.js new file mode 100644 index 000000000..a0383bbc8 --- /dev/null +++ b/client/js/views/started_boards_listing_view.js @@ -0,0 +1,45 @@ +/** + * @fileOverview This file has functions related to starred boards listing view. This view calling from footer view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * StartedBoardsListing View + * @class StartedBoardsListingView + * @constructor + * @extends Backbone.View + */ +App.StartedBoardsListingView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.user = options.user; + this.render(); + }, + template: JST['templates/started_boards_listing'], + tagName: 'li', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + board: this.model, + user: this.user + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/switch_to_list_form_view.js b/client/js/views/switch_to_list_form_view.js new file mode 100644 index 000000000..5053aecf8 --- /dev/null +++ b/client/js/views/switch_to_list_form_view.js @@ -0,0 +1,157 @@ +/** + * @fileOverview This file has functions related to switch to list view. This view calling from footer view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board model. It contain all board based object @see Available Object in App.BoardView + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * CardLabelsForm View + * @class CardLabelsFormView + * @constructor + * @extends Backbone.View + */ +App.SwitchToListView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.sort_by = null; + this.render(); + }, + template: JST['templates/switch_to_list_form'], + tagName: 'table', + className: 'table', + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-sort-by': 'sortBy' + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + board: this.model + })); + this.showTooltip(); + return this; + }, + /** + * sortBy() + * toggle thr label filter list + * @param e + * @type Object(DOM event) + * + */ + sortBy: function(e) { + e.preventDefault(); + var self = this; + var sort_by = $(e.target).data('sort-by'); + var filtered_cards = self.model.cards.filter(function(card) { + return card.attributes.is_archived === false || card.attributes.is_archived === 'f'; + }); + var is_card_empty = true; + var view = ''; + $('.js-card-list-view-' + self.model.attributes.id).html(''); + if (!_.isEmpty(filtered_cards)) { + var cards = new App.CardCollection(); + if (this.sort_by === sort_by) { + cards.sortDirection = 'asc'; + this.sort_by = '-' + sort_by; + if ($(e.target).children('span').hasClass('icon-caret-down')) { + $('.js-sort-by').children('span').removeClass('icon-caret-up hide icon-caret-down'); + $(e.target).children('span').removeClass('icon-caret-down').addClass('icon-caret-up'); + } else { + $('.js-sort-by').children('span').removeClass('icon-caret-up hide icon-caret-down'); + $(e.target).children('span').removeClass('icon-caret-up').addClass('icon-caret-down'); + } + } else { + cards.sortDirection = 'desc'; + this.sort_by = sort_by; + if ($(e.target).children('span').hasClass('icon-caret-up')) { + $('.js-sort-by').children('span').removeClass('icon-caret-up hide icon-caret-down'); + $(e.target).children('span').removeClass('icon-caret-up').addClass('icon-caret-down'); + } else { + $('.js-sort-by').children('span').removeClass('icon-caret-up hide icon-caret-down'); + $(e.target).children('span').removeClass('icon-caret-down').addClass('icon-caret-up'); + } + } + cards.comparator = function(item) { + var str = '' + item.get(sort_by); + if (sort_by !== 'id' && sort_by !== 'card_voter_count' && sort_by !== 'attachment_count' && sort_by !== 'checklist_item_count' && sort_by !== 'checklist_item_completed_count' && sort_by !== 'cards_subscriber_count') { + str = str.toLowerCase(); + str = str.split(''); + str = _.map(str, function(letter) { + if (cards.sortDirection.toLowerCase() === 'desc') { + return String.fromCharCode(-(letter.charCodeAt(0))); + } else { + return String.fromCharCode((letter.charCodeAt(0))); + } + + }); + return str; + } else { + if (cards.sortDirection === 'desc') { + return -item.get(sort_by); + } else { + return item.get(sort_by); + } + } + }; + cards.reset(filtered_cards); + for (var i = 0; i < cards.models.length; i++) { + var card = cards.models[i]; + var list = self.model.lists.findWhere({ + id: card.attributes.list_id, + is_archived: false + }); + if (!_.isUndefined(list)) { + is_card_empty = false; + card.list_name = _.escape(list.attributes.name); + card.list_id = list.attributes.id; + card.board_users = self.model.board_users; + card.labels.add(card.attributes.card_labels, { + silent: true + }); + card.cards.add(self.model.cards, { + silent: true + }); + card.list = list; + card.board_activities.add(self.model.activities, { + silent: true + }); + view = new App.CardView({ + tagName: 'tr', + className: 'js-show-modal-card-view', + model: card, + template: 'list_view' + }); + $('.js-card-list-view-' + self.model.attributes.id).append(view.render().el); + } + } + } + if (is_card_empty) { + view = new App.CardView({ + tagName: 'tr', + className: '', + model: null, + board_id: this.model.id, + template: 'list_view' + }); + view.render(); + } + } +}); diff --git a/client/js/views/user_activity_menu_view.js b/client/js/views/user_activity_menu_view.js new file mode 100644 index 000000000..416fcb066 --- /dev/null +++ b/client/js/views/user_activity_menu_view.js @@ -0,0 +1,44 @@ +/** + * @fileOverview This file has functions related to user activity menu view. This view calling from user index view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : user model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * CardLabelsForm View + * @class CardLabelsFormView + * @constructor + * @extends Backbone.View + */ +App.UserActivityMenuView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/user_activity_menu'], + tagName: 'ul', + className: 'list-unstyled', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + user: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/user_activity_view.js b/client/js/views/user_activity_view.js new file mode 100644 index 000000000..a5cc5a55f --- /dev/null +++ b/client/js/views/user_activity_view.js @@ -0,0 +1,81 @@ +/** + * @fileOverview This file has functions related to user activity view. This view calling from user view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : activity model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * User Activity View + * @class UserActivityView + * @constructor + * @extends Backbone.View + */ +App.UserActivityView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + converter: new Showdown.converter(), + template: JST['templates/user_activity'], + className: 'list-group-item-text', + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-undo2': 'undo_all' + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + var current_user_can_undo_it = false; + if (!_.isUndefined(App.boards) && !_.isEmpty(this.model) && !_.isUndefined(this.model.attributes.board_id) && !_.isUndefined(App.boards.get(this.model.attributes.board_id))) { + var board_users = App.boards.get(this.model.attributes.board_id).attributes.users; + _.each(board_users, function(board_user) { + if (parseInt(board_user.user_id) === parseInt(authuser.user.id) && board_user.is_admin === true) { + current_user_can_undo_it = true; + } + }); + } + this.$el.html(this.template({ + activity: this.model, + type: this.type, + converter: this.converter, + current_user_can_undo_it: current_user_can_undo_it + })); + this.showTooltip(); + return this; + }, + /** + * undo_all() + * revert changes to the previous version + * @param NULL + * @return false + */ + undo_all: function(e) { + var self = this; + e.preventDefault(); + this.model.url = api_url + 'activities/undo/' + this.model.id + '.json'; + this.model.save({}, { + patch: true, + success: function(model, response) { + self.flash('danger', "Undo Succeed"); + } + }); + return false; + } +}); diff --git a/client/js/views/user_board_list_view.js b/client/js/views/user_board_list_view.js new file mode 100644 index 000000000..07088b1d2 --- /dev/null +++ b/client/js/views/user_board_list_view.js @@ -0,0 +1,43 @@ +/** + * @fileOverview This file has functions related to user board list view. This view calling from user index view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * CardLabelsForm View + * @class CardLabelsFormView + * @constructor + * @extends Backbone.View + */ +App.UserBoardListView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/user_board_list'], + tagName: 'li', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + user_board: this.model, + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/user_boards_listing_menu_view.js b/client/js/views/user_boards_listing_menu_view.js new file mode 100644 index 000000000..37fa87dc8 --- /dev/null +++ b/client/js/views/user_boards_listing_menu_view.js @@ -0,0 +1,43 @@ +/** + * @fileOverview This file has functions related to user board listing menu view. This view calling from organization view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : board users model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * CardLabelsForm View + * @class CardLabelsFormView + * @constructor + * @extends Backbone.View + */ +App.UserBoardslistingMenuView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/user_boards_listing_menu'], + tagName: 'div', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + boards: this.model + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/user_cards_view.js b/client/js/views/user_cards_view.js new file mode 100644 index 000000000..e6e253a86 --- /dev/null +++ b/client/js/views/user_cards_view.js @@ -0,0 +1,46 @@ +/** + * @fileOverview This file has functions related to user cards view. This view calling from user view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : card model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * UserCards View + * @class UserCardsView + * @constructor + * @extends Backbone.View + */ +App.UserCardsView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.key = options.key; + this.render(); + }, + template: JST['templates/user_cards'], + tagName: 'section', + className: 'clearfix', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + card_user: this.model, + key: this.key + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/user_index_container_view.js b/client/js/views/user_index_container_view.js new file mode 100644 index 000000000..949133191 --- /dev/null +++ b/client/js/views/user_index_container_view.js @@ -0,0 +1,67 @@ +/** + * @fileOverview This file has functions related to user index container view. This view calling from application view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : user model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * UserIndexContainer View + * @class UserIndexContainerView + * @constructor + * @extends Backbone.View + */ +App.UserIndexContainerView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + this.sortField = options.sortField; + this.sortDirection = options.sortDirection; + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/user_index_container'], + tag: 'section', + className: 'clearfix row', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + user: this.model + })); + if (!_.isUndefined(this.sortField)) { + this.renderUserCollection(); + } + this.showTooltip(); + return this; + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + renderUserCollection: function() { + var view = this.$el.find('.js-user-list'); + this.model.setSortField(this.sortField, this.sortDirection); + this.model.sort(); + this.model.each(function(user) { + view.append(new App.UserIndex({ + model: user + }).el); + }); + return this; + } +}); diff --git a/client/js/views/user_index_view.js b/client/js/views/user_index_view.js new file mode 100644 index 000000000..62b6cc444 --- /dev/null +++ b/client/js/views/user_index_view.js @@ -0,0 +1,186 @@ +/** + * @fileOverview This file has functions related to user index view. This view calling from application view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : user model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * UserIndex View + * @class UserIndex + * @constructor + * @extends Backbone.View + */ +App.UserIndex = Backbone.View.extend({ + template: JST['templates/user'], + tagName: 'tr', + className: '', + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'click .js-user-activity-menu': 'userActivityMenu', + 'click .js-user-board-list': 'userBoardList', + 'click .js-no-action': 'noAction', + 'click .js-change-user-role': 'changeUserRole', + 'click .js-block-user': 'blockUser', + 'click .js-unblock-user': 'unBlockUser', + 'click .js-remove-user': 'removeUser', + 'click .js-all-user-activities': 'showUserActivities', + }, + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + _.bindAll(this, 'render'); + this.model.bind('change:role_id', this.render); + this.model.bind('change:is_active', this.render); + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + user: this.model + })); + if (this.model.attributes.joined_board_count !== 0) { + this.userBoardList(); + } + $('.js-admin-user-menu').addClass('active'); + $('.js-admin-activity-menu, .js-admin-setting-menu, .js-admin-email-menu, .js-admin-role-menu').removeClass('active'); + this.showTooltip(); + return this; + }, + /** + * deleteUser() + * delete user + * @param e + * @type Object(DOM event) + * @return false + */ + deleteUser: function() { + this.model.destroy(); + this.model.url = api_url + 'users/' + this.model.id + '.json'; + this.$el.remove(); + return false; + }, + /** + * userActivityMenu() + * display user activity menu + * @param e + * @type Object(DOM event) + */ + userActivityMenu: function(e) { + e.preventDefault(); + $('#js-user-activity-menu-response-' + this.model.id).html(new App.UserActivityMenuView({ + model: this.model + }).el); + }, + /** + * userBoardList() + * display user baords list + * @param e + * @type Object(DOM event) + */ + userBoardList: function(e) { + var self = this; + var user_boards = new App.BoardsUserCollection(); + user_boards.add(this.model.attributes.boards_users, { + silent: true + }); + for (var i = 0; i < user_boards.models.length; i++) { + var user_board = user_boards.models[i]; + self.$el.find('#js-user-activity-menu-response-' + self.model.id).append(new App.UserBoardListView({ + model: user_board + }).el); + } + }, + /** + * noAction() + * @param e + * @type Object(DOM event) + */ + noAction: function(e) { + e.preventDefault(); + return false; + }, + /** + * changeUserRole() + * @param e + * @type Object(DOM event) + */ + changeUserRole: function(e) { + e.preventDefault(); + var role_id = $(e.currentTarget).data('role-id'); + this.model.set('role_id', role_id); + this.model.url = api_url + 'users/' + this.model.attributes.id + '.json'; + this.model.save({ + role_id: role_id + }, { + patch: true + }); + }, + /** + * blockUser() + * @param e + * @type Object(DOM event) + */ + blockUser: function(e) { + e.preventDefault(); + this.model.set('is_active', false); + this.model.url = api_url + 'users/' + this.model.attributes.id + '.json'; + this.model.save({ + is_active: false + }, { + patch: true + }); + }, + /** + * unBlockUser() + * @param e + * @type Object(DOM event) + */ + unBlockUser: function(e) { + e.preventDefault(); + this.model.set('is_active', true); + this.model.url = api_url + 'users/' + this.model.attributes.id + '.json'; + this.model.save({ + is_active: true + }, { + patch: true + }); + }, + /** + * removeUser() + * @param e + * @type Object(DOM event) + */ + removeUser: function(e) { + e.preventDefault(); + this.$el.remove(); + this.model.url = api_url + 'users/' + this.model.attributes.id + '.json'; + this.flash('success', 'User deleted successfully.'); + this.model.destroy(); + }, + showUserActivities: function(e) { + e.preventDefault(); + var modalView = new App.ModalActivityView({ + model: this.model, + type: 'user_listing' + }); + modalView.show(); + return false; + } +}); diff --git a/client/js/views/user_search_result_view.js b/client/js/views/user_search_result_view.js new file mode 100644 index 000000000..f33343577 --- /dev/null +++ b/client/js/views/user_search_result_view.js @@ -0,0 +1,45 @@ +/** + * @fileOverview This file has functions related to user search result view. This view calling from organization user view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : user model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * UserSearchResult View + * @class UserSearchResultView + * @constructor + * @extends Backbone.View + */ +App.UserSearchResultView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.q = options.q; + this.render(); + }, + template: JST['templates/user_search_result'], + tagName: 'li', + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + user: this.model, + q: this.q + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/user_view.js b/client/js/views/user_view.js new file mode 100644 index 000000000..1f9c210cb --- /dev/null +++ b/client/js/views/user_view.js @@ -0,0 +1,312 @@ +/** + * @fileOverview This file has functions related to user view. This view calling from application view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : user model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * User View + * @class UserView + * @constructor + * @extends Backbone.View + */ +App.UserView = Backbone.View.extend({ + template: JST['templates/user_view'], + tagName: 'div', + className: '', + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'submit form.js-user-profile-edit': 'userProfileEdit', + 'click .js-user-cards': 'userCards', + 'click #js-user-activites-load-more': 'loadActivities', + 'click .js-remove-image': 'removeImage', + 'click .js-use-uploaded-avatar': 'computerOpenUserProfile', + 'change #js-user-profile-attachment': 'addUserProfile', + + }, + /** + * Constructor + * initialize default values and actions + */ + initialize: function(options) { + var last_activity_id = 0; + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.type = 'profile'; + if (!_.isUndefined(options.type)) { + this.type = options.type; + } + this.model.bind('change', this.render, this); + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + if (!_.isUndefined(this.type) && (this.type == 'cards' || this.type == 'settings')) { + this.renderType(); + } else { + var self = this; + changeTitle('User - ' + _.escape(this.model.attributes.username)); + var activities = new App.ActivityCollection(); + activities.url = api_url + 'users/' + self.model.id + '/activities.json?&type=profile'; + activities.fetch({ + cache: false, + success: function(user, response) { + self.$el.html(self.template({ + user: self.model, + type: self.type + })); + if (!_.isEmpty(activities.models)) { + var last_activity = _.min(activities.models, function(activity) { + return activity.id; + }); + last_activity_id = last_activity.id; + $('#js-user-activites-load-more').removeClass('hide'); + for (var i = 0; i < activities.models.length; i++) { + var activity = activities.models[i]; + self.$('#js-user-activites').append(new App.UserActivityView({ + model: activity, + type: self.type + }).el).find('.timeago').timeago(); + } + } else { + $('#js-user-activites-load-more').addClass('hide'); + self.$('#js-user-activites').append(new App.UserActivityView({ + model: null, + type: self.type + }).el); + } + } + }); + this.showTooltip(); + return this; + } + }, + /** + * renderCards() + * populate the html to the dom + * @param NULL + * @return object + * + */ + renderType: function() { + this.$el.html(this.template({ + user: this.model, + type: this.type + })); + if (this.type == 'cards') { + this.userCards(); + } else { + var _this = this; + _(function() { + Backbone.TemplateManager.baseUrl = '{name}'; + var uploadManager = new Backbone.UploadManager({ + uploadUrl: api_url + 'users/' + _this.model.id + '.json?token=' + api_token, + autoUpload: true, + dropZone: $('#dropzone'), + singleFileUploads: true, + formData: $('form.js-user-profile-edit').serialize(), + fileUploadHTML: '', + }); + uploadManager.on('fileadd', function(file) { + $('#dropzone-cssloader').addClass('cssloader'); + }); + uploadManager.on('filedone', function(file, data) { + if (!_.isUndefined(data.result.profile_picture_path)) { + $('#dropzone-cssloader').removeClass('cssloader'); + _this.model.set('profile_picture_path', data.result.profile_picture_path); + _this.render(); + } + }); + uploadManager.renderTo($('#manager-area')); + }).defer(); + } + }, + /** + * userBoardList() + * delete user + * @return false + */ + deleteUser: function() { + this.model.destroy(); + this.model.url = api_url + 'users/' + this.model.id + '.json'; + this.$el.remove(); + return false; + }, + /** + * userProfileEdit() + * update user profile + * @param e + * @type Object(DOM event) + */ + userProfileEdit: function(e) { + e.preventDefault(); + var self = this; + var form = $(e.target); + var fileData = new FormData(form[0]); + var data = $(e.target).serializeObject(); + this.model.set(data); + this.render(); + this.model.url = api_url + 'users/' + this.model.id + '.json'; + this.model.save(fileData, { + patch: true, + type: 'POST', + data: fileData, + processData: false, + cache: false, + contentType: false, + error: function(e, s) { + self.flash('danger', 'Unable to update. Please try again.'); + }, + success: function(model, response) { + if (!_.isEmpty(response.success)) { + self.flash('success', response.success); + } else if (!_.isEmpty(response.error)) { + self.flash('danger', response.error); + } else { + self.flash('danger', 'User Profile could not be updated. Please, try again.'); + } + if (!_.isUndefined(response.profile_picture_path)) { + self.model.set('profile_picture_path', self.showImage('User', user.attributes.id, 'small_thumb')); + } + } + }); + }, + /** + * userCards() + * display user cards + * @param e + * @type Object(DOM event) + * @return false + */ + userCards: function() { + var self = this; + self.model.cards.url = api_url + 'users/' + self.model.id + '/cards.json?'; + self.model.cards.fetch({ + cache: false, + success: function(card, response) { + self.$('#cards').html(''); + var card_users = new App.CardUserCollection(); + card_users = self.model.cards.groupBy(function(model) { + return [model.get('board_name')]; + }); + if (!_.isEmpty(card_users)) { + _.map(card_users, function(card_user, key) { + self.$('#cards').append(new App.UserCardsView({ + key: key, + model: card_user + }).el); + }); + } else { + self.$('#cards').html('No cards available.'); + } + $('#tab-loaded-content').load(); + } + }); + }, + /** + * loadActivities() + * display load Activities + */ + loadActivities: function() { + var self = this; + var activities = new App.ActivityCollection(); + var query_string = '?last_activity_id=' + last_activity_id + '&type=' + self.type; + activities.url = api_url + 'users/' + self.model.id + '/activities.json' + query_string; + activities.fetch({ + cache: false, + success: function(user, response) { + if (!_.isEmpty(activities) && !_.isEmpty(activities.models)) { + for (var i = 0; i < activities.models.length; i++) { + var activity = activities.models[i]; + self.$('#js-user-activites').append(new App.UserActivityView({ + model: activity + }).el).find('.timeago').timeago(); + } + var last_activity = _.min(activities.models, function(activity) { + return activity.id; + }); + last_activity_id = last_activity.id; + } else { + self.$('#js-user-activites-load-more').removeClass(); + } + } + }); + }, + /** + * removeImage() + * remive image + * @param e + * @type Object(DOM event) + */ + removeImage: function(e) { + e.preventDefault(); + this.model.set('profile_picture_path', null); + this.render(); + this.model.url = api_url + 'users/' + this.model.id + '.json'; + this.model.save({ + profile_picture_path: 'NULL' + }, { + patch: true + }); + return false; + }, + /** + * computerOpenUserProfile() + * trigger file upload + * @param e + * @type Object(DOM event) + * @return false + */ + computerOpenUserProfile: function(e) { + e.preventDefault(); + var fileLi = $(e.target); + $('#js-user-profile-attachment').remove(); + var form = $('#js-user-profile-edit'); + $(form).append(''); + $('#js-user-profile-attachment', form).trigger('click'); + return false; + }, + /** + * addUserProfile() + * add card attachment + * @param e + * @type Object(DOM event) + */ + addUserProfile: function(e) { + e.preventDefault(); + var self = this; + $('#dropzone-cssloader').addClass('cssloader'); + var form = $('#js-user-profile-edit'); + var target = $(e.target); + var fileData = new FormData(form[0]); + this.model.url = api_url + 'users/' + this.model.id + '.json'; + this.model.save(fileData, { + type: 'POST', + data: fileData, + processData: false, + cache: false, + contentType: false, + success: function(model, response) { + $('#dropzone-cssloader').removeClass('cssloader'); + self.model.set('profile_picture_path', response.profile_picture_path); + self.render(); + } + }); + } +}); diff --git a/client/js/views/user_view_header_view.js b/client/js/views/user_view_header_view.js new file mode 100644 index 000000000..b18d587f3 --- /dev/null +++ b/client/js/views/user_view_header_view.js @@ -0,0 +1,44 @@ +/** + * @fileOverview This file has functions related to user header view. This view calling from application view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : user model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Header View + * @class HeaderView + * @constructor + * @extends Backbone.View + */ +App.UserViewHeaderView = Backbone.View.extend({ + className: '', + template: JST['templates/user_view_header'], + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template({ + user: this.model, + type: this.type + })); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/users_activation_view.js b/client/js/views/users_activation_view.js new file mode 100644 index 000000000..1ebb9b9f2 --- /dev/null +++ b/client/js/views/users_activation_view.js @@ -0,0 +1,64 @@ +/** + * @fileOverview This file has functions related to user header view. This view calling from application view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : user model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * UseractivationView View + * @class UseractivationView + * @constructor + * @extends Backbone.View + */ +App.UseractivationView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + var self = this; + var user = new App.User(); + user.url = api_url + 'users/activation/' + self.model.user_id + '.json'; + user.set('id', self.model.user_id); + user.save({ + id: self.model.user_id, + hash: self.model.hash + }, { + patch: true, + success: function(model, response) { + if (!_.isEmpty(response.success)) { + self.flash('success', response.success); + } else { + self.flash('danger', response.error); + } + app.navigate('#/users/login', { + trigger: true, + replace: true + }); + } + }); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/users_change_password_view.js b/client/js/views/users_change_password_view.js new file mode 100644 index 000000000..5976b2fc5 --- /dev/null +++ b/client/js/views/users_change_password_view.js @@ -0,0 +1,77 @@ +/** + * @fileOverview This file has functions related to user change password view. This view calling from application view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : user model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Changepassword View + * @class ChangepasswordView + * @constructor + * @extends Backbone.View + */ +App.ChangepasswordView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + template: JST['templates/change_password'], + tagName: 'article', + className: 'clearfix', + id: 'change_password', + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'submit': 'changepassword' + }, + /** + * changepassword() + * update user password + * @return false + */ + changepassword: function(e) { + var target = $(e.target); + var data = $('form#UserChangePasswordForm').serializeObject(); + target[0].reset(); + var user = new App.User(); + var self = this; + user.url = api_url + 'users/' + this.model.user_id + '/changepassword.json'; + user.save(data, { + success: function(model, response) { + if (!_.isEmpty(response.error)) { + self.flash('danger', response.error); + } else { + self.flash('success', 'Password has been changed successfully.'); + app.navigate('#/users/logout', { + trigger: true, + replace: true + }); + } + } + }); + return false; + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template(this.model)); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/views/users_forgot_password_view.js b/client/js/views/users_forgot_password_view.js new file mode 100644 index 000000000..4befb308e --- /dev/null +++ b/client/js/views/users_forgot_password_view.js @@ -0,0 +1,77 @@ +/** + * @fileOverview This file has functions related to user forgot password view. This view calling from application view. + * Available Object: + * App.boards : this object contain all boards(Based on logged in user) + * this.model : user model. + */ +if (typeof App == 'undefined') { + App = {}; +} +/** + * Forgotpassword View + * @class ForgotpasswordView + * @constructor + * @extends Backbone.View + */ +App.ForgotpasswordView = Backbone.View.extend({ + /** + * Constructor + * initialize default values and actions + */ + initialize: function() { + if (!_.isUndefined(this.model) && this.model !== null) { + this.model.showImage = this.showImage; + } + this.render(); + }, + tagName: 'article', + id: 'forgot-password', + className: 'clearfix', + template: JST['templates/users_forgot_password'], + /** + * Events + * functions to fire on events (Mouse events, Keyboard Events, Frame/Object Events, Form Events, Drag Events, etc...) + */ + events: { + 'submit form#UserForgotPasswordForm': 'forgotpassword', + }, + /** + * forgotpassword() + * send password to user + * @return false + */ + forgotpassword: function(e) { + var target = $(e.target); + var self = this; + var data = target.serializeObject(); + target[0].reset(); + var user = new App.User(); + user.url = api_url + 'users/forgotpassword.json'; + user.save(data, { + success: function(model, response) { + if (!_.isEmpty(response.success)) { + app.navigate('#/users/login', { + trigger: true, + replace: true + }); + self.flash('success', response.success); + } else { + self.flash('danger', response.error); + } + } + }); + return false; + }, + /** + * render() + * populate the html to the dom + * @param NULL + * @return object + * + */ + render: function() { + this.$el.html(this.template(this.model)); + this.showTooltip(); + return this; + } +}); diff --git a/client/js/workflow_templates/bug.json b/client/js/workflow_templates/bug.json new file mode 100644 index 000000000..1ccc5c68f --- /dev/null +++ b/client/js/workflow_templates/bug.json @@ -0,0 +1,12 @@ +{ + "name": "Bug", + "lists": [ + "New", + "Feedback", + "Acknowledged", + "Confirmed", + "Assigned", + "Resolved", + "Closed" + ] +} \ No newline at end of file diff --git a/client/js/workflow_templates/crm.json b/client/js/workflow_templates/crm.json new file mode 100644 index 000000000..e15a71041 --- /dev/null +++ b/client/js/workflow_templates/crm.json @@ -0,0 +1,11 @@ +{ + "name": "CRM", + "lists": [ + "Initial engagement", + "Potential sale", + "Qualification", + "Probable sale", + "Pending sale", + "Completed sale" + ] +} \ No newline at end of file diff --git a/client/js/workflow_templates/scrum.json b/client/js/workflow_templates/scrum.json new file mode 100644 index 000000000..6080dbd3b --- /dev/null +++ b/client/js/workflow_templates/scrum.json @@ -0,0 +1,13 @@ +{ + "name": "Scrum", + "lists": [ + "Vision", + "Release Planning", + "Sprint planning", + "Sprint backlog", + "Sprint execution", + "Sprint review", + "Sprint retrospective", + "Release" + ] +} \ No newline at end of file diff --git a/client/js/workflow_templates/todo.json b/client/js/workflow_templates/todo.json new file mode 100644 index 000000000..7b4ff2eb1 --- /dev/null +++ b/client/js/workflow_templates/todo.json @@ -0,0 +1,8 @@ +{ + "name": "Todo", + "lists": [ + "Todo", + "Doing", + "Done" + ] +} \ No newline at end of file diff --git a/media/User/1/default-admin-user.png b/media/User/1/default-admin-user.png new file mode 100644 index 0000000000000000000000000000000000000000..e3a72b00e21de26eee3531bfce9a0335ab2702e7 GIT binary patch literal 4750 zcmaJ_c{r5q+a6>oYhseZ7>w+OF`5ZuXKYh;qA)XdW{iokC0k0#n#jJDvW3c0q9~^9 z*@uaw z)884tA2vn+fT0zE3ZV~9k@alJ76eyvpfd>%(83X1@F290vm4$V?~DubqvF*704A`9 zr7hXk)C7qo_{cl|#>i8Ah;%jppoXFlov~hcGROt*=HaUjUU}IB26^Dr!8R(Uil#&z zyt{{CFbQuFY-WiK_QE1?V3Y<(je?{L_~6OTAc~K-uRoHa4*p9QNwnZ(sD0I@p~|CL$FS0s{l(1C`|oBsT>p0)hCgp`;{7N67gH`I4O}a=!jz ze-w1_{#cR+k?cY61^rfZb|G9PtApv0{$~k3#D8Rc{r^rAJz)wIXQBdBUh(&m{s2u) z|9_~D&p&8?vN`_0@&2E}{+2;Ryn;F2pKz6gr5Das?6)f-Qip_hCKE`O1cLXUEM9OY zkO}_o1R_Ys0uDNBSCGr=mi)|8R8)*sDHxU-BO=?!R34zjA-Kf)A1ISr_{m)`9p zdRtG5+Vlee+>B^lElbL%%RY^UVyeG>D}u3^G5>X1aZ*wIOD|wCTzN|Ao`4oaElyyE zSs+Gtb;0e;jKt`TtrFK`Dzu4O&ij2$s`4j#_PyFz93F&sWSR_*G@Gn#cX8s9!n<>w zIuXMb!wGg_FmA>73^Hf{Xvs*J2;jtF1v|)nMIH92^{2fbrteu^vWgcnNXD?*`{I;{56D(-|uA}3#hLC!yyO)IL8U*e;9NOZ}`*v7I4;?R7 z82xJ(U>?V7d8gYN$fUeg!bd3sMBOZ{xStez@^&98{IxY+bc|EBEPQ#?@Ej)=nb;m6 zorG7ZjxZ7T5EI@Z**1PTm`T>*K`u&v5cs@pJ#oTK8U8(;m(Xm0R@(Iff!AE*`Qgf zJ=)yvm;cTgIE^jR{-+()Zv1-9L3sg#?t)y?`lCEYh9)+pQa*^ERBCln%I#7=#w~rl zOYdmhD?BZ%aI+?OdEPT%i-?1Rl~7(5yfAY@Dow1A{LEJJy-_v$Zj+5d&&$2$G9N|3 z`-Rqh{U2zk*9raC-=S{A6ZWV3?3Bu*IqFiBDOETKbN2WXZ}a87i6KKfhr^wNm0tDf z+u!QpVwkkS!RV*RFa^W$~4a4Agaa~Cv;_M}ufVF7LL54T6(y_v>Xvj)N|or)mtD66bZ`c~-L7kn7)ZTKkW zP6e;ZC%LD7aZBiLjIAUcp!GEC!^a}Iy7nC|&fUU1)aK|~_NJhLm6nj6y<~yss-uGE zf`6rmH450gW|XL3145jk)27&U&M1YP9ol+Z?Wo$UFN@VHqlc)BdjnN_sPdbAEUM>O z<#s)BUop>^Lt0)O?hjsuDvchc9fp-ti<|tZ^0DoeHp5Gw7nacbOhwODCD~<4OBEW& ze1qScye%MaO4|5rU&kqTtywqhp75H3k6t;oIbVYE_>KWf9tmHbPy=-x_#cWp&a7_( zcGlNm`4H$;F%t@;lms?)mypcj1)Dt*4YXG$t$Jyg?ttjM*s*USZ)?kIOjw#uvE zriJhRDlblz*1zcUYGrR0eqk-q3J{!rY_40Fu%LT*HLG3%TX?nY@TcG;Mj^o0o1(9S_E3^shoiOG z5qFKhoP8XXuCi$>q@E#OR27(RFeJ$#1K`o5eHQ!v6{(vi9c z5G4}PyuKiW!Y$xQ-w?DxdySvaqvT`bvEeJ9AKL2$5>J+NQWe->xpr+tkoqmSCJTy? z*l=A;K4qZqnv)v!cy3J2`-xWtp7_`W#8YPd!fXRgNsNmxr`zn;H)F?LjtI5qJ)xF8 zVYVAJ%^M&I6%RztXR+GztJxZ(^o2DxKzu+fdz|(J;8yXSAo9vkno79HxYqqM zohqW>f!eL-8aQk7m>Rofhmlb2TcwWoKyC1vH-Xx9ExfjR zC`!+2kU!}{PPvmEgd_EY@3DXNt{rC;(p zeOU{F~2qeMo4^r5<}jcZED+IT%n@e?)HV?zHIk~kvrpy&a@b* zU=By?B6D)~HHxtSwWA+0)*-!Zu@w2(f_h2XMBN&#jT=efC1IzUDZFg22j+T5WBXtI zas>!QbpQO8#ZW4_ID_daPiP9~1Y-K$<~E-z8Vs^A>F-pd<){^DhkyITdLi-P;i#p; zy;w{W#`E2)jN8v%j1`>fw~sND!6-G$GhzpOrVPR5Uk>U?hq*Ov9!a$xDm+|e+FIzl zF)^J32?GtFO02TPEaCfdNh)@^6t~*UTxG#Rwx%k zw;z#lUDRG$Vu{)-jq6XLD{hLH12nI1b!k9KrSHycZw86EnP}Z`)?g9LD9(J!Yn~_A zDiWVm>M-}fYasZd|0naKFP^cLT8WjT;=Tee`W(M>dIY#yaH5^pPv;?sO<*POdiTf1 z?#B6m3PqRZ^$#s!i0#lo)%WYcfg0OclRMe-IV;^~Z>Xe809|jz*ttk`@E4gQY zvvJ|$oGrjuHlDL4y;wBbu0TxgN2eJT%#?!7E?f1e8g%U}1jMGJ;d}=cJuisl-Q6Bne(h1*awEogDSu-5+X}I6O6r-Vx zX{mSY65DH`g!@x)N(OwP+D{)YvbMgk{xQD-q(+h|LM>CQYpy6!AAemw->5Eg*>muW zA>!SP+UE16!L~;tc_(;%R@x<8cH)$}M?7ab!vx^d?p550q$cuspQ|nJCHao;K&KBQ z4CguTY_PbX-;IcAocl2ZvU(|7`8+EB8ln4b2_Y)#>G7``{n9TtD=kUdB z%URL~VT@im=)S&rZ&mf41UmR*ofaVhrU8lPh|6AfqG&cic{}(FBUdC|(b)9#Wg~?8 zcMclq2P;k{jx7$AA#32 z{B{q3V^i3!w^45#C6F$s@-kNYvs{+LaI@94vB-j9pA2vNuLZuR^G=9dXI<@QUqbY##8?Cr#r)Is$iBiFQVPho_~TQ|6GU$MJ9602k! zY|5CD<(aA%$zQdUM#kAyk5zNrbEsalbFYrhPBt5NI-54zX9$s!$P!toafl_!>d5Fk z4wMSoksekUs!#bXew7c^C%kjfOgqw&*WQ5ay;g2?)zxT>bd(LoVKopjvpH>|D_;5Fc?DdvS_VXlAW0()Cj?MF2uVmx7O0P0XhrF%Bv{TcbZqk}v>@{06)?Qx3M_1ud&#th`Uh`jfYLZb_`JFO%dY7In!1~`r)ir*~ zHnrGsHJJ)!vbk*ZGb_!2{YKBLUmr*mp4#%W!_Q1S=cHraPQ@SZ&X5+811U#e6NlwS zwe|1sRlLmDQ-x+);4lw~Z~R}zJY-p&MokZV49>c!9Ie4z5xDv@{dfcJd-=KZ{PlO@ z;zSFvhL?_bC>?iY*XpfaW(KUi@lTG8^#^+3*fzF}R1Gu<+ya7{*s4g;3rVm5i!)xl zM`w + * @copyright 2014 Restya + * @license http://www.restya.com/ Restya Licence + * @link http://www.restya.com + */ +define('R_DEBUG', false); +ini_set('display_errors', R_DEBUG); +define('R_API_VERSION', 1); +define('APP_PATH', dirname(dirname(dirname(dirname(__FILE__))))); +// While changing below oAuth credentials, have to update in oauth_clients table also. +define('OAUTH_CLIENTID', '7742632501382313'); +define('OAUTH_CLIENT_SECRET', '4g7C4l1Y2b0S6a7L8c1E7B3K0e'); +$default_timezone = 'Europe/Berlin'; +if (ini_get('date.timezone')) { + $default_timezone = ini_get('date.timezone'); +} +date_default_timezone_set($default_timezone); +define('R_DB_HOST', 'localhost'); +define('R_DB_USER', 'restya'); +define('R_DB_PASSWORD', 'hjVl2!rGd'); +define('R_DB_NAME', 'restyaboard'); +define('R_DB_PORT', 5432); +define('SecuritySalt', 'e9a556134534545ab47c6c81c14f06c0b8sdfsdf'); +if (!($db_lnk = @pg_connect('host=' . R_DB_HOST . ' port=' . R_DB_PORT . ' dbname=' . R_DB_NAME . ' user=' . R_DB_USER . ' password=' . R_DB_PASSWORD . ' options=--client_encoding=UTF8'))) { + header($_SERVER['SERVER_PROTOCOL'] . ' 500 Internal Server Error', true, 500); + $r_debug.= __LINE__ . ': ' . pg_last_error($db_lnk) . '\n'; +} +$settings = pg_query_params($db_lnk, 'SELECT name, value FROM settings WHERE setting_category_id in (1,2,3) OR setting_category_parent_id in (1,2,3)', array()); +while ($setting = pg_fetch_assoc($settings)) { + if ($setting['name'] == 'LDAP_LOGIN_ENABLED') { + $setting['value'] = (in_array(strtolower($setting['value']) , array( + 'true' + ))) ? true : false; + } + define($setting['name'], $setting['value']); +} +$thumbsizes = array( + 'User' => array( + 'micro_thumb' => '16x16', + 'small_thumb' => '32x32', + 'normal_thumb' => '64x64', + 'medium_thumb' => '153x153' + ) , + 'Organization' => array( + 'medium_thumb' => '153x153', + 'small_thumb' => '32x32' + ) , + 'Board' => array( + 'micro_thumb' => '16x16', + 'small_thumb' => '32x32', + 'medium_thumb' => '153x153', + 'extra_large_thumb' => '2000x1263' + ) , + 'CardAttachment' => array( + 'small_thumb' => '108x78', + 'medium_thumb' => '153x153', + 'large_thumb' => '202x151' + ) +); +$aspect['CardAttachment']['large_thumb'] = 1; diff --git a/server/php/R/download.php b/server/php/R/download.php new file mode 100644 index 000000000..b2e36bcf4 --- /dev/null +++ b/server/php/R/download.php @@ -0,0 +1,47 @@ + + * @copyright 2014 Restya + * @license http://www.restya.com/ Restya Licence + * @link http://www.restya.com + */ +require_once ('config.inc.php'); +if (!empty($_GET['id']) && !empty($_GET['hash'])) { + $md5_hash = md5(SecuritySalt . 'download' . $_GET['id']); + if ($md5_hash == $_GET['hash']) { + if ($db_lnk) { + $result = pg_query_params($db_lnk, 'SELECT * FROM card_attachments WHERE id = $1', array( + $_GET['id'] + )); + $attachment = pg_fetch_assoc($result); + $mediadir = APP_PATH . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'Card' . DIRECTORY_SEPARATOR . $attachment['card_id']; + $file = $mediadir . DIRECTORY_SEPARATOR . $attachment['name']; + if (file_exists($file)) { + $quoted = sprintf('"%s"', addcslashes(basename($file) , '"\\')); + $size = filesize($file); + header('Content-Description: File Transfer'); + header('Content-Type: application/octet-stream'); + header('Content-Disposition: attachment; filename=' . $quoted); + header('Content-Transfer-Encoding: binary'); + header('Connection: Keep-Alive'); + header('Content-length: ' . $size); + header('Expires: 0'); + header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); + header('Pragma: public'); + readfile($file); + exit; + } + } + } else { + header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found', true, 404); + } +} else { + header($_SERVER['SERVER_PROTOCOL'] . ' 400 Not Found', true, 400); +} diff --git a/server/php/R/ical.php b/server/php/R/ical.php new file mode 100644 index 000000000..e6686f1c6 --- /dev/null +++ b/server/php/R/ical.php @@ -0,0 +1,61 @@ + + * @copyright 2014 Restya + * @license http://www.restya.com/ Restya Licence + * @link http://www.restya.com + */ +require_once ('config.inc.php'); +if (!empty($_GET['id']) && !empty($_GET['hash'])) { + $md5_hash = md5(SecuritySalt . $_GET['id']); + if ($md5_hash == $_GET['hash']) { + $r_debug = ''; + if ($db_lnk) { + $result = pg_query_params($db_lnk, 'SELECT board.name, card.id, card.name as card_name, card.description, card.due_date FROM boards board LEFT JOIN cards card ON card.board_id = board.id WHERE card.is_archived = FALSE AND card.due_date IS NOT NULL AND board.id = $1', array( + $_GET['id'] + )); + $count = pg_num_rows($result); + $ical = 'BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//' . SITE_NAME . '//EN +X-PUBLISHED-TTL:PT1H +X-ORIGINAL-URL:http://' . $_SERVER['HTTP_HOST'] . ' +CALSCALE:GREGORIAN +METHOD:PUBLISH'; + if ($count > 0) { + $event = ''; + $board_name = ''; + while ($row = pg_fetch_assoc($result)) { + $board_name = $row['name']; + $event.= ' +BEGIN:VEVENT +UID:' . $row['id'] . ' +DTSTART:' . date('Ymd\THis\Z', strtotime($row['due_date'])) . ' +DTEND:' . date('Ymd\THis\Z', strtotime($row['due_date'])) . ' +SUMMARY:' . $row['card_name'] . ' +URL:http://' . $_SERVER['HTTP_HOST'] . '/client/#/board/' . $_GET['id'] . ' +DESCRIPTION:' . $row['description'] . ' +END:VEVENT +END:VCALENDAR'; + } + $ical.= 'X-WR-CALNAME:' . $board_name . ' (via ' . SITE_NAME . ')'; + $ical.= $event; + } + header('Content-type: text/calendar; charset=utf-8'); + header('Content-Disposition: inline; filename=calendar.ics'); + echo $ical; + exit; + } + } else { + header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found', true, 404); + } +} else { + header($_SERVER['SERVER_PROTOCOL'] . ' 400 Not Found', true, 400); +} diff --git a/server/php/R/image.php b/server/php/R/image.php new file mode 100644 index 000000000..95fb78e0e --- /dev/null +++ b/server/php/R/image.php @@ -0,0 +1,177 @@ + + * @copyright 2014 Restya + * @license http://www.restya.com/ Restya Licence + * @link http://www.restya.com + */ +require_once 'config.inc.php'; +function imageResize() +{ + global $thumbsizes, $settings, $db_lnk, $is_aspect; + $size = $_GET['size']; + $model = $_GET['model']; + $filename = $_GET['filename']; + $val = $thumbsizes[$model][$size]; + list($width, $height) = explode('x', $val); + list($id, $hash, $ext) = explode('.', $filename); + if ($hash == md5(SecuritySalt . $model . $id . $ext . $size . SITE_NAME)) { + if ($model == 'User') { + $s_result = pg_query_params($db_lnk, 'SELECT profile_picture_path FROM users WHERE id = $1', array( + $id + )); + $row = pg_fetch_assoc($s_result); + $fullPath = $row['profile_picture_path']; + } else if ($model == 'Organization') { + $s_result = pg_query_params($db_lnk, 'SELECT logo_url FROM organizations WHERE id = $1', array( + $id + )); + $row = pg_fetch_assoc($s_result); + $fullPath = $row['logo_url']; + } else if ($model == 'Board') { + $s_result = pg_query_params($db_lnk, 'SELECT background_picture_path FROM boards WHERE id = $1', array( + $id + )); + $row = pg_fetch_assoc($s_result); + $fullPath = $row['background_picture_path']; + } else if ($model == 'CardAttachment') { + $s_result = pg_query_params($db_lnk, 'SELECT path FROM card_attachments WHERE id = $1', array( + $id + )); + $row = pg_fetch_assoc($s_result); + $fullPath = $row['path']; + } + $fullPath = dirname(dirname(dirname(dirname(__FILE__)))) . '/' . $fullPath; + $query_string = $_GET; + $query_string['id'] = $id; + $query_string['ext'] = $ext; + $query_string['hash'] = $hash; + $is_aspect = false; + if (!empty($aspect[$model][$size])) { + $is_aspect = true; + } + $mediadir = dirname(dirname(dirname(dirname(__FILE__)))) . '/client/img/' . $query_string['size'] . '/' . $query_string['model'] . '/'; + if (!file_exists($mediadir)) { + mkdir($mediadir, 0777, true); + } + $filename = $query_string['id'] . '.' . $query_string['hash'] . '.' . $query_string['ext']; + $writeTo = $mediadir . $filename; + $img_string = resizeFile($fullPath, $width, $height, $writeTo, $is_aspect, false, $query_string); + header('Location:' . $_SERVER['REQUEST_URI'] . '?chrome-3xx-fix'); + } else { + // Invalid request + return false; + } +} +function _setMemoryLimitForImage($image_path) +{ + $imageInfo = getimagesize($image_path); + $imageInfo['channels'] = !empty($imageInfo['channels']) ? $imageInfo['channels'] : 1; + $imageInfo['bits'] = !empty($imageInfo['bits']) ? $imageInfo['bits'] : 1; + $memoryNeeded = round(($imageInfo[0] * $imageInfo[1] * $imageInfo['bits'] * $imageInfo['channels'] / 8 + Pow(2, 16)) * 1.65); + if (function_exists('memory_get_usage') && memory_get_usage() + $memoryNeeded > (integer)ini_get('memory_limit') * pow(1024, 2)) { + ini_set('memory_limit', (integer)ini_get('memory_limit') + ceil(((memory_get_usage() + $memoryNeeded) - (integer)ini_get('memory_limit') * pow(1024, 2)) / pow(1024, 2)) . 'M'); + } +} +function resizeFile($fullPath, $width = 600, $height = 400, $writeTo, $aspect = true, $is_beyond_original = false, $query_string = array()) +{ + if (!$width || !$height) { + return false; + } + if (!($size = getimagesize($fullPath))) { + return false; + } + list($currentWidth, $currentHeight, $currentType) = $size; + $return = false; + if (class_exists('imagick')) { + $new_image_obj = new imagick($fullPath); + $new_image = $new_image_obj->clone(); + $new_image->setImageColorspace(Imagick::COLORSPACE_RGB); + $new_image->flattenImages(); + if ($is_beyond_original && ($width > $currentWidth || $height > $currentHeight)) { + $width = $currentWidth; + $height = $currentHeight; + } + if (!$aspect) { + $new_image->cropThumbnailImage($width, $height); + } else { + $new_image->scaleImage($width, $height, false); + } + if ($new_image->writeImage($writeTo)) { + $return = true; + } + } else { + $target['width'] = $currentWidth; + $target['height'] = $currentHeight; + $target['x'] = $target['y'] = 0; + $types = array( + 1 => 'gif', + 'jpeg', + 'png', + 'swf', + 'psd', + 'wbmp' + ); + _setMemoryLimitForImage($fullPath); + $image = call_user_func('imagecreatefrom' . $types[$currentType], $fullPath); + ini_restore('memory_limit'); + if ($aspect) { + if (($currentHeight / $height) > ($currentWidth / $width)) { + $width = ceil(($currentWidth / $currentHeight) * $height); + } else { + $height = ceil($width / ($currentWidth / $currentHeight)); + } + } else { + $proportion_X = $currentWidth / $width; + $proportion_Y = $currentHeight / $height; + if ($proportion_X > $proportion_Y) { + $proportion = $proportion_Y; + } else { + $proportion = $proportion_X; + } + $target['width'] = $width * $proportion; + $target['height'] = $height * $proportion; + $original['diagonal_center'] = round(sqrt(($currentWidth * $currentWidth) + ($currentHeight * $currentHeight)) / 2); + $target['diagonal_center'] = round(sqrt(($target['width'] * $target['width']) + ($target['height'] * $target['height'])) / 2); + $crop = round($original['diagonal_center'] - $target['diagonal_center']); + if ($proportion_X < $proportion_Y) { + $target['x'] = 0; + $target['y'] = round((($currentHeight / 2) * $crop) / $target['diagonal_center']); + } else { + $target['x'] = round((($currentWidth / 2) * $crop) / $target['diagonal_center']); + $target['y'] = 0; + } + } + if ($is_beyond_original && ($width > $currentWidth || $height > $currentHeight)) { + $width = $currentWidth; + $height = $currentHeight; + } + if (function_exists('imagecreatetruecolor') && ($temp = imagecreatetruecolor($width, $height))) { + imagecopyresampled($temp, $image, 0, 0, $target['x'], $target['y'], $width, $height, $target['width'], $target['height']); + } else { + $temp = imagecreate($width, $height); + imagecopyresized($temp, $image, 0, 0, 0, 0, $width, $height, $currentWidth, $currentHeight); + } + if (strtolower($query_string['ext']) == 'png') { + imagepng($temp, $writeTo); + } else if (strtolower($query_string['ext']) == 'jpg' || strtolower($query_string['ext']) == 'jpeg') { + imagejpeg($temp, $writeTo, 100); + } else if (strtolower($query_string['ext']) == 'gif') { + imagegif($temp, $writeTo); + } + ob_start(); + call_user_func('image' . $types[$currentType], $temp); + $return = ob_get_clean(); + imagedestroy($image); + imagedestroy($temp); + } + return $return; +} +imageResize(); diff --git a/server/php/R/libs/core.php b/server/php/R/libs/core.php new file mode 100644 index 000000000..621fd0154 --- /dev/null +++ b/server/php/R/libs/core.php @@ -0,0 +1,932 @@ + + * @copyright 2014 Restya + * @license http://www.restya.com/ Restya Licence + * @link http://www.restya.com + */ +function getRandomStr($arr_characters, $length) +{ + $rand_str = ''; + $characters_length = count($arr_characters); + for ($i = 0; $i < $length; ++$i) { + $rand_str.= $arr_characters[rand(0, $characters_length - 1) ]; + } + return $rand_str; +} +/** + * To generate the encrypted password + * + * @param $str + * @return string + */ +function getCryptHash($str) +{ + if (CRYPT_BLOWFISH) { + if (version_compare(PHP_VERSION, '5.3.7') >= 0) { // http://www.php.net/security/crypt_blowfish.php + $algo_selector = '$2y$'; + } else { + $algo_selector = '$2a$'; + } + $workload_factor = '12$'; // (around 300ms on Core i7 machine) + $salt = $algo_selector . $workload_factor . getRandomStr(array_merge(array( + '.', + '/' + ) , range('0', '9') , range('a', 'z') , range('A', 'Z')) , 22); // './0-9A-Za-z' + + } else if (CRYPT_MD5) { + $algo_selector = '$1$'; + $salt = $algo_selector . getRandomStr(range(chr(33) , chr(127)) , 12); // actually chr(0) - chr(255), but used ASCII only + + } else if (CRYPT_SHA512) { + $algo_selector = '$6$'; + $workload_factor = 'rounds=5000$'; + $salt = $algo_selector . $workload_factor . getRandomStr(range(chr(33) , chr(127)) , 16); // actually chr(0) - chr(255) + + } else if (CRYPT_SHA256) { + $algo_selector = '$5$'; + $workload_factor = 'rounds=5000$'; + $salt = $algo_selector . $workload_factor . getRandomStr(range(chr(33) , chr(127)) , 16); // actually chr(0) - chr(255) + + } else if (CRYPT_EXT_DES) { + $algo_selector = '_'; + $salt = $algo_selector . getRandomStr(array_merge(array( + '.', + '/' + ) , range('0', '9') , range('a', 'z') , range('A', 'Z')) , 8); // './0-9A-Za-z'. + + } else if (CRYPT_STD_DES) { + $algo_selector = ''; + $salt = $algo_selector . getRandomStr(array_merge(array( + '.', + '/' + ) , range('0', '9') , range('a', 'z') , range('A', 'Z')) , 2); // './0-9A-Za-z' + + } + return crypt($str, $salt); +} +/** + * Execute CURL Request + * + * @param $url + * @param $method[optional] default value : get + * @param $post[optional] default value : array () + * @param $format[optional] default value : plain + * @return mixed + */ +function curlExecute($url, $method = 'get', $post = array() , $format = 'plain') +{ + $filename = ''; + $mediadir = ''; + if ($format == 'image') { + $mediadir = $post; + if (!file_exists($mediadir)) { + mkdir($mediadir, 0777, true); + } + $path = explode('/', $url); + $filename = $path[count($path) - 1]; + } + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + if ($format != 'image') { + curl_setopt($ch, CURLOPT_TIMEOUT, 300); // 300 seconds (5min) + + } + if ($method == 'get') { + curl_setopt($ch, CURLOPT_POST, false); + if ($format == 'image') { + curl_setopt($ch, CURLOPT_BINARYTRANSFER, true); + curl_setopt($ch, CURLOPT_HEADER, false); + } + } elseif ($method == 'post') { + if ($format == 'json') { + $post_string = json_encode($post); + curl_setopt($ch, CURLOPT_HTTPHEADER, array( + 'Content-Type: application/json', + 'Content-Length: ' . strlen($post_string) + )); + } else { + $post_string = http_build_query($post, '', '&'); + } + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $post_string); + } + $response = curl_exec($ch); + if ($format == 'image') { + $info = curl_getinfo($ch); + array_change_key_case($info); + $content_type = explode('/', $info['content_type']); + $filename = (strpos($filename, '.') !== false) ? $filename : $filename . '.' . $content_type[1]; + $filename = preg_replace('/[^A-Za-z0-9\-.]/', '', $filename); + curl_close($ch); + if (file_exists($mediadir . DIRECTORY_SEPARATOR . $filename)) { + unlink($mediadir . DIRECTORY_SEPARATOR . $filename); + } + $fp = fopen($mediadir . DIRECTORY_SEPARATOR . $filename, 'x'); + fwrite($fp, $response); + fclose($fp); + return $filename; + } + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if (curl_errno($ch)) { + $return['error']['message'] = curl_error($ch); + curl_close($ch); + return $return; + } + switch ($http_code) { + case 201: + case 200: + $return = json_decode($response, true); + if ($return === NULL) { + $error['error']['code'] = 1; + $error['error']['message'] = 'Syntax error, malformed JSON'; + $return = $error; + } + break; + + case 401: + $return['error']['code'] = 1; + $return['error']['message'] = 'Unauthorized'; + break; + + default: + $return['error']['code'] = 1; + $return['error']['message'] = 'Not Found'; + } + curl_close($ch); + return $return; +} +/** + * Post url by using CURL + * + * @param $url + * @param $post[optional] default value : array () + * @param $format[optional] default value : plain + * @return mixed + */ +function doPost($url, $post = array() , $format = 'plain') +{ + return curlExecute($url, 'post', $post, $format); +} +/** + * Get url by using CURL + * + * @param $url + * @return mixed + */ +function doGet($url) +{ + $return = curlExecute($url); + return $return; +} +/** + * Record each activities + * + * @param $user_id + * @param $comment + * @param $type + * @param $foreign_id[optional] default value : array () + * @param $revision[optional] default value : NULL + * @return mixed + */ +function insertActivity($user_id, $comment, $type, $foreign_ids = array() , $revision = NULL, $foreign_id = NULL) +{ + global $r_debug, $db_lnk; + $fields = array( + 'created', + 'modified', + 'user_id', + 'comment', + 'type', + 'revisions' + ); + $values = array( + 'now()', + 'now()', + $user_id, + $comment, + $type, + $revision + ); + if ($foreign_id != NULL) { + array_push($fields, 'foreign_id'); + array_push($values, $foreign_id); + } + $all_foreign_ids = $foreign_ids; + foreach ($foreign_ids as $key => $value) { + if ($key != 'id') { + array_push($fields, $key); + if ($value === false) { + array_push($values, 'false'); + } else { + array_push($values, $value); + } + } + } + if (!empty($foreign_ids['board_id'])) { + $val = ''; + for ($i = 1, $len = count($values); $i <= $len; $i++) { + $val.= '$' . $i; + $val.= ($i != $len) ? ', ' : ''; + } + $result = pg_query_params($db_lnk, 'INSERT INTO activities (' . implode(', ', $fields) . ') VALUES (' . $val . ') RETURNING *', $values); + } + $row = pg_fetch_assoc($result); + $id_converted = base_convert($row['id'], 10, 36); + $materialized_path = sprintf("%08s", $id_converted); + $freshness_ts = date('Y-m-d h:i:s'); + $path = 'P' . $row['id']; + $depth = 0; + $result = pg_query_params($db_lnk, 'UPDATE activities SET materialized_path = $1, path = $2, depth = $3, freshness_ts = $4 WHERE id = $5 RETURNING *', array( + $materialized_path, + $path, + $depth, + $freshness_ts, + $row['id'] + )); + $row = pg_fetch_assoc($result); + $s_row = pg_query_params($db_lnk, 'SELECT * FROM activities_listing WHERE id = $1', array( + $row['id'] + )); + $row = pg_fetch_assoc($s_row); + return $row; +} +/** + * Get difference between current and previous version + * + * @param $from_text + * @param $to_text + * @return difference + */ +function getRevisiondifference($from_text, $to_text) +{ + // limit input + $from_text = substr($from_text, 0, 1024 * 100); + $to_text = substr($to_text, 0, 1024 * 100); + // ensure input is suitable for diff + $from_text = mb_convert_encoding($from_text, 'HTML-ENTITIES', 'UTF-8'); + $to_text = mb_convert_encoding($to_text, 'HTML-ENTITIES', 'UTF-8'); + $granularity = 2; // 0: Paragraph/lines, 1: Sentence, 2: Word, 3: Character + $granularityStacks = array( + FineDiff::$paragraphGranularity, + FineDiff::$sentenceGranularity, + FineDiff::$wordGranularity, + FineDiff::$characterGranularity + ); + $diff_opcodes = FineDiff::getDiffOpcodes($from_text, $to_text, $granularityStacks[$granularity]); + $difference = FineDiff::renderDiffToHTMLFromOpcodes($from_text, $diff_opcodes); + return $difference; +} +/** + * LDAP authetication to login + * + * @param $p_user_id + * @param $p_password + * @return user + */ +function ldapAuthenticate($p_user_id, $p_password) +{ + $g_ldap_protocol_version = LDAP_PROTOCOL_VERSION; + $g_ldap_server = LDAP_SERVER; + $g_ldap_port = LDAP_PORT; + $g_ldap_root_dn = LDAP_ROOT_DN; + $g_ldap_organisation = ''; + $g_ldap_uid_field = LDAP_UID_FIELD; + $g_ldap_bind_dn = LDAP_BIND_DN; // A system account to login to LDAP + $g_ldap_bind_passwd = LDAP_BIND_PASSWD; // System account password + // if password is empty and ldap allows anonymous login, then + // the user will be able to login, hence, we need to check + // for this special case. + if (empty($p_password)) { + return false; + } + $t_ldap_organization = $g_ldap_organisation; + $t_ldap_root_dn = $g_ldap_root_dn; + $t_username = $p_user_id; + $t_ldap_uid_field = $g_ldap_uid_field; + $t_search_filter = "(&$t_ldap_organization($t_ldap_uid_field=$t_username))"; + $t_search_attrs = array( + $t_ldap_uid_field, + 'dn', + '*' + ); + $t_ldap_server = $g_ldap_server; + $t_ldap_port = $g_ldap_port; + $t_ds = @ldap_connect($t_ldap_server, $t_ldap_port); + if ($t_ds > 0) { + $t_protocol_version = $g_ldap_protocol_version; + if ($t_protocol_version > 0) { + ldap_set_option($t_ds, LDAP_OPT_PROTOCOL_VERSION, $t_protocol_version); + } + // If no Bind DN and Password is set, attempt to login as the configured + // Bind DN. + $t_password = ''; + $t_binddn = ''; + if (empty($t_binddn) && empty($t_password)) { + $t_binddn = $g_ldap_bind_dn; + $t_password = $g_ldap_bind_passwd; + } + if (!empty($t_binddn) && !empty($t_password)) { + $t_br = @ldap_bind($t_ds, $t_binddn, $t_password); + } else { + // Either the Bind DN or the Password are empty, so attempt an anonymous bind. + $t_br = @ldap_bind($t_ds); + } + if (!$t_br) { + trigger_error(ERROR_LDAP_AUTH_FAILED, ERROR); + } + } else { + trigger_error(ERROR_LDAP_SERVER_CONNECT_FAILED, ERROR); + } + // Search for the user id + $t_sr = ldap_search($t_ds, $t_ldap_root_dn, $t_search_filter, $t_search_attrs); + $t_info = ldap_get_entries($t_ds, $t_sr); + $user['User']['is_username_exits'] = false; + $user['User']['is_password_matched'] = false; + if ($t_info) { + $user['User']['is_username_exits'] = true; + // Try to authenticate to each until we get a match + for ($i = 0; $i < $t_info['count']; $i++) { + $t_dn = $t_info[$i]['dn']; + // Attempt to bind with the DN and password + if ($_data1 = @ldap_bind($t_ds, $t_dn, $p_password)) { + $user['User']['is_password_matched'] = true; + if (isset($t_info[$i]['name'])) { + $user['User']['first_name'] = $t_info[$i]['name'][0]; + } + if (isset($t_info[$i]['mail'])) { + $user['User']['email'] = $t_info[$i]['mail'][0]; + } + break; // Don't need to go any further + + } + } + } + ldap_free_result($t_sr); + ldap_unbind($t_ds); + return $user; +} +/** + * Check the current url and method can access by the user + * + * @param $r_request_method[optional] default value : 'GET' + * @param $r_resource_cmd[optional] default value : '/users' + * @param $r_resource_vars + * @return true if links allowed false otherwise + */ +function checkAclLinks($r_request_method = 'GET', $r_resource_cmd = '/users') +{ + global $r_debug, $db_lnk, $authUser; + $role = 3; // Guest role id + if ($authUser) { + $role = $authUser['role_id']; + } + $allowed_link = executeQuery('SELECT * FROM acl_links_listing WHERE role_id = $1 AND method = $2 AND url = $3', array( + $role, + $r_request_method, + $r_resource_cmd + )); + if (!empty($allowed_link)) { + return true; + } + return false; +} +/** + * To execute the query + * + * @param $qry + * @return mixed query results + */ +function executeQuery($qry, $arr = array()) +{ + global $db_lnk; + $result = pg_query_params($db_lnk, $qry, $arr); + if (pg_num_rows($result)) { + return pg_fetch_assoc($result); + } else { + return false; + } +} +/** + * Common method to send mail + * + * @param $data + * @return + */ +function sendMail($data) +{ + global $r_debug, $db_lnk; + $data['from'] = DEFAULT_FROM_EMAIL; + $data['##FROM_EMAIL##'] = DEFAULT_FROM_EMAIL; + $data['##SITE_NAME##'] = SITE_NAME; + $data['##SITE_URL##'] = 'http://' . $_SERVER['HTTP_HOST']; + $data['##CONTACT_MAIL##'] = DEFAULT_FROM_EMAIL; + $data['##SUPPORT_EMAIL##'] = DEFAULT_FROM_EMAIL; + $to = $data['to']; + $from = $data['from']; + $headers = 'From:' . SITE_NAME . '<' . $data['from'] . '>'; + $template = executeQuery('SELECT * FROM email_templates WHERE name = $1', array( + $data['mail'] + )); + if ($template) { + unset($data['mail']); + unset($data['from']); + unset($data['to']); + $subject = strtr($template['subject'], $data); + $message = strtr($template['email_text_content'], $data); + mail($to, $subject, $message, $headers); + } +} +/** + * Insert current access ip address into IPs table + * + * @return int IP id + */ +function saveIp() +{ + global $db_lnk; + $ip_row = executeQuery('SELECT id FROM ips WHERE ip = $1', array( + $_SERVER['REMOTE_ADDR'] + )); + if (!$ip_row) { + $country_id = $state_id = $city_id = 0; + $lat = $lng = 0.00; + if (!empty($_COOKIE['_geo'])) { + $_geo = explode('|', $_COOKIE['_geo']); + $country_row = executeQuery('SELECT id FROM countries WHERE iso_alpha2 = $1', array( + $_geo[0] + )); + if ($country_row) { + $country_id = $country_row['id']; + } + $state_row = executeQuery('SELECT id FROM states WHERE name = $1', array( + $_geo[1] + )); + if (!$state_row) { + $result = pg_query_params($db_lnk, 'INSERT INTO states (created, modified, name, country_id) VALUES (now(), now(), $1, $2) RETURNING id', array( + $_geo[1], + $country_id + )); + $state_row = pg_fetch_assoc($result); + } + $city_row = executeQuery('SELECT id FROM cities WHERE name = $1', array( + $_geo[2] + )); + if (!$city_row) { + $result = pg_query_params($db_lnk, 'INSERT INTO cities (created, modified, name, state_id, country_id, latitude, longitude) VALUES (now(), now(), $1, $2, $3, $4, $5) RETURNING id ', array( + $_geo[2], + $state_row['id'], + $country_id, + $_geo[3], + $_geo[4] + )); + $city_row = pg_fetch_assoc($result); + } + } + $user_agent = !empty($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; + $country_id = $country_id; + $state_id = (!empty($state_row['id'])) ? $state_row['id'] : $city_id; + $city_id = (!empty($city_row['id'])) ? $city_row['id'] : $city_id; + $lat = (!empty($_geo[3])) ? $_geo[3] : 0.00; + $lng = (!empty($_geo[4])) ? $_geo[4] : 0.00; + $result = pg_query_params($db_lnk, 'INSERT INTO ips (created, modified, ip, host, city_id, state_id, country_id, latitude, longitude, user_agent) VALUES (now(), now(), $1, $2, $3, $4, $5, $6, $7, $8) RETURNING id', array( + $_SERVER['REMOTE_ADDR'], + gethostbyaddr($_SERVER['REMOTE_ADDR']) , + $city_id, + $state_id, + $country_id, + $lat, + $lng, + $user_agent + )); + $ip_row = pg_fetch_assoc($result); + } + return $ip_row['id']; +} +/** + * Copy Card + * + * + * @param $card_fields + * @param $cards + * @param $new_list_id + * @param $name + * + */ +function copyCards($card_fields, $cards, $new_list_id, $name, $new_board_id = '') +{ + global $db_lnk, $authUser; + while ($card = pg_fetch_object($cards)) { + $card->list_id = $new_list_id; + $card_id = $card->id; + if ($card->due_date === NULL) { + unset($card->due_date); + } + $card_result = pg_execute_insert('cards', $card); + if ($card_result) { + $card_result = pg_fetch_assoc($card_result); + $new_card_id = $card_result['id']; + $foreign_ids['card_id'] = $new_card_id; + $foreign_ids['board_id'] = $new_board_id; + $foreign_ids['list_id'] = $new_list_id; + $comment = $authUser['username'] . ' added ' . $card_result['name'] . ' card to ' . $name . '.'; + insertActivity($authUser['id'], $comment, 'add_card', $foreign_ids); + //Copy card attachments + $attachment_fields = 'list_id, card_id, name, path, mimetype'; + if (!empty($new_board_id)) { + $attachment_fields = 'board_id, list_id, card_id, name, path, mimetype'; + } + $attachments = pg_query_params($db_lnk, 'SELECT id, ' . $attachment_fields . ' FROM card_attachments WHERE card_id = $1 ORDER BY id', array( + $card_id + )); + if ($attachments && pg_num_rows($attachments)) { + while ($attachment = pg_fetch_object($attachments)) { + $attachment->board_id = $new_board_id; + $attachment->list_id = $new_list_id; + $attachment->card_id = $new_card_id; + $attachment_result = pg_execute_insert('card_attachments', $attachment); + $attachment_result = pg_fetch_assoc($attachment_result); + $comment = $authUser['username'] . ' added attachment to this card ##CARD_LINK##'; + insertActivity($authUser['id'], $comment, 'add_card_attachment', $foreign_ids, NULL, $attachment_result['id']); + } + } + //Copy card comments + $comment_fields = 'list_id, card_id, board_id, user_id, type, comment, root, freshness_ts, depth, path, materialized_path'; + $comments = pg_query_params($db_lnk, 'SELECT id, ' . $comment_fields . ' FROM activities WHERE card_id = $1 AND type = $2 ORDER BY id', array( + $card_id, + 'add_comment' + )); + if ($comments && pg_num_rows($comments)) { + while ($comment = pg_fetch_object($comments)) { + $comment->board_id = $new_board_id; + $comment->list_id = $new_list_id; + $comment->card_id = $new_card_id; + $card_result = pg_execute_insert('activities', $comment); + } + } + //Copy checklists + $checklist_fields = 'card_id, user_id, name, checklist_item_count, checklist_item_completed_count, position'; + $checklists = pg_query_params($db_lnk, 'SELECT id, ' . $checklist_fields . ' FROM checklists WHERE card_id = $1 ORDER BY id', array( + $card_id + )); + if ($checklists && pg_num_rows($checklists)) { + while ($checklist = pg_fetch_object($checklists)) { + $checklist_id = $checklist->id; + $checklist->card_id = $new_card_id; + $checklist_result = pg_execute_insert('checklists', $checklist); + if ($checklist_result) { + $checklist_result = pg_fetch_assoc($checklist_result); + $new_checklist_id = $checklist_result['id']; + $comment = $authUser['username'] . ' added checklist to this card ##CARD_LINK##'; + insertActivity($authUser['id'], $comment, 'add_card_checklist', $foreign_ids, '', $new_checklist_id); + $copy_checklists[] = $checklist_result; + //Copy checklist items + $checklist_item_fields = 'card_id, checklist_id, user_id, name, position'; + $checklist_items = pg_query_params($db_lnk, 'SELECT id, ' . $checklist_item_fields . ' FROM checklist_items WHERE checklist_id = $1 ORDER BY id', array( + $checklist_id + )); + if ($checklist_items && pg_num_rows($checklist_items)) { + while ($checklist_item = pg_fetch_object($checklist_items)) { + $checklist_item->card_id = $new_card_id; + $checklist_item->checklist_id = $new_checklist_id; + $checklist_item_result = pg_execute_insert('checklist_items', $checklist_item); + $checklist_item_result = pg_fetch_assoc($checklist_item_result); + $copy_checklists_items[] = $checklist_item_result; + $comment = $authUser['username'] . ' added checklist item to this card ##CARD_LINK##'; + insertActivity($authUser['id'], $comment, 'add_checklist_item', $foreign_ids, '', $checklist_item_result['id']); + } + } + } + } + } + //Copy card labels + $cards_label_fields = 'list_id, card_id, board_id, label_id'; + if (!empty($new_board_id)) { + $cards_label_fields = 'board_id, list_id, card_id, label_id'; + } + $cards_labels = pg_query_params($db_lnk, 'SELECT id, ' . $cards_label_fields . ' FROM cards_labels WHERE card_id = $1 ORDER BY id', array( + $card_id + )); + if ($cards_labels && pg_num_rows($cards_labels)) { + while ($cards_label = pg_fetch_object($cards_labels)) { + if (!empty($new_board_id)) { + $cards_label->board_id = $new_board_id; + $cards_label->list_id = $new_list_id; + $cards_label->card_id = $new_card_id; + $cards_label_values = $new_board_id . ', ' . $new_list_id . ', ' . $new_card_id; + } else { + $cards_label_values = $new_list_id . ', ' . $new_card_id; + } + $cards_label_result = pg_execute_insert('cards_labels', $cards_label); + $cards_label_result = pg_fetch_assoc($cards_label_result); + $comment = $authUser['username'] . ' added label(s) to this card ##CARD_LINK## - ##LABEL_NAME##'; + insertActivity($authUser['id'], $comment, 'add_card_label', $foreign_ids); + } + } + //Copy card users + $cards_user_fields = 'card_id, user_id'; + $cards_users = pg_query_params($db_lnk, 'SELECT id, ' . $cards_user_fields . ' FROM cards_users WHERE card_id = $1 ORDER BY id', array( + $card_id + )); + if ($cards_users && pg_num_rows($cards_users)) { + while ($cards_user = pg_fetch_object($cards_users)) { + $cards_user->card_id = $new_card_id; + $cards_user_result = pg_execute_insert('cards_users', $cards_user); + $cards_user_result = pg_fetch_assoc($cards_user_result); + $_user = executeQuery('SELECT username FROM users WHERE id = $1', array( + $cards_user->user_id + )); + $comment = $authUser['username'] . ' added ' . $_user['username'] . ' as member to this card ##CARD_LINK##'; + $response['activity'] = insertActivity($authUser['id'], $comment, 'add_card_user', $foreign_ids, '', $cards_user_result['id']); + } + } + } + } +} +/** + * To generate query by passed args and insert into table + * + * @param $table_name + * @param $r_post + * @return mixed + */ +function pg_execute_insert($table_name, $r_post, $return_row = 1) +{ + global $db_lnk; + $fields = 'created, modified'; + $values = 'now(), now()'; + $val_arr = array(); + $i = 1; + foreach ($r_post as $key => $value) { + if ($key != 'id') { + $fields.= ', "' . $key . '"'; + $values.= ', $' . $i; + if ($value === false) { + $val_arr[] = 'false'; + } else if ($value === NULL) { + $val_arr[] = NULL; + } else { + $val_arr[] = $value; + } + $i++; + } + } + if (!empty($return_row)) { + $row = pg_query_params($db_lnk, 'INSERT INTO ' . $table_name . ' (' . $fields . ') VALUES (' . $values . ') RETURNING *', $val_arr); + } else { + $row = pg_query_params($db_lnk, 'INSERT INTO ' . $table_name . ' (' . $fields . ') VALUES (' . $values . ')', $val_arr); + } + return $row; +} +/** + * Common method to get binded values + * + * @param $table + * @param $data + * @param $expected_fields_arr[optional] default value : array () + * @return mixed + */ +function getbindValues($table, $data, $expected_fields_arr = array()) +{ + global $db_lnk; + $result = pg_query_params($db_lnk, 'SELECT * FROM information_schema.columns WHERE table_name = $1 ', array( + $table + )); + $bindValues = array(); + while ($field_details = pg_fetch_assoc($result)) { + $field = $field_details['column_name']; + if (in_array($field, array( + 'created', + 'modified' + ))) { + continue; + } + //todo : get list_id from lists table + if ($field == 'id' && $table == 'lists' && array_key_exists('list_id', $data)) { + $bindValues['id'] = $data['list_id']; + } + if ($field == 'ip_id') { + $data['ip'] = !empty($data['ip']) ? $data['ip'] : ''; + $ip_id = saveIp(); + $bindValues[$field] = $ip_id; + } elseif (array_key_exists($field, $data)) { + if ($field == 'is_active' || $field == 'is_allow_email_alias') { + $boolean = !empty($data[$field]) ? 'true' : 'false'; + $bindValues[$field] = $boolean; + } else if ($field == 'due_date' && $data[$field] == NULL) { + $bindValues[$field] = NULL; + } else { + $bindValues[$field] = $data[$field]; + } + } + } + return $bindValues; +} +/** + * Import trello + * + * @param $url + * @return mixed + */ +function importTrelloBoard($board = array()) +{ + global $r_debug, $db_lnk, $authUser, $_server_domain_url; + $users = array(); + if (!empty($board)) { + $user_id = $authUser['id']; + $board_visibility = array( + 'Private', + 'Organization', + 'Public' + ); + $board_visibility = 0; + if ($board['prefs']['permissionLevel'] == 'public') { + $board_visibility = 2; + } + $background_image = $background_pattern = ''; + if (!empty($board['prefs']['backgroundImage'])) { + if ($board['prefs']['backgroundTile'] == 'true') { + $background_pattern = $board['prefs']['backgroundImage']; + } else { + $background_image = $board['prefs']['backgroundImage']; + } + } + $new_board = pg_fetch_assoc(pg_query_params($db_lnk, 'INSERT INTO boards (created, modified, name, background_color, background_picture_url, background_pattern_url, user_id, board_visibility) VALUES (now(), now(), $1, $2, $3, $4, $5, $6) RETURNING id', array( + $board['name'], + $board['prefs']['backgroundColor'], + $background_image, + $background_pattern, + $user_id, + $board_visibility + ))); + $admin_user_id = array(); + if (!empty($board['members'])) { + foreach ($board['memberships'] as $membership) { + if ($membership['memberType'] == 'admin') { + $admin_user_id[] = $membership['idMember']; + } + } + } + if (!empty($board['members'])) { + foreach ($board['members'] as $member) { + $userExist = executeQuery('SELECT * FROM users WHERE username = $1', array( + $member['username'] + )); + if (!$userExist) { + $user = pg_fetch_assoc(pg_query_params($db_lnk, 'INSERT INTO users (created, modified, role_id, username, email, password, is_active, is_email_confirmed, initials, full_name) VALUES (now(), now(), 2, $1, \'\', $2, TRUE, TRUE, $3, $4) RETURNING id', array( + $member['username'], + getCryptHash('restya') , + $member['initials'], + $member['fullName'] + ))); + $users[$member['id']] = $user['id']; + } else { + $users[$member['id']] = $userExist['id']; + } + $is_admin = 'false'; + if (in_array($member['id'], $admin_user_id)) { + $is_admin = 'true'; + } + pg_fetch_assoc(pg_query_params($db_lnk, 'INSERT INTO boards_users (created, modified, user_id, board_id, is_admin) VALUES (now(), now(), $1, $2, $3) RETURNING id', array( + $users[$member['id']], + $new_board['id'], + $is_admin + ))); + } + } + pg_fetch_assoc(pg_query_params($db_lnk, 'INSERT INTO boards_users (created, modified, user_id, board_id, is_admin) VALUES (now(), now(), $1, $2, $3) RETURNING id', array( + $authUser['id'], + $new_board['id'], + 'true' + ))); + if (!empty($board['lists'])) { + $lists = array(); + $i = 0; + foreach ($board['lists'] as $list) { + $i+= 1; + $is_closed = ($list['closed']) ? 'true' : 'false'; + $_list = pg_fetch_assoc(pg_query_params($db_lnk, 'INSERT INTO lists (created, modified, name, board_id, position, user_id, is_archived) VALUES (now(), now(), $1, $2, $3, $4, $5) RETURNING id', array( + $list['name'], + $new_board['id'], + $i, + $user_id, + $is_closed + ))); + $lists[$list['id']] = $_list['id']; + } + } + if (!empty($board['cards'])) { + $cards = array(); + foreach ($board['cards'] as $card) { + $is_closed = ($card['closed']) ? 'true' : 'false'; + $date = NULL; + if (!empty($card['due'])) { + $date = str_replace('T', ' ', $card['due']); + } + $_card = pg_fetch_assoc(pg_query_params($db_lnk, 'INSERT INTO cards (created, modified, board_id, list_id, name, description, is_archived, position, due_date, user_id) VALUES (now(), now(), $1, $2, $3, $4, $5, $6, $7, $8) RETURNING id', array( + $new_board['id'], + $lists[$card['idList']], + $card['name'], + $card['desc'], + $is_closed, + $card['pos'], + $date, + $user_id + ))); + $cards[$card['id']] = $_card['id']; + if (!empty($card['labels'])) { + foreach ($card['labels'] as $label) { + $check_label = executeQuery('SELECT id FROM labels WHERE name = $1', array( + $label['color'] + )); + if (empty($check_label)) { + $check_label = pg_fetch_assoc(pg_query_params($db_lnk, 'INSERT INTO labels (created, modified, name) VALUES (now(), now(), $1) RETURNING id', array( + $label['color'] + ))); + } + $_label = pg_fetch_assoc(pg_query_params($db_lnk, 'INSERT INTO cards_labels (created, modified, board_id, list_id, card_id, label_id) VALUES (now(), now(), $1, $2, $3, $4) RETURNING id', array( + $new_board['id'], + $lists[$card['idList']], + $_card['id'], + $check_label['id'] + ))); + } + } + if (!empty($card['attachments'])) { + foreach ($card['attachments'] as $attachment) { + $mediadir = APP_PATH . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'Card' . DIRECTORY_SEPARATOR . $_card['id']; + $save_path = $_server_domain_url . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'Card' . DIRECTORY_SEPARATOR . $_card['id']; + $filename = curlExecute($attachment['url'], 'get', $mediadir, 'image'); + $path = $save_path . DIRECTORY_SEPARATOR . $filename; + $name = $filename; + $created = $modified = str_replace('T', ' ', $attachment['date']); + pg_fetch_assoc(pg_query_params($db_lnk, 'INSERT INTO card_attachments (created, modified, board_id, list_id, card_id, name, path, mimetype) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id', array( + $created, + $modified, + $new_board['id'], + $lists[$card['idList']], + $_card['id'], + $filename, + $path, + $attachment['mimeType'] + ))); + } + } + } + } + if (!empty($board['checklists'])) { + $checklists = array(); + foreach ($board['checklists'] as $checklist) { + $_checklist = pg_fetch_assoc(pg_query_params($db_lnk, 'INSERT INTO checklists (created, modified, name, position, card_id, user_id) VALUES (now(), now(), $1, $2, $3, $4) RETURNING id', array( + $checklist['name'], + $checklist['pos'], + $cards[$checklist['idCard']], + $user_id + ))); + $checklists[$checklist['id']] = $_checklist['id']; + if (!empty($checklist['checkItems'])) { + foreach ($checklist['checkItems'] as $checkItem) { + $is_completed = ($checkItem['state'] == 'complete') ? 'true' : 'false'; + pg_fetch_assoc(pg_query_params($db_lnk, 'INSERT INTO checklist_items (created, modified, name, position, card_id, checklist_id, is_completed, user_id) VALUES (now(), now(), $1, $2, $3, $4, $5, $6) RETURNING id', array( + $checkItem['name'], + $checkItem['pos'], + $cards[$checklist['idCard']], + $_checklist['id'], + $is_completed, + $user_id + ))); + } + } + } + } + if (!empty($board['actions'])) { + foreach ($board['actions'] as $action) { + if ($action['type'] == 'commentCard') { + $created = $modified = str_replace('T', ' ', $action['date']); + pg_fetch_assoc(pg_query_params($db_lnk, 'INSERT INTO activities (created, modified, board_id, list_id, card_id, user_id, type, comment) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id', array( + $created, + $modified, + $new_board['id'], + $lists[$action['data']['list']['id']], + $cards[$action['data']['card']['id']], + $user_id, + 'add_comment', + $action['data']['text'] + ))); + } + } + } + return $new_board; + } +} diff --git a/server/php/R/libs/vendors/OAuth2/Autoloader.php b/server/php/R/libs/vendors/OAuth2/Autoloader.php new file mode 100644 index 000000000..ecfb6ba75 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/Autoloader.php @@ -0,0 +1,48 @@ + + * @license MIT License + */ +class Autoloader +{ + private $dir; + + public function __construct($dir = null) + { + if (is_null($dir)) { + $dir = dirname(__FILE__).'/..'; + } + $this->dir = $dir; + } + /** + * Registers OAuth2\Autoloader as an SPL autoloader. + */ + public static function register($dir = null) + { + ini_set('unserialize_callback_func', 'spl_autoload_call'); + spl_autoload_register(array(new self($dir), 'autoload')); + } + + /** + * Handles autoloading of classes. + * + * @param string $class A class name. + * + * @return boolean Returns true if the class has been loaded + */ + public function autoload($class) + { + if (0 !== strpos($class, 'OAuth2')) { + return; + } + + if (file_exists($file = $this->dir.'/'.str_replace('\\', '/', $class).'.php')) { + require $file; + } + } +} diff --git a/server/php/R/libs/vendors/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php b/server/php/R/libs/vendors/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php new file mode 100644 index 000000000..5d0bc88ff --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php @@ -0,0 +1,15 @@ + + */ +class HttpBasic implements ClientAssertionTypeInterface +{ + private $clientData; + + protected $storage; + protected $config; + + /** + * @param OAuth2\Storage\ClientCredentialsInterface $clientStorage + * REQUIRED Storage class for retrieving client credentials information + * @param array $config + * OPTIONAL Configuration options for the server + * @code + * $config = array( + * 'allow_credentials_in_request_body' => true, // whether to look for credentials in the POST body in addition to the Authorize HTTP Header + * 'allow_public_clients' => true // if true, "public clients" (clients without a secret) may be authenticated + * ); + * @endcode + */ + public function __construct(ClientCredentialsInterface $storage, array $config = array()) + { + $this->storage = $storage; + $this->config = array_merge(array( + 'allow_credentials_in_request_body' => true, + 'allow_public_clients' => true, + ), $config); + } + + public function validateRequest(RequestInterface $request, ResponseInterface $response) + { + if (!$clientData = $this->getClientCredentials($request, $response)) { + return false; + } + + if (!isset($clientData['client_id'])) { + throw new \LogicException('the clientData array must have "client_id" set'); + } + + if (!isset($clientData['client_secret']) || $clientData['client_secret'] == '') { + if (!$this->config['allow_public_clients']) { + $response->setError(400, 'invalid_client', 'client credentials are required'); + + return false; + } + + if (!$this->storage->isPublicClient($clientData['client_id'])) { + $response->setError(400, 'invalid_client', 'This client is invalid or must authenticate using a client secret'); + + return false; + } + } elseif ($this->storage->checkClientCredentials($clientData['client_id'], $clientData['client_secret']) === false) { + $response->setError(400, 'invalid_client', 'The client credentials are invalid'); + + return false; + } + + $this->clientData = $clientData; + + return true; + } + + public function getClientId() + { + return $this->clientData['client_id']; + } + + /** + * Internal function used to get the client credentials from HTTP basic + * auth or POST data. + * + * According to the spec (draft 20), the client_id can be provided in + * the Basic Authorization header (recommended) or via GET/POST. + * + * @return + * A list containing the client identifier and password, for example + * @code + * return array( + * "client_id" => CLIENT_ID, // REQUIRED the client id + * "client_secret" => CLIENT_SECRET, // OPTIONAL the client secret (may be omitted for public clients) + * ); + * @endcode + * + * @see http://tools.ietf.org/html/rfc6749#section-2.3.1 + * + * @ingroup oauth2_section_2 + */ + public function getClientCredentials(RequestInterface $request, ResponseInterface $response = null) + { + if (!is_null($request->headers('PHP_AUTH_USER')) && !is_null($request->headers('PHP_AUTH_PW'))) { + return array('client_id' => $request->headers('PHP_AUTH_USER'), 'client_secret' => $request->headers('PHP_AUTH_PW')); + } + + if ($this->config['allow_credentials_in_request_body']) { + // Using POST for HttpBasic authorization is not recommended, but is supported by specification + if (!is_null($request->request('client_id'))) { + /** + * client_secret can be null if the client's password is an empty string + * @see http://tools.ietf.org/html/rfc6749#section-2.3.1 + */ + return array('client_id' => $request->request('client_id'), 'client_secret' => $request->request('client_secret')); + } + } + + if ($response) { + $message = $this->config['allow_credentials_in_request_body'] ? ' or body' : ''; + $response->setError(400, 'invalid_client', 'Client credentials were not found in the headers'.$message); + } + + return null; + } +} diff --git a/server/php/R/libs/vendors/OAuth2/Controller/AuthorizeController.php b/server/php/R/libs/vendors/OAuth2/Controller/AuthorizeController.php new file mode 100644 index 000000000..e669824c7 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/Controller/AuthorizeController.php @@ -0,0 +1,375 @@ + false, // if the controller should allow the "implicit" grant type + * 'enforce_state' => true // if the controller should require the "state" parameter + * 'require_exact_redirect_uri' => true, // if the controller should require an exact match on the "redirect_uri" parameter + * 'redirect_status_code' => 302, // HTTP status code to use for redirect responses + * ); + * @endcode + * @param OAuth2\ScopeInterface $scopeUtil + * OPTIONAL Instance of OAuth2\ScopeInterface to validate the requested scope + */ + public function __construct(ClientInterface $clientStorage, array $responseTypes = array(), array $config = array(), ScopeInterface $scopeUtil = null) + { + $this->clientStorage = $clientStorage; + $this->responseTypes = $responseTypes; + $this->config = array_merge(array( + 'allow_implicit' => false, + 'enforce_state' => true, + 'require_exact_redirect_uri' => true, + 'redirect_status_code' => 302, + ), $config); + + if (is_null($scopeUtil)) { + $scopeUtil = new Scope(); + } + $this->scopeUtil = $scopeUtil; + } + + public function handleAuthorizeRequest(RequestInterface $request, ResponseInterface $response, $is_authorized, $user_id = null) + { + if (!is_bool($is_authorized)) { + throw new \InvalidArgumentException('Argument "is_authorized" must be a boolean. This method must know if the user has granted access to the client.'); + } + + // We repeat this, because we need to re-validate. The request could be POSTed + // by a 3rd-party (because we are not internally enforcing NONCEs, etc) + if (!$this->validateAuthorizeRequest($request, $response)) { + return; + } + + // If no redirect_uri is passed in the request, use client's registered one + if (empty($this->redirect_uri)) { + $clientData = $this->clientStorage->getClientDetails($this->client_id); + $registered_redirect_uri = $clientData['redirect_uri']; + } + + // the user declined access to the client's application + if ($is_authorized === false) { + $redirect_uri = $this->redirect_uri ?: $registered_redirect_uri; + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $this->state, 'access_denied', "The user denied access to your application"); + + return; + } + + // build the parameters to set in the redirect URI + if (!$params = $this->buildAuthorizeParameters($request, $response, $user_id)) { + return; + } + + $authResult = $this->responseTypes[$this->response_type]->getAuthorizeResponse($params, $user_id); + + list($redirect_uri, $uri_params) = $authResult; + + if (empty($redirect_uri) && !empty($registered_redirect_uri)) { + $redirect_uri = $registered_redirect_uri; + } + + $uri = $this->buildUri($redirect_uri, $uri_params); + + // return redirect response + $response->setRedirect($this->config['redirect_status_code'], $uri); + } + + /* + * We have made this protected so this class can be extended to add/modify + * these parameters + */ + protected function buildAuthorizeParameters($request, $response, $user_id) + { + // @TODO: we should be explicit with this in the future + $params = array( + 'scope' => $this->scope, + 'state' => $this->state, + 'client_id' => $this->client_id, + 'redirect_uri' => $this->redirect_uri, + 'response_type' => $this->response_type, + ); + + return $params; + } + + public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response) + { + // Make sure a valid client id was supplied (we can not redirect because we were unable to verify the URI) + if (!$client_id = $request->query("client_id")) { + // We don't have a good URI to use + $response->setError(400, 'invalid_client', "No client id supplied"); + + return false; + } + + // Get client details + if (!$clientData = $this->clientStorage->getClientDetails($client_id)) { + $response->setError(400, 'invalid_client', 'The client id supplied is invalid'); + + return false; + } + + $registered_redirect_uri = isset($clientData['redirect_uri']) ? $clientData['redirect_uri'] : ''; + + // Make sure a valid redirect_uri was supplied. If specified, it must match the clientData URI. + // @see http://tools.ietf.org/html/rfc6749#section-3.1.2 + // @see http://tools.ietf.org/html/rfc6749#section-4.1.2.1 + // @see http://tools.ietf.org/html/rfc6749#section-4.2.2.1 + if ($supplied_redirect_uri = $request->query('redirect_uri')) { + // validate there is no fragment supplied + $parts = parse_url($supplied_redirect_uri); + if (isset($parts['fragment']) && $parts['fragment']) { + $response->setError(400, 'invalid_uri', 'The redirect URI must not contain a fragment'); + + return false; + } + + // validate against the registered redirect uri(s) if available + if ($registered_redirect_uri && !$this->validateRedirectUri($supplied_redirect_uri, $registered_redirect_uri)) { + $response->setError(400, 'redirect_uri_mismatch', 'The redirect URI provided is missing or does not match', '#section-3.1.2'); + + return false; + } + $redirect_uri = $supplied_redirect_uri; + } else { + // use the registered redirect_uri if none has been supplied, if possible + if (!$registered_redirect_uri) { + $response->setError(400, 'invalid_uri', 'No redirect URI was supplied or stored'); + + return false; + } + + if (count(explode(' ', $registered_redirect_uri)) > 1) { + $response->setError(400, 'invalid_uri', 'A redirect URI must be supplied when multiple redirect URIs are registered', '#section-3.1.2.3'); + + return false; + } + $redirect_uri = $registered_redirect_uri; + } + + // Select the redirect URI + $response_type = $request->query('response_type'); + $state = $request->query('state'); + + // type and client_id are required + if (!$response_type || !in_array($response_type, $this->getValidResponseTypes())) { + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'invalid_request', 'Invalid or missing response type', null); + + return false; + } + if ($response_type == self::RESPONSE_TYPE_AUTHORIZATION_CODE) { + if (!isset($this->responseTypes['code'])) { + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unsupported_response_type', 'authorization code grant type not supported', null); + + return false; + } + if (!$this->clientStorage->checkRestrictedGrantType($client_id, 'authorization_code')) { + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unauthorized_client', 'The grant type is unauthorized for this client_id', null); + + return false; + } + if ($this->responseTypes['code']->enforceRedirect() && !$redirect_uri) { + $response->setError(400, 'redirect_uri_mismatch', 'The redirect URI is mandatory and was not supplied'); + + return false; + } + } else { + if (!$this->config['allow_implicit']) { + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unsupported_response_type', 'implicit grant type not supported', null); + + return false; + } + if (!$this->clientStorage->checkRestrictedGrantType($client_id, 'implicit')) { + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unauthorized_client', 'The grant type is unauthorized for this client_id', null); + + return false; + } + } + + // validate requested scope if it exists + $requestedScope = $this->scopeUtil->getScopeFromRequest($request); + + if ($requestedScope) { + // restrict scope by client specific scope if applicable, + // otherwise verify the scope exists + $clientScope = $this->clientStorage->getClientScope($client_id); + if ((is_null($clientScope) && !$this->scopeUtil->scopeExists($requestedScope)) + || ($clientScope && !$this->scopeUtil->checkScope($requestedScope, $clientScope))) { + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'invalid_scope', 'An unsupported scope was requested', null); + + return false; + } + } else { + // use a globally-defined default scope + $defaultScope = $this->scopeUtil->getDefaultScope($client_id); + + if (false === $defaultScope) { + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'invalid_client', 'This application requires you specify a scope parameter', null); + + return false; + } + + $requestedScope = $defaultScope; + } + + // Validate state parameter exists (if configured to enforce this) + if ($this->config['enforce_state'] && !$state) { + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, null, 'invalid_request', 'The state parameter is required'); + + return false; + } + + // save the input data and return true + $this->scope = $requestedScope; + $this->state = $state; + $this->client_id = $client_id; + // Only save the SUPPLIED redirect URI (@see http://tools.ietf.org/html/rfc6749#section-4.1.3) + $this->redirect_uri = $supplied_redirect_uri; + $this->response_type = $response_type; + + return true; + } + + /** + * Build the absolute URI based on supplied URI and parameters. + * + * @param $uri + * An absolute URI. + * @param $params + * Parameters to be append as GET. + * + * @return + * An absolute URI with supplied parameters. + * + * @ingroup oauth2_section_4 + */ + private function buildUri($uri, $params) + { + $parse_url = parse_url($uri); + + // Add our params to the parsed uri + foreach ($params as $k => $v) { + if (isset($parse_url[$k])) { + $parse_url[$k] .= "&" . http_build_query($v); + } else { + $parse_url[$k] = http_build_query($v); + } + } + + // Put humpty dumpty back together + return + ((isset($parse_url["scheme"])) ? $parse_url["scheme"] . "://" : "") + . ((isset($parse_url["user"])) ? $parse_url["user"] + . ((isset($parse_url["pass"])) ? ":" . $parse_url["pass"] : "") . "@" : "") + . ((isset($parse_url["host"])) ? $parse_url["host"] : "") + . ((isset($parse_url["port"])) ? ":" . $parse_url["port"] : "") + . ((isset($parse_url["path"])) ? $parse_url["path"] : "") + . ((isset($parse_url["query"]) && !empty($parse_url['query'])) ? "?" . $parse_url["query"] : "") + . ((isset($parse_url["fragment"])) ? "#" . $parse_url["fragment"] : "") + ; + } + + protected function getValidResponseTypes() + { + return array( + self::RESPONSE_TYPE_ACCESS_TOKEN, + self::RESPONSE_TYPE_AUTHORIZATION_CODE, + ); + } + + /** + * Internal method for validating redirect URI supplied + * + * @param string $inputUri + * The submitted URI to be validated + * @param string $registeredUriString + * The allowed URI(s) to validate against. Can be a space-delimited string of URIs to + * allow for multiple URIs + * + * @see http://tools.ietf.org/html/rfc6749#section-3.1.2 + */ + private function validateRedirectUri($inputUri, $registeredUriString) + { + if (!$inputUri || !$registeredUriString) { + return false; // if either one is missing, assume INVALID + } + + $registered_uris = explode(' ', $registeredUriString); + foreach ($registered_uris as $registered_uri) { + if ($this->config['require_exact_redirect_uri']) { + // the input uri is validated against the registered uri using exact match + if (strcmp($inputUri, $registered_uri) === 0) { + return true; + } + } else { + // the input uri is validated against the registered uri using case-insensitive match of the initial string + // i.e. additional query parameters may be applied + if (strcasecmp(substr($inputUri, 0, strlen($registered_uri)), $registered_uri) === 0) { + return true; + } + } + } + + return false; + } + + /** + * Convenience methods to access the parameters derived from the validated request + */ + + public function getScope() + { + return $this->scope; + } + + public function getState() + { + return $this->state; + } + + public function getClientId() + { + return $this->client_id; + } + + public function getRedirectUri() + { + return $this->redirect_uri; + } + + public function getResponseType() + { + return $this->response_type; + } +} diff --git a/server/php/R/libs/vendors/OAuth2/Controller/AuthorizeControllerInterface.php b/server/php/R/libs/vendors/OAuth2/Controller/AuthorizeControllerInterface.php new file mode 100644 index 000000000..fa07ae8d2 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/Controller/AuthorizeControllerInterface.php @@ -0,0 +1,43 @@ + $user_id = $this->somehowDetermineUserId(); + * > $is_authorized = $this->somehowDetermineUserAuthorization(); + * > $response = new OAuth2\Response(); + * > $authorizeController->handleAuthorizeRequest( + * > OAuth2\Request::createFromGlobals(), + * > $response, + * > $is_authorized, + * > $user_id); + * > $response->send(); + * + */ +interface AuthorizeControllerInterface +{ + /** + * List of possible authentication response types. + * The "authorization_code" mechanism exclusively supports 'code' + * and the "implicit" mechanism exclusively supports 'token'. + * + * @var string + * @see http://tools.ietf.org/html/rfc6749#section-4.1.1 + * @see http://tools.ietf.org/html/rfc6749#section-4.2.1 + */ + const RESPONSE_TYPE_AUTHORIZATION_CODE = 'code'; + const RESPONSE_TYPE_ACCESS_TOKEN = 'token'; + + public function handleAuthorizeRequest(RequestInterface $request, ResponseInterface $response, $is_authorized, $user_id = null); + + public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response); +} diff --git a/server/php/R/libs/vendors/OAuth2/Controller/ResourceController.php b/server/php/R/libs/vendors/OAuth2/Controller/ResourceController.php new file mode 100644 index 000000000..9623b9f92 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/Controller/ResourceController.php @@ -0,0 +1,111 @@ +tokenType = $tokenType; + $this->tokenStorage = $tokenStorage; + + $this->config = array_merge(array( + 'www_realm' => 'Service', + ), $config); + + if (is_null($scopeUtil)) { + $scopeUtil = new Scope(); + } + $this->scopeUtil = $scopeUtil; + } + + public function verifyResourceRequest(RequestInterface $request, ResponseInterface $response, $scope = null) + { + $token = $this->getAccessTokenData($request, $response); + + // Check if we have token data + if (is_null($token)) { + return false; + } + + /** + * Check scope, if provided + * If token doesn't have a scope, it's null/empty, or it's insufficient, then throw 403 + * @see http://tools.ietf.org/html/rfc6750#section-3.1 + */ + if ($scope && (!isset($token["scope"]) || !$token["scope"] || !$this->scopeUtil->checkScope($scope, $token["scope"]))) { + $response->setError(403, 'insufficient_scope', 'The request requires higher privileges than provided by the access token'); + $response->addHttpHeaders(array( + 'WWW-Authenticate' => sprintf('%s realm="%s", scope="%s", error="%s", error_description="%s"', + $this->tokenType->getTokenType(), + $this->config['www_realm'], + $scope, + $response->getParameter('error'), + $response->getParameter('error_description') + ) + )); + + return false; + } + + // allow retrieval of the token + $this->token = $token; + + return (bool) $token; + } + + public function getAccessTokenData(RequestInterface $request, ResponseInterface $response) + { + // Get the token parameter + if ($token_param = $this->tokenType->getAccessTokenParameter($request, $response)) { + // Get the stored token data (from the implementing subclass) + // Check we have a well formed token + // Check token expiration (expires is a mandatory paramter) + if (!$token = $this->tokenStorage->getAccessToken($token_param)) { + $response->setError(401, 'invalid_token', 'The access token provided is invalid'); + } elseif (!isset($token["expires"]) || !isset($token["client_id"])) { + $response->setError(401, 'invalid_token', 'Malformed token (missing "expires")'); + } elseif (time() > $token["expires"]) { + $response->setError(401, 'invalid_token', 'The access token provided has expired'); + } else { + return $token; + } + } + + $authHeader = sprintf('%s realm="%s"', $this->tokenType->getTokenType(), $this->config['www_realm']); + + if ($error = $response->getParameter('error')) { + $authHeader = sprintf('%s, error="%s"', $authHeader, $error); + if ($error_description = $response->getParameter('error_description')) { + $authHeader = sprintf('%s, error_description="%s"', $authHeader, $error_description); + } + } + + $response->addHttpHeaders(array('WWW-Authenticate' => $authHeader)); + + return null; + } + + // convenience method to allow retrieval of the token + public function getToken() + { + return $this->token; + } +} diff --git a/server/php/R/libs/vendors/OAuth2/Controller/ResourceControllerInterface.php b/server/php/R/libs/vendors/OAuth2/Controller/ResourceControllerInterface.php new file mode 100644 index 000000000..611421935 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/Controller/ResourceControllerInterface.php @@ -0,0 +1,26 @@ + if (!$resourceController->verifyResourceRequest(OAuth2\Request::createFromGlobals(), $response = new OAuth2\Response())) { + * > $response->send(); // authorization failed + * > die(); + * > } + * > return json_encode($resource); // valid token! Send the stuff! + * + */ +interface ResourceControllerInterface +{ + public function verifyResourceRequest(RequestInterface $request, ResponseInterface $response, $scope = null); + + public function getAccessTokenData(RequestInterface $request, ResponseInterface $response); +} diff --git a/server/php/R/libs/vendors/OAuth2/Controller/TokenController.php b/server/php/R/libs/vendors/OAuth2/Controller/TokenController.php new file mode 100644 index 000000000..46cfb1755 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/Controller/TokenController.php @@ -0,0 +1,220 @@ +clientAssertionType = $clientAssertionType; + $this->accessToken = $accessToken; + $this->clientStorage = $clientStorage; + foreach ($grantTypes as $grantType) { + $this->addGrantType($grantType); + } + + if (is_null($scopeUtil)) { + $scopeUtil = new Scope(); + } + $this->scopeUtil = $scopeUtil; + } + + public function handleTokenRequest(RequestInterface $request, ResponseInterface $response) + { + if ($token = $this->grantAccessToken($request, $response)) { + // @see http://tools.ietf.org/html/rfc6749#section-5.1 + // server MUST disable caching in headers when tokens are involved + $response->setStatusCode(200); + $response->addParameters($token); + $response->addHttpHeaders(array('Cache-Control' => 'no-store', 'Pragma' => 'no-cache')); + } + } + + /** + * Grant or deny a requested access token. + * This would be called from the "/token" endpoint as defined in the spec. + * You can call your endpoint whatever you want. + * + * @param $request - RequestInterface + * Request object to grant access token + * + * @throws InvalidArgumentException + * @throws LogicException + * + * @see http://tools.ietf.org/html/rfc6749#section-4 + * @see http://tools.ietf.org/html/rfc6749#section-10.6 + * @see http://tools.ietf.org/html/rfc6749#section-4.1.3 + * + * @ingroup oauth2_section_4 + */ + public function grantAccessToken(RequestInterface $request, ResponseInterface $response) + { + if (strtolower($request->server('REQUEST_METHOD')) != 'post') { + $response->setError(405, 'invalid_request', 'The request method must be POST when requesting an access token', '#section-3.2'); + $response->addHttpHeaders(array('Allow' => 'POST')); + + return null; + } + + /** + * Determine grant type from request + * and validate the request for that grant type + */ + if (!$grantTypeIdentifier = $request->request('grant_type')) { + $response->setError(400, 'invalid_request', 'The grant type was not specified in the request'); + + return null; + } + + if (!isset($this->grantTypes[$grantTypeIdentifier])) { + /* TODO: If this is an OAuth2 supported grant type that we have chosen not to implement, throw a 501 Not Implemented instead */ + $response->setError(400, 'unsupported_grant_type', sprintf('Grant type "%s" not supported', $grantTypeIdentifier)); + + return null; + } + + $grantType = $this->grantTypes[$grantTypeIdentifier]; + + /** + * Retrieve the client information from the request + * ClientAssertionTypes allow for grant types which also assert the client data + * in which case ClientAssertion is handled in the validateRequest method + * + * @see OAuth2\GrantType\JWTBearer + * @see OAuth2\GrantType\ClientCredentials + */ + if (!$grantType instanceof ClientAssertionTypeInterface) { + if (!$this->clientAssertionType->validateRequest($request, $response)) { + return null; + } + $clientId = $this->clientAssertionType->getClientId(); + } + + /** + * Retrieve the grant type information from the request + * The GrantTypeInterface object handles all validation + * If the object is an instance of ClientAssertionTypeInterface, + * That logic is handled here as well + */ + if (!$grantType->validateRequest($request, $response)) { + return null; + } + + if ($grantType instanceof ClientAssertionTypeInterface) { + $clientId = $grantType->getClientId(); + } else { + // validate the Client ID (if applicable) + if (!is_null($storedClientId = $grantType->getClientId()) && $storedClientId != $clientId) { + $response->setError(400, 'invalid_grant', sprintf('%s doesn\'t exist or is invalid for the client', $grantTypeIdentifier)); + + return null; + } + } + + /** + * Validate the client can use the requested grant type + */ + if (!$this->clientStorage->checkRestrictedGrantType($clientId, $grantTypeIdentifier)) { + $response->setError(400, 'unauthorized_client', 'The grant type is unauthorized for this client_id'); + + return false; + } + + /** + * Validate the scope of the token + * + * requestedScope - the scope specified in the token request + * availableScope - the scope associated with the grant type + * ex: in the case of the "Authorization Code" grant type, + * the scope is specified in the authorize request + * + * @see http://tools.ietf.org/html/rfc6749#section-3.3 + */ + + $requestedScope = $this->scopeUtil->getScopeFromRequest($request); + $availableScope = $grantType->getScope(); + + if ($requestedScope) { + // validate the requested scope + if ($availableScope) { + if (!$this->scopeUtil->checkScope($requestedScope, $availableScope)) { + $response->setError(400, 'invalid_scope', 'The scope requested is invalid for this request'); + + return null; + } + } else { + // validate the client has access to this scope + if ($clientScope = $this->clientStorage->getClientScope($clientId)) { + if (!$this->scopeUtil->checkScope($requestedScope, $clientScope)) { + $response->setError(400, 'invalid_scope', 'The scope requested is invalid for this client'); + + return false; + } + } elseif (!$this->scopeUtil->scopeExists($requestedScope)) { + $response->setError(400, 'invalid_scope', 'An unsupported scope was requested'); + + return null; + } + } + } elseif ($availableScope) { + // use the scope associated with this grant type + $requestedScope = $availableScope; + } else { + // use a globally-defined default scope + $defaultScope = $this->scopeUtil->getDefaultScope($clientId); + + // "false" means default scopes are not allowed + if (false === $defaultScope) { + $response->setError(400, 'invalid_scope', 'This application requires you specify a scope parameter'); + + return null; + } + + $requestedScope = $defaultScope; + } + + return $grantType->createAccessToken($this->accessToken, $clientId, $grantType->getUserId(), $requestedScope); + } + + /** + * addGrantType + * + * @param grantType - OAuth2\GrantTypeInterface + * the grant type to add for the specified identifier + * @param identifier - string + * a string passed in as "grant_type" in the response that will call this grantType + */ + public function addGrantType(GrantTypeInterface $grantType, $identifier = null) + { + if (is_null($identifier) || is_numeric($identifier)) { + $identifier = $grantType->getQuerystringIdentifier(); + } + + $this->grantTypes[$identifier] = $grantType; + } +} diff --git a/server/php/R/libs/vendors/OAuth2/Controller/TokenControllerInterface.php b/server/php/R/libs/vendors/OAuth2/Controller/TokenControllerInterface.php new file mode 100644 index 000000000..72d72570f --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/Controller/TokenControllerInterface.php @@ -0,0 +1,32 @@ + $tokenController->handleTokenRequest(OAuth2\Request::createFromGlobals(), $response = new OAuth2\Response()); + * > $response->send(); + * + */ +interface TokenControllerInterface +{ + /** + * handleTokenRequest + * + * @param $request + * OAuth2\RequestInterface - The current http request + * @param $response + * OAuth2\ResponseInterface - An instance of OAuth2\ResponseInterface to contain the response data + * + */ + public function handleTokenRequest(RequestInterface $request, ResponseInterface $response); + + public function grantAccessToken(RequestInterface $request, ResponseInterface $response); +} diff --git a/server/php/R/libs/vendors/OAuth2/Encryption/EncryptionInterface.php b/server/php/R/libs/vendors/OAuth2/Encryption/EncryptionInterface.php new file mode 100644 index 000000000..2d336c664 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/Encryption/EncryptionInterface.php @@ -0,0 +1,11 @@ +generateJwtHeader($payload, $algo); + + $segments = array( + $this->urlSafeB64Encode(json_encode($header)), + $this->urlSafeB64Encode(json_encode($payload)) + ); + + $signing_input = implode('.', $segments); + + $signature = $this->sign($signing_input, $key, $algo); + $segments[] = $this->urlsafeB64Encode($signature); + + return implode('.', $segments); + } + + public function decode($jwt, $key = null, $verify = true) + { + if (!strpos($jwt, '.')) { + return false; + } + + $tks = explode('.', $jwt); + + if (count($tks) != 3) { + return false; + } + + list($headb64, $payloadb64, $cryptob64) = $tks; + + if (null === ($header = json_decode($this->urlSafeB64Decode($headb64), true))) { + return false; + } + + if (null === $payload = json_decode($this->urlSafeB64Decode($payloadb64), true)) { + return false; + } + + $sig = $this->urlSafeB64Decode($cryptob64); + + if ($verify) { + if (!isset($header['alg'])) { + return false; + } + + if (!$this->verifySignature($sig, "$headb64.$payloadb64", $key, $header['alg'])) { + return false; + } + } + + return $payload; + } + + private function verifySignature($signature, $input, $key, $algo = 'HS256') + { + // use constants when possible, for HipHop support + switch ($algo) { + case'HS256': + case'HS384': + case'HS512': + return $this->sign($input, $key, $algo) === $signature; + + case 'RS256': + return openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA256') ? OPENSSL_ALGO_SHA256 : 'sha256') === 1; + + case 'RS384': + return @openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA384') ? OPENSSL_ALGO_SHA384 : 'sha384') === 1; + + case 'RS512': + return @openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA512') ? OPENSSL_ALGO_SHA512 : 'sha512') === 1; + + default: + throw new \InvalidArgumentException("Unsupported or invalid signing algorithm."); + } + } + + private function sign($input, $key, $algo = 'HS256') + { + switch ($algo) { + case 'HS256': + return hash_hmac('sha256', $input, $key, true); + + case 'HS384': + return hash_hmac('sha384', $input, $key, true); + + case 'HS512': + return hash_hmac('sha512', $input, $key, true); + + case 'RS256': + return $this->generateRSASignature($input, $key, defined('OPENSSL_ALGO_SHA256') ? OPENSSL_ALGO_SHA256 : 'sha256'); + + case 'RS384': + return $this->generateRSASignature($input, $key, defined('OPENSSL_ALGO_SHA384') ? OPENSSL_ALGO_SHA384 : 'sha384'); + + case 'RS512': + return $this->generateRSASignature($input, $key, defined('OPENSSL_ALGO_SHA512') ? OPENSSL_ALGO_SHA512 : 'sha512'); + + default: + throw new \Exception("Unsupported or invalid signing algorithm."); + } + } + + private function generateRSASignature($input, $key, $algo) + { + if (!openssl_sign($input, $signature, $key, $algo)) { + throw new \Exception("Unable to sign data."); + } + + return $signature; + } + + public function urlSafeB64Encode($data) + { + $b64 = base64_encode($data); + $b64 = str_replace(array('+', '/', "\r", "\n", '='), + array('-', '_'), + $b64); + + return $b64; + } + + public function urlSafeB64Decode($b64) + { + $b64 = str_replace(array('-', '_'), + array('+', '/'), + $b64); + + return base64_decode($b64); + } + + /** + * Override to create a custom header + */ + protected function generateJwtHeader($payload, $algorithm) + { + return array( + 'typ' => 'JWT', + 'alg' => $algorithm, + ); + } +} diff --git a/server/php/R/libs/vendors/OAuth2/GrantType/AuthorizationCode.php b/server/php/R/libs/vendors/OAuth2/GrantType/AuthorizationCode.php new file mode 100644 index 000000000..69f291ee3 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/GrantType/AuthorizationCode.php @@ -0,0 +1,101 @@ + + */ +class AuthorizationCode implements GrantTypeInterface +{ + protected $storage; + protected $authCode; + + /** + * @param OAuth2\Storage\AuthorizationCodeInterface $storage + * REQUIRED Storage class for retrieving authorization code information + */ + public function __construct(AuthorizationCodeInterface $storage) + { + $this->storage = $storage; + } + + public function getQuerystringIdentifier() + { + return 'authorization_code'; + } + + public function validateRequest(RequestInterface $request, ResponseInterface $response) + { + if (!$request->request('code')) { + $response->setError(400, 'invalid_request', 'Missing parameter: "code" is required'); + + return false; + } + + $code = $request->request('code'); + if (!$authCode = $this->storage->getAuthorizationCode($code)) { + $response->setError(400, 'invalid_grant', 'Authorization code doesn\'t exist or is invalid for the client'); + + return false; + } + + /* + * 4.1.3 - ensure that the "redirect_uri" parameter is present if the "redirect_uri" parameter was included in the initial authorization request + * @uri - http://tools.ietf.org/html/rfc6749#section-4.1.3 + */ + if (isset($authCode['redirect_uri']) && $authCode['redirect_uri']) { + if (!$request->request('redirect_uri') || urldecode($request->request('redirect_uri')) != $authCode['redirect_uri']) { + $response->setError(400, 'redirect_uri_mismatch', "The redirect URI is missing or do not match", "#section-4.1.3"); + + return false; + } + } + + if (!isset($authCode['expires'])) { + throw new \Exception('Storage must return authcode with a value for "expires"'); + } + + if ($authCode["expires"] < time()) { + $response->setError(400, 'invalid_grant', "The authorization code has expired"); + + return false; + } + + if (!isset($authCode['code'])) { + $authCode['code'] = $code; // used to expire the code after the access token is granted + } + + $this->authCode = $authCode; + + return true; + } + + public function getClientId() + { + return $this->authCode['client_id']; + } + + public function getScope() + { + return isset($this->authCode['scope']) ? $this->authCode['scope'] : null; + } + + public function getUserId() + { + return isset($this->authCode['user_id']) ? $this->authCode['user_id'] : null; + } + + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + $token = $accessToken->createAccessToken($client_id, $user_id, $scope); + $this->storage->expireAuthorizationCode($this->authCode['code']); + + return $token; + } +} diff --git a/server/php/R/libs/vendors/OAuth2/GrantType/ClientCredentials.php b/server/php/R/libs/vendors/OAuth2/GrantType/ClientCredentials.php new file mode 100644 index 000000000..f953e4e8d --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/GrantType/ClientCredentials.php @@ -0,0 +1,67 @@ + + * + * @see OAuth2\ClientAssertionType_HttpBasic + */ +class ClientCredentials extends HttpBasic implements GrantTypeInterface +{ + private $clientData; + + public function __construct(ClientCredentialsInterface $storage, array $config = array()) + { + /** + * The client credentials grant type MUST only be used by confidential clients + * + * @see http://tools.ietf.org/html/rfc6749#section-4.4 + */ + $config['allow_public_clients'] = false; + + parent::__construct($storage, $config); + } + + public function getQuerystringIdentifier() + { + return 'client_credentials'; + } + + public function getScope() + { + $this->loadClientData(); + + return isset($this->clientData['scope']) ? $this->clientData['scope'] : null; + } + + public function getUserId() + { + $this->loadClientData(); + + return isset($this->clientData['user_id']) ? $this->clientData['user_id'] : null; + } + + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + /** + * Client Credentials Grant does NOT include a refresh token + * + * @see http://tools.ietf.org/html/rfc6749#section-4.4.3 + */ + $includeRefreshToken = false; + + return $accessToken->createAccessToken($client_id, $user_id, $scope, $includeRefreshToken); + } + + private function loadClientData() + { + if (!$this->clientData) { + $this->clientData = $this->storage->getClientDetails($this->getClientId()); + } + } +} diff --git a/server/php/R/libs/vendors/OAuth2/GrantType/GrantTypeInterface.php b/server/php/R/libs/vendors/OAuth2/GrantType/GrantTypeInterface.php new file mode 100644 index 000000000..98489e9c1 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/GrantType/GrantTypeInterface.php @@ -0,0 +1,20 @@ + + */ +class JwtBearer implements GrantTypeInterface, ClientAssertionTypeInterface +{ + private $jwt; + + protected $storage; + protected $audience; + protected $jwtUtil; + + /** + * Creates an instance of the JWT bearer grant type. + * + * @param OAuth2\Storage\JWTBearerInterface $storage + * A valid storage interface that implements storage hooks for the JWT bearer grant type. + * @param string $audience + * The audience to validate the token against. This is usually the full URI of the OAuth token requests endpoint. + * @param OAuth2\Encryption\JWT OPTIONAL $jwtUtil + * The class used to decode, encode and verify JWTs. + */ + public function __construct(JwtBearerInterface $storage, $audience, EncryptionInterface $jwtUtil = null) + { + $this->storage = $storage; + $this->audience = $audience; + + if (is_null($jwtUtil)) { + $jwtUtil = new Jwt(); + } + + $this->jwtUtil = $jwtUtil; + } + + /** + * Returns the grant_type get parameter to identify the grant type request as JWT bearer authorization grant. + * + * @return + * The string identifier for grant_type. + * + * @see OAuth2\GrantType\GrantTypeInterface::getQuerystringIdentifier() + */ + public function getQuerystringIdentifier() + { + return 'urn:ietf:params:oauth:grant-type:jwt-bearer'; + } + + /** + * Validates the data from the decoded JWT. + * + * @return + * TRUE if the JWT request is valid and can be decoded. Otherwise, FALSE is returned. + * + * @see OAuth2\GrantType\GrantTypeInterface::getTokenData() + */ + public function validateRequest(RequestInterface $request, ResponseInterface $response) + { + if (!$request->request("assertion")) { + $response->setError(400, 'invalid_request', 'Missing parameters: "assertion" required'); + + return null; + } + + // Store the undecoded JWT for later use + $undecodedJWT = $request->request('assertion'); + + // Decode the JWT + $jwt = $this->jwtUtil->decode($request->request('assertion'), null, false); + + if (!$jwt) { + $response->setError(400, 'invalid_request', "JWT is malformed"); + + return null; + } + + // ensure these properties contain a value + // @todo: throw malformed error for missing properties + $jwt = array_merge(array( + 'scope' => null, + 'iss' => null, + 'sub' => null, + 'aud' => null, + 'exp' => null, + 'nbf' => null, + 'iat' => null, + 'jti' => null, + 'typ' => null, + ), $jwt); + + if (!isset($jwt['iss'])) { + $response->setError(400, 'invalid_grant', "Invalid issuer (iss) provided"); + + return null; + } + + if (!isset($jwt['sub'])) { + $response->setError(400, 'invalid_grant', "Invalid subject (sub) provided"); + + return null; + } + + if (!isset($jwt['exp'])) { + $response->setError(400, 'invalid_grant', "Expiration (exp) time must be present"); + + return null; + } + + // Check expiration + if (ctype_digit($jwt['exp'])) { + if ($jwt['exp'] <= time()) { + $response->setError(400, 'invalid_grant', "JWT has expired"); + + return null; + } + } else { + $response->setError(400, 'invalid_grant', "Expiration (exp) time must be a unix time stamp"); + + return null; + } + + // Check the not before time + if ($notBefore = $jwt['nbf']) { + if (ctype_digit($notBefore)) { + if ($notBefore > time()) { + $response->setError(400, 'invalid_grant', "JWT cannot be used before the Not Before (nbf) time"); + + return null; + } + } else { + $response->setError(400, 'invalid_grant', "Not Before (nbf) time must be a unix time stamp"); + + return null; + } + } + + // Check the audience if required to match + if (!isset($jwt['aud']) || ($jwt['aud'] != $this->audience)) { + $response->setError(400, 'invalid_grant', "Invalid audience (aud)"); + + return null; + } + + // Check the jti (nonce) + // @see http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-13#section-4.1.7 + if (isset($jwt['jti'])) { + $jti = $this->storage->getJti($jwt['iss'], $jwt['sub'], $jwt['aud'], $jwt['exp'], $jwt['jti']); + + //Reject if jti is used and jwt is still valid (exp parameter has not expired). + if ($jti && $jti['expires'] > time()) { + $response->setError(400, 'invalid_grant', "JSON Token Identifier (jti) has already been used"); + + return null; + } else { + $this->storage->setJti($jwt['iss'], $jwt['sub'], $jwt['aud'], $jwt['exp'], $jwt['jti']); + } + } + + // Get the iss's public key + // @see http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06#section-4.1.1 + if (!$key = $this->storage->getClientKey($jwt['iss'], $jwt['sub'])) { + $response->setError(400, 'invalid_grant', "Invalid issuer (iss) or subject (sub) provided"); + + return null; + } + + // Verify the JWT + if (!$this->jwtUtil->decode($undecodedJWT, $key, true)) { + $response->setError(400, 'invalid_grant', "JWT failed signature verification"); + + return null; + } + + $this->jwt = $jwt; + + return true; + } + + public function getClientId() + { + return $this->jwt['iss']; + } + + public function getUserId() + { + return $this->jwt['sub']; + } + + public function getScope() + { + return null; + } + + /** + * Creates an access token that is NOT associated with a refresh token. + * If a subject (sub) the name of the user/account we are accessing data on behalf of. + * + * @see OAuth2\GrantType\GrantTypeInterface::createAccessToken() + */ + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + $includeRefreshToken = false; + + return $accessToken->createAccessToken($client_id, $user_id, $scope, $includeRefreshToken); + } +} diff --git a/server/php/R/libs/vendors/OAuth2/GrantType/RefreshToken.php b/server/php/R/libs/vendors/OAuth2/GrantType/RefreshToken.php new file mode 100644 index 000000000..71b1bdbe2 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/GrantType/RefreshToken.php @@ -0,0 +1,102 @@ + + */ +class RefreshToken implements GrantTypeInterface +{ + private $refreshToken; + + protected $storage; + protected $config; + + /** + * @param OAuth2\Storage\RefreshTokenInterface $storage + * REQUIRED Storage class for retrieving refresh token information + * @param array $config + * OPTIONAL Configuration options for the server + * @code + * $config = array( + * 'always_issue_new_refresh_token' => true, // whether to issue a new refresh token upon successful token request + * ); + * @endcode + */ + public function __construct(RefreshTokenInterface $storage, $config = array()) + { + $this->config = array_merge(array( + 'always_issue_new_refresh_token' => false + ), $config); + $this->storage = $storage; + } + + public function getQuerystringIdentifier() + { + return 'refresh_token'; + } + + public function validateRequest(RequestInterface $request, ResponseInterface $response) + { + if (!$request->request("refresh_token")) { + $response->setError(400, 'invalid_request', 'Missing parameter: "refresh_token" is required'); + + return null; + } + + if (!$refreshToken = $this->storage->getRefreshToken($request->request("refresh_token"))) { + $response->setError(400, 'invalid_grant', 'Invalid refresh token'); + + return null; + } + + if ($refreshToken['expires'] > 0 && $refreshToken["expires"] < time()) { + $response->setError(400, 'invalid_grant', 'Refresh token has expired'); + + return null; + } + + // store the refresh token locally so we can delete it when a new refresh token is generated + $this->refreshToken = $refreshToken; + + return true; + } + + public function getClientId() + { + return $this->refreshToken['client_id']; + } + + public function getUserId() + { + return isset($this->refreshToken['user_id']) ? $this->refreshToken['user_id'] : null; + } + + public function getScope() + { + return $this->refreshToken['scope']; + } + + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + /* + * It is optional to force a new refresh token when a refresh token is used. + * However, if a new refresh token is issued, the old one MUST be expired + * @see http://tools.ietf.org/html/rfc6749#section-6 + */ + $issueNewRefreshToken = $this->config['always_issue_new_refresh_token']; + $token = $accessToken->createAccessToken($client_id, $user_id, $scope, $issueNewRefreshToken); + + if ($issueNewRefreshToken) { + $this->storage->unsetRefreshToken($this->refreshToken['refresh_token']); + } + + return $token; + } +} diff --git a/server/php/R/libs/vendors/OAuth2/GrantType/UserCredentials.php b/server/php/R/libs/vendors/OAuth2/GrantType/UserCredentials.php new file mode 100644 index 000000000..5d72aaf64 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/GrantType/UserCredentials.php @@ -0,0 +1,84 @@ + + */ +class UserCredentials implements GrantTypeInterface +{ + private $userInfo; + + protected $storage; + + /** + * @param OAuth2\Storage\UserCredentialsInterface $storage + * REQUIRED Storage class for retrieving user credentials information + */ + public function __construct(UserCredentialsInterface $storage) + { + $this->storage = $storage; + } + + public function getQuerystringIdentifier() + { + return 'password'; + } + + public function validateRequest(RequestInterface $request, ResponseInterface $response) + { + if (!$request->request("password") || !$request->request("username")) { + $response->setError(400, 'invalid_request', 'Missing parameters: "username" and "password" required'); + + return null; + } + + if (!$this->storage->checkUserCredentials($request->request("username"), $request->request("password"))) { + $response->setError(401, 'invalid_grant', 'Invalid username and password combination'); + + return null; + } + + $userInfo = $this->storage->getUserDetails($request->request("username")); + + if (empty($userInfo)) { + $response->setError(400, 'invalid_grant', 'Unable to retrieve user information'); + + return null; + } + + if (!isset($userInfo['user_id'])) { + throw new \LogicException("you must set the user_id on the array returned by getUserDetails"); + } + + $this->userInfo = $userInfo; + + return true; + } + + public function getClientId() + { + return null; + } + + public function getUserId() + { + return $this->userInfo['user_id']; + } + + public function getScope() + { + return isset($this->userInfo['scope']) ? $this->userInfo['scope'] : null; + } + + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + return $accessToken->createAccessToken($client_id, $user_id, $scope); + } +} diff --git a/server/php/R/libs/vendors/OAuth2/OpenID/Controller/AuthorizeController.php b/server/php/R/libs/vendors/OAuth2/OpenID/Controller/AuthorizeController.php new file mode 100644 index 000000000..4850ebe6a --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/OpenID/Controller/AuthorizeController.php @@ -0,0 +1,86 @@ +needsIdToken($this->getScope()) && $this->getResponseType() == self::RESPONSE_TYPE_AUTHORIZATION_CODE) { + $params['id_token'] = $this->responseTypes['id_token']->createIdToken($this->getClientId(), $user_id, $this->nonce); + } + + // add the nonce to return with the redirect URI + $params['nonce'] = $this->nonce; + + return $params; + } + + public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response) + { + if (!parent::validateAuthorizeRequest($request, $response)) { + return false; + } + + $nonce = $request->query('nonce'); + + // Validate required nonce for "id_token" and "token id_token" + if (!$nonce && in_array($this->getResponseType(), array(self::RESPONSE_TYPE_ID_TOKEN, self::RESPONSE_TYPE_TOKEN_ID_TOKEN))) { + $response->setError(400, 'invalid_nonce', 'This application requires you specify a nonce parameter'); + + return false; + } + + $this->nonce = $nonce; + + return true; + } + + protected function getValidResponseTypes() + { + return array( + self::RESPONSE_TYPE_ACCESS_TOKEN, + self::RESPONSE_TYPE_AUTHORIZATION_CODE, + self::RESPONSE_TYPE_ID_TOKEN, + self::RESPONSE_TYPE_TOKEN_ID_TOKEN, + ); + } + + /** + * Returns whether the current request needs to generate an id token. + * + * ID Tokens are a part of the OpenID Connect specification, so this + * method checks whether OpenID Connect is enabled in the server settings + * and whether the openid scope was requested. + * + * @param $request_scope + * A space-separated string of scopes. + * + * @return + * TRUE if an id token is needed, FALSE otherwise. + */ + public function needsIdToken($request_scope) + { + // see if the "openid" scope exists in the requested scope + return $this->scopeUtil->checkScope('openid', $request_scope); + } + + public function getNonce() + { + return $this->nonce; + } +} diff --git a/server/php/R/libs/vendors/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php b/server/php/R/libs/vendors/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php new file mode 100644 index 000000000..af47cd0ad --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php @@ -0,0 +1,12 @@ +tokenType = $tokenType; + $this->tokenStorage = $tokenStorage; + $this->userClaimsStorage = $userClaimsStorage; + + $this->config = array_merge(array( + 'www_realm' => 'Service', + ), $config); + + if (is_null($scopeUtil)) { + $scopeUtil = new Scope(); + } + $this->scopeUtil = $scopeUtil; + } + + public function handleUserInfoRequest(RequestInterface $request, ResponseInterface $response) + { + if (!$this->verifyResourceRequest($request, $response, 'openid')) { + return; + } + + $token = $this->getToken(); + $claims = $this->userClaimsStorage->getUserClaims($token['user_id'], $token['scope']); + // The sub Claim MUST always be returned in the UserInfo Response. + // http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse + $claims += array( + 'sub' => $token['user_id'], + ); + $response->setParameters($claims); + } +} diff --git a/server/php/R/libs/vendors/OAuth2/OpenID/Controller/UserInfoControllerInterface.php b/server/php/R/libs/vendors/OAuth2/OpenID/Controller/UserInfoControllerInterface.php new file mode 100644 index 000000000..a89049d49 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/OpenID/Controller/UserInfoControllerInterface.php @@ -0,0 +1,23 @@ + $response = new OAuth2\Response(); + * > $userInfoController->handleUserInfoRequest( + * > OAuth2\Request::createFromGlobals(), + * > $response; + * > $response->send(); + * + */ +interface UserInfoControllerInterface +{ + public function handleUserInfoRequest(RequestInterface $request, ResponseInterface $response); +} diff --git a/server/php/R/libs/vendors/OAuth2/OpenID/GrantType/AuthorizationCode.php b/server/php/R/libs/vendors/OAuth2/OpenID/GrantType/AuthorizationCode.php new file mode 100644 index 000000000..4a5da018d --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/OpenID/GrantType/AuthorizationCode.php @@ -0,0 +1,36 @@ + + */ +class AuthorizationCode extends BaseAuthorizationCode +{ + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + $includeRefreshToken = true; + if (isset($this->authCode['id_token'])) { + // OpenID Connect requests include the refresh token only if the + // offline_access scope has been requested and granted. + $scopes = explode(' ', trim($scope)); + $includeRefreshToken = in_array('offline_access', $scopes); + } + + $token = $accessToken->createAccessToken($client_id, $user_id, $scope, $includeRefreshToken); + if (isset($this->authCode['id_token'])) { + $token['id_token'] = $this->authCode['id_token']; + } + + $this->storage->expireAuthorizationCode($this->authCode['code']); + + return $token; + } +} diff --git a/server/php/R/libs/vendors/OAuth2/OpenID/ResponseType/AuthorizationCode.php b/server/php/R/libs/vendors/OAuth2/OpenID/ResponseType/AuthorizationCode.php new file mode 100644 index 000000000..8971954c5 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/OpenID/ResponseType/AuthorizationCode.php @@ -0,0 +1,60 @@ + + */ +class AuthorizationCode extends BaseAuthorizationCode implements AuthorizationCodeInterface +{ + public function __construct(AuthorizationCodeStorageInterface $storage, array $config = array()) + { + parent::__construct($storage, $config); + } + + public function getAuthorizeResponse($params, $user_id = null) + { + // build the URL to redirect to + $result = array('query' => array()); + + $params += array('scope' => null, 'state' => null, 'id_token' => null); + + $result['query']['code'] = $this->createAuthorizationCode($params['client_id'], $user_id, $params['redirect_uri'], $params['scope'], $params['id_token']); + + if (isset($params['state'])) { + $result['query']['state'] = $params['state']; + } + + return array($params['redirect_uri'], $result); + } + + /** + * Handle the creation of the authorization code. + * + * @param $client_id + * Client identifier related to the authorization code + * @param $user_id + * User ID associated with the authorization code + * @param $redirect_uri + * An absolute URI to which the authorization server will redirect the + * user-agent to when the end-user authorization step is completed. + * @param $scope + * (optional) Scopes to be stored in space-separated string. + * @param $id_token + * (optional) The OpenID Connect id_token. + * + * @see http://tools.ietf.org/html/rfc6749#section-4 + * @ingroup oauth2_section_4 + */ + public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null, $id_token = null) + { + $code = $this->generateAuthorizationCode(); + $this->storage->setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, time() + $this->config['auth_code_lifetime'], $scope, $id_token); + + return $code; + } +} diff --git a/server/php/R/libs/vendors/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php b/server/php/R/libs/vendors/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php new file mode 100644 index 000000000..7b8de7c8c --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php @@ -0,0 +1,32 @@ + + */ +interface AuthorizationCodeInterface extends BaseAuthorizationCodeInterface +{ + /** + * Handle the creation of the authorization code. + * + * @param $client_id + * Client identifier related to the authorization code + * @param $user_id + * User ID associated with the authorization code + * @param $redirect_uri + * An absolute URI to which the authorization server will redirect the + * user-agent to when the end-user authorization step is completed. + * @param $scope + * (optional) Scopes to be stored in space-separated string. + * @param $id_token + * (optional) The OpenID Connect id_token. + * + * @see http://tools.ietf.org/html/rfc6749#section-4 + * @ingroup oauth2_section_4 + */ + public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null, $id_token = null); +} diff --git a/server/php/R/libs/vendors/OAuth2/OpenID/ResponseType/IdToken.php b/server/php/R/libs/vendors/OAuth2/OpenID/ResponseType/IdToken.php new file mode 100644 index 000000000..8d907bca0 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/OpenID/ResponseType/IdToken.php @@ -0,0 +1,125 @@ +userClaimsStorage = $userClaimsStorage; + $this->publicKeyStorage = $publicKeyStorage; + if (is_null($encryptionUtil)) { + $encryptionUtil = new Jwt(); + } + $this->encryptionUtil = $encryptionUtil; + + if (!isset($config['issuer'])) { + throw new \LogicException('config parameter "issuer" must be set'); + } + $this->config = array_merge(array( + 'id_lifetime' => 3600, + ), $config); + } + + public function getAuthorizeResponse($params, $userInfo = null) + { + // build the URL to redirect to + $result = array('query' => array()); + $params += array('scope' => null, 'state' => null, 'nonce' => null); + + // create the id token. + list($user_id, $auth_time) = $this->getUserIdAndAuthTime($userInfo); + $userClaims = $this->userClaimsStorage->getUserClaims($user_id, $params['scope']); + + $id_token = $this->createIdToken($params['client_id'], $userInfo, $params['nonce'], $userClaims, null); + $result["fragment"] = array('id_token' => $id_token); + if (isset($params['state'])) { + $result["fragment"]["state"] = $params['state']; + } + + return array($params['redirect_uri'], $result); + } + + public function createIdToken($client_id, $userInfo, $nonce = null, $userClaims = null, $access_token = null) + { + // pull auth_time from user info if supplied + list($user_id, $auth_time) = $this->getUserIdAndAuthTime($userInfo); + + $token = array( + 'iss' => $this->config['issuer'], + 'sub' => $user_id, + 'aud' => $client_id, + 'iat' => time(), + 'exp' => time() + $this->config['id_lifetime'], + 'auth_time' => $auth_time, + ); + + if ($nonce) { + $token['nonce'] = $nonce; + } + + if ($userClaims) { + $token += $userClaims; + } + + if ($access_token) { + $token['at_hash'] = $this->createAtHash($access_token, $client_id); + } + + return $this->encodeToken($token, $client_id); + } + + protected function createAtHash($access_token, $client_id = null) + { + // maps HS256 and RS256 to sha256, etc. + $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id); + $hash_algorithm = 'sha' . substr($algorithm, 2); + $hash = hash($hash_algorithm, $access_token); + $at_hash = substr($hash, 0, strlen($hash) / 2); + + return $this->encryptionUtil->urlSafeB64Encode($at_hash); + } + + protected function encodeToken(array $token, $client_id = null) + { + $private_key = $this->publicKeyStorage->getPrivateKey($client_id); + $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id); + + return $this->encryptionUtil->encode($token, $private_key, $algorithm); + } + + private function getUserIdAndAuthTime($userInfo) + { + $auth_time = null; + + // support an array for user_id / auth_time + if (is_array($userInfo)) { + if (!isset($userInfo['user_id'])) { + throw new \LogicException('if $user_id argument is an array, user_id index must be set'); + } + + $auth_time = isset($userInfo['auth_time']) ? $userInfo['auth_time'] : null; + $user_id = $userInfo['user_id']; + } else { + $user_id = $userInfo; + } + + if (is_null($auth_time)) { + $auth_time = time(); + } + + // userInfo is a scalar, and so this is the $user_id. Auth Time is null + return array($user_id, $auth_time); + } +} diff --git a/server/php/R/libs/vendors/OAuth2/OpenID/ResponseType/IdTokenInterface.php b/server/php/R/libs/vendors/OAuth2/OpenID/ResponseType/IdTokenInterface.php new file mode 100644 index 000000000..3dfb57e71 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/OpenID/ResponseType/IdTokenInterface.php @@ -0,0 +1,29 @@ +accessToken = $accessToken; + $this->idToken = $idToken; + } + + public function getAuthorizeResponse($params, $user_id = null) + { + $result = $this->accessToken->getAuthorizeResponse($params, $user_id); + $access_token = $result[1]['fragment']['access_token']; + $id_token = $this->idToken->createIdToken($params['client_id'], $user_id, $params['nonce'], null, $access_token); + $result[1]['fragment']['id_token'] = $id_token; + + return $result; + } +} diff --git a/server/php/R/libs/vendors/OAuth2/OpenID/ResponseType/TokenIdTokenInterface.php b/server/php/R/libs/vendors/OAuth2/OpenID/ResponseType/TokenIdTokenInterface.php new file mode 100644 index 000000000..1b4c5139a --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/OpenID/ResponseType/TokenIdTokenInterface.php @@ -0,0 +1,9 @@ + + */ +interface AuthorizationCodeInterface extends BaseAuthorizationCodeInterface +{ + /** + * Take the provided authorization code values and store them somewhere. + * + * This function should be the storage counterpart to getAuthCode(). + * + * If storage fails for some reason, we're not currently checking for + * any sort of success/failure, so you should bail out of the script + * and provide a descriptive fail message. + * + * Required for OAuth2::GRANT_TYPE_AUTH_CODE. + * + * @param $code + * Authorization code to be stored. + * @param $client_id + * Client identifier to be stored. + * @param $user_id + * User identifier to be stored. + * @param string $redirect_uri + * Redirect URI(s) to be stored in a space-separated string. + * @param int $expires + * Expiration to be stored as a Unix timestamp. + * @param string $scope + * (optional) Scopes to be stored in space-separated string. + * @param string $id_token + * (optional) The OpenID Connect id_token. + * + * @ingroup oauth2_section_4 + */ + //public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null); +} diff --git a/server/php/R/libs/vendors/OAuth2/OpenID/Storage/UserClaimsInterface.php b/server/php/R/libs/vendors/OAuth2/OpenID/Storage/UserClaimsInterface.php new file mode 100644 index 000000000..f230bef9e --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/OpenID/Storage/UserClaimsInterface.php @@ -0,0 +1,38 @@ + value format. + * + * @see http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims + */ + public function getUserClaims($user_id, $scope); +} diff --git a/server/php/R/libs/vendors/OAuth2/Request.php b/server/php/R/libs/vendors/OAuth2/Request.php new file mode 100644 index 000000000..b37c59152 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/Request.php @@ -0,0 +1,212 @@ +initialize($query, $request, $attributes, $cookies, $files, $server, $content, $headers); + } + + /** + * Sets the parameters for this request. + * + * This method also re-initializes all properties. + * + * @param array $query The GET parameters + * @param array $request The POST parameters + * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...) + * @param array $cookies The COOKIE parameters + * @param array $files The FILES parameters + * @param array $server The SERVER parameters + * @param string $content The raw body data + * + * @api + */ + public function initialize(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null, array $headers = null) + { + $this->request = $request; + $this->query = $query; + $this->attributes = $attributes; + $this->cookies = $cookies; + $this->files = $files; + $this->server = $server; + $this->content = $content; + $this->headers = is_null($headers) ? $this->getHeadersFromServer($this->server) : $headers; + } + + public function query($name, $default = null) + { + return isset($this->query[$name]) ? $this->query[$name] : $default; + } + + public function request($name, $default = null) + { + return isset($this->request[$name]) ? $this->request[$name] : $default; + } + + public function server($name, $default = null) + { + return isset($this->server[$name]) ? $this->server[$name] : $default; + } + + public function headers($name, $default = null) + { + $headers = array_change_key_case($this->headers); + $name = strtolower($name); + return isset($headers[$name]) ? $headers[$name] : $default; + } + + public function getAllQueryParameters() + { + return $this->query; + } + + /** + * Returns the request body content. + * + * @param Boolean $asResource If true, a resource will be returned + * + * @return string|resource The request body content or a resource to read the body stream. + */ + public function getContent($asResource = false) + { + if (false === $this->content || (true === $asResource && null !== $this->content)) { + throw new \LogicException('getContent() can only be called once when using the resource return type.'); + } + + if (true === $asResource) { + $this->content = false; + + return fopen('php://input', 'rb'); + } + + if (null === $this->content) { + $this->content = file_get_contents('php://input'); + } + + return $this->content; + } + + private function getHeadersFromServer($server) + { + $headers = array(); + foreach ($server as $key => $value) { + if (0 === strpos($key, 'HTTP_')) { + $headers[substr($key, 5)] = $value; + } + // CONTENT_* are not prefixed with HTTP_ + elseif (in_array($key, array('CONTENT_LENGTH', 'CONTENT_MD5', 'CONTENT_TYPE'))) { + $headers[$key] = $value; + } + } + + if (isset($server['PHP_AUTH_USER'])) { + $headers['PHP_AUTH_USER'] = $server['PHP_AUTH_USER']; + $headers['PHP_AUTH_PW'] = isset($server['PHP_AUTH_PW']) ? $server['PHP_AUTH_PW'] : ''; + } else { + /* + * php-cgi under Apache does not pass HTTP Basic user/pass to PHP by default + * For this workaround to work, add this line to your .htaccess file: + * RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + * + * A sample .htaccess file: + * RewriteEngine On + * RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + * RewriteCond %{REQUEST_FILENAME} !-f + * RewriteRule ^(.*)$ app.php [QSA,L] + */ + + $authorizationHeader = null; + if (isset($server['HTTP_AUTHORIZATION'])) { + $authorizationHeader = $server['HTTP_AUTHORIZATION']; + } elseif (isset($server['REDIRECT_HTTP_AUTHORIZATION'])) { + $authorizationHeader = $server['REDIRECT_HTTP_AUTHORIZATION']; + } elseif (function_exists('apache_request_headers')) { + $requestHeaders = (array) apache_request_headers(); + + // Server-side fix for bug in old Android versions (a nice side-effect of this fix means we don't care about capitalization for Authorization) + $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders)); + + if (isset($requestHeaders['Authorization'])) { + $authorizationHeader = trim($requestHeaders['Authorization']); + } + } + + if (null !== $authorizationHeader) { + $headers['AUTHORIZATION'] = $authorizationHeader; + // Decode AUTHORIZATION header into PHP_AUTH_USER and PHP_AUTH_PW when authorization header is basic + if (0 === stripos($authorizationHeader, 'basic')) { + $exploded = explode(':', base64_decode(substr($authorizationHeader, 6))); + if (count($exploded) == 2) { + list($headers['PHP_AUTH_USER'], $headers['PHP_AUTH_PW']) = $exploded; + } + } + } + } + + // PHP_AUTH_USER/PHP_AUTH_PW + if (isset($headers['PHP_AUTH_USER'])) { + $headers['AUTHORIZATION'] = 'Basic '.base64_encode($headers['PHP_AUTH_USER'].':'.$headers['PHP_AUTH_PW']); + } + + return $headers; + } + + /** + * Creates a new request with values from PHP's super globals. + * + * @return Request A new request + * + * @api + */ + public static function createFromGlobals() + { + $class = __CLASS__; + $request = new $class($_GET, $_POST, array(), $_COOKIE, $_FILES, $_SERVER); + + $contentType = $request->server('CONTENT_TYPE', ''); + $requestMethod = $request->server('REQUEST_METHOD', 'GET'); + if (0 === strpos($contentType, 'application/x-www-form-urlencoded') + && in_array(strtoupper($requestMethod), array('PUT', 'DELETE')) + ) { + parse_str($request->getContent(), $data); + $request->request = $data; + } elseif (0 === strpos($contentType, 'application/json') + && in_array(strtoupper($requestMethod), array('POST', 'PUT', 'DELETE')) + ) { + $data = json_decode($request->getContent(), true); + $request->request = $data; + } + + return $request; + } +} diff --git a/server/php/R/libs/vendors/OAuth2/RequestInterface.php b/server/php/R/libs/vendors/OAuth2/RequestInterface.php new file mode 100644 index 000000000..8a70d5fad --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/RequestInterface.php @@ -0,0 +1,16 @@ + 'Continue', + 101 => 'Switching Protocols', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + 418 => 'I\'m a teapot', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + ); + + public function __construct($parameters = array(), $statusCode = 200, $headers = array()) + { + $this->setParameters($parameters); + $this->setStatusCode($statusCode); + $this->setHttpHeaders($headers); + $this->version = '1.1'; + } + + /** + * Converts the response object to string containing all headers and the response content. + * + * @return string The response with headers and content + */ + public function __toString() + { + $headers = array(); + foreach ($this->httpHeaders as $name => $value) { + $headers[$name] = (array) $value; + } + + return + sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)."\r\n". + $this->getHttpHeadersAsString($headers)."\r\n". + $this->getResponseBody(); + } + + /** + * Returns the build header line. + * + * @param string $name The header name + * @param string $value The header value + * + * @return string The built header line + */ + protected function buildHeader($name, $value) + { + return sprintf("%s: %s\n", $name, $value); + } + + public function getStatusCode() + { + return $this->statusCode; + } + + public function setStatusCode($statusCode, $text = null) + { + $this->statusCode = (int) $statusCode; + if ($this->isInvalid()) { + throw new \InvalidArgumentException(sprintf('The HTTP status code "%s" is not valid.', $statusCode)); + } + + $this->statusText = false === $text ? '' : (null === $text ? self::$statusTexts[$this->statusCode] : $text); + } + + public function getStatusText() + { + return $this->statusText; + } + + public function getParameters() + { + return $this->parameters; + } + + public function setParameters(array $parameters) + { + $this->parameters = $parameters; + } + + public function addParameters(array $parameters) + { + $this->parameters = array_merge($this->parameters, $parameters); + } + + public function getParameter($name, $default = null) + { + return isset($this->parameters[$name]) ? $this->parameters[$name] : $default; + } + + public function setParameter($name, $value) + { + $this->parameters[$name] = $value; + } + + public function setHttpHeaders(array $httpHeaders) + { + $this->httpHeaders = $httpHeaders; + } + + public function setHttpHeader($name, $value) + { + $this->httpHeaders[$name] = $value; + } + + public function addHttpHeaders(array $httpHeaders) + { + $this->httpHeaders = array_merge($this->httpHeaders, $httpHeaders); + } + + public function getHttpHeaders() + { + return $this->httpHeaders; + } + + public function getHttpHeader($name, $default = null) + { + return isset($this->httpHeaders[$name]) ? $this->httpHeaders[$name] : $default; + } + + public function getResponseBody($format = 'json') + { + switch ($format) { + case 'json': + return json_encode($this->parameters); + case 'xml': + // this only works for single-level arrays + $xml = new \SimpleXMLElement(''); + foreach ($this->parameters as $key => $param) { + $xml->addChild($param, $key); + } + + return $xml->asXML(); + } + + throw new \InvalidArgumentException(sprintf('The format %s is not supported')); + + } + + public function send($format = 'json') + { + // headers have already been sent by the developer + if (headers_sent()) { + return; + } + + switch ($format) { + case 'json': + $this->setHttpHeader('Content-Type', 'application/json'); + break; + case 'xml': + $this->setHttpHeader('Content-Type', 'text/xml'); + break; + } + // status + header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)); + + foreach ($this->getHttpHeaders() as $name => $header) { + header(sprintf('%s: %s', $name, $header)); + } + echo $this->getResponseBody($format); + } + + public function setError($statusCode, $error, $errorDescription = null, $errorUri = null) + { + $parameters = array( + 'error' => $error, + 'error_description' => $errorDescription, + ); + + if (!is_null($errorUri)) { + if (strlen($errorUri) > 0 && $errorUri[0] == '#') { + // we are referencing an oauth bookmark (for brevity) + $errorUri = 'http://tools.ietf.org/html/rfc6749' . $errorUri; + } + $parameters['error_uri'] = $errorUri; + } + + $httpHeaders = array( + 'Cache-Control' => 'no-store' + ); + + $this->setStatusCode($statusCode); + $this->addParameters($parameters); + $this->addHttpHeaders($httpHeaders); + + if (!$this->isClientError() && !$this->isServerError()) { + throw new \InvalidArgumentException(sprintf('The HTTP status code is not an error ("%s" given).', $statusCode)); + } + } + + public function setRedirect($statusCode = 302, $url, $state = null, $error = null, $errorDescription = null, $errorUri = null) + { + if (empty($url)) { + throw new \InvalidArgumentException('Cannot redirect to an empty URL.'); + } + + $parameters = array(); + + if (!is_null($state)) { + $parameters['state'] = $state; + } + + if (!is_null($error)) { + $this->setError(400, $error, $errorDescription, $errorUri); + } + $this->setStatusCode($statusCode); + $this->addParameters($parameters); + + if (count($this->parameters) > 0) { + // add parameters to URL redirection + $parts = parse_url($url); + $sep = isset($parts['query']) && count($parts['query']) > 0 ? '&' : '?'; + $url .= $sep . http_build_query($this->parameters); + } + + $this->addHttpHeaders(array('Location' => $url)); + + if (!$this->isRedirection()) { + throw new \InvalidArgumentException(sprintf('The HTTP status code is not a redirect ("%s" given).', $statusCode)); + } + } + + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html + /** + * @return Boolean + * + * @api + */ + public function isInvalid() + { + return $this->statusCode < 100 || $this->statusCode >= 600; + } + + /** + * @return Boolean + * + * @api + */ + public function isInformational() + { + return $this->statusCode >= 100 && $this->statusCode < 200; + } + + /** + * @return Boolean + * + * @api + */ + public function isSuccessful() + { + return $this->statusCode >= 200 && $this->statusCode < 300; + } + + /** + * @return Boolean + * + * @api + */ + public function isRedirection() + { + return $this->statusCode >= 300 && $this->statusCode < 400; + } + + /** + * @return Boolean + * + * @api + */ + public function isClientError() + { + return $this->statusCode >= 400 && $this->statusCode < 500; + } + + /** + * @return Boolean + * + * @api + */ + public function isServerError() + { + return $this->statusCode >= 500 && $this->statusCode < 600; + } + + /* + * Functions from Symfony2 HttpFoundation - output pretty header + */ + private function getHttpHeadersAsString($headers) + { + if (count($headers) == 0) { + return ''; + } + + $max = max(array_map('strlen', array_keys($headers))) + 1; + $content = ''; + ksort($headers); + foreach ($headers as $name => $values) { + foreach ($values as $value) { + $content .= sprintf("%-{$max}s %s\r\n", $this->beautifyHeaderName($name).':', $value); + } + } + + return $content; + } + + private function beautifyHeaderName($name) + { + return preg_replace_callback('/\-(.)/', array($this, 'beautifyCallback'), ucfirst($name)); + } + + private function beautifyCallback($match) + { + return '-'.strtoupper($match[1]); + } +} diff --git a/server/php/R/libs/vendors/OAuth2/ResponseInterface.php b/server/php/R/libs/vendors/OAuth2/ResponseInterface.php new file mode 100644 index 000000000..842002bcd --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/ResponseInterface.php @@ -0,0 +1,24 @@ + + */ +class AccessToken implements AccessTokenInterface +{ + protected $tokenStorage; + protected $refreshStorage; + protected $config; + + /** + * @param OAuth2\Storage\AccessTokenInterface $tokenStorage + * REQUIRED Storage class for saving access token information + * @param OAuth2\Storage\RefreshTokenInterface $refreshStorage + * OPTIONAL Storage class for saving refresh token information + * @param array $config + * OPTIONAL Configuration options for the server + * @code + * $config = array( + * 'token_type' => 'bearer', // token type identifier + * 'access_lifetime' => 3600, // time before access token expires + * 'refresh_token_lifetime' => 1209600, // time before refresh token expires + * ); + * @endcode + */ + public function __construct(AccessTokenStorageInterface $tokenStorage, RefreshTokenInterface $refreshStorage = null, array $config = array()) + { + $this->tokenStorage = $tokenStorage; + $this->refreshStorage = $refreshStorage; + + $this->config = array_merge(array( + 'token_type' => 'bearer', + 'access_lifetime' => 3600, + 'refresh_token_lifetime' => 1209600, + ), $config); + } + + public function getAuthorizeResponse($params, $user_id = null) + { + // build the URL to redirect to + $result = array('query' => array()); + + $params += array('scope' => null, 'state' => null); + + /* + * a refresh token MUST NOT be included in the fragment + * + * @see http://tools.ietf.org/html/rfc6749#section-4.2.2 + */ + $includeRefreshToken = false; + $result["fragment"] = $this->createAccessToken($params['client_id'], $user_id, $params['scope'], $includeRefreshToken); + + if (isset($params['state'])) { + $result["fragment"]["state"] = $params['state']; + } + + return array($params['redirect_uri'], $result); + } + + /** + * Handle the creation of access token, also issue refresh token if supported / desirable. + * + * @param $client_id + * Client identifier related to the access token. + * @param $user_id + * User ID associated with the access token + * @param $scope + * (optional) Scopes to be stored in space-separated string. + * @param bool $includeRefreshToken + * If true, a new refresh_token will be added to the response + * + * @see http://tools.ietf.org/html/rfc6749#section-5 + * @ingroup oauth2_section_5 + */ + public function createAccessToken($client_id, $user_id, $scope = null, $includeRefreshToken = true) + { + $token = array( + "access_token" => $this->generateAccessToken(), + "expires_in" => $this->config['access_lifetime'], + "token_type" => $this->config['token_type'], + "scope" => $scope + ); + + $this->tokenStorage->setAccessToken($token["access_token"], $client_id, $user_id, $this->config['access_lifetime'] ? time() + $this->config['access_lifetime'] : null, $scope); + + /* + * Issue a refresh token also, if we support them + * + * Refresh Tokens are considered supported if an instance of OAuth2\Storage\RefreshTokenInterface + * is supplied in the constructor + */ + if ($includeRefreshToken && $this->refreshStorage) { + $token["refresh_token"] = $this->generateRefreshToken(); + $expires = 0; + if ($this->config['refresh_token_lifetime'] > 0) { + $expires = time() + $this->config['refresh_token_lifetime']; + } + $this->refreshStorage->setRefreshToken($token['refresh_token'], $client_id, $user_id, $expires, $scope); + } + + return $token; + } + + /** + * Generates an unique access token. + * + * Implementing classes may want to override this function to implement + * other access token generation schemes. + * + * @return + * An unique access token. + * + * @ingroup oauth2_section_4 + */ + protected function generateAccessToken() + { + $tokenLen = 40; + if (function_exists('mcrypt_create_iv')) { + $randomData = mcrypt_create_iv(100, MCRYPT_DEV_URANDOM); + } else if (function_exists('openssl_random_pseudo_bytes')) { + $randomData = openssl_random_pseudo_bytes(100); + } else if (@file_exists('/dev/urandom')) { // Get 100 bytes of random data + $randomData = file_get_contents('/dev/urandom', false, null, 0, 100) . uniqid(mt_rand(), true); + } else { + $randomData = mt_rand() . mt_rand() . mt_rand() . mt_rand() . microtime(true) . uniqid(mt_rand(), true); + } + + return substr(hash('sha512', $randomData), 0, $tokenLen); + } + + /** + * Generates an unique refresh token + * + * Implementing classes may want to override this function to implement + * other refresh token generation schemes. + * + * @return + * An unique refresh. + * + * @ingroup oauth2_section_4 + * @see OAuth2::generateAccessToken() + */ + protected function generateRefreshToken() + { + return $this->generateAccessToken(); // let's reuse the same scheme for token generation + } +} diff --git a/server/php/R/libs/vendors/OAuth2/ResponseType/AccessTokenInterface.php b/server/php/R/libs/vendors/OAuth2/ResponseType/AccessTokenInterface.php new file mode 100644 index 000000000..91903276d --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/ResponseType/AccessTokenInterface.php @@ -0,0 +1,27 @@ + + */ +interface AccessTokenInterface extends ResponseTypeInterface +{ + /** + * Handle the creation of access token, also issue refresh token if supported / desirable. + * + * @param $client_id + * Client identifier related to the access token. + * @param $user_id + * User ID associated with the access token + * @param $scope + * (optional) Scopes to be stored in space-separated string. + * @param bool $includeRefreshToken + * If true, a new refresh_token will be added to the response + * + * @see http://tools.ietf.org/html/rfc6749#section-5 + * @ingroup oauth2_section_5 + */ + public function createAccessToken($client_id, $user_id, $scope = null, $includeRefreshToken = true); +} diff --git a/server/php/R/libs/vendors/OAuth2/ResponseType/AuthorizationCode.php b/server/php/R/libs/vendors/OAuth2/ResponseType/AuthorizationCode.php new file mode 100644 index 000000000..51bd437e9 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/ResponseType/AuthorizationCode.php @@ -0,0 +1,100 @@ + + */ +class AuthorizationCode implements AuthorizationCodeInterface +{ + protected $storage; + protected $config; + + public function __construct(AuthorizationCodeStorageInterface $storage, array $config = array()) + { + $this->storage = $storage; + $this->config = array_merge(array( + 'enforce_redirect' => false, + 'auth_code_lifetime' => 30, + ), $config); + } + + public function getAuthorizeResponse($params, $user_id = null) + { + // build the URL to redirect to + $result = array('query' => array()); + + $params += array('scope' => null, 'state' => null); + + $result['query']['code'] = $this->createAuthorizationCode($params['client_id'], $user_id, $params['redirect_uri'], $params['scope']); + + if (isset($params['state'])) { + $result['query']['state'] = $params['state']; + } + + return array($params['redirect_uri'], $result); + } + + /** + * Handle the creation of the authorization code. + * + * @param $client_id + * Client identifier related to the authorization code + * @param $user_id + * User ID associated with the authorization code + * @param $redirect_uri + * An absolute URI to which the authorization server will redirect the + * user-agent to when the end-user authorization step is completed. + * @param $scope + * (optional) Scopes to be stored in space-separated string. + * + * @see http://tools.ietf.org/html/rfc6749#section-4 + * @ingroup oauth2_section_4 + */ + public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null) + { + $code = $this->generateAuthorizationCode(); + $this->storage->setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, time() + $this->config['auth_code_lifetime'], $scope); + + return $code; + } + + /** + * @return + * TRUE if the grant type requires a redirect_uri, FALSE if not + */ + public function enforceRedirect() + { + return $this->config['enforce_redirect']; + } + + /** + * Generates an unique auth code. + * + * Implementing classes may want to override this function to implement + * other auth code generation schemes. + * + * @return + * An unique auth code. + * + * @ingroup oauth2_section_4 + */ + protected function generateAuthorizationCode() + { + $tokenLen = 40; + if (function_exists('mcrypt_create_iv')) { + $randomData = mcrypt_create_iv(100, MCRYPT_DEV_URANDOM); + } else if (function_exists('openssl_random_pseudo_bytes')) { + $randomData = openssl_random_pseudo_bytes(100); + } else if (@file_exists('/dev/urandom')) { // Get 100 bytes of random data + $randomData = file_get_contents('/dev/urandom', false, null, 0, 100) . uniqid(mt_rand(), true); + } else { + $randomData = mt_rand() . mt_rand() . mt_rand() . mt_rand() . microtime(true) . uniqid(mt_rand(), true); + } + + return substr(hash('sha512', $randomData), 0, $tokenLen); + } +} diff --git a/server/php/R/libs/vendors/OAuth2/ResponseType/AuthorizationCodeInterface.php b/server/php/R/libs/vendors/OAuth2/ResponseType/AuthorizationCodeInterface.php new file mode 100644 index 000000000..9d5b90fd9 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/ResponseType/AuthorizationCodeInterface.php @@ -0,0 +1,34 @@ + + */ +interface AuthorizationCodeInterface extends ResponseTypeInterface +{ + /** + * @return + * TRUE if the grant type requires a redirect_uri, FALSE if not + */ + public function enforceRedirect(); + + /** + * Handle the creation of the authorization code. + * + * @param $client_id + * Client identifier related to the authorization code + * @param $user_id + * User ID associated with the authorization code + * @param $redirect_uri + * An absolute URI to which the authorization server will redirect the + * user-agent to when the end-user authorization step is completed. + * @param $scope + * (optional) Scopes to be stored in space-separated string. + * + * @see http://tools.ietf.org/html/rfc6749#section-4 + * @ingroup oauth2_section_4 + */ + public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null); +} diff --git a/server/php/R/libs/vendors/OAuth2/ResponseType/CryptoToken.php b/server/php/R/libs/vendors/OAuth2/ResponseType/CryptoToken.php new file mode 100644 index 000000000..3f5ca37e1 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/ResponseType/CryptoToken.php @@ -0,0 +1,118 @@ + + */ +class CryptoToken extends AccessToken +{ + protected $publicKeyStorage; + protected $encryptionUtil; + + /** + * @param $config + * - store_encrypted_token_string (bool true) + * whether the entire encrypted string is stored, + * or just the token ID is stored + */ + public function __construct(PublicKeyInterface $publicKeyStorage = null, AccessTokenStorageInterface $tokenStorage = null, RefreshTokenInterface $refreshStorage = null, array $config = array(), EncryptionInterface $encryptionUtil = null) + { + $this->publicKeyStorage = $publicKeyStorage; + $config = array_merge(array( + 'store_encrypted_token_string' => true, + ), $config); + if (is_null($tokenStorage)) { + // a pass-thru, so we can call the parent constructor + $tokenStorage = new Memory(); + } + if (is_null($encryptionUtil)) { + $encryptionUtil = new Jwt(); + } + $this->encryptionUtil = $encryptionUtil; + parent::__construct($tokenStorage, $refreshStorage, $config); + } + + /** + * Handle the creation of access token, also issue refresh token if supported / desirable. + * + * @param $client_id + * Client identifier related to the access token. + * @param $user_id + * User ID associated with the access token + * @param $scope + * (optional) Scopes to be stored in space-separated string. + * @param bool $includeRefreshToken + * If true, a new refresh_token will be added to the response + * + * @see http://tools.ietf.org/html/rfc6749#section-5 + * @ingroup oauth2_section_5 + */ + public function createAccessToken($client_id, $user_id, $scope = null, $includeRefreshToken = true) + { + // token to encrypt + $expires = time() + $this->config['access_lifetime']; + $cryptoToken = array( + 'id' => $this->generateAccessToken(), + 'client_id' => $client_id, + 'user_id' => $user_id, + 'expires' => $expires, + 'token_type' => $this->config['token_type'], + 'scope' => $scope + ); + + /* + * Issue a refresh token also, if we support them + * + * Refresh Tokens are considered supported if an instance of OAuth2\Storage\RefreshTokenInterface + * is supplied in the constructor + */ + if ($includeRefreshToken && $this->refreshStorage) { + $cryptoToken["refresh_token"] = $this->generateRefreshToken(); + $expires = 0; + if ($this->config['refresh_token_lifetime'] > 0) { + $expires = time() + $this->config['refresh_token_lifetime']; + } + $this->refreshStorage->setRefreshToken($cryptoToken['refresh_token'], $client_id, $user_id, $expires, $scope); + } + + /* + * Encode the token data into a single access_token string + */ + $access_token = $this->encodeToken($cryptoToken, $client_id); + + /* + * Save the token to a secondary storage. This is implemented on the + * OAuth2\Storage\CryptoToken side, and will not actually store anything, + * if no secondary storage has been supplied + */ + $token_to_store = $this->config['store_encrypted_token_string'] ? $access_token : $cryptoToken['id']; + $this->tokenStorage->setAccessToken($token_to_store, $client_id, $user_id, $this->config['access_lifetime'] ? time() + $this->config['access_lifetime'] : null, $scope); + + // token to return to the client + $token = array( + 'access_token' => $access_token, + 'expires_in' => $this->config['access_lifetime'], + 'token_type' => $this->config['token_type'], + 'scope' => $scope + ); + + return $token; + } + + protected function encodeToken(array $token, $client_id = null) + { + $private_key = $this->publicKeyStorage->getPrivateKey($client_id); + $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id); + + return $this->encryptionUtil->encode($token, $private_key, $algorithm); + } +} diff --git a/server/php/R/libs/vendors/OAuth2/ResponseType/ResponseTypeInterface.php b/server/php/R/libs/vendors/OAuth2/ResponseType/ResponseTypeInterface.php new file mode 100644 index 000000000..f8e26a5b0 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/ResponseType/ResponseTypeInterface.php @@ -0,0 +1,8 @@ +storage = $storage; + } + + /** + * Check if everything in required scope is contained in available scope. + * + * @param $required_scope + * A space-separated string of scopes. + * + * @return + * TRUE if everything in required scope is contained in available scope, + * and FALSE if it isn't. + * + * @see http://tools.ietf.org/html/rfc6749#section-7 + * + * @ingroup oauth2_section_7 + */ + public function checkScope($required_scope, $available_scope) + { + $required_scope = explode(' ', trim($required_scope)); + $available_scope = explode(' ', trim($available_scope)); + + return (count(array_diff($required_scope, $available_scope)) == 0); + } + + /** + * Check if the provided scope exists in storage. + * + * @param $scope + * A space-separated string of scopes. + * + * @return + * TRUE if it exists, FALSE otherwise. + */ + public function scopeExists($scope) + { + // Check reserved scopes first. + $scope = explode(' ', trim($scope)); + $reservedScope = $this->getReservedScopes(); + $nonReservedScopes = array_diff($scope, $reservedScope); + if (count($nonReservedScopes) == 0) { + return true; + } + else { + // Check the storage for non-reserved scopes. + $nonReservedScopes = implode(' ', $nonReservedScopes); + return $this->storage->scopeExists($nonReservedScopes); + } + } + + public function getScopeFromRequest(RequestInterface $request) + { + // "scope" is valid if passed in either POST or QUERY + return $request->request('scope', $request->query('scope')); + } + + public function getDefaultScope($client_id = null) + { + return $this->storage->getDefaultScope($client_id); + } + + /** + * Get reserved scopes needed by the server. + * + * In case OpenID Connect is used, these scopes must include: + * 'openid', offline_access'. + * + * @return + * An array of reserved scopes. + */ + public function getReservedScopes() + { + return array('openid', 'offline_access'); + } +} diff --git a/server/php/R/libs/vendors/OAuth2/ScopeInterface.php b/server/php/R/libs/vendors/OAuth2/ScopeInterface.php new file mode 100644 index 000000000..5b60f9aee --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/ScopeInterface.php @@ -0,0 +1,40 @@ + 'OAuth2\Storage\AccessTokenInterface', + 'authorization_code' => 'OAuth2\Storage\AuthorizationCodeInterface', + 'client_credentials' => 'OAuth2\Storage\ClientCredentialsInterface', + 'client' => 'OAuth2\Storage\ClientInterface', + 'refresh_token' => 'OAuth2\Storage\RefreshTokenInterface', + 'user_credentials' => 'OAuth2\Storage\UserCredentialsInterface', + 'user_claims' => 'OAuth2\OpenID\Storage\UserClaimsInterface', + 'public_key' => 'OAuth2\Storage\PublicKeyInterface', + 'jwt_bearer' => 'OAuth2\Storage\JWTBearerInterface', + 'scope' => 'OAuth2\Storage\ScopeInterface', + ); + protected $responseTypeMap = array( + 'token' => 'OAuth2\ResponseType\AccessTokenInterface', + 'code' => 'OAuth2\ResponseType\AuthorizationCodeInterface', + 'id_token' => 'OAuth2\OpenID\ResponseType\IdTokenInterface', + 'token id_token' => 'OAuth2\OpenID\ResponseType\TokenIdTokenInterface', + ); + + /** + * @param mixed $storage + * array - array of Objects to implement storage + * OAuth2\Storage object implementing all required storage types (ClientCredentialsInterface and AccessTokenInterface as a minimum) + * @param array $config + * specify a different token lifetime, token header name, etc + * @param array $grantTypes + * An array of OAuth2\GrantType\GrantTypeInterface to use for granting access tokens + * @param array $responseTypes + * Response types to use. array keys should be "code" and and "token" for + * Access Token and Authorization Code response types + * @param OAuth2\TokenType\TokenTypeInterface $tokenType + * The token type object to use. Valid token types are "bearer" and "mac" + * @param OAuth2\ScopeInterface $scopeUtil + * The scope utility class to use to validate scope + * @param OAuth2\ClientAssertionType\ClientAssertionTypeInterface $clientAssertionType + * The method in which to verify the client identity. Default is HttpBasic + * + * @ingroup oauth2_section_7 + */ + public function __construct($storage = array(), array $config = array(), array $grantTypes = array(), array $responseTypes = array(), TokenTypeInterface $tokenType = null, ScopeInterface $scopeUtil = null, ClientAssertionTypeInterface $clientAssertionType = null) + { + $storage = is_array($storage) ? $storage : array($storage); + $this->storages = array(); + foreach ($storage as $key => $service) { + $this->addStorage($service, $key); + } + + // merge all config values. These get passed to our controller objects + $this->config = array_merge(array( + 'use_crypto_tokens' => false, + 'store_encrypted_token_string' => true, + 'use_openid_connect' => false, + 'id_lifetime' => 3600, + 'access_lifetime' => 3600, + 'www_realm' => 'Service', + 'token_param_name' => 'access_token', + 'token_bearer_header_name' => 'Bearer', + 'enforce_state' => true, + 'require_exact_redirect_uri' => true, + 'allow_implicit' => false, + 'allow_credentials_in_request_body' => true, + 'allow_public_clients' => true, + 'always_issue_new_refresh_token' => false, + ), $config); + + foreach ($grantTypes as $key => $grantType) { + $this->addGrantType($grantType, $key); + } + foreach ($responseTypes as $key => $responseType) { + $this->addResponseType($responseType, $key); + } + $this->tokenType = $tokenType; + $this->scopeUtil = $scopeUtil; + $this->clientAssertionType = $clientAssertionType; + } + + public function getAuthorizeController() + { + if (is_null($this->authorizeController)) { + $this->authorizeController = $this->createDefaultAuthorizeController(); + } + + return $this->authorizeController; + } + + public function getTokenController() + { + if (is_null($this->tokenController)) { + $this->tokenController = $this->createDefaultTokenController(); + } + + return $this->tokenController; + } + + public function getResourceController() + { + if (is_null($this->resourceController)) { + $this->resourceController = $this->createDefaultResourceController(); + } + + return $this->resourceController; + } + + public function getUserInfoController() + { + if (is_null($this->userInfoController)) { + $this->userInfoController = $this->createDefaultUserInfoController(); + } + + return $this->userInfoController; + } + + /** + * every getter deserves a setter + */ + public function setAuthorizeController(AuthorizeControllerInterface $authorizeController) + { + $this->authorizeController = $authorizeController; + } + + /** + * every getter deserves a setter + */ + public function setTokenController(TokenControllerInterface $tokenController) + { + $this->tokenController = $tokenController; + } + + /** + * every getter deserves a setter + */ + public function setResourceController(ResourceControllerInterface $resourceController) + { + $this->resourceController = $resourceController; + } + + /** + * every getter deserves a setter + */ + public function setUserInfoController(UserInfoControllerInterface $userInfoController) + { + $this->userInfoController = $userInfoController; + } + + /** + * Return claims about the authenticated end-user. + * This would be called from the "/UserInfo" endpoint as defined in the spec. + * + * @param $request - OAuth2\RequestInterface + * Request object to grant access token + * + * @param $response - OAuth2\ResponseInterface + * Response object containing error messages (failure) or user claims (success) + * + * @throws InvalidArgumentException + * @throws LogicException + * + * @see http://openid.net/specs/openid-connect-core-1_0.html#UserInfo + */ + public function handleUserInfoRequest(RequestInterface $request, ResponseInterface $response = null) + { + $this->response = is_null($response) ? new Response() : $response; + $this->getUserInfoController()->handleUserInfoRequest($request, $this->response); + + return $this->response; + } + + /** + * Grant or deny a requested access token. + * This would be called from the "/token" endpoint as defined in the spec. + * Obviously, you can call your endpoint whatever you want. + * + * @param $request - OAuth2\RequestInterface + * Request object to grant access token + * + * @param $response - OAuth2\ResponseInterface + * Response object containing error messages (failure) or access token (success) + * + * @throws InvalidArgumentException + * @throws LogicException + * + * @see http://tools.ietf.org/html/rfc6749#section-4 + * @see http://tools.ietf.org/html/rfc6749#section-10.6 + * @see http://tools.ietf.org/html/rfc6749#section-4.1.3 + * + * @ingroup oauth2_section_4 + */ + public function handleTokenRequest(RequestInterface $request, ResponseInterface $response = null) + { + $this->response = is_null($response) ? new Response() : $response; + $this->getTokenController()->handleTokenRequest($request, $this->response); + + return $this->response; + } + + public function grantAccessToken(RequestInterface $request, ResponseInterface $response = null) + { + $this->response = is_null($response) ? new Response() : $response; + $value = $this->getTokenController()->grantAccessToken($request, $this->response); + + return $value; + } + + /** + * Redirect the user appropriately after approval. + * + * After the user has approved or denied the resource request the + * authorization server should call this function to redirect the user + * appropriately. + * + * @param $request + * The request should have the follow parameters set in the querystring: + * - response_type: The requested response: an access token, an + * authorization code, or both. + * - client_id: The client identifier as described in Section 2. + * - redirect_uri: An absolute URI to which the authorization server + * will redirect the user-agent to when the end-user authorization + * step is completed. + * - scope: (optional) The scope of the resource request expressed as a + * list of space-delimited strings. + * - state: (optional) An opaque value used by the client to maintain + * state between the request and callback. + * @param $is_authorized + * TRUE or FALSE depending on whether the user authorized the access. + * @param $user_id + * Identifier of user who authorized the client + * + * @see http://tools.ietf.org/html/rfc6749#section-4 + * + * @ingroup oauth2_section_4 + */ + public function handleAuthorizeRequest(RequestInterface $request, ResponseInterface $response, $is_authorized, $user_id = null) + { + $this->response = $response; + $this->getAuthorizeController()->handleAuthorizeRequest($request, $this->response, $is_authorized, $user_id); + + return $this->response; + } + + /** + * Pull the authorization request data out of the HTTP request. + * - The redirect_uri is OPTIONAL as per draft 20. But your implementation can enforce it + * by setting $config['enforce_redirect'] to true. + * - The state is OPTIONAL but recommended to enforce CSRF. Draft 21 states, however, that + * CSRF protection is MANDATORY. You can enforce this by setting the $config['enforce_state'] to true. + * + * The draft specifies that the parameters should be retrieved from GET, override the Response + * object to change this + * + * @return + * The authorization parameters so the authorization server can prompt + * the user for approval if valid. + * + * @see http://tools.ietf.org/html/rfc6749#section-4.1.1 + * @see http://tools.ietf.org/html/rfc6749#section-10.12 + * + * @ingroup oauth2_section_3 + */ + public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response = null) + { + $this->response = is_null($response) ? new Response() : $response; + $value = $this->getAuthorizeController()->validateAuthorizeRequest($request, $this->response); + + return $value; + } + + public function verifyResourceRequest(RequestInterface $request, ResponseInterface $response = null, $scope = null) + { + $this->response = is_null($response) ? new Response() : $response; + $value = $this->getResourceController()->verifyResourceRequest($request, $this->response, $scope); + + return $value; + } + + public function getAccessTokenData(RequestInterface $request, ResponseInterface $response = null) + { + $this->response = is_null($response) ? new Response() : $response; + $value = $this->getResourceController()->getAccessTokenData($request, $this->response); + + return $value; + } + + public function addGrantType(GrantTypeInterface $grantType, $key = null) + { + if (is_string($key)) { + $this->grantTypes[$key] = $grantType; + } else { + $this->grantTypes[$grantType->getQuerystringIdentifier()] = $grantType; + } + + // persist added grant type down to TokenController + if (!is_null($this->tokenController)) { + $this->getTokenController()->addGrantType($grantType); + } + } + + /** + * Set a storage object for the server + * + * @param $storage + * An object implementing one of the Storage interfaces + * @param $key + * If null, the storage is set to the key of each storage interface it implements + * + * @see storageMap + */ + public function addStorage($storage, $key = null) + { + // if explicitly set to a valid key, do not "magically" set below + if (isset($this->storageMap[$key])) { + if (!is_null($storage) && !$storage instanceof $this->storageMap[$key]) { + throw new \InvalidArgumentException(sprintf('storage of type "%s" must implement interface "%s"', $key, $this->storageMap[$key])); + } + $this->storages[$key] = $storage; + + // special logic to handle "client" and "client_credentials" strangeness + if ($key === 'client' && !isset($this->storages['client_credentials'])) { + if ($storage instanceof \OAuth2\Storage\ClientCredentialsInterface) { + $this->storages['client_credentials'] = $storage; + } + } elseif ($key === 'client_credentials' && !isset($this->storages['client'])) { + if ($storage instanceof \OAuth2\Storage\ClientInterface) { + $this->storages['client'] = $storage; + } + } + } elseif (!is_null($key) && !is_numeric($key)) { + throw new \InvalidArgumentException(sprintf('unknown storage key "%s", must be one of [%s]', $key, implode(', ', array_keys($this->storageMap)))); + } else { + $set = false; + foreach ($this->storageMap as $type => $interface) { + if ($storage instanceof $interface) { + $this->storages[$type] = $storage; + $set = true; + } + } + + if (!$set) { + throw new \InvalidArgumentException(sprintf('storage of class "%s" must implement one of [%s]', get_class($storage), implode(', ', $this->storageMap))); + } + } + } + + public function addResponseType(ResponseTypeInterface $responseType, $key = null) + { + if (isset($this->responseTypeMap[$key])) { + if (!$responseType instanceof $this->responseTypeMap[$key]) { + throw new \InvalidArgumentException(sprintf('responseType of type "%s" must implement interface "%s"', $key, $this->responseTypeMap[$key])); + } + $this->responseTypes[$key] = $responseType; + } elseif (!is_null($key) && !is_numeric($key)) { + throw new \InvalidArgumentException(sprintf('unknown responseType key "%s", must be one of [%s]', $key, implode(', ', array_keys($this->responseTypeMap)))); + } else { + $set = false; + foreach ($this->responseTypeMap as $type => $interface) { + if ($responseType instanceof $interface) { + $this->responseTypes[$type] = $responseType; + $set = true; + } + } + + if (!$set) { + throw new \InvalidArgumentException(sprintf('Unknown response type %s. Please implement one of [%s]', get_class($responseType), implode(', ', $this->responseTypeMap))); + } + } + } + + public function getScopeUtil() + { + if (!$this->scopeUtil) { + $storage = isset($this->storages['scope']) ? $this->storages['scope'] : null; + $this->scopeUtil = new Scope($storage); + } + + return $this->scopeUtil; + } + + /** + * every getter deserves a setter + */ + public function setScopeUtil($scopeUtil) + { + $this->scopeUtil = $scopeUtil; + } + + protected function createDefaultAuthorizeController() + { + if (!isset($this->storages['client'])) { + throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\ClientInterface to use the authorize server"); + } + if (0 == count($this->responseTypes)) { + $this->responseTypes = $this->getDefaultResponseTypes(); + } + if ($this->config['use_openid_connect'] && !isset($this->responseTypes['id_token'])) { + $this->responseTypes['id_token'] = $this->createDefaultIdTokenResponseType(); + if ($this->config['allow_implicit']) { + $this->responseTypes['token id_token'] = $this->createDefaultTokenIdTokenResponseType(); + } + } + + $config = array_intersect_key($this->config, array_flip(explode(' ', 'allow_implicit enforce_state require_exact_redirect_uri'))); + + if ($this->config['use_openid_connect']) { + return new OpenIDAuthorizeController($this->storages['client'], $this->responseTypes, $config, $this->getScopeUtil()); + } + + return new AuthorizeController($this->storages['client'], $this->responseTypes, $config, $this->getScopeUtil()); + } + + protected function createDefaultTokenController() + { + if (0 == count($this->grantTypes)) { + $this->grantTypes = $this->getDefaultGrantTypes(); + } + + if (is_null($this->clientAssertionType)) { + // see if HttpBasic assertion type is requred. If so, then create it from storage classes. + foreach ($this->grantTypes as $grantType) { + if (!$grantType instanceof ClientAssertionTypeInterface) { + if (!isset($this->storages['client_credentials'])) { + throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\ClientCredentialsInterface to use the token server"); + } + $config = array_intersect_key($this->config, array_flip(explode(' ', 'allow_credentials_in_request_body allow_public_clients'))); + $this->clientAssertionType = new HttpBasic($this->storages['client_credentials'], $config); + break; + } + } + } + + if (!isset($this->storages['client'])) { + throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\ClientInterface to use the token server"); + } + + $accessTokenResponseType = $this->getAccessTokenResponseType(); + + return new TokenController($accessTokenResponseType, $this->storages['client'], $this->grantTypes, $this->clientAssertionType, $this->getScopeUtil()); + } + + protected function createDefaultResourceController() + { + if ($this->config['use_crypto_tokens']) { + // overwrites access token storage with crypto token storage if "use_crypto_tokens" is set + if (!isset($this->storages['access_token']) || !$this->storages['access_token'] instanceof CryptoTokenInterface) { + $this->storages['access_token'] = $this->createDefaultCryptoTokenStorage(); + } + } elseif (!isset($this->storages['access_token'])) { + throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\AccessTokenInterface or use CryptoTokens to use the resource server"); + } + + if (!$this->tokenType) { + $this->tokenType = $this->getDefaultTokenType(); + } + + $config = array_intersect_key($this->config, array('www_realm' => '')); + + return new ResourceController($this->tokenType, $this->storages['access_token'], $config, $this->getScopeUtil()); + } + + protected function createDefaultUserInfoController() + { + if ($this->config['use_crypto_tokens']) { + // overwrites access token storage with crypto token storage if "use_crypto_tokens" is set + if (!isset($this->storages['access_token']) || !$this->storages['access_token'] instanceof CryptoTokenInterface) { + $this->storages['access_token'] = $this->createDefaultCryptoTokenStorage(); + } + } elseif (!isset($this->storages['access_token'])) { + throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\AccessTokenInterface or use CryptoTokens to use the UserInfo server"); + } + + if (!isset($this->storages['user_claims'])) { + throw new \LogicException("You must supply a storage object implementing OAuth2\OpenID\Storage\UserClaimsInterface to use the UserInfo server"); + } + + if (!$this->tokenType) { + $this->tokenType = $this->getDefaultTokenType(); + } + + $config = array_intersect_key($this->config, array('www_realm' => '')); + + return new UserInfoController($this->tokenType, $this->storages['access_token'], $this->storages['user_claims'], $config, $this->getScopeUtil()); + } + + protected function getDefaultTokenType() + { + $config = array_intersect_key($this->config, array_flip(explode(' ', 'token_param_name token_bearer_header_name'))); + + return new Bearer($config); + } + + protected function getDefaultResponseTypes() + { + $responseTypes = array(); + + if ($this->config['allow_implicit']) { + $responseTypes['token'] = $this->getAccessTokenResponseType(); + } + + if ($this->config['use_openid_connect']) { + $responseTypes['id_token'] = $this->getIdTokenResponseType(); + if ($this->config['allow_implicit']) { + $responseTypes['token id_token'] = $this->getTokenIdTokenResponseType(); + } + } + + if (isset($this->storages['authorization_code'])) { + $config = array_intersect_key($this->config, array_flip(explode(' ', 'enforce_redirect auth_code_lifetime'))); + if ($this->config['use_openid_connect']) { + if (!$this->storages['authorization_code'] instanceof OpenIDAuthorizationCodeInterface) { + throw new \LogicException("Your authorization_code storage must implement OAuth2\OpenID\Storage\AuthorizationCodeInterface to work when 'use_openid_connect' is true"); + } + $responseTypes['code'] = new OpenIDAuthorizationCodeResponseType($this->storages['authorization_code'], $config); + } else { + $responseTypes['code'] = new AuthorizationCodeResponseType($this->storages['authorization_code'], $config); + } + } + + if (count($responseTypes) == 0) { + throw new \LogicException("You must supply an array of response_types in the constructor or implement a OAuth2\Storage\AuthorizationCodeInterface storage object or set 'allow_implicit' to true and implement a OAuth2\Storage\AccessTokenInterface storage object"); + } + + return $responseTypes; + } + + protected function getDefaultGrantTypes() + { + $grantTypes = array(); + + if (isset($this->storages['user_credentials'])) { + $grantTypes['password'] = new UserCredentials($this->storages['user_credentials']); + } + + if (isset($this->storages['client_credentials'])) { + $config = array_intersect_key($this->config, array('allow_credentials_in_request_body' => '')); + $grantTypes['client_credentials'] = new ClientCredentials($this->storages['client_credentials'], $config); + } + + if (isset($this->storages['refresh_token'])) { + $config = array_intersect_key($this->config, array('always_issue_new_refresh_token' => '')); + $grantTypes['refresh_token'] = new RefreshToken($this->storages['refresh_token'], $config); + } + + if (isset($this->storages['authorization_code'])) { + if ($this->config['use_openid_connect']) { + if (!$this->storages['authorization_code'] instanceof OpenIDAuthorizationCodeInterface) { + throw new \LogicException("Your authorization_code storage must implement OAuth2\OpenID\Storage\AuthorizationCodeInterface to work when 'use_openid_connect' is true"); + } + $grantTypes['authorization_code'] = new OpenIDAuthorizationCodeGrantType($this->storages['authorization_code']); + } else { + $grantTypes['authorization_code'] = new AuthorizationCode($this->storages['authorization_code']); + } + } + + if (count($grantTypes) == 0) { + throw new \LogicException("Unable to build default grant types - You must supply an array of grant_types in the constructor"); + } + + return $grantTypes; + } + + protected function getAccessTokenResponseType() + { + if (isset($this->responseTypes['token'])) { + return $this->responseTypes['token']; + } + + if ($this->config['use_crypto_tokens']) { + return $this->createDefaultCryptoTokenResponseType(); + } + + return $this->createDefaultAccessTokenResponseType(); + } + + protected function getIdTokenResponseType() + { + if (isset($this->responseTypes['id_token'])) { + return $this->responseTypes['id_token']; + } + + return $this->createDefaultIdTokenResponseType(); + } + + protected function getTokenIdTokenResponseType() + { + if (isset($this->responseTypes['token id_token'])) { + return $this->responseTypes['token id_token']; + } + + return $this->createDefaultTokenIdTokenResponseType(); + } + + /** + * For Resource Controller + */ + protected function createDefaultCryptoTokenStorage() + { + if (!isset($this->storages['public_key'])) { + throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use crypto tokens"); + } + $tokenStorage = null; + if (!empty($this->config['store_encrypted_token_string']) && isset($this->storages['access_token'])) { + $tokenStorage = $this->storages['access_token']; + } + // wrap the access token storage as required. + return new CryptoTokenStorage($this->storages['public_key'], $tokenStorage); + } + + /** + * For Authorize and Token Controllers + */ + protected function createDefaultCryptoTokenResponseType() + { + if (!isset($this->storages['public_key'])) { + throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use crypto tokens"); + } + + $tokenStorage = null; + if (isset($this->storages['access_token'])) { + $tokenStorage = $this->storages['access_token']; + } + + $refreshStorage = null; + if (isset($this->storages['refresh_token'])) { + $refreshStorage = $this->storages['refresh_token']; + } + + $config = array_intersect_key($this->config, array_flip(explode(' ', 'store_encrypted_token_string'))); + + return new CryptoToken($this->storages['public_key'], $tokenStorage, $refreshStorage, $config); + } + + protected function createDefaultAccessTokenResponseType() + { + if (!isset($this->storages['access_token'])) { + throw new \LogicException("You must supply a response type implementing OAuth2\ResponseType\AccessTokenInterface, or a storage object implementing OAuth2\Storage\AccessTokenInterface to use the token server"); + } + + $refreshStorage = null; + if (isset($this->storages['refresh_token'])) { + $refreshStorage = $this->storages['refresh_token']; + } + + $config = array_intersect_key($this->config, array_flip(explode(' ', 'access_lifetime refresh_token_lifetime'))); + $config['token_type'] = $this->tokenType ? $this->tokenType->getTokenType() : $this->getDefaultTokenType()->getTokenType(); + + return new AccessToken($this->storages['access_token'], $refreshStorage, $config); + } + + protected function createDefaultIdTokenResponseType() + { + if (!isset($this->storages['user_claims'])) { + throw new \LogicException("You must supply a storage object implementing OAuth2\OpenID\Storage\UserClaimsInterface to use openid connect"); + } + if (!isset($this->storages['public_key'])) { + throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use openid connect"); + } + + $config = array_intersect_key($this->config, array_flip(explode(' ', 'issuer id_lifetime'))); + return new IdToken($this->storages['user_claims'], $this->storages['public_key'], $config); + } + + protected function createDefaultTokenIdTokenResponseType() + { + return new TokenIdToken($this->getAccessTokenResponseType(), $this->getIdTokenResponseType()); + } + + public function getResponse() + { + return $this->response; + } + + public function getStorages() + { + return $this->storages; + } + + public function getStorage($name) + { + return isset($this->storages[$name]) ? $this->storages[$name] : null; + } + + public function getGrantTypes() + { + return $this->grantTypes; + } + + public function getGrantType($name) + { + return isset($this->grantTypes[$name]) ? $this->grantTypes[$name] : null; + } + + public function getResponseTypes() + { + return $this->responseTypes; + } + + public function getResponseType($name) + { + return isset($this->responseTypes[$name]) ? $this->responseTypes[$name] : null; + } + + public function getTokenType() + { + return $this->tokenType; + } + + public function getClientAssertionType() + { + return $this->clientAssertionType; + } + + public function setConfig($name, $value) + { + $this->config[$name] = $value; + } + + public function getConfig($name, $default = null) + { + return isset($this->config[$name]) ? $this->config[$name] : $default; + } +} diff --git a/server/php/R/libs/vendors/OAuth2/Storage/AccessTokenInterface.php b/server/php/R/libs/vendors/OAuth2/Storage/AccessTokenInterface.php new file mode 100644 index 000000000..b55c3d6c4 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/Storage/AccessTokenInterface.php @@ -0,0 +1,53 @@ + + */ +interface AccessTokenInterface +{ + /** + * Look up the supplied oauth_token from storage. + * + * We need to retrieve access token data as we create and verify tokens. + * + * @param $oauth_token + * oauth_token to be check with. + * + * @return + * An associative array as below, and return NULL if the supplied oauth_token + * is invalid: + * - expires: Stored expiration in unix timestamp. + * - client_id: (optional) Stored client identifier. + * - user_id: (optional) Stored user identifier. + * - scope: (optional) Stored scope values in space-separated string. + * - id_token: (optional) Stored id_token (if "use_openid_connect" is true). + * + * @ingroup oauth2_section_7 + */ + public function getAccessToken($oauth_token); + + /** + * Store the supplied access token values to storage. + * + * We need to store access token data as we create and verify tokens. + * + * @param $oauth_token + * oauth_token to be stored. + * @param $client_id + * Client identifier to be stored. + * @param $user_id + * User identifier to be stored. + * @param int $expires + * Expiration to be stored as a Unix timestamp. + * @param string $scope + * (optional) Scopes to be stored in space-separated string. + * + * @ingroup oauth2_section_4 + */ + public function setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope = null); +} diff --git a/server/php/R/libs/vendors/OAuth2/Storage/AuthorizationCodeInterface.php b/server/php/R/libs/vendors/OAuth2/Storage/AuthorizationCodeInterface.php new file mode 100644 index 000000000..d59a61f02 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/Storage/AuthorizationCodeInterface.php @@ -0,0 +1,92 @@ + + */ +interface AuthorizationCodeInterface +{ + /** + * The Authorization Code grant type supports a response type of "code". + * + * @var string + * @see http://tools.ietf.org/html/rfc6749#section-1.4.1 + * @see http://tools.ietf.org/html/rfc6749#section-4.2 + */ + const RESPONSE_TYPE_CODE = "code"; + + /** + * Fetch authorization code data (probably the most common grant type). + * + * Retrieve the stored data for the given authorization code. + * + * Required for OAuth2::GRANT_TYPE_AUTH_CODE. + * + * @param $code + * Authorization code to be check with. + * + * @return + * An associative array as below, and NULL if the code is invalid + * @code + * return array( + * "client_id" => CLIENT_ID, // REQUIRED Stored client identifier + * "user_id" => USER_ID, // REQUIRED Stored user identifier + * "expires" => EXPIRES, // REQUIRED Stored expiration in unix timestamp + * "redirect_uri" => REDIRECT_URI, // REQUIRED Stored redirect URI + * "scope" => SCOPE, // OPTIONAL Stored scope values in space-separated string + * ); + * @endcode + * + * @see http://tools.ietf.org/html/rfc6749#section-4.1 + * + * @ingroup oauth2_section_4 + */ + public function getAuthorizationCode($code); + + /** + * Take the provided authorization code values and store them somewhere. + * + * This function should be the storage counterpart to getAuthCode(). + * + * If storage fails for some reason, we're not currently checking for + * any sort of success/failure, so you should bail out of the script + * and provide a descriptive fail message. + * + * Required for OAuth2::GRANT_TYPE_AUTH_CODE. + * + * @param $code + * Authorization code to be stored. + * @param $client_id + * Client identifier to be stored. + * @param $user_id + * User identifier to be stored. + * @param string $redirect_uri + * Redirect URI(s) to be stored in a space-separated string. + * @param int $expires + * Expiration to be stored as a Unix timestamp. + * @param string $scope + * (optional) Scopes to be stored in space-separated string. + * + * @ingroup oauth2_section_4 + */ + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null); + + /** + * once an Authorization Code is used, it must be exipired + * + * @see http://tools.ietf.org/html/rfc6749#section-4.1.2 + * + * The client MUST NOT use the authorization code + * more than once. If an authorization code is used more than + * once, the authorization server MUST deny the request and SHOULD + * revoke (when possible) all tokens previously issued based on + * that authorization code + * + */ + public function expireAuthorizationCode($code); +} diff --git a/server/php/R/libs/vendors/OAuth2/Storage/Cassandra.php b/server/php/R/libs/vendors/OAuth2/Storage/Cassandra.php new file mode 100644 index 000000000..f6d1e05dc --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/Storage/Cassandra.php @@ -0,0 +1,367 @@ + + * composer require thobbs/phpcassa:dev-master + * + * + * Once this is done, instantiate the + * + * $cassandra = new \phpcassa\Connection\ConnectionPool('oauth2_server', array('127.0.0.1:9160')); + * + * + * Then, register the storage client: + * + * $storage = new OAuth2\Storage\Cassandra($cassandra); + * $storage->setClientDetails($client_id, $client_secret, $redirect_uri); + * + * + * @see test/lib/OAuth2/Storage/Bootstrap::getCassandraStorage + */ +class Cassandra implements AuthorizationCodeInterface, + AccessTokenInterface, + ClientCredentialsInterface, + UserCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + ScopeInterface, + OpenIDAuthorizationCodeInterface +{ + + private $cache; + + /* The cassandra client */ + protected $cassandra; + + /* Configuration array */ + protected $config; + + /** + * Cassandra Storage! uses phpCassa + * + * @param \phpcassa\ConnectionPool $cassandra + * @param array $config + */ + public function __construct($connection = array(), array $config = array()) + { + if ($connection instanceof ConnectionPool) { + $this->cassandra = $connection; + } else { + if (!is_array($connection)) { + throw new \InvalidArgumentException('First argument to OAuth2\Storage\Mongo must be an instance of MongoDB or a configuration array'); + } + $connection = array_merge(array( + 'keyspace' => 'oauth2', + 'servers' => null, + ), $connection); + + $this->cassandra = new ConnectionPool($connection['keyspace'], $connection['servers']); + } + + $this->config = array_merge(array( + // cassandra config + 'column_family' => 'auth', + + // key names + 'client_key' => 'oauth_clients:', + 'access_token_key' => 'oauth_access_tokens:', + 'refresh_token_key' => 'oauth_refresh_tokens:', + 'code_key' => 'oauth_authorization_codes:', + 'user_key' => 'oauth_users:', + 'jwt_key' => 'oauth_jwt:', + 'scope_key' => 'oauth_scopes:', + ), $config); + } + + protected function getValue($key) + { + if (isset($this->cache[$key])) { + return $this->cache[$key]; + } + + $cf = new ColumnFamily($this->cassandra, $this->config['column_family']); + + try { + $value = array_shift($cf->get($key, new ColumnSlice("", ""))); + } catch (\cassandra\NotFoundException $e) { + return false; + } + + return json_decode($value, true); + } + + protected function setValue($key, $value, $expire = 0) + { + $this->cache[$key] = $value; + + $cf = new ColumnFamily($this->cassandra, $this->config['column_family']); + + $str = json_encode($value); + if ($expire > 0) { + try { + $seconds = $expire - time(); + // __data key set as C* requires a field, note: max TTL can only be 630720000 seconds + $cf->insert($key, array('__data' => $str), null, $seconds); + } catch(\Exception $e) { + return false; + } + } else { + try { + // __data key set as C* requires a field + $cf->insert($key, array('__data' => $str)); + } catch(\Exception $e) { + return false; + } + } + + return true; + } + + protected function expireValue($key) + { + unset($this->cache[$key]); + + $cf = new ColumnFamily($this->cassandra, $this->config['column_family']); + try { + // __data key set as C* requires a field + $cf->remove($key, array('__data')); + } catch(\Exception $e) { + return false; + } + + return true; + } + + /* AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + return $this->getValue($this->config['code_key'] . $code); + } + + public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + return $this->setValue( + $this->config['code_key'] . $authorization_code, + compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'), + $expires + ); + } + + public function expireAuthorizationCode($code) + { + $key = $this->config['code_key'] . $code; + unset($this->cache[$key]); + + return $this->expireValue($key); + } + + /* UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + $user = $this->getUserDetails($username); + + return $user && $user['password'] === $password; + } + + public function getUserDetails($username) + { + return $this->getUser($username); + } + + public function getUser($username) + { + if (!$userInfo = $this->getValue($this->config['user_key'] . $username)) { + return false; + } + + // the default behavior is to use "username" as the user_id + return array_merge(array( + 'user_id' => $username, + ), $userInfo); + } + + public function setUser($username, $password, $first_name = null, $last_name = null) + { + return $this->setValue( + $this->config['user_key'] . $username, + compact('username', 'password', 'first_name', 'last_name') + ); + } + + /* ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + if (!$client = $this->getClientDetails($client_id)) { + return false; + } + + return isset($client['client_secret']) + && $client['client_secret'] == $client_secret; + } + + public function isPublicClient($client_id) + { + if (!$client = $this->getClientDetails($client_id)) { + return false; + } + + return empty($result['client_secret']);; + } + + /* ClientInterface */ + public function getClientDetails($client_id) + { + return $this->getValue($this->config['client_key'] . $client_id); + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + return $this->setValue( + $this->config['client_key'] . $client_id, + compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id') + ); + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + $details = $this->getClientDetails($client_id); + if (isset($details['grant_types'])) { + $grant_types = explode(' ', $details['grant_types']); + + return in_array($grant_type, (array) $grant_types); + } + + // if grant_types are not defined, then none are restricted + return true; + } + + /* RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + return $this->getValue($this->config['refresh_token_key'] . $refresh_token); + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + return $this->setValue( + $this->config['refresh_token_key'] . $refresh_token, + compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope'), + $expires + ); + } + + public function unsetRefreshToken($refresh_token) + { + return $this->expireValue($this->config['refresh_token_key'] . $refresh_token); + } + + /* AccessTokenInterface */ + public function getAccessToken($access_token) + { + return $this->getValue($this->config['access_token_key'].$access_token); + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + return $this->setValue( + $this->config['access_token_key'].$access_token, + compact('access_token', 'client_id', 'user_id', 'expires', 'scope'), + $expires + ); + } + + /* ScopeInterface */ + public function scopeExists($scope) + { + $scope = explode(' ', $scope); + + $result = $this->getValue($this->config['scope_key'].'supported:global'); + + $supportedScope = explode(' ', (string) $result); + + return (count(array_diff($scope, $supportedScope)) == 0); + } + + public function getDefaultScope($client_id = null) + { + if (is_null($client_id) || !$result = $this->getValue($this->config['scope_key'].'default:'.$client_id)) { + $result = $this->getValue($this->config['scope_key'].'default:global'); + } + + return $result; + } + + public function setScope($scope, $client_id = null, $type = 'supported') + { + if (!in_array($type, array('default', 'supported'))) { + throw new \InvalidArgumentException('"$type" must be one of "default", "supported"'); + } + + if (is_null($client_id)) { + $key = $this->config['scope_key'].$type.':global'; + } else { + $key = $this->config['scope_key'].$type.':'.$client_id; + } + + return $this->setValue($key, $scope); + } + + /*JWTBearerInterface */ + public function getClientKey($client_id, $subject) + { + if (!$jwt = $this->getValue($this->config['jwt_key'] . $client_id)) { + return false; + } + + if (isset($jwt['subject']) && $jwt['subject'] == $subject ) { + return $jwt['key']; + } + + return null; + } + + public function setClientKey($client_id, $key, $subject = null) + { + return $this->setValue($this->config['jwt_key'] . $client_id, array( + 'key' => $key, + 'subject' => $subject + )); + } + + /*ScopeInterface */ + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + + return null; + } + + public function getJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs cassandra implementation. + throw new \Exception('getJti() for the Cassandra driver is currently unimplemented.'); + } + + public function setJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs cassandra implementation. + throw new \Exception('setJti() for the Cassandra driver is currently unimplemented.'); + } +} + diff --git a/server/php/R/libs/vendors/OAuth2/Storage/ClientCredentialsInterface.php b/server/php/R/libs/vendors/OAuth2/Storage/ClientCredentialsInterface.php new file mode 100644 index 000000000..3318c6966 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/Storage/ClientCredentialsInterface.php @@ -0,0 +1,49 @@ + + */ +interface ClientCredentialsInterface extends ClientInterface +{ + + /** + * Make sure that the client credentials is valid. + * + * @param $client_id + * Client identifier to be check with. + * @param $client_secret + * (optional) If a secret is required, check that they've given the right one. + * + * @return + * TRUE if the client credentials are valid, and MUST return FALSE if it isn't. + * @endcode + * + * @see http://tools.ietf.org/html/rfc6749#section-3.1 + * + * @ingroup oauth2_section_3 + */ + public function checkClientCredentials($client_id, $client_secret = null); + + /** + * Determine if the client is a "public" client, and therefore + * does not require passing credentials for certain grant types + * + * @param $client_id + * Client identifier to be check with. + * + * @return + * TRUE if the client is public, and FALSE if it isn't. + * @endcode + * + * @see http://tools.ietf.org/html/rfc6749#section-2.3 + * @see https://github.com/bshaffer/oauth2-server-php/issues/257 + * + * @ingroup oauth2_section_2 + */ + public function isPublicClient($client_id); +} diff --git a/server/php/R/libs/vendors/OAuth2/Storage/ClientInterface.php b/server/php/R/libs/vendors/OAuth2/Storage/ClientInterface.php new file mode 100644 index 000000000..ec8d7fdd8 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/Storage/ClientInterface.php @@ -0,0 +1,66 @@ + + */ +interface ClientInterface +{ + /** + * Get client details corresponding client_id. + * + * OAuth says we should store request URIs for each registered client. + * Implement this function to grab the stored URI for a given client id. + * + * @param $client_id + * Client identifier to be check with. + * + * @return array + * Client details. The only mandatory key in the array is "redirect_uri". + * This function MUST return FALSE if the given client does not exist or is + * invalid. "redirect_uri" can be space-delimited to allow for multiple valid uris. + * @code + * return array( + * "redirect_uri" => REDIRECT_URI, // REQUIRED redirect_uri registered for the client + * "client_id" => CLIENT_ID, // OPTIONAL the client id + * "grant_types" => GRANT_TYPES, // OPTIONAL an array of restricted grant types + * "user_id" => USER_ID, // OPTIONAL the user identifier associated with this client + * "scope" => SCOPE, // OPTIONAL the scopes allowed for this client + * ); + * @endcode + * + * @ingroup oauth2_section_4 + */ + public function getClientDetails($client_id); + + /** + * Get the scope associated with this client + * + * @return + * STRING the space-delineated scope list for the specified client_id + */ + public function getClientScope($client_id); + + /** + * Check restricted grant types of corresponding client identifier. + * + * If you want to restrict clients to certain grant types, override this + * function. + * + * @param $client_id + * Client identifier to be check with. + * @param $grant_type + * Grant type to be check with + * + * @return + * TRUE if the grant type is supported by this client identifier, and + * FALSE if it isn't. + * + * @ingroup oauth2_section_4 + */ + public function checkRestrictedGrantType($client_id, $grant_type); +} diff --git a/server/php/R/libs/vendors/OAuth2/Storage/CryptoToken.php b/server/php/R/libs/vendors/OAuth2/Storage/CryptoToken.php new file mode 100644 index 000000000..f107e45f9 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/Storage/CryptoToken.php @@ -0,0 +1,77 @@ + + */ +class CryptoToken implements CryptoTokenInterface +{ + protected $publicKeyStorage; + protected $tokenStorage; + protected $encryptionUtil; + + /** + * @param string $publicKey + * the public key encryption to use + * + * @param string $privateKey (optional) + * (optional) the private key to use to sign tokens + * this is only required for token granting, and can be omitted for resource servers, + * as only the publc key is required for crypto token verification + * + * @param array $config + * (optional) configuration array. Valid parameters are + * - encryption_algorithm + * the algorithm to use for encryption. This is passed to the + * EncryptionInterface object. + * @see OAuth2\Encryption\Jwt::verifySignature + * + * @param OAuth2\Storage\AccessTokenInterface $tokenStorage + * (optional) persist the access token to another storage. This is useful if + * you want to retain access token grant information somewhere, but + * is not necessary when using this grant type. + * + * @param OAuth2\Encryption\EncryptionInterface $encryptionUtil + * (optional) class to use for "encode" and "decode" functions. + */ + public function __construct(PublicKeyInterface $publicKeyStorage, AccessTokenInterface $tokenStorage = null, EncryptionInterface $encryptionUtil = null) + { + $this->publicKeyStorage = $publicKeyStorage; + $this->tokenStorage = $tokenStorage; + if (is_null($encryptionUtil)) { + $encryptionUtil = new Jwt; + } + $this->encryptionUtil = $encryptionUtil; + } + + public function getAccessToken($oauth_token) + { + // just decode the token, don't verify + if (!$tokenData = $this->encryptionUtil->decode($oauth_token, null, false)) { + return false; + } + + $client_id = isset($tokenData['client_id']) ? $tokenData['client_id'] : null; + $public_key = $this->publicKeyStorage->getPublicKey($client_id); + $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id); + + // now that we have the client_id, verify the token + if (false === $this->encryptionUtil->decode($oauth_token, $public_key, true)) { + return false; + } + + return $tokenData; + } + + public function setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope = null) + { + if ($this->tokenStorage) { + return $this->tokenStorage->setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope); + } + } +} diff --git a/server/php/R/libs/vendors/OAuth2/Storage/CryptoTokenInterface.php b/server/php/R/libs/vendors/OAuth2/Storage/CryptoTokenInterface.php new file mode 100644 index 000000000..867710c11 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/Storage/CryptoTokenInterface.php @@ -0,0 +1,14 @@ + + */ +interface CryptoTokenInterface extends AccessTokenInterface +{ + +} diff --git a/server/php/R/libs/vendors/OAuth2/Storage/JwtBearerInterface.php b/server/php/R/libs/vendors/OAuth2/Storage/JwtBearerInterface.php new file mode 100644 index 000000000..c83aa72ea --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/Storage/JwtBearerInterface.php @@ -0,0 +1,74 @@ + + */ +interface JwtBearerInterface +{ + /** + * Get the public key associated with a client_id + * + * @param $client_id + * Client identifier to be checked with. + * + * @return + * STRING Return the public key for the client_id if it exists, and MUST return FALSE if it doesn't. + */ + public function getClientKey($client_id, $subject); + + /** + * Get a jti (JSON token identifier) by matching against the client_id, subject, audience and expiration. + * + * @param $client_id + * Client identifier to match. + * + * @param $subject + * The subject to match. + * + * @param $audience + * The audience to match. + * + * @param $expiration + * The expiration of the jti. + * + * @param $jti + * The jti to match. + * + * @return + * An associative array as below, and return NULL if the jti does not exist. + * - issuer: Stored client identifier. + * - subject: Stored subject. + * - audience: Stored audience. + * - expires: Stored expiration in unix timestamp. + * - jti: The stored jti. + */ + public function getJti($client_id, $subject, $audience, $expiration, $jti); + + /** + * Store a used jti so that we can check against it to prevent replay attacks. + * @param $client_id + * Client identifier to insert. + * + * @param $subject + * The subject to insert. + * + * @param $audience + * The audience to insert. + * + * @param $expiration + * The expiration of the jti. + * + * @param $jti + * The jti to insert. + */ + public function setJti($client_id, $subject, $audience, $expiration, $jti); +} diff --git a/server/php/R/libs/vendors/OAuth2/Storage/Memory.php b/server/php/R/libs/vendors/OAuth2/Storage/Memory.php new file mode 100644 index 000000000..35e6cbacf --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/Storage/Memory.php @@ -0,0 +1,364 @@ + + */ +class Memory implements AuthorizationCodeInterface, + UserCredentialsInterface, + UserClaimsInterface, + AccessTokenInterface, + ClientCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + ScopeInterface, + PublicKeyInterface, + OpenIDAuthorizationCodeInterface +{ + public $authorizationCodes; + public $userCredentials; + public $clientCredentials; + public $refreshTokens; + public $accessTokens; + public $jwt; + public $jti; + public $supportedScopes; + public $defaultScope; + public $keys; + + public function __construct($params = array()) + { + $params = array_merge(array( + 'authorization_codes' => array(), + 'user_credentials' => array(), + 'client_credentials' => array(), + 'refresh_tokens' => array(), + 'access_tokens' => array(), + 'jwt' => array(), + 'jti' => array(), + 'default_scope' => null, + 'supported_scopes' => array(), + 'keys' => array(), + ), $params); + + $this->authorizationCodes = $params['authorization_codes']; + $this->userCredentials = $params['user_credentials']; + $this->clientCredentials = $params['client_credentials']; + $this->refreshTokens = $params['refresh_tokens']; + $this->accessTokens = $params['access_tokens']; + $this->jwt = $params['jwt']; + $this->jti = $params['jti']; + $this->supportedScopes = $params['supported_scopes']; + $this->defaultScope = $params['default_scope']; + $this->keys = $params['keys']; + } + + /* AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + if (!isset($this->authorizationCodes[$code])) { + return false; + } + + return array_merge(array( + 'authorization_code' => $code, + ), $this->authorizationCodes[$code]); + } + + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + $this->authorizationCodes[$code] = compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'); + + return true; + } + + public function setAuthorizationCodes($authorization_codes) + { + $this->authorizationCodes = $authorization_codes; + } + + public function expireAuthorizationCode($code) + { + unset($this->authorizationCodes[$code]); + } + + /* UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + $userDetails = $this->getUserDetails($username); + + return $userDetails && $userDetails['password'] && $userDetails['password'] === $password; + } + + public function setUser($username, $password, $firstName = null, $lastName = null) + { + $this->userCredentials[$username] = array( + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName, + ); + + return true; + } + + public function getUserDetails($username) + { + if (!isset($this->userCredentials[$username])) { + return false; + } + + return array_merge(array( + 'user_id' => $username, + 'password' => null, + 'first_name' => null, + 'last_name' => null, + ), $this->userCredentials[$username]); + } + + /* UserClaimsInterface */ + public function getUserClaims($user_id, $claims) + { + if (!$userDetails = $this->getUserDetails($user_id)) { + return false; + } + + $claims = explode(' ', trim($claims)); + $userClaims = array(); + + // for each requested claim, if the user has the claim, set it in the response + $validClaims = explode(' ', self::VALID_CLAIMS); + foreach ($validClaims as $validClaim) { + if (in_array($validClaim, $claims)) { + if ($validClaim == 'address') { + // address is an object with subfields + $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails); + } else { + $userClaims = array_merge($this->getUserClaim($validClaim, $userDetails)); + } + } + } + + return $userClaims; + } + + protected function getUserClaim($claim, $userDetails) + { + $userClaims = array(); + $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim))); + $claimValues = explode(' ', $claimValuesString); + + foreach ($claimValues as $value) { + $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null; + } + + return $userClaims; + } + + /* ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + return isset($this->clientCredentials[$client_id]['client_secret']) && $this->clientCredentials[$client_id]['client_secret'] === $client_secret; + } + + public function isPublicClient($client_id) + { + if (!isset($this->clientCredentials[$client_id])) { + return false; + } + + return empty($this->clientCredentials[$client_id]['client_secret']); + } + + /* ClientInterface */ + public function getClientDetails($client_id) + { + if (!isset($this->clientCredentials[$client_id])) { + return false; + } + + $clientDetails = array_merge(array( + 'client_id' => $client_id, + 'client_secret' => null, + 'redirect_uri' => null, + 'scope' => null, + ), $this->clientCredentials[$client_id]); + + return $clientDetails; + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + if (isset($this->clientCredentials[$client_id]['grant_types'])) { + $grant_types = explode(' ', $this->clientCredentials[$client_id]['grant_types']); + + return in_array($grant_type, $grant_types); + } + + // if grant_types are not defined, then none are restricted + return true; + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + $this->clientCredentials[$client_id] = array( + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + ); + + return true; + } + + /* RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + return isset($this->refreshTokens[$refresh_token]) ? $this->refreshTokens[$refresh_token] : false; + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + $this->refreshTokens[$refresh_token] = compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope'); + + return true; + } + + public function unsetRefreshToken($refresh_token) + { + unset($this->refreshTokens[$refresh_token]); + } + + public function setRefreshTokens($refresh_tokens) + { + $this->refreshTokens = $refresh_tokens; + } + + /* AccessTokenInterface */ + public function getAccessToken($access_token) + { + return isset($this->accessTokens[$access_token]) ? $this->accessTokens[$access_token] : false; + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null, $id_token = null) + { + $this->accessTokens[$access_token] = compact('access_token', 'client_id', 'user_id', 'expires', 'scope', 'id_token'); + + return true; + } + + public function scopeExists($scope) + { + $scope = explode(' ', trim($scope)); + + return (count(array_diff($scope, $this->supportedScopes)) == 0); + } + + public function getDefaultScope($client_id = null) + { + return $this->defaultScope; + } + + /*JWTBearerInterface */ + public function getClientKey($client_id, $subject) + { + if (isset($this->jwt[$client_id])) { + $jwt = $this->jwt[$client_id]; + if ($jwt) { + if ($jwt["subject"] == $subject) { + return $jwt["key"]; + } + } + } + + return false; + } + + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + + return null; + } + + public function getJti($client_id, $subject, $audience, $expires, $jti) + { + foreach ($this->jti as $storedJti) { + if ($storedJti['issuer'] == $client_id && $storedJti['subject'] == $subject && $storedJti['audience'] == $audience && $storedJti['expires'] == $expires && $storedJti['jti'] == $jti) { + return array( + 'issuer' => $storedJti['issuer'], + 'subject' => $storedJti['subject'], + 'audience' => $storedJti['audience'], + 'expires' => $storedJti['expires'], + 'jti' => $storedJti['jti'] + ); + } + } + + return null; + } + + public function setJti($client_id, $subject, $audience, $expires, $jti) + { + $this->jti[] = array('issuer' => $client_id, 'subject' => $subject, 'audience' => $audience, 'expires' => $expires, 'jti' => $jti); + } + + /*PublicKeyInterface */ + public function getPublicKey($client_id = null) + { + if (isset($this->keys[$client_id])) { + return $this->keys[$client_id]['public_key']; + } + + // use a global encryption pair + if (isset($this->keys['public_key'])) { + return $this->keys['public_key']; + } + + return false; + } + + public function getPrivateKey($client_id = null) + { + if (isset($this->keys[$client_id])) { + return $this->keys[$client_id]['private_key']; + } + + // use a global encryption pair + if (isset($this->keys['private_key'])) { + return $this->keys['private_key']; + } + + return false; + } + + public function getEncryptionAlgorithm($client_id = null) + { + if (isset($this->keys[$client_id]['encryption_algorithm'])) { + return $this->keys[$client_id]['encryption_algorithm']; + } + + // use a global encryption algorithm + if (isset($this->keys['encryption_algorithm'])) { + return $this->keys['encryption_algorithm']; + } + + return 'RS256'; + } +} diff --git a/server/php/R/libs/vendors/OAuth2/Storage/Mongo.php b/server/php/R/libs/vendors/OAuth2/Storage/Mongo.php new file mode 100644 index 000000000..87935fe71 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/Storage/Mongo.php @@ -0,0 +1,339 @@ + + */ +class Mongo implements AuthorizationCodeInterface, + AccessTokenInterface, + ClientCredentialsInterface, + UserCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + OpenIDAuthorizationCodeInterface +{ + protected $db; + protected $config; + + public function __construct($connection, $config = array()) + { + if ($connection instanceof \MongoDB) { + $this->db = $connection; + } else { + if (!is_array($connection)) { + throw new \InvalidArgumentException('First argument to OAuth2\Storage\Mongo must be an instance of MongoDB or a configuration array'); + } + $server = sprintf('mongodb://%s:%d', $connection['host'], $connection['port']); + $m = new \MongoClient($server); + $this->db = $m->{$connection['database']}; + } + + // Unix timestamps might get larger than 32 bits, + // so let's add native support for 64 bit ints. + ini_set('mongo.native_long', 1); + + $this->config = array_merge(array( + 'client_table' => 'oauth_clients', + 'access_token_table' => 'oauth_access_tokens', + 'refresh_token_table' => 'oauth_refresh_tokens', + 'code_table' => 'oauth_authorization_codes', + 'user_table' => 'oauth_users', + 'jwt_table' => 'oauth_jwt', + ), $config); + } + + // Helper function to access a MongoDB collection by `type`: + protected function collection($name) + { + return $this->db->{$this->config[$name]}; + } + + /* ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + if ($result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) { + return $result['client_secret'] == $client_secret; + } + + return false; + } + + public function isPublicClient($client_id) + { + if (!$result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) { + return false; + } + + return empty($result['client_secret']); + } + + /* ClientInterface */ + public function getClientDetails($client_id) + { + $result = $this->collection('client_table')->findOne(array('client_id' => $client_id)); + + return is_null($result) ? false : $result; + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + if ($this->getClientDetails($client_id)) { + $this->collection('client_table')->update( + array('client_id' => $client_id), + array('$set' => array( + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + )) + ); + } else { + $this->collection('client_table')->insert( + array( + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + ) + ); + } + + return true; + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + $details = $this->getClientDetails($client_id); + if (isset($details['grant_types'])) { + $grant_types = explode(' ', $details['grant_types']); + + return in_array($grant_type, $grant_types); + } + + // if grant_types are not defined, then none are restricted + return true; + } + + /* AccessTokenInterface */ + public function getAccessToken($access_token) + { + $token = $this->collection('access_token_table')->findOne(array('access_token' => $access_token)); + + return is_null($token) ? false : $token; + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + // if it exists, update it. + if ($this->getAccessToken($access_token)) { + $this->collection('access_token_table')->update( + array('access_token' => $access_token), + array('$set' => array( + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope + )) + ); + } else { + $this->collection('access_token_table')->insert( + array( + 'access_token' => $access_token, + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope + ) + ); + } + + return true; + } + + + /* AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + $code = $this->collection('code_table')->findOne(array('authorization_code' => $code)); + + return is_null($code) ? false : $code; + } + + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + // if it exists, update it. + if ($this->getAuthorizationCode($code)) { + $this->collection('code_table')->update( + array('authorization_code' => $code), + array('$set' => array( + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, + )) + ); + } else { + $this->collection('code_table')->insert( + array( + 'authorization_code' => $code, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, + ) + ); + } + + return true; + } + + public function expireAuthorizationCode($code) + { + $this->collection('code_table')->remove(array('authorization_code' => $code)); + + return true; + } + + + /* UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + if ($user = $this->getUser($username)) { + return $this->checkPassword($user, $password); + } + + return false; + } + + public function getUserDetails($username) + { + if ($user = $this->getUser($username)) { + $user['user_id'] = $user['username']; + } + + return $user; + } + + /* RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + $token = $this->collection('refresh_token_table')->findOne(array('refresh_token' => $refresh_token)); + + return is_null($token) ? false : $token; + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + $this->collection('refresh_token_table')->insert( + array( + 'refresh_token' => $refresh_token, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'expires' => $expires, + 'scope' => $scope + ) + ); + + return true; + } + + public function unsetRefreshToken($refresh_token) + { + $this->collection('refresh_token_table')->remove(array('refresh_token' => $refresh_token)); + + return true; + } + + + // plaintext passwords are bad! Override this for your application + protected function checkPassword($user, $password) + { + return $user['password'] == $password; + } + + public function getUser($username) + { + $result = $this->collection('user_table')->findOne(array('username' => $username)); + + return is_null($result) ? false : $result; + } + + public function setUser($username, $password, $firstName = null, $lastName = null) + { + if ($this->getUser($username)) { + $this->collection('user_table')->update( + array('username' => $username), + array('$set' => array( + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName + )) + ); + } else { + $this->collection('user_table')->insert( + array( + 'username' => $username, + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName + ) + ); + } + + return true; + } + + public function getClientKey($client_id, $subject) + { + $result = $this->collection('jwt_table')->findOne(array( + 'client_id' => $client_id, + 'subject' => $subject + )); + + return is_null($result) ? false : $result['key']; + } + + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + + return null; + } + + public function getJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs mongodb implementation. + throw new \Exception('getJti() for the MongoDB driver is currently unimplemented.'); + } + + public function setJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs mongodb implementation. + throw new \Exception('setJti() for the MongoDB driver is currently unimplemented.'); + } +} diff --git a/server/php/R/libs/vendors/OAuth2/Storage/Pdo.php b/server/php/R/libs/vendors/OAuth2/Storage/Pdo.php new file mode 100644 index 000000000..0cc2c8ec5 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/Storage/Pdo.php @@ -0,0 +1,444 @@ + + */ +class Pdo implements AuthorizationCodeInterface, + AccessTokenInterface, + ClientCredentialsInterface, + UserCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + ScopeInterface, + PublicKeyInterface, + UserClaimsInterface, + OpenIDAuthorizationCodeInterface +{ + protected $db; + protected $config; + + public function __construct($connection, $config = array()) + { + if (!$connection instanceof \PDO) { + if (is_string($connection)) { + $connection = array('dsn' => $connection); + } + if (!is_array($connection)) { + throw new \InvalidArgumentException('First argument to OAuth2\Storage\Pdo must be an instance of PDO, a DSN string, or a configuration array'); + } + if (!isset($connection['dsn'])) { + throw new \InvalidArgumentException('configuration array must contain "dsn"'); + } + // merge optional parameters + $connection = array_merge(array( + 'username' => null, + 'password' => null, + ), $connection); + $connection = new \PDO($connection['dsn'], $connection['username'], $connection['password']); + } + $this->db = $connection; + + // debugging + $connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + + $this->config = array_merge(array( + 'client_table' => 'oauth_clients', + 'access_token_table' => 'oauth_access_tokens', + 'refresh_token_table' => 'oauth_refresh_tokens', + 'code_table' => 'oauth_authorization_codes', + 'user_table' => 'oauth_users', + 'jwt_table' => 'oauth_jwt', + 'scope_table' => 'oauth_scopes', + 'public_key_table' => 'oauth_public_keys', + ), $config); + } + + /* OAuth2\Storage\ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table'])); + $stmt->execute(compact('client_id')); + $result = $stmt->fetch(); + + // make this extensible + return $result && $result['client_secret'] == $client_secret; + } + + public function isPublicClient($client_id) + { + $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table'])); + $stmt->execute(compact('client_id')); + + if (!$result = $stmt->fetch()) { + return false; + } + + return empty($result['client_secret']);; + } + + /* OAuth2\Storage\ClientInterface */ + public function getClientDetails($client_id) + { + $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table'])); + $stmt->execute(compact('client_id')); + + return $stmt->fetch(); + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + // if it exists, update it. + if ($this->getClientDetails($client_id)) { + $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET client_secret=:client_secret, redirect_uri=:redirect_uri, grant_types=:grant_types, scope=:scope, user_id=:user_id where client_id=:client_id', $this->config['client_table'])); + } else { + $stmt = $this->db->prepare(sprintf('INSERT INTO %s (client_id, client_secret, redirect_uri, grant_types, scope, user_id) VALUES (:client_id, :client_secret, :redirect_uri, :grant_types, :scope, :user_id)', $this->config['client_table'])); + } + + return $stmt->execute(compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id')); + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + $details = $this->getClientDetails($client_id); + if (isset($details['grant_types'])) { + $grant_types = explode(' ', $details['grant_types']); + + return in_array($grant_type, (array) $grant_types); + } + + // if grant_types are not defined, then none are restricted + return true; + } + + /* OAuth2\Storage\AccessTokenInterface */ + public function getAccessToken($access_token) + { + $stmt = $this->db->prepare(sprintf('SELECT * from %s where access_token = :access_token', $this->config['access_token_table'])); + + $token = $stmt->execute(compact('access_token')); + if ($token = $stmt->fetch()) { + // convert date string back to timestamp + $token['expires'] = strtotime($token['expires']); + } + + return $token; + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + // convert expires to datestring + $expires = date('Y-m-d H:i:s', $expires); + + // if it exists, update it. + if ($this->getAccessToken($access_token)) { + $stmt = $this->db->prepare(sprintf('UPDATE %s SET client_id=:client_id, expires=:expires, user_id=:user_id, scope=:scope where access_token=:access_token', $this->config['access_token_table'])); + } else { + $stmt = $this->db->prepare(sprintf('INSERT INTO %s (access_token, client_id, expires, user_id, scope) VALUES (:access_token, :client_id, :expires, :user_id, :scope)', $this->config['access_token_table'])); + } + + return $stmt->execute(compact('access_token', 'client_id', 'user_id', 'expires', 'scope')); + } + + /* OAuth2\Storage\AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + $stmt = $this->db->prepare(sprintf('SELECT * from %s where authorization_code = :code', $this->config['code_table'])); + $stmt->execute(compact('code')); + + if ($code = $stmt->fetch()) { + // convert date string back to timestamp + $code['expires'] = strtotime($code['expires']); + } + + return $code; + } + + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + if (func_num_args() > 6) { + // we are calling with an id token + return call_user_func_array(array($this, 'setAuthorizationCodeWithIdToken'), func_get_args()); + } + + // convert expires to datestring + $expires = date('Y-m-d H:i:s', $expires); + + // if it exists, update it. + if ($this->getAuthorizationCode($code)) { + $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET client_id=:client_id, user_id=:user_id, redirect_uri=:redirect_uri, expires=:expires, scope=:scope where authorization_code=:code', $this->config['code_table'])); + } else { + $stmt = $this->db->prepare(sprintf('INSERT INTO %s (authorization_code, client_id, user_id, redirect_uri, expires, scope) VALUES (:code, :client_id, :user_id, :redirect_uri, :expires, :scope)', $this->config['code_table'])); + } + + return $stmt->execute(compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope')); + } + + private function setAuthorizationCodeWithIdToken($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + // convert expires to datestring + $expires = date('Y-m-d H:i:s', $expires); + + // if it exists, update it. + if ($this->getAuthorizationCode($code)) { + $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET client_id=:client_id, user_id=:user_id, redirect_uri=:redirect_uri, expires=:expires, scope=:scope, id_token =:id_token where authorization_code=:code', $this->config['code_table'])); + } else { + $stmt = $this->db->prepare(sprintf('INSERT INTO %s (authorization_code, client_id, user_id, redirect_uri, expires, scope, id_token) VALUES (:code, :client_id, :user_id, :redirect_uri, :expires, :scope, :id_token)', $this->config['code_table'])); + } + + return $stmt->execute(compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token')); + } + + public function expireAuthorizationCode($code) + { + $stmt = $this->db->prepare(sprintf('DELETE FROM %s WHERE authorization_code = :code', $this->config['code_table'])); + + return $stmt->execute(compact('code')); + } + + /* OAuth2\Storage\UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + if ($user = $this->getUser($username)) { + return $this->checkPassword($user, $password); + } + + return false; + } + + public function getUserDetails($username) + { + return $this->getUser($username); + } + + /* UserClaimsInterface */ + public function getUserClaims($user_id, $claims) + { + if (!$userDetails = $this->getUserDetails($user_id)) { + return false; + } + + $claims = explode(' ', trim($claims)); + $userClaims = array(); + + // for each requested claim, if the user has the claim, set it in the response + $validClaims = explode(' ', self::VALID_CLAIMS); + foreach ($validClaims as $validClaim) { + if (in_array($validClaim, $claims)) { + if ($validClaim == 'address') { + // address is an object with subfields + $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails); + } else { + $userClaims = array_merge($this->getUserClaim($validClaim, $userDetails)); + } + } + } + + return $userClaims; + } + + protected function getUserClaim($claim, $userDetails) + { + $userClaims = array(); + $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim))); + $claimValues = explode(' ', $claimValuesString); + + foreach ($claimValues as $value) { + $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null; + } + + return $userClaims; + } + + /* OAuth2\Storage\RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + $stmt = $this->db->prepare(sprintf('SELECT * FROM %s WHERE refresh_token = :refresh_token', $this->config['refresh_token_table'])); + + $token = $stmt->execute(compact('refresh_token')); + if ($token = $stmt->fetch()) { + // convert expires to epoch time + $token['expires'] = strtotime($token['expires']); + } + + return $token; + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + // convert expires to datestring + $expires = date('Y-m-d H:i:s', $expires); + + $stmt = $this->db->prepare(sprintf('INSERT INTO %s (refresh_token, client_id, user_id, expires, scope) VALUES (:refresh_token, :client_id, :user_id, :expires, :scope)', $this->config['refresh_token_table'])); + + return $stmt->execute(compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope')); + } + + public function unsetRefreshToken($refresh_token) + { + $stmt = $this->db->prepare(sprintf('DELETE FROM %s WHERE refresh_token = :refresh_token', $this->config['refresh_token_table'])); + + return $stmt->execute(compact('refresh_token')); + } + + // plaintext passwords are bad! Override this for your application + protected function checkPassword($user, $password) + { + return $user['password'] == sha1($password); + } + + public function getUser($username) + { + $stmt = $this->db->prepare($sql = sprintf('SELECT * from %s where username=:username', $this->config['user_table'])); + $stmt->execute(array('username' => $username)); + + if (!$userInfo = $stmt->fetch()) { + return false; + } + + // the default behavior is to use "username" as the user_id + return array_merge(array( + 'user_id' => $username + ), $userInfo); + } + + public function setUser($username, $password, $firstName = null, $lastName = null) + { + // do not store in plaintext + $password = sha1($password); + + // if it exists, update it. + if ($this->getUser($username)) { + $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET password=:password, first_name=:firstName, last_name=:lastName where username=:username', $this->config['user_table'])); + } else { + $stmt = $this->db->prepare(sprintf('INSERT INTO %s (username, password, first_name, last_name) VALUES (:username, :password, :firstName, :lastName)', $this->config['user_table'])); + } + + return $stmt->execute(compact('username', 'password', 'firstName', 'lastName')); + } + + /* ScopeInterface */ + public function scopeExists($scope) + { + $scope = explode(' ', $scope); + $whereIn = implode(',', array_fill(0, count($scope), '?')); + $stmt = $this->db->prepare(sprintf('SELECT count(scope) as count FROM %s WHERE scope IN (%s)', $this->config['scope_table'], $whereIn)); + $stmt->execute($scope); + + if ($result = $stmt->fetch()) { + return $result['count'] == count($scope); + } + + return false; + } + + public function getDefaultScope($client_id = null) + { + $stmt = $this->db->prepare(sprintf('SELECT scope FROM %s WHERE is_default=:is_default', $this->config['scope_table'])); + $stmt->execute(array('is_default' => true)); + + if ($result = $stmt->fetchAll()) { + $defaultScope = array_map(function ($row) { + return $row['scope']; + }, $result); + + return implode(' ', $defaultScope); + } + + return null; + } + + /* JWTBearerInterface */ + public function getClientKey($client_id, $subject) + { + $stmt = $this->db->prepare($sql = sprintf('SELECT public_key from %s where client_id=:client_id AND subject=:subject', $this->config['jwt_table'])); + + $stmt->execute(array('client_id' => $client_id, 'subject' => $subject)); + + return $stmt->fetchColumn(); + } + + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + + return null; + } + + public function getJti($client_id, $subject, $audience, $expires, $jti) + { + $stmt = $this->db->prepare($sql = sprintf('SELECT * FROM %s WHERE issuer=:client_id AND subject=:subject AND audience=:audience AND expires=:expires AND jti=:jti', $this->config['jti_table'])); + + $stmt->execute(compact('client_id', 'subject', 'audience', 'expires', 'jti')); + + if ($result = $stmt->fetch()) { + return array( + 'issuer' => $result['issuer'], + 'subject' => $result['subject'], + 'audience' => $result['audience'], + 'expires' => $result['expires'], + 'jti' => $result['jti'], + ); + } + + return null; + } + + public function setJti($client_id, $subject, $audience, $expires, $jti) + { + $stmt = $this->db->prepare(sprintf('INSERT INTO %s (issuer, subject, audience, expires, jti) VALUES (:client_id, :subject, :audience, :expires, :jti)', $this->config['jti_table'])); + + return $stmt->execute(compact('client_id', 'subject', 'audience', 'expires', 'jti')); + } + + /* PublicKeyInterface */ + public function getPublicKey($client_id = null) + { + $stmt = $this->db->prepare($sql = sprintf('SELECT public_key FROM %s WHERE client_id=:client_id OR client_id IS NULL ORDER BY client_id IS NOT NULL DESC', $this->config['public_key_table'])); + + $stmt->execute(compact('client_id')); + if ($result = $stmt->fetch()) { + return $result['public_key']; + } + } + + public function getPrivateKey($client_id = null) + { + $stmt = $this->db->prepare($sql = sprintf('SELECT private_key FROM %s WHERE client_id=:client_id OR client_id IS NULL ORDER BY client_id IS NOT NULL DESC', $this->config['public_key_table'])); + + $stmt->execute(compact('client_id')); + if ($result = $stmt->fetch()) { + return $result['private_key']; + } + } + + public function getEncryptionAlgorithm($client_id = null) + { + $stmt = $this->db->prepare($sql = sprintf('SELECT encryption_algorithm FROM %s WHERE client_id=:client_id OR client_id IS NULL ORDER BY client_id IS NOT NULL DESC', $this->config['public_key_table'])); + + $stmt->execute(compact('client_id')); + if ($result = $stmt->fetch()) { + return $result['encryption_algorithm']; + } + + return 'RS256'; + } +} diff --git a/server/php/R/libs/vendors/OAuth2/Storage/PublicKeyInterface.php b/server/php/R/libs/vendors/OAuth2/Storage/PublicKeyInterface.php new file mode 100644 index 000000000..108418d3a --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/Storage/PublicKeyInterface.php @@ -0,0 +1,16 @@ + + */ +interface PublicKeyInterface +{ + public function getPublicKey($client_id = null); + public function getPrivateKey($client_id = null); + public function getEncryptionAlgorithm($client_id = null); +} diff --git a/server/php/R/libs/vendors/OAuth2/Storage/Redis.php b/server/php/R/libs/vendors/OAuth2/Storage/Redis.php new file mode 100644 index 000000000..fd94f475d --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/Storage/Redis.php @@ -0,0 +1,312 @@ + + * $storage = new OAuth2\Storage\Redis($redis); + * $storage->setClientDetails($client_id, $client_secret, $redirect_uri); + * + */ +class Redis implements AuthorizationCodeInterface, + AccessTokenInterface, + ClientCredentialsInterface, + UserCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + ScopeInterface, + OpenIDAuthorizationCodeInterface +{ + + private $cache; + + /* The redis client */ + protected $redis; + + /* Configuration array */ + protected $config; + + /** + * Redis Storage! + * + * @param \Predis\Client $redis + * @param array $config + */ + public function __construct($redis, $config=array()) + { + $this->redis = $redis; + $this->config = array_merge(array( + 'client_key' => 'oauth_clients:', + 'access_token_key' => 'oauth_access_tokens:', + 'refresh_token_key' => 'oauth_refresh_tokens:', + 'code_key' => 'oauth_authorization_codes:', + 'user_key' => 'oauth_users:', + 'jwt_key' => 'oauth_jwt:', + 'scope_key' => 'oauth_scopes:', + ), $config); + } + + protected function getValue($key) + { + if ( isset($this->cache[$key]) ) { + return $this->cache[$key]; + } + $value = $this->redis->get($key); + if ( isset($value) ) { + return json_decode($value, true); + } else { + return false; + } + } + + protected function setValue($key, $value, $expire=0) + { + $this->cache[$key] = $value; + $str = json_encode($value); + if ($expire > 0) { + $seconds = $expire - time(); + $ret = $this->redis->setex($key, $seconds, $str); + } else { + $ret = $this->redis->set($key, $str); + } + + // check that the key was set properly + // if this fails, an exception will usually thrown, so this step isn't strictly necessary + return is_bool($ret) ? $ret : $ret->getPayload() == 'OK'; + } + + protected function expireValue($key) + { + unset($this->cache[$key]); + + return $this->redis->del($key); + } + + /* AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + return $this->getValue($this->config['code_key'] . $code); + } + + public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + return $this->setValue( + $this->config['code_key'] . $authorization_code, + compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'), + $expires + ); + } + + public function expireAuthorizationCode($code) + { + $key = $this->config['code_key'] . $code; + unset($this->cache[$key]); + + return $this->expireValue($key); + } + + /* UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + $user = $this->getUserDetails($username); + + return $user && $user['password'] === $password; + } + + public function getUserDetails($username) + { + return $this->getUser($username); + } + + public function getUser($username) + { + if (!$userInfo = $this->getValue($this->config['user_key'] . $username)) { + return false; + } + + // the default behavior is to use "username" as the user_id + return array_merge(array( + 'user_id' => $username, + ), $userInfo); + } + + public function setUser($username, $password, $first_name = null, $last_name = null) + { + return $this->setValue( + $this->config['user_key'] . $username, + compact('username', 'password', 'first_name', 'last_name') + ); + } + + /* ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + if (!$client = $this->getClientDetails($client_id)) { + return false; + } + + return isset($client['client_secret']) + && $client['client_secret'] == $client_secret; + } + + public function isPublicClient($client_id) + { + if (!$client = $this->getClientDetails($client_id)) { + return false; + } + + return empty($result['client_secret']);; + } + + /* ClientInterface */ + public function getClientDetails($client_id) + { + return $this->getValue($this->config['client_key'] . $client_id); + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + return $this->setValue( + $this->config['client_key'] . $client_id, + compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id') + ); + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + $details = $this->getClientDetails($client_id); + if (isset($details['grant_types'])) { + $grant_types = explode(' ', $details['grant_types']); + + return in_array($grant_type, (array) $grant_types); + } + + // if grant_types are not defined, then none are restricted + return true; + } + + /* RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + return $this->getValue($this->config['refresh_token_key'] . $refresh_token); + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + return $this->setValue( + $this->config['refresh_token_key'] . $refresh_token, + compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope'), + $expires + ); + } + + public function unsetRefreshToken($refresh_token) + { + return $this->expireValue($this->config['refresh_token_key'] . $refresh_token); + } + + /* AccessTokenInterface */ + public function getAccessToken($access_token) + { + return $this->getValue($this->config['access_token_key'].$access_token); + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + return $this->setValue( + $this->config['access_token_key'].$access_token, + compact('access_token', 'client_id', 'user_id', 'expires', 'scope'), + $expires + ); + } + + /* ScopeInterface */ + public function scopeExists($scope) + { + $scope = explode(' ', $scope); + + $result = $this->getValue($this->config['scope_key'].'supported:global'); + + $supportedScope = explode(' ', (string) $result); + + return (count(array_diff($scope, $supportedScope)) == 0); + } + + public function getDefaultScope($client_id = null) + { + if (is_null($client_id) || !$result = $this->getValue($this->config['scope_key'].'default:'.$client_id)) { + $result = $this->getValue($this->config['scope_key'].'default:global'); + } + + return $result; + } + + public function setScope($scope, $client_id = null, $type = 'supported') + { + if (!in_array($type, array('default', 'supported'))) { + throw new \InvalidArgumentException('"$type" must be one of "default", "supported"'); + } + + if (is_null($client_id)) { + $key = $this->config['scope_key'].$type.':global'; + } else { + $key = $this->config['scope_key'].$type.':'.$client_id; + } + + return $this->setValue($key, $scope); + } + + /*JWTBearerInterface */ + public function getClientKey($client_id, $subject) + { + if (!$jwt = $this->getValue($this->config['jwt_key'] . $client_id)) { + return false; + } + + if (isset($jwt['subject']) && $jwt['subject'] == $subject) { + return $jwt['key']; + } + + return null; + } + + public function setClientKey($client_id, $key, $subject = null) + { + return $this->setValue($this->config['jwt_key'] . $client_id, array( + 'key' => $key, + 'subject' => $subject + )); + } + + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + + return null; + } + + public function getJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs redis implementation. + throw new \Exception('getJti() for the Redis driver is currently unimplemented.'); + } + + public function setJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs redis implementation. + throw new \Exception('setJti() for the Redis driver is currently unimplemented.'); + } +} diff --git a/server/php/R/libs/vendors/OAuth2/Storage/RefreshTokenInterface.php b/server/php/R/libs/vendors/OAuth2/Storage/RefreshTokenInterface.php new file mode 100644 index 000000000..0273f2125 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/Storage/RefreshTokenInterface.php @@ -0,0 +1,82 @@ + + */ +interface RefreshTokenInterface +{ + /** + * Grant refresh access tokens. + * + * Retrieve the stored data for the given refresh token. + * + * Required for OAuth2::GRANT_TYPE_REFRESH_TOKEN. + * + * @param $refresh_token + * Refresh token to be check with. + * + * @return + * An associative array as below, and NULL if the refresh_token is + * invalid: + * - refresh_token: Refresh token identifier. + * - client_id: Client identifier. + * - user_id: User identifier. + * - expires: Expiration unix timestamp, or 0 if the token doesn't expire. + * - scope: (optional) Scope values in space-separated string. + * + * @see http://tools.ietf.org/html/rfc6749#section-6 + * + * @ingroup oauth2_section_6 + */ + public function getRefreshToken($refresh_token); + + /** + * Take the provided refresh token values and store them somewhere. + * + * This function should be the storage counterpart to getRefreshToken(). + * + * If storage fails for some reason, we're not currently checking for + * any sort of success/failure, so you should bail out of the script + * and provide a descriptive fail message. + * + * Required for OAuth2::GRANT_TYPE_REFRESH_TOKEN. + * + * @param $refresh_token + * Refresh token to be stored. + * @param $client_id + * Client identifier to be stored. + * @param $user_id + * User identifier to be stored. + * @param $expires + * Expiration timestamp to be stored. 0 if the token doesn't expire. + * @param $scope + * (optional) Scopes to be stored in space-separated string. + * + * @ingroup oauth2_section_6 + */ + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null); + + /** + * Expire a used refresh token. + * + * This is not explicitly required in the spec, but is almost implied. + * After granting a new refresh token, the old one is no longer useful and + * so should be forcibly expired in the data store so it can't be used again. + * + * If storage fails for some reason, we're not currently checking for + * any sort of success/failure, so you should bail out of the script + * and provide a descriptive fail message. + * + * @param $refresh_token + * Refresh token to be expirse. + * + * @ingroup oauth2_section_6 + */ + public function unsetRefreshToken($refresh_token); +} diff --git a/server/php/R/libs/vendors/OAuth2/Storage/ScopeInterface.php b/server/php/R/libs/vendors/OAuth2/Storage/ScopeInterface.php new file mode 100644 index 000000000..e88ff7f0e --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/Storage/ScopeInterface.php @@ -0,0 +1,46 @@ + + */ +interface ScopeInterface +{ + /** + * Check if the provided scope exists. + * + * @param $scope + * A space-separated string of scopes. + * + * @return + * TRUE if it exists, FALSE otherwise. + */ + public function scopeExists($scope); + + /** + * The default scope to use in the event the client + * does not request one. By returning "false", a + * request_error is returned by the server to force a + * scope request by the client. By returning "null", + * opt out of requiring scopes + * + * @param $client_id + * An optional client id that can be used to return customized default scopes. + * + * @return + * string representation of default scope, null if + * scopes are not defined, or false to force scope + * request by the client + * + * ex: + * 'default' + * ex: + * null + */ + public function getDefaultScope($client_id = null); +} diff --git a/server/php/R/libs/vendors/OAuth2/Storage/UserCredentialsInterface.php b/server/php/R/libs/vendors/OAuth2/Storage/UserCredentialsInterface.php new file mode 100644 index 000000000..6e0fd7bad --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/Storage/UserCredentialsInterface.php @@ -0,0 +1,52 @@ + + */ +interface UserCredentialsInterface +{ + /** + * Grant access tokens for basic user credentials. + * + * Check the supplied username and password for validity. + * + * You can also use the $client_id param to do any checks required based + * on a client, if you need that. + * + * Required for OAuth2::GRANT_TYPE_USER_CREDENTIALS. + * + * @param $username + * Username to be check with. + * @param $password + * Password to be check with. + * + * @return + * TRUE if the username and password are valid, and FALSE if it isn't. + * Moreover, if the username and password are valid, and you want to + * + * @see http://tools.ietf.org/html/rfc6749#section-4.3 + * + * @ingroup oauth2_section_4 + */ + public function checkUserCredentials($username, $password); + + /** + * @return + * ARRAY the associated "user_id" and optional "scope" values + * This function MUST return FALSE if the requested user does not exist or is + * invalid. "scope" is a space-separated list of restricted scopes. + * @code + * return array( + * "user_id" => USER_ID, // REQUIRED user_id to be stored with the authorization code or access token + * "scope" => SCOPE // OPTIONAL space-separated list of restricted scopes + * ); + * @endcode + */ + public function getUserDetails($username); +} diff --git a/server/php/R/libs/vendors/OAuth2/TokenType/Bearer.php b/server/php/R/libs/vendors/OAuth2/TokenType/Bearer.php new file mode 100644 index 000000000..067257276 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/TokenType/Bearer.php @@ -0,0 +1,130 @@ +config = array_merge(array( + 'token_param_name' => 'access_token', + 'token_bearer_header_name' => 'Bearer', + ), $config); + } + + public function getTokenType() + { + return 'Bearer'; + } + + /** + * Check if the request has supplied token + * + * @see https://github.com/bshaffer/oauth2-server-php/issues/349#issuecomment-37993588 + */ + public function requestHasToken(RequestInterface $request) + { + $headers = $request->headers('AUTHORIZATION'); + + // check the header, then the querystring, then the request body + return !empty($headers) || (bool)($request->request($this->config['token_param_name'])) || (bool)($request->query($this->config['token_param_name'])); + } + + /** + * This is a convenience function that can be used to get the token, which can then + * be passed to getAccessTokenData(). The constraints specified by the draft are + * attempted to be adheared to in this method. + * + * As per the Bearer spec (draft 8, section 2) - there are three ways for a client + * to specify the bearer token, in order of preference: Authorization Header, + * POST and GET. + * + * NB: Resource servers MUST accept tokens via the Authorization scheme + * (http://tools.ietf.org/html/rfc6750#section-2). + * + * @todo Should we enforce TLS/SSL in this function? + * + * @see http://tools.ietf.org/html/rfc6750#section-2.1 + * @see http://tools.ietf.org/html/rfc6750#section-2.2 + * @see http://tools.ietf.org/html/rfc6750#section-2.3 + * + * Old Android version bug (at least with version 2.2) + * @see http://code.google.com/p/android/issues/detail?id=6684 + * + */ + public function getAccessTokenParameter(RequestInterface $request, ResponseInterface $response) + { + $headers = $request->headers('AUTHORIZATION'); + + /** + * Ensure more than one method is not used for including an + * access token + * + * @see http://tools.ietf.org/html/rfc6750#section-3.1 + */ + $methodsUsed = !empty($headers) + (bool)($request->query($this->config['token_param_name'])) + (bool)($request->request($this->config['token_param_name'])); + if ($methodsUsed > 1) { + $response->setError(400, 'invalid_request', 'Only one method may be used to authenticate at a time (Auth header, GET or POST)'); + + return null; + } + + /** + * If no authentication is provided, set the status code + * to 401 and return no other error information + * + * @see http://tools.ietf.org/html/rfc6750#section-3.1 + */ + if ($methodsUsed == 0) { + $response->setStatusCode(401); + + return null; + } + + // HEADER: Get the access token from the header + if (!empty($headers)) { + if (!preg_match('/' . $this->config['token_bearer_header_name'] . '\s(\S+)/', $headers, $matches)) { + $response->setError(400, 'invalid_request', 'Malformed auth header'); + + return null; + } + + return $matches[1]; + } + + if ($request->request($this->config['token_param_name'])) { + // // POST: Get the token from POST data + if (!in_array(strtolower($request->server('REQUEST_METHOD')), array('post', 'put'))) { + $response->setError(400, 'invalid_request', 'When putting the token in the body, the method must be POST or PUT', '#section-2.2'); + + return null; + } + + $contentType = $request->server('CONTENT_TYPE'); + if (false !== $pos = strpos($contentType, ';')) { + $contentType = substr($contentType, 0, $pos); + } + + if ($contentType !== null && $contentType != 'application/x-www-form-urlencoded') { + // IETF specifies content-type. NB: Not all webservers populate this _SERVER variable + // @see http://tools.ietf.org/html/rfc6750#section-2.2 + $response->setError(400, 'invalid_request', 'The content type for POST requests must be "application/x-www-form-urlencoded"'); + + return null; + } + + return $request->request($this->config['token_param_name']); + } + + // GET method + return $request->query($this->config['token_param_name']); + } +} diff --git a/server/php/R/libs/vendors/OAuth2/TokenType/Mac.php b/server/php/R/libs/vendors/OAuth2/TokenType/Mac.php new file mode 100644 index 000000000..fe6a86aa6 --- /dev/null +++ b/server/php/R/libs/vendors/OAuth2/TokenType/Mac.php @@ -0,0 +1,22 @@ +copy->insert +* command (swap) for when the inserted segment is exactly the same +* as the deleted one, and with only a copy operation in between. +* TODO: How often this case occurs? Is it worth it? Can only +* be done as a postprocessing method (->optimize()?) +*/ +abstract class FineDiffOp { + abstract public function getFromLen(); + abstract public function getToLen(); + abstract public function getOpcode(); + } + +class FineDiffDeleteOp extends FineDiffOp { + public function __construct($len) { + $this->fromLen = $len; + } + public function getFromLen() { + return $this->fromLen; + } + public function getToLen() { + return 0; + } + public function getOpcode() { + if ( $this->fromLen === 1 ) { + return 'd'; + } + return "d{$this->fromLen}"; + } + } + +class FineDiffInsertOp extends FineDiffOp { + public function __construct($text) { + $this->text = $text; + } + public function getFromLen() { + return 0; + } + public function getToLen() { + return strlen($this->text); + } + public function getText() { + return $this->text; + } + public function getOpcode() { + $to_len = strlen($this->text); + if ( $to_len === 1 ) { + return "i:{$this->text}"; + } + return "i{$to_len}:{$this->text}"; + } + } + +class FineDiffReplaceOp extends FineDiffOp { + public function __construct($fromLen, $text) { + $this->fromLen = $fromLen; + $this->text = $text; + } + public function getFromLen() { + return $this->fromLen; + } + public function getToLen() { + return strlen($this->text); + } + public function getText() { + return $this->text; + } + public function getOpcode() { + if ( $this->fromLen === 1 ) { + $del_opcode = 'd'; + } + else { + $del_opcode = "d{$this->fromLen}"; + } + $to_len = strlen($this->text); + if ( $to_len === 1 ) { + return "{$del_opcode}i:{$this->text}"; + } + return "{$del_opcode}i{$to_len}:{$this->text}"; + } + } + +class FineDiffCopyOp extends FineDiffOp { + public function __construct($len) { + $this->len = $len; + } + public function getFromLen() { + return $this->len; + } + public function getToLen() { + return $this->len; + } + public function getOpcode() { + if ( $this->len === 1 ) { + return 'c'; + } + return "c{$this->len}"; + } + public function increase($size) { + return $this->len += $size; + } + } + +/** +* FineDiff ops +* +* Collection of ops +*/ +class FineDiffOps { + public function appendOpcode($opcode, $from, $from_offset, $from_len) { + if ( $opcode === 'c' ) { + $edits[] = new FineDiffCopyOp($from_len); + } + else if ( $opcode === 'd' ) { + $edits[] = new FineDiffDeleteOp($from_len); + } + else /* if ( $opcode === 'i' ) */ { + $edits[] = new FineDiffInsertOp(substr($from, $from_offset, $from_len)); + } + } + public $edits = array(); + } + +/** +* FineDiff class +* +* TODO: Document +* +*/ +class FineDiff { + + /**------------------------------------------------------------------------ + * + * Public section + * + */ + + /** + * Constructor + * ... + * The $granularityStack allows FineDiff to be configurable so that + * a particular stack tailored to the specific content of a document can + * be passed. + */ + public function __construct($from_text = '', $to_text = '', $granularityStack = null) { + // setup stack for generic text documents by default + $this->granularityStack = $granularityStack ? $granularityStack : FineDiff::$characterGranularity; + $this->edits = array(); + $this->from_text = $from_text; + $this->doDiff($from_text, $to_text); + } + + public function getOps() { + return $this->edits; + } + + public function getOpcodes() { + $opcodes = array(); + foreach ( $this->edits as $edit ) { + $opcodes[] = $edit->getOpcode(); + } + return implode('', $opcodes); + } + + public function renderDiffToHTML() { + $in_offset = 0; + ob_start(); + foreach ( $this->edits as $edit ) { + $n = $edit->getFromLen(); + if ( $edit instanceof FineDiffCopyOp ) { + FineDiff::renderDiffToHTMLFromOpcode('c', $this->from_text, $in_offset, $n); + } + else if ( $edit instanceof FineDiffDeleteOp ) { + FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n); + } + else if ( $edit instanceof FineDiffInsertOp ) { + FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen()); + } + else /* if ( $edit instanceof FineDiffReplaceOp ) */ { + FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n); + FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen()); + } + $in_offset += $n; + } + return ob_get_clean(); + } + + /**------------------------------------------------------------------------ + * Return an opcodes string describing the diff between a "From" and a + * "To" string + */ + public static function getDiffOpcodes($from, $to, $granularities = null) { + $diff = new FineDiff($from, $to, $granularities); + return $diff->getOpcodes(); + } + + /**------------------------------------------------------------------------ + * Return an iterable collection of diff ops from an opcodes string + */ + public static function getDiffOpsFromOpcodes($opcodes) { + $diffops = new FineDiffOps(); + FineDiff::renderFromOpcodes(null, $opcodes, array($diffops,'appendOpcode')); + return $diffops->edits; + } + + /**------------------------------------------------------------------------ + * Re-create the "To" string from the "From" string and an "Opcodes" string + */ + public static function renderToTextFromOpcodes($from, $opcodes) { + ob_start(); + FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderToTextFromOpcode')); + return ob_get_clean(); + } + + /**------------------------------------------------------------------------ + * Render the diff to an HTML string + */ + public static function renderDiffToHTMLFromOpcodes($from, $opcodes) { + ob_start(); + FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderDiffToHTMLFromOpcode')); + return ob_get_clean(); + } + + /**------------------------------------------------------------------------ + * Generic opcodes parser, user must supply callback for handling + * single opcode + */ + public static function renderFromOpcodes($from, $opcodes, $callback) { + if ( !is_callable($callback) ) { + return; + } + $opcodes_len = strlen($opcodes); + $from_offset = $opcodes_offset = 0; + while ( $opcodes_offset < $opcodes_len ) { + $opcode = substr($opcodes, $opcodes_offset, 1); + $opcodes_offset++; + $n = intval(substr($opcodes, $opcodes_offset)); + if ( $n ) { + $opcodes_offset += strlen(strval($n)); + } + else { + $n = 1; + } + if ( $opcode === 'c' ) { // copy n characters from source + call_user_func($callback, 'c', $from, $from_offset, $n, ''); + $from_offset += $n; + } + else if ( $opcode === 'd' ) { // delete n characters from source + call_user_func($callback, 'd', $from, $from_offset, $n, ''); + $from_offset += $n; + } + else /* if ( $opcode === 'i' ) */ { // insert n characters from opcodes + call_user_func($callback, 'i', $opcodes, $opcodes_offset + 1, $n); + $opcodes_offset += 1 + $n; + } + } + } + + /** + * Stock granularity stacks and delimiters + */ + + const paragraphDelimiters = "\n\r"; + public static $paragraphGranularity = array( + FineDiff::paragraphDelimiters + ); + const sentenceDelimiters = ".\n\r"; + public static $sentenceGranularity = array( + FineDiff::paragraphDelimiters, + FineDiff::sentenceDelimiters + ); + const wordDelimiters = " \t.\n\r"; + public static $wordGranularity = array( + FineDiff::paragraphDelimiters, + FineDiff::sentenceDelimiters, + FineDiff::wordDelimiters + ); + const characterDelimiters = ""; + public static $characterGranularity = array( + FineDiff::paragraphDelimiters, + FineDiff::sentenceDelimiters, + FineDiff::wordDelimiters, + FineDiff::characterDelimiters + ); + + public static $textStack = array( + ".", + " \t.\n\r", + "" + ); + + /**------------------------------------------------------------------------ + * + * Private section + * + */ + + /** + * Entry point to compute the diff. + */ + private function doDiff($from_text, $to_text) { + $this->last_edit = false; + $this->stackpointer = 0; + $this->from_text = $from_text; + $this->from_offset = 0; + // can't diff without at least one granularity specifier + if ( empty($this->granularityStack) ) { + return; + } + $this->_processGranularity($from_text, $to_text); + } + + /** + * This is the recursive function which is responsible for + * handling/increasing granularity. + * + * Incrementally increasing the granularity is key to compute the + * overall diff in a very efficient way. + */ + private function _processGranularity($from_segment, $to_segment) { + $delimiters = $this->granularityStack[$this->stackpointer++]; + $has_next_stage = $this->stackpointer < count($this->granularityStack); + foreach ( FineDiff::doFragmentDiff($from_segment, $to_segment, $delimiters) as $fragment_edit ) { + // increase granularity + if ( $fragment_edit instanceof FineDiffReplaceOp && $has_next_stage ) { + $this->_processGranularity( + substr($this->from_text, $this->from_offset, $fragment_edit->getFromLen()), + $fragment_edit->getText() + ); + } + // fuse copy ops whenever possible + else if ( $fragment_edit instanceof FineDiffCopyOp && $this->last_edit instanceof FineDiffCopyOp ) { + $this->edits[count($this->edits)-1]->increase($fragment_edit->getFromLen()); + $this->from_offset += $fragment_edit->getFromLen(); + } + else { + /* $fragment_edit instanceof FineDiffCopyOp */ + /* $fragment_edit instanceof FineDiffDeleteOp */ + /* $fragment_edit instanceof FineDiffInsertOp */ + $this->edits[] = $this->last_edit = $fragment_edit; + $this->from_offset += $fragment_edit->getFromLen(); + } + } + $this->stackpointer--; + } + + /** + * This is the core algorithm which actually perform the diff itself, + * fragmenting the strings as per specified delimiters. + * + * This function is naturally recursive, however for performance purpose + * a local job queue is used instead of outright recursivity. + */ + private static function doFragmentDiff($from_text, $to_text, $delimiters) { + // Empty delimiter means character-level diffing. + // In such case, use code path optimized for character-level + // diffing. + if ( empty($delimiters) ) { + return FineDiff::doCharDiff($from_text, $to_text); + } + + $result = array(); + + // fragment-level diffing + $from_text_len = strlen($from_text); + $to_text_len = strlen($to_text); + $from_fragments = FineDiff::extractFragments($from_text, $delimiters); + $to_fragments = FineDiff::extractFragments($to_text, $delimiters); + + $jobs = array(array(0, $from_text_len, 0, $to_text_len)); + + $cached_array_keys = array(); + + while ( $job = array_pop($jobs) ) { + + // get the segments which must be diff'ed + list($from_segment_start, $from_segment_end, $to_segment_start, $to_segment_end) = $job; + + // catch easy cases first + $from_segment_length = $from_segment_end - $from_segment_start; + $to_segment_length = $to_segment_end - $to_segment_start; + if ( !$from_segment_length || !$to_segment_length ) { + if ( $from_segment_length ) { + $result[$from_segment_start * 4] = new FineDiffDeleteOp($from_segment_length); + } + else if ( $to_segment_length ) { + $result[$from_segment_start * 4 + 1] = new FineDiffInsertOp(substr($to_text, $to_segment_start, $to_segment_length)); + } + continue; + } + + // find longest copy operation for the current segments + $best_copy_length = 0; + + $from_base_fragment_index = $from_segment_start; + + $cached_array_keys_for_current_segment = array(); + + while ( $from_base_fragment_index < $from_segment_end ) { + $from_base_fragment = $from_fragments[$from_base_fragment_index]; + $from_base_fragment_length = strlen($from_base_fragment); + // performance boost: cache array keys + if ( !isset($cached_array_keys_for_current_segment[$from_base_fragment]) ) { + if ( !isset($cached_array_keys[$from_base_fragment]) ) { + $to_all_fragment_indices = $cached_array_keys[$from_base_fragment] = array_keys($to_fragments, $from_base_fragment, true); + } + else { + $to_all_fragment_indices = $cached_array_keys[$from_base_fragment]; + } + // get only indices which falls within current segment + if ( $to_segment_start > 0 || $to_segment_end < $to_text_len ) { + $to_fragment_indices = array(); + foreach ( $to_all_fragment_indices as $to_fragment_index ) { + if ( $to_fragment_index < $to_segment_start ) { continue; } + if ( $to_fragment_index >= $to_segment_end ) { break; } + $to_fragment_indices[] = $to_fragment_index; + } + $cached_array_keys_for_current_segment[$from_base_fragment] = $to_fragment_indices; + } + else { + $to_fragment_indices = $to_all_fragment_indices; + } + } + else { + $to_fragment_indices = $cached_array_keys_for_current_segment[$from_base_fragment]; + } + // iterate through collected indices + foreach ( $to_fragment_indices as $to_base_fragment_index ) { + $fragment_index_offset = $from_base_fragment_length; + // iterate until no more match + for (;;) { + $fragment_from_index = $from_base_fragment_index + $fragment_index_offset; + if ( $fragment_from_index >= $from_segment_end ) { + break; + } + $fragment_to_index = $to_base_fragment_index + $fragment_index_offset; + if ( $fragment_to_index >= $to_segment_end ) { + break; + } + if ( $from_fragments[$fragment_from_index] !== $to_fragments[$fragment_to_index] ) { + break; + } + $fragment_length = strlen($from_fragments[$fragment_from_index]); + $fragment_index_offset += $fragment_length; + } + if ( $fragment_index_offset > $best_copy_length ) { + $best_copy_length = $fragment_index_offset; + $best_from_start = $from_base_fragment_index; + $best_to_start = $to_base_fragment_index; + } + } + $from_base_fragment_index += strlen($from_base_fragment); + // If match is larger than half segment size, no point trying to find better + // TODO: Really? + if ( $best_copy_length >= $from_segment_length / 2) { + break; + } + // no point to keep looking if what is left is less than + // current best match + if ( $from_base_fragment_index + $best_copy_length >= $from_segment_end ) { + break; + } + } + + if ( $best_copy_length ) { + $jobs[] = array($from_segment_start, $best_from_start, $to_segment_start, $best_to_start); + $result[$best_from_start * 4 + 2] = new FineDiffCopyOp($best_copy_length); + $jobs[] = array($best_from_start + $best_copy_length, $from_segment_end, $best_to_start + $best_copy_length, $to_segment_end); + } + else { + $result[$from_segment_start * 4 ] = new FineDiffReplaceOp($from_segment_length, substr($to_text, $to_segment_start, $to_segment_length)); + } + } + + ksort($result, SORT_NUMERIC); + return array_values($result); + } + + /** + * Perform a character-level diff. + * + * The algorithm is quite similar to doFragmentDiff(), except that + * the code path is optimized for character-level diff -- strpos() is + * used to find out the longest common subequence of characters. + * + * We try to find a match using the longest possible subsequence, which + * is at most the length of the shortest of the two strings, then incrementally + * reduce the size until a match is found. + * + * I still need to study more the performance of this function. It + * appears that for long strings, the generic doFragmentDiff() is more + * performant. For word-sized strings, doCharDiff() is somewhat more + * performant. + */ + private static function doCharDiff($from_text, $to_text) { + $result = array(); + $jobs = array(array(0, strlen($from_text), 0, strlen($to_text))); + while ( $job = array_pop($jobs) ) { + // get the segments which must be diff'ed + list($from_segment_start, $from_segment_end, $to_segment_start, $to_segment_end) = $job; + $from_segment_len = $from_segment_end - $from_segment_start; + $to_segment_len = $to_segment_end - $to_segment_start; + + // catch easy cases first + if ( !$from_segment_len || !$to_segment_len ) { + if ( $from_segment_len ) { + $result[$from_segment_start * 4 + 0] = new FineDiffDeleteOp($from_segment_len); + } + else if ( $to_segment_len ) { + $result[$from_segment_start * 4 + 1] = new FineDiffInsertOp(substr($to_text, $to_segment_start, $to_segment_len)); + } + continue; + } + if ( $from_segment_len >= $to_segment_len ) { + $copy_len = $to_segment_len; + while ( $copy_len ) { + $to_copy_start = $to_segment_start; + $to_copy_start_max = $to_segment_end - $copy_len; + while ( $to_copy_start <= $to_copy_start_max ) { + $from_copy_start = strpos(substr($from_text, $from_segment_start, $from_segment_len), substr($to_text, $to_copy_start, $copy_len)); + if ( $from_copy_start !== false ) { + $from_copy_start += $from_segment_start; + break 2; + } + $to_copy_start++; + } + $copy_len--; + } + } + else { + $copy_len = $from_segment_len; + while ( $copy_len ) { + $from_copy_start = $from_segment_start; + $from_copy_start_max = $from_segment_end - $copy_len; + while ( $from_copy_start <= $from_copy_start_max ) { + $to_copy_start = strpos(substr($to_text, $to_segment_start, $to_segment_len), substr($from_text, $from_copy_start, $copy_len)); + if ( $to_copy_start !== false ) { + $to_copy_start += $to_segment_start; + break 2; + } + $from_copy_start++; + } + $copy_len--; + } + } + // match found + if ( $copy_len ) { + $jobs[] = array($from_segment_start, $from_copy_start, $to_segment_start, $to_copy_start); + $result[$from_copy_start * 4 + 2] = new FineDiffCopyOp($copy_len); + $jobs[] = array($from_copy_start + $copy_len, $from_segment_end, $to_copy_start + $copy_len, $to_segment_end); + } + // no match, so delete all, insert all + else { + $result[$from_segment_start * 4] = new FineDiffReplaceOp($from_segment_len, substr($to_text, $to_segment_start, $to_segment_len)); + } + } + ksort($result, SORT_NUMERIC); + return array_values($result); + } + + /** + * Efficiently fragment the text into an array according to + * specified delimiters. + * No delimiters means fragment into single character. + * The array indices are the offset of the fragments into + * the input string. + * A sentinel empty fragment is always added at the end. + * Careful: No check is performed as to the validity of the + * delimiters. + */ + private static function extractFragments($text, $delimiters) { + // special case: split into characters + if ( empty($delimiters) ) { + $chars = str_split($text, 1); + $chars[strlen($text)] = ''; + return $chars; + } + $fragments = array(); + $start = $end = 0; + for (;;) { + $end += strcspn($text, $delimiters, $end); + $end += strspn($text, $delimiters, $end); + if ( $end === $start ) { + break; + } + $fragments[$start] = substr($text, $start, $end - $start); + $start = $end; + } + $fragments[$start] = ''; + return $fragments; + } + + /** + * Stock opcode renderers + */ + private static function renderToTextFromOpcode($opcode, $from, $from_offset, $from_len) { + if ( $opcode === 'c' || $opcode === 'i' ) { + echo substr($from, $from_offset, $from_len); + } + } + + private static function renderDiffToHTMLFromOpcode($opcode, $from, $from_offset, $from_len) { + if ( $opcode === 'c' ) { + echo htmlentities(substr($from, $from_offset, $from_len)); + } + else if ( $opcode === 'd' ) { + $deletion = substr($from, $from_offset, $from_len); + if ( strcspn($deletion, " \n\r") === 0 ) { + $deletion = str_replace(array("\n","\r"), array('\n','\r'), $deletion); + } + echo '', htmlentities($deletion), ''; + } + else /* if ( $opcode === 'i' ) */ { + echo '', htmlentities(substr($from, $from_offset, $from_len)), ''; + } + } + } + diff --git a/server/php/R/r.php b/server/php/R/r.php new file mode 100644 index 000000000..714e728c7 --- /dev/null +++ b/server/php/R/r.php @@ -0,0 +1,3270 @@ + + * @copyright 2014 Restya + * @license http://www.restya.com/ Restya Licence + * @link http://www.restya.com + * @todo Fix code duplication & make it really lightweight + * @since 2013-08-23 + */ +$r_debug = ''; +$authUser = array(); +$_server_protocol = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') ? 'https' : 'http'; +$_server_domain_url = $_server_protocol . '://' . $_SERVER['HTTP_HOST']; // http://localhost +header('Access-Control-Allow-Origin: ' . $_server_domain_url); +header('Access-Control-Allow-Methods: *'); +require_once ('config.inc.php'); +require_once ('libs/vendors/finediff.php'); +require_once ('libs/core.php'); +/** * Common method to handle GET method + * + * @param $r_resource_cmd + * @param $r_resource_vars + * @param $r_resource_filters + * @return + */ +function r_get($r_resource_cmd, $r_resource_vars, $r_resource_filters) +{ + global $r_debug, $db_lnk, $authUser, $_server_domain_url; + // switch case.. if taking more length, then associative array... + $sql = false; + $response = array(); + $pg_params = array(); + switch ($r_resource_cmd) { + case '/users/logout': + $response['user'] = $authUser = array(); + break; + + case '/boards': + if (!empty($r_resource_filters['type']) && $r_resource_filters['type'] == 'simple') { + $sql = 'SELECT row_to_json(d) FROM (SELECT * FROM simple_board_listing ul '; + if (!empty($authUser) && $authUser['role_id'] != 1) { + $s_result = pg_query_params($db_lnk, 'SELECT board_id FROM board_stars WHERE user_id = $1', array( + $authUser['id'] + )); + $response['starred_boards'] = array(); + while ($row = pg_fetch_assoc($s_result)) { + $response['starred_boards'][] = $row['board_id']; + } + $s_result = pg_query_params($db_lnk, 'SELECT board_id FROM boards_users WHERE user_id = $1', array( + $authUser['id'] + )); + $response['user_boards'] = array(); + while ($row = pg_fetch_assoc($s_result)) { + $response['user_boards'][] = $row['board_id']; + } + $board_ids = array_merge($response['starred_boards'], $response['user_boards']); + $ids = 0; + if (!empty($board_ids)) { + $board_ids = array_unique($board_ids); + $ids = '{' . implode($board_ids, ',') . '}'; + } + $sql.= 'WHERE ul.id =ANY($1)'; + array_push($pg_params, $ids); + } + $sql.= ' ORDER BY id DESC) as d '; + if ($authUser['role_id'] != 1 && empty($board_ids)) { + $sql = false; + } + } else { + $sql = 'SELECT row_to_json(d) FROM (SELECT * FROM boards_listing ul '; + if (!empty($authUser) && $authUser['role_id'] != 1) { + $s_result = pg_query_params($db_lnk, 'SELECT board_id FROM board_subscribers WHERE user_id = $1', array( + $authUser['id'] + )); + $response['starred_boards'] = array(); + while ($row = pg_fetch_assoc($s_result)) { + $response['starred_boards'][] = $row['board_id']; + } + $s_result = pg_query_params($db_lnk, 'SELECT board_id FROM boards_users WHERE user_id = $1', array( + $authUser['id'] + )); + $response['user_boards'] = array(); + while ($row = pg_fetch_assoc($s_result)) { + $response['user_boards'][] = $row['board_id']; + } + $board_ids = array_merge($response['starred_boards'], $response['user_boards']); + $ids = 0; + if (!empty($board_ids)) { + $board_ids = array_unique($board_ids); + $ids = '{' . implode($board_ids, ',') . '}'; + } + $sql.= 'WHERE ul.id = ANY ($1)'; + array_push($pg_params, $ids); + } + $sql.= ' ORDER BY id DESC) as d '; + if ($authUser['role_id'] != 1 && empty($board_ids)) { + $sql = false; + } + } + break; + + case '/users': + $response['users'] = array(); + $sql = 'SELECT row_to_json(d) FROM (SELECT * FROM users_listing ul ORDER BY id DESC) as d '; + break; + + case '/settings/?': + $response = array(); + $sql = false; + $s_sql = 'SELECT id, name, parent_id FROM setting_categories WHERE parent_id IS NULL ORDER BY "order" ASC'; + $s_result = pg_query_params($db_lnk, $s_sql, array()); + while ($row = pg_fetch_assoc($s_result)) { + if ($row['id'] == $r_resource_vars['settings'] || $row['parent_id'] == $r_resource_vars['settings']) { + $s_sql = 'SELECT s.*, sc.name as category_name FROM settings s LEFT JOIN setting_categories sc ON sc.id = s.setting_category_id WHERE setting_category_id = $1 OR setting_category_parent_id = $2 ORDER BY "order" ASC'; + $ss_result = pg_query_params($db_lnk, $s_sql, array( + $row['id'], + $row['id'] + )); + while ($srow = pg_fetch_assoc($ss_result)) { + $row['settings'][] = $srow; + } + } + $response[] = $row; + } + break; + + case '/email_templates/?': + $response = array(); + $sql = false; + $s_sql = 'SELECT id, display_name FROM email_templates ORDER BY id ASC'; + $s_result = pg_query_params($db_lnk, $s_sql, array()); + while ($row = pg_fetch_assoc($s_result)) { + if ($row['id'] == $r_resource_vars['email_templates']) { + $s_sql = 'SELECT from_email, reply_to_email, name, description, subject, email_text_content, email_variables, display_name FROM email_templates WHERE id = $1'; + $ss_result = pg_query_params($db_lnk, $s_sql, array( + $row['id'] + )); + while ($srow = pg_fetch_assoc($ss_result)) { + $row['template'] = $srow; + } + } + $response[] = $row; + } + break; + + case '/boards/?': + $s_sql = 'SELECT b.board_visibility, bu.user_id FROM boards AS b LEFT JOIN boards_users AS bu ON bu.board_id = b.id WHERE b.id = $1'; + $arr[] = $r_resource_vars['boards']; + if (!empty($authUser) && $authUser['role_id'] != 1) { + $s_sql.= ' AND (b.board_visibility = 2 OR bu.user_id = $2)'; + $arr[] = $authUser['id']; + } else if (empty($authUser)) { + $s_sql.= ' AND b.board_visibility = 2 '; + } + $check_visibility = executeQuery($s_sql, $arr); + if (!empty($check_visibility)) { + $sql = 'SELECT row_to_json(d) FROM (SELECT * FROM boards_listing ul WHERE id = $1 ORDER BY id DESC) as d '; + array_push($pg_params, $r_resource_vars['boards']); + } else { + $response['error']['type'] = 'visibility'; + $response['error']['message'] = 'Unauthorized'; + } + break; + + case '/organizations': + $sql = 'SELECT row_to_json(d) FROM (SELECT * FROM organizations_listing'; + if (!empty($authUser) && $authUser['role_id'] != 1) { + $sql.= ' WHERE user_id = $1'; + array_push($pg_params, $authUser['id']); + } + $sql.= ' ORDER BY id ASC) as d '; + break; + + case '/organizations/?': + $s_sql = 'SELECT o.organization_visibility, ou.user_id FROM organizations AS o LEFT JOIN organizations_users AS ou ON ou.organization_id = o.id WHERE o.id = $1'; + $arr[] = $r_resource_vars['organizations']; + if (!empty($authUser) && $authUser['role_id'] != 1) { + $s_sql.= ' AND (o.organization_visibility = 1 OR ou.user_id = $2)'; + $arr[] = $authUser['id']; + } else if (empty($authUser)) { + $s_sql.= ' AND o.organization_visibility = 1 '; + } + $check_visibility = executeQuery($s_sql, $arr); + if (!empty($check_visibility)) { + $sql = 'SELECT row_to_json(d) FROM (SELECT * FROM organizations_listing ul WHERE id = $1 ORDER BY id DESC) as d '; + array_push($pg_params, $r_resource_vars['organizations']); + } else { + $response['error']['type'] = 'visibility'; + $response['error']['message'] = 'Unauthorized'; + } + break; + + case '/boards/?/activities': + $condition = ''; + if (isset($r_resource_filters['last_activity_id']) && $r_resource_filters['last_activity_id'] > 0) { + if (!empty($r_resource_filters['type']) && $r_resource_filters['type'] == 'all') { + $condition = ' AND al.id < $2'; + } else { + $condition = ' AND al.id > $2'; + } + } + $sql = 'SELECT row_to_json(d) FROM (SELECT al.*, c.name as card_name FROM activities_listing al left join cards c on al.card_id = c.id WHERE al.board_id = $1' . $condition . ' ORDER BY al.id DESC LIMIT ' . PAGING_COUNT . ') as d '; + array_push($pg_params, $r_resource_vars['boards']); + if (!empty($condition)) { + array_push($pg_params, $r_resource_filters['last_activity_id']); + } + break; + + case '/users/?/activities': + $condition = ''; + $condition1 = ''; + if (isset($r_resource_filters['last_activity_id']) && $r_resource_filters['last_activity_id'] > 0) { + $condition = ' AND al.id > $2'; + $condition1 = ' AND al.id > $3'; + if (!empty($r_resource_filters['type']) && $r_resource_filters['type'] == 'profile') { + $condition = ' AND al.id < $2'; + $condition1 = ' AND al.id < $3'; + } + } + $user = executeQuery('SELECT boards_users FROM users_listing WHERE id = $1', array( + $r_resource_vars['users'] + )); + $board_ids = array(); + if (!empty($user['boards_users'])) { + $boards_users = json_decode($user['boards_users'], true); + foreach ($boards_users as $boards_user) { + $board_ids[] = $boards_user['board_id']; + } + } + if (empty($board_ids)) { + $board_ids[] = 0; + } + if (!empty($r_resource_filters['type']) && $r_resource_filters['type'] == 'profile') { + $sql = 'SELECT row_to_json(d) FROM (SELECT * FROM activities_listing al WHERE user_id = $1 ' . $condition . ' ORDER BY id DESC LIMIT ' . PAGING_COUNT . ') as d'; + array_push($pg_params, $r_resource_vars['users']); + } else if (!empty($r_resource_filters['organization_id'])) { + $sql = 'SELECT row_to_json(d) FROM (SELECT * FROM activities_listing al WHERE user_id = $1 AND board_id IN (SELECT id FROM boards WHERE organization_id = $2)' . $condition1 . ' ORDER BY id DESC LIMIT ' . PAGING_COUNT . ') as d'; + array_push($pg_params, $r_resource_vars['users'], $r_resource_filters['organization_id']); + } else if (!empty($r_resource_filters['type']) && $r_resource_filters['type'] = 'all') { + $sql = 'SELECT row_to_json(d) FROM (SELECT * FROM activities_listing al WHERE board_id = ANY ( $1 )' . $condition . ' ORDER BY id DESC LIMIT ' . PAGING_COUNT . ') as d'; + array_push($pg_params, '{' . implode(',', $board_ids) . '}'); + } else if (!empty($r_resource_filters['board_id']) && $r_resource_filters['board_id']) { + $sql = 'SELECT row_to_json(d) FROM (SELECT * FROM activities_listing al WHERE user_id = $1 AND board_id = $2' . $condition1 . ' ORDER BY freshness_ts DESC, materialized_path ASC LIMIT ' . PAGING_COUNT . ') as d'; + array_push($pg_params, $r_resource_vars['users'], $r_resource_filters['board_id']); + } else { + $sql = 'SELECT row_to_json(d) FROM (SELECT * FROM activities_listing al WHERE board_id = ANY( $1 )' . $condition . ' ORDER BY id DESC LIMIT ' . PAGING_COUNT . ') as d'; + array_push($pg_params, '{' . implode(',', $board_ids) . '}'); + } + if (!empty($condition) || !empty($condition1)) { + array_push($pg_params, $r_resource_filters['last_activity_id']); + } + break; + + case '/boards/?/boards_stars': + $sql = 'SELECT row_to_json(d) FROM (SELECT * FROM board_stars bs WHERE board_id = $1'; + array_push($pg_params, $r_resource_vars['boards']); + if (!empty($authUser) && $authUser['role_id'] != 1) { + $sql.= ' and user_id = $2'; + array_push($pg_params, $authUser['id']); + } + $sql.= ' ORDER BY id DESC) as d '; + break; + + case '/boards/?/board_subscribers': + $sql = 'SELECT row_to_json(d) FROM (SELECT * FROM board_subscribers ul WHERE board_id = $1'; + array_push($pg_params, $r_resource_vars['boards']); + if (!empty($authUser) && $authUser['role_id'] != 1) { + $sql.= ' and user_id = $2'; + array_push($pg_params, $authUser['id']); + } + $sql.= ' ORDER BY id DESC) as d '; + break; + + case '/boards/search': + $sql = 'SELECT row_to_json(d) FROM (SELECT id, name, background_color FROM boards ul WHERE name ILIKE $1 ORDER BY id DESC) as d '; + array_push($pg_params, '%' . $r_resource_filters['q'] . '%'); + break; + + case '/boards/?/lists/?/cards/?': + $sql = 'SELECT row_to_json(d) FROM (SELECT * FROM cards_listing cll WHERE id = $1) as d '; + array_push($pg_params, $r_resource_vars['cards']); + break; + + case '/boards/?/lists/?/cards/?/activities': + $sql = 'SELECT row_to_json(d) FROM (SELECT al.*, u.username, u.profile_picture_path, u.initials, c.description, c.name as card_name FROM activities_listing al LEFT JOIN users u ON al.user_id = u.id LEFT JOIN cards c ON al.card_id = c.id WHERE card_id = $1 ORDER BY freshness_ts DESC, materialized_path ASC) as d '; + array_push($pg_params, $r_resource_vars['cards']); + break; + + case '/activities': + $condition = ''; + if (isset($r_resource_filters['last_activity_id'])) { + $condition = ' WHERE al.id < $1'; + } + $sql = 'SELECT row_to_json(d) FROM (SELECT al.*, u.username, u.profile_picture_path, u.initials, c.description FROM activities_listing al LEFT JOIN users u ON al.user_id = u.id LEFT JOIN cards c ON al.card_id = c.id ' . $condition . ' ORDER BY id DESC limit ' . PAGING_COUNT . ') as d '; + if (!empty($condition)) { + array_push($pg_params, $r_resource_filters['last_activity_id']); + } + break; + + case '/boards/?/lists/?/cards/?/checklists': + $sql = 'SELECT row_to_json(d) FROM (SELECT * FROM checklist_add_listing al WHERE board_id = $1) as d '; + array_push($pg_params, $r_resource_vars['boards']); + break; + + case '/users/search': + if (!empty($r_resource_filters['organizations'])) { + $sql = 'SELECT row_to_json(d) FROM (SELECT u.id, u.username, u.profile_picture_path,u.initials FROM users u LEFT JOIN organizations_users ou ON ou.user_id = u.id WHERE u.is_active = true AND u.is_email_confirmed = true AND '; + $sql.= '(ou.organization_id != $1 OR ou.user_id IS NULL) AND'; + array_push($pg_params, $r_resource_filters['organizations']); + } else if (!empty($r_resource_filters['board_id'])) { + $sql = 'SELECT row_to_json(d) FROM (SELECT u.id, u.username, u.profile_picture_path,u.initials FROM users u JOIN boards_users bu ON bu.user_id = u.id WHERE u.is_active = true AND u.is_email_confirmed = true AND '; + $sql.= 'bu.board_id = $1 AND'; + array_push($pg_params, $r_resource_filters['board_id']); + } else { + $sql = 'SELECT row_to_json(d) FROM (SELECT u.id, u.username, u.profile_picture_path,u.initials FROM users u WHERE u.is_active = true AND u.is_email_confirmed = true AND '; + } + if (empty($pg_params)) { + $sql.= '(LOWER(u.username) LIKE LOWER($1) OR LOWER(u.email) LIKE LOWER($2))) as d '; + } else { + $sql.= '(LOWER(u.username) LIKE LOWER($2) OR LOWER(u.email) LIKE LOWER($3))) as d '; + } + array_push($pg_params, $r_resource_filters['q'] . '%', $r_resource_filters['q'] . '%'); + if (empty($r_resource_filters['q'])) { + $sql = false; + $response = array(); + $pg_params = array(); + } + $table = 'users'; + break; + + case '/boards/?/visibility': + $sql = 'SELECT board_visibility FROM boards bl WHERE bl.id = $1'; + array_push($pg_params, $r_resource_vars['boards']); + break; + + case '/workflow_templates': + $files = glob(APP_PATH . '/client/js/workflow_templates/*.json', GLOB_BRACE); + $i = 0; + foreach ($files as $file) { + $file_name = basename($file, '.json'); + $data = file_get_contents($file); + $json = json_decode($data, true); + $response[] = array( + 'name' => $json['name'], + 'value' => implode($json['lists'], ', ') + ); + } + break; + + case '/search': + if (isset($_GET['q'])) { + $q_string = $_GET['q']; + preg_match_all('/(?P\w+):(?P\w+)/', $q_string, $search); + if (!empty($search['name'])) { + foreach ($search['name'] as $key => $name) { + $filter['term'][$name . '_name'] = $search['search'][$key]; + $filter_query['match'][$name . '_name'] = $search['search'][$key]; + } + } + preg_match_all('/(.*)@(?P\w+)/', $q_string, $user_search); + if (!empty($user_search['search'])) { + foreach ($user_search['search'] as $value) { + $filter['term']['user_name'] = $value; + $filter_query['match']['user_name'] = $value; + } + } + preg_match_all('/(.*)#(?P\w+)/', $q_string, $label_search); + if (!empty($label_search['search'])) { + foreach ($user_search['search'] as $value) { + $filter['term']['label_name'] = $value; + $filter_query['match']['label_name'] = $value; + } + } + $response = array(); + if (!empty($r_resource_filters['q'])) { + $elasticsearch_url = ELASTICSEARCH_URL . ELASTICSEARCH_INDEX . '/cards/_search?q=*' . $r_resource_filters['q'] . '*'; + $search_response = doGet($elasticsearch_url); + $response['result'] = array(); + if (!empty($search_response['hits']['hits'])) { + foreach ($search_response['hits']['hits'] as $result) { + $s_result = executeQuery('SELECT board_visibility,user_id FROM boards WHERE id = $1', array( + $result['_source']['board_id'] + )); + if ($s_result['board_visibility'] == '2' || $s_result['user_id'] == $authUser['id'] || $authUser['role_id'] == 1) { + $card['name'] = $result['_source']['card_name']; + $card['id'] = $result['_id']; + $card['list_name'] = $result['_source']['list_name']; + $card['list_id'] = $result['_source']['list_id']; + $card['board_name'] = $result['_source']['board_name']; + $card['board_id'] = $result['_source']['board_id']; + $card['type'] = $result['_type']; + $response['result'][] = $card; + } + } + } + $elasticsearch_params['suggest']['text'] = $r_resource_filters['q']; + $elasticsearch_params['suggest']['card-name-suggest']['term']['size'] = 5; + $elasticsearch_params['suggest']['card-name-suggest']['term']['field'] = 'card_name'; + $elasticsearch_params['suggest']['card-description-suggest']['term']['size'] = 5; + $elasticsearch_params['suggest']['card-description-suggest']['term']['field'] = 'card_description'; + $elasticsearch_url = ELASTICSEARCH_URL . ELASTICSEARCH_INDEX . '/_search'; + $result_arr = doPost($elasticsearch_url, $elasticsearch_params, 'json'); + $words = $r_resource_filters['q']; + $word_count = str_word_count($words); + $word_arr = explode(' ', $words); + $tmp_suggested_arr = array(); + $max_suggested_count = 0; + if (!empty($result_arr['suggest']['card-name-suggest'])) { + for ($i = 0; $i < count($result_arr['suggest']['card-name-suggest']); $i++) { + for ($j = 0; $j <= 2; $j++) { + if (!empty($result_arr['suggest']['card-name-suggest'][$i]['options'][$j]['text'])) { + $tmp_suggested_arr[$i][] = $result_arr['suggest']['card-name-suggest'][$i]['options'][$j]['text']; + } + if (!empty($result_arr['suggest']['card-description-suggest'][$i]['options'][$j]['text'])) { + $tmp_suggested_arr[$i][] = $result_arr['suggest']['card-description-suggest'][$i]['options'][$j]['text']; + } + } + if (!empty($tmp_suggested_arr[$i])) { + $tmp_suggested_arr[$i] = array_unique($tmp_suggested_arr[$i]); + if (count($tmp_suggested_arr[$i]) > $max_suggested_count) { + $max_suggested_count = count($tmp_suggested_arr[$i]); + } + } + } + } + $response['suggestion'] = array(); + if (!empty($tmp_suggested_arr)) { + for ($i = 0; $i < $max_suggested_count; $i++) { + $response['suggestion'][$i] = ''; + for ($j = 0; $j < $word_count; $j++) { + if (isset($response[$i])) { + $response[$i].= ' '; + } + $response['suggestion'][$i].= !empty($tmp_suggested_arr[$j][$i]) ? $tmp_suggested_arr[$j][$i] : (!empty($tmp_suggested_arr[$j][0]) ? $tmp_suggested_arr[$j][0] : $word_arr[$j]); + } + } + } + $response['suggestion'] = array_unique($response['suggestion']); + } + } + break; + + case '/users/?': + $sql = 'SELECT row_to_json(d) FROM (SELECT * FROM users ul WHERE id = $1) as d '; + array_push($pg_params, $r_resource_vars['users']); + break; + + case '/users/?/boards': + if (!empty($authUser)) { + $s_result = pg_query_params($db_lnk, 'SELECT board_id FROM board_stars WHERE is_starred = true AND user_id = $1', array( + $authUser['id'] + )); + $response['starred_boards'] = array(); + while ($row = pg_fetch_assoc($s_result)) { + $response['starred_boards'][] = $row['board_id']; + } + $s_result = pg_query_params($db_lnk, 'SELECT o.id as organization_id, o.name as organization_name, bu.board_id FROM boards_users bu LEFT JOIN boards b ON b.id = bu.board_id LEFT JOIN organizations o ON o.id = b.organization_id WHERE bu.user_id = $1', array( + $authUser['id'] + )); + $response['user_boards'] = array(); + $user_boards = array(); + while ($row = pg_fetch_assoc($s_result)) { + $response['user_boards'][] = $row; + } + } + break; + + case '/users/?/cards': + $sql = 'SELECT row_to_json(d) FROM (SELECT * FROM users_cards_listing ucl WHERE user_id = $1 ORDER BY board_id ASC) as d '; + array_push($pg_params, $r_resource_vars['users']); + break; + + case '/users/?/boards': + $sql = 'SELECT row_to_json(d) FROM (SELECT * FROM boards_users_listing bul WHERE user_id = $1 ORDER BY board_id ASC) as d '; + array_push($pg_params, $r_resource_vars['users']); + break; + + case '/boards/?/lists/?/cards/?/search': + $sql = 'SELECT row_to_json(d) FROM (SELECT bul.id, bul.user_id, bul.username, bul.profile_picture_path,bul.initials FROM boards_users_listing bul WHERE'; + $sql.= '(bul.username LIKE $1 OR bul.email LIKE $2) AND bul.board_id = $3) as d '; + array_push($pg_params, '%' . $r_resource_filters['q'] . '%', '%' . $r_resource_filters['q'] . '%', $r_resource_vars['boards']); + if (empty($r_resource_filters['q'])) { + $sql = false; + $response = array(); + $pg_params = array(); + } + $table = 'users'; + break; + + case '/cards/search': + $user_id = (!empty($authUser['id'])) ? $authUser['id'] : 0; + $sql = 'SELECT row_to_json(d) FROM (SELECT DISTINCT c.id, c.name, bu.board_id FROM boards_users bu join cards c on c.board_id = bu.board_id WHERE bu.board_id IN (SELECT board_id FROM boards_users WHERE user_id = $1) AND c.name LIKE $2 ORDER BY id ASC) as d'; + array_push($pg_params, $user_id, '%' . $r_resource_filters['q'] . '%'); + if (empty($r_resource_filters['q'])) { + $sql = false; + $response = array(); + $pg_params = array(); + } + break; + + case '/acl_links': + $sql = false; + $s_sql = 'SELECT row_to_json(d) FROM (SELECT acl_links.id, acl_links.name, acl_links.group_id, ( SELECT array_to_json(array_agg(row_to_json(alr.*))) AS array_to_json FROM ( SELECT acl_links_roles.role_id FROM acl_links_roles acl_links_roles WHERE acl_links_roles.acl_link_id = acl_links.id ORDER BY acl_links_roles.role_id) alr) AS acl_links_roles, acl_links.is_allow_only_to_admin, acl_links.is_allow_only_to_user FROM acl_links acl_links ORDER BY group_id ASC, id ASC) as d'; + $s_result = pg_query_params($db_lnk, $s_sql, array()); + $response['acl_links'] = array(); + while ($row = pg_fetch_assoc($s_result)) { + $response['acl_links'][] = json_decode($row['row_to_json'], true); + } + $s_sql = 'SELECT id, name FROM roles'; + $s_result = pg_query_params($db_lnk, $s_sql, array()); + $response['roles'] = array(); + while ($row = pg_fetch_assoc($s_result)) { + $response['roles'][] = $row; + } + break; + + case '/settings': + $role_id = (empty($user['role_id'])) ? 3 : $user['role_id']; + $s_sql = pg_query_params($db_lnk, 'SELECT name, value FROM settings WHERE name = \'SITE_NAME\' OR name = \'SITE_TIMEZONE\' OR name = \'DROPBOX_APPKEY\' OR name = \'LABEL_ICON\' OR name = \'FLICKR_API_KEY\' or name = \'LDAP_LOGIN_ENABLED\'', array()); + while ($row = pg_fetch_assoc($s_sql)) { + $response[$row['name']] = $row['value']; + } + break; + + default: + header($_SERVER['SERVER_PROTOCOL'] . ' 501 Not Implemented', true, 501); + } + if (!empty($sql)) { + $arrayResponse = array( + '/boards', + '/boards/?/lists/?/cards/?/activities', + '/users/?/activities', + '/boards/?/activities', + '/users/?/cards', + '/cards/search', + '/boards/?/lists/?/cards/?/search', + '/users/search', + '/organizations', + '/boards/?/activities', + '/activities' + ); + if ($result = pg_query_params($db_lnk, $sql, $pg_params)) { + $data = array(); + $count = pg_num_rows($result); + $i = 0; + if (in_array($r_resource_cmd, $arrayResponse) && ($count == 1 || $count == 0)) { + echo '['; + } + while ($row = pg_fetch_row($result)) { + $obj = json_decode($row[0], true); + if (isset($obj['board_activities']) && !empty($obj['board_activities'])) { + for ($k = 0; $k < count($obj['board_activities']); $k++) { + if (!empty($obj['board_activities'][$k]['revisions']) && trim($obj['board_activities'][$k]['revisions']) != '') { + $revisions = unserialize($obj['board_activities'][$k]['revisions']); + unset($dif); + if (!empty($revisions['new_value'])) { + foreach ($revisions['new_value'] as $key => $value) { + if ($key != 'is_archived' && $key != 'is_deleted' && $key != 'created' && $key != 'modified' && $obj['type'] != 'moved_card_checklist_item' && $obj['type'] != 'add_card_desc' && $obj['type'] != 'add_card_duedate' && $obj['type'] != 'delete_card_duedate' && $obj['type'] != 'change_visibility' && $obj['type'] != 'add_background' && $obj['type'] != 'change_background') { + $old_val = ($revisions['old_value'][$key] != NULL && $revisions['old_value'][$key] != 'NULL') ? $revisions['old_value'][$key] : ''; + $new_val = ($revisions['new_value'][$key] != NULL && $revisions['new_value'][$key] != 'NULL') ? $revisions['new_value'][$key] : ''; + $dif[] = nl2br(getRevisiondifference($old_val, $old_val)); + } + if ($obj['type'] == 'add_card_desc' || $obj['type'] == 'add_card_desc' || $obj['type'] == ' edit_card_duedate' || $obj['type'] == 'change_visibility' || $obj['type'] == 'add_background' || $obj['type'] == 'change_background') { + $dif[] = $revisions['new_value'][$key]; + } + } + if (isset($dif)) { + $obj['board_activities'][$k]['difference'] = $dif; + } + } + } + } + $row[0] = json_encode($obj); + if ($r_resource_cmd == '/boards/?') { + $obj = json_decode($row[0], true); + global $_server_domain_url; + $md5_hash = md5(SecuritySalt . $r_resource_vars['boards']); + $obj['google_syn_url'] = $_server_domain_url . '/ical/' . $r_resource_vars['boards'] . '/' . $md5_hash . '.ics'; + $row[0] = json_encode($obj); + } + } else if ($r_resource_cmd == '/boards/?/lists/?/cards/?/activities' || $r_resource_cmd == '/users/?/activities' || $r_resource_cmd == '/users/?/notify_count' || $r_resource_cmd == '/boards/?/activities') { + if (!empty($obj['revisions']) && trim($obj['revisions']) !== '') { + $revisions = unserialize($obj['revisions']); + $obj['revisions'] = $revisions; + unset($dif); + foreach ($revisions['new_value'] as $key => $value) { + if ($key != 'is_archived' && $key != 'is_deleted' && $key != 'created' && $key != 'modified' && $key != 'is_offline' && $key != 'uuid' && $key != 'to_date' && $key != 'temp_id' && $obj['type'] != 'moved_card_checklist_item' && $obj['type'] != 'add_card_desc' && $obj['type'] != 'add_card_duedate' && $obj['type'] != 'delete_card_duedate' && $obj['type'] != 'add_background' && $obj['type'] != 'change_background' && $obj['type'] != 'change_visibility') { + $old_val = (isset($revisions['old_value'][$key]) && $revisions['old_value'][$key] != NULL && $revisions['old_value'][$key] != 'NULL') ? $revisions['old_value'][$key] : ''; + $new_val = (isset($revisions['new_value'][$key]) && $revisions['new_value'][$key] != NULL && $revisions['new_value'][$key] != 'NULL') ? $revisions['new_value'][$key] : ''; + $dif[] = nl2br(getRevisiondifference($old_val, $new_val)); + } + if ($obj['type'] == 'add_card_desc' || $obj['type'] == 'add_card_desc' || $obj['type'] == ' edit_card_duedate' || $obj['type'] == 'add_background' || $obj['type'] == 'change_background' || $obj['type'] == 'change_visibility') { + $dif[] = $revisions['new_value'][$key]; + } + } + if (isset($dif)) { + $obj['difference'] = $dif; + } + } + if ($obj['type'] === 'add_board_user') { + $obj['board_user'] = executeQuery('SELECT * FROM boards_users_listing WHERE id = $1', array( + $obj['foreign_id'] + )); + } else if ($obj['type'] === 'add_list') { + $obj['list'] = executeQuery('SELECT * FROM lists WHERE id = $1', array( + $obj['list_id'] + )); + } else if ($obj['type'] === 'change_list_position') { + $obj['list'] = executeQuery('SELECT position, board_id FROM lists WHERE id = $1', array( + $obj['list_id'] + )); + } else if ($obj['type'] === 'add_card') { + $obj['card'] = executeQuery('SELECT * FROM cards WHERE id = $1', array( + $obj['card_id'] + )); + } else if ($obj['type'] === 'copy_card') { + $obj['card'] = executeQuery('SELECT * FROM cards WHERE id = $1', array( + $obj['foreign_id'] + )); + } else if ($obj['type'] === 'add_card_checklist') { + $obj['checklist'] = executeQuery('SELECT * FROM checklists_listing WHERE id = $1', array( + $obj['foreign_id'] + )); + $obj['checklist']['checklists_items'] = json_decode($obj['checklist']['checklists_items'], true); + } else if ($obj['type'] === 'add_card_label') { + $s_result = pg_query_params($db_lnk, 'SELECT * FROM cards_labels_listing WHERE card_id = $1', array( + $obj['card_id'] + )); + while ($row = pg_fetch_assoc($s_result)) { + $obj['labels'][] = $row; + } + } else if ($obj['type'] === 'add_card_voter') { + $obj['voter'] = executeQuery('SELECT * FROM card_voters_listing WHERE id = $1', array( + $obj['foreign_id'] + )); + } else if ($obj['type'] === 'add_card_user') { + $obj['user'] = executeQuery('SELECT * FROM cards_users_listing WHERE id = $1', array( + $obj['foreign_id'] + )); + } else if ($obj['type'] === 'update_card_checklist') { + $obj['checklist'] = executeQuery('SELECT * FROM checklists WHERE id = $1', array( + $obj['foreign_id'] + )); + } else if ($obj['type'] === 'add_checklist_item' || $obj['type'] === 'update_card_checklist_item' || $obj['type'] === 'moved_card_checklist_item') { + $obj['item'] = executeQuery('SELECT * FROM checklist_items WHERE id = $1', array( + $obj['foreign_id'] + )); + } else if ($obj['type'] === 'add_card_attachment') { + $obj['attachment'] = executeQuery('SELECT * FROM card_attachments WHERE id = $1', array( + $obj['foreign_id'] + )); + } else if ($obj['type'] === 'change_card_position') { + $obj['card'] = executeQuery('SELECT position FROM cards WHERE id = $1', array( + $obj['card_id'] + )); + } + $row[0] = json_encode($obj); + } else if ($r_resource_cmd == '/boards/?') { + $obj = json_decode($row[0], true); + global $_server_domain_url; + $md5_hash = md5(SecuritySalt . $r_resource_vars['boards']); + $obj['google_syn_url'] = $_server_domain_url . '/ical/' . $r_resource_vars['boards'] . '/' . $md5_hash . '.ics'; + $row[0] = json_encode($obj); + } + if ($i == 0 && $count > 1) { + echo '['; + } + echo $row[0]; + $i++; + if ($i < $count) { + echo ','; + } else { + if ($count > 1) { + echo ']'; + } + } + } + if (in_array($r_resource_cmd, $arrayResponse) && ($count == 1 || $count == 0)) { + echo ']'; + } + pg_free_result($result); + } else { + $r_debug.= __LINE__ . ': ' . pg_last_error($db_lnk) . '\n'; + } + } else { + echo json_encode($response); + } +} +/** + * Common method to handle POST method + * + * @param $r_resource_cmd + * @param $r_resource_vars + * @param $r_resource_filters + * @param $r_post + * @return mixed + */ +function r_post($r_resource_cmd, $r_resource_vars, $r_resource_filters, $r_post) +{ + global $r_debug, $db_lnk, $authUser, $thumbsizes, $_server_domain_url; + $emailFindReplace = $response = array(); + $fields = 'created, modified'; + $values = 'now(), now()'; + $json = $sql = $is_return_vlaue = false; + $uuid = ''; + if (isset($r_post['uuid'])) { + $uuid = $r_post['uuid']; + } + unset($r_post['temp_id']); + unset($r_post['uuid']); + unset($r_post['id']); + switch ($r_resource_cmd) { + case '/users/forgotpassword': //users forgot password + $user = executeQuery('SELECT * FROM users WHERE email = $1', array( + $r_post['email'] + )); + if ($user) { + $password = uniqid(); + pg_query_params($db_lnk, 'UPDATE users SET (password) = ($1) WHERE id = $2', array( + getCryptHash($password) , + $user['id'] + )); + $emailFindReplace = array( + 'mail' => 'forgetpassword', + '##USERNAME##' => $user['username'], + '##PASSWORD##' => $password, + 'to' => $user['email'] + ); + $response = array( + 'success' => 'An email has been sent with your new password.' + ); + sendMail($emailFindReplace); + } else { + $response = array( + 'error' => 'Please enter valid email id.' + ); + } + break; + + case '/settings': //settings update + foreach ($r_post as $key => $value) { + pg_query_params($db_lnk, 'UPDATE settings SET value = $1 WHERE name = $2', array( + $value, + trim($key) + )); + } + $response = array( + 'success' => 'Settings updated successfully.' + ); + break; + + case '/users/admin_user_add': //Admin user add + $table_name = 'users'; + $user = executeQuery('SELECT * FROM users WHERE username = $1 OR email = $2', array( + $r_post['username'], + $r_post['email'] + )); + if (!$user) { + $sql = true; + $table_name = 'users'; + $r_post['password'] = getCryptHash($r_post['password']); + $r_post['role_id'] = 2; // user + $r_post['is_active'] = true; + $r_post['is_email_confirmed'] = true; + $r_post['role_id'] = 2; // user + $r_post['initials'] = strtoupper(substr($r_post['username'], 0, 1)); + $r_post['ip_id'] = saveIp(); + } else { + $msg = ''; + if ($user['email'] == $r_post['email']) { + $msg = 'Email address already exists. Your registration process is not completed. Please, try again.'; + } else if ($user['username'] == $r_post['username']) { + $msg = 'Username already exists. Your registration process is not completed. Please, try again.'; + } + $response = array( + 'error' => $msg + ); + } + break; + + case '/users/register': //users register + $table_name = 'users'; + $user = executeQuery('SELECT * FROM users WHERE username = $1 OR email = $2', array( + $r_post['username'], + $r_post['email'] + )); + if (!$user) { + $sql = true; + $table_name = 'users'; + $r_post['password'] = getCryptHash($r_post['password']); + $r_post['role_id'] = 2; // user + $r_post['initials'] = strtoupper(substr($r_post['username'], 0, 1)); + $r_post['ip_id'] = saveIp(); + } else { + $msg = ''; + if ($user['email'] == $r_post['email']) { + $msg = 'Email address is already exist. Your registration process is not completed. Please, try again.'; + } else if ($user['username'] == $r_post['username']) { + $msg = 'Username address is already exist. Your registration process is not completed. Please, try again.'; + } + $response = array( + 'error' => $msg + ); + } + break; + + case '/users/login': //users login + $is_login = false; + $user = array(); + $table_name = 'users'; + $log_user = executeQuery('SELECT * FROM users WHERE email = $1 or username = $1', array( + $r_post['email'] + )); + if (LDAP_LOGIN_ENABLED && (empty($log_user) || (!empty($log_user) && $log_user['role_id'] != 1))) { + $check_user = ldapAuthenticate($r_post['email'], $r_post['password']); + if (!empty($check_user['User']) && $check_user['User']['is_username_exits'] && $check_user['User']['is_password_matched'] && isset($check_user['User']['email']) && !empty($check_user['User']['email'])) { + $user = executeQuery('SELECT * FROM users_listing WHERE email = $1', array( + $check_user['User']['email'] + )); + if (!$user) { + $r_post['password'] = getCryptHash($r_post['password']); + $r_post['role_id'] = 2; // user + $result = pg_query_params($db_lnk, 'INSERT INTO ' . $table_name . ' (created, modified, role_id, username, email, password, initials, is_active, is_email_confirmed) VALUES (now(), now(), 2, $1, $2, $3, $4, TRUE, TRUE) RETURNING * ', array( + $r_post['email'], + $check_user['User']['email'], + $r_post['password'], + strtoupper(substr($r_post['email'], 0, 1)) + )); + $user = pg_fetch_assoc($result); + $user = executeQuery('SELECT * FROM users_listing WHERE id = $1', array( + $user['id'] + )); + } + } + } else { + if ($log_user) { + $r_post['password'] = crypt($r_post['password'], $log_user['password']); + $user = executeQuery('SELECT * FROM users_listing WHERE (email = $1 or username = $1) AND password = $2 AND is_active = $3', array( + $r_post['email'], + $r_post['password'], + true + )); + } + } + if (!empty($user)) { + if (LDAP_LOGIN_ENABLED) { + $login_type_id = 1; + } else { + $login_type_id = 2; + } + $last_login_ip_id = saveIp(); + pg_query_params($db_lnk, 'UPDATE users SET last_login_date = now(), login_type_id = $1, last_login_ip_id = $2 WHERE id = $3', array( + $login_type_id, + $last_login_ip_id, + $user['id'] + )); + unset($user['password']); + $user_agent = !empty($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; + pg_query_params($db_lnk, 'INSERT INTO user_logins (created, modified, user_id, ip_id, user_agent) VALUES (now(), now(), $1, $2, $3)', array( + $user['id'], + $last_login_ip_id, + $user_agent + )); + $role_links = executeQuery('SELECT * FROM role_links_listing WHERE id = $1', array( + $user['role_id'] + )); + $post_url = $_server_domain_url . str_replace('r.php', 'token.php', $_SERVER['PHP_SELF']); + $response = doPost($post_url, array( + 'grant_type' => 'password', + 'username' => $user['username'], + 'password' => $r_post['password'], + 'client_id' => OAUTH_CLIENTID, + 'client_secret' => OAUTH_CLIENT_SECRET + )); + $response = array_merge($role_links, $response); + $board_ids = array(); + if (!empty($user['boards_users'])) { + $boards_users = json_decode($user['boards_users'], true); + foreach ($boards_users as $boards_user) { + $board_ids[] = $boards_user['board_id']; + } + } + $notify_count = executeQuery('SELECT count(a.*) AS notify_count FROM activities a WHERE a.id > $1 AND board_id = ANY ($2) ', array( + $user['last_activity_id'], + '{' . implode(',', $board_ids) . '}' + )); + $user = array_merge($user, $notify_count); + $response['user'] = $user; + $response['user']['organizations'] = json_decode($user['organizations'], true); + } else { + $response = array( + 'error' => 'Sorry, login failed. Either your username or password are incorrect or admin deactivated your account.' + ); + } + break; + + case '/organizations/?/users/?': //organization users add + $table_name = 'organizations_users'; + $sql = true; + $is_return_vlaue = true; + break; + + case '/organizations': //organizations add + $sql = true; + $table_name = 'organizations'; + $r_post['user_id'] = (!empty($authUser['id'])) ? $authUser['id'] : 1; + $r_post['organization_visibility'] = 2; + break; + + case '/boards': //boards add + $is_import_board = false; + if (!empty($_FILES['board_import'])) { + if ($_FILES['board_import']['error'] == 0) { + $imported_board = json_decode(file_get_contents($_FILES['board_import']['tmp_name']) , true); + if (!empty($imported_board)) { + $board = importTrelloBoard($imported_board); + $response['id'] = $board['id']; + } else { + $response['error'] = 'Unable to import. please try again.'; + } + } else { + $response['error'] = 'Unable to import. please try again.'; + } + } else { + $table_name = 'boards'; + $board = executeQuery('SELECT id, name FROM ' . $table_name . ' WHERE name = $1', array( + $r_post['name'] + )); + if (isset($r_post['template']) && !empty($r_post['template'])) { + $lists = explode(',', $r_post['template']); + } + unset($r_post['template']); + $sql = true; + $r_post['user_id'] = (!empty($authUser['id'])) ? $authUser['id'] : 1; + } + break; + + case '/boards/?/boards_stars': //stars add + $table_name = 'board_stars'; + $subcriber = executeQuery('SELECT id, is_starred FROM ' . $table_name . ' WHERE board_id = $1 and user_id = $2', array( + $r_resource_vars['boards'], + $authUser['id'] + )); + if (!$subcriber) { + $result = pg_query_params($db_lnk, 'INSERT INTO ' . $table_name . ' (created, modified, board_id, user_id, is_starred) VALUES (now(), now(), $1, $2, TRUE) RETURNING id', array( + $r_resource_vars['boards'], + $authUser['id'] + )); + } else { + if ($subcriber['is_starred'] == 't') { + $result = pg_query_params($db_lnk, 'UPDATE ' . $table_name . ' SET is_starred = False Where board_id = $1 and user_id = $2 RETURNING id', array( + $r_resource_vars['boards'], + $authUser['id'] + )); + } else { + $result = pg_query_params($db_lnk, 'UPDATE ' . $table_name . ' SET is_starred = True Where board_id = $1 and user_id = $2 RETURNING id', array( + $r_resource_vars['boards'], + $authUser['id'] + )); + } + } + $star = pg_fetch_assoc($result); + $response['id'] = $star['id']; + break; + + case '/boards/?/board_subscribers': //subscriber add + $table_name = 'board_subscribers'; + $subcriber = executeQuery('SELECT id, is_subscribed FROM ' . $table_name . ' WHERE board_id = $1 and user_id = $2', array( + $r_resource_vars['boards'], + $authUser['id'] + )); + if (!$subcriber) { + $result = pg_query_params($db_lnk, 'INSERT INTO ' . $table_name . ' (created, modified, board_id, user_id, is_subscribed) VALUES (now(), now(), $1, $2, TRUE) RETURNING *', array( + $r_resource_vars['boards'], + $authUser['id'] + )); + } else { + if ($subcriber['is_subscribed'] == 't') { + $result = pg_query_params($db_lnk, 'UPDATE ' . $table_name . ' SET is_subscribed = False Where board_id = $1 and user_id = $2 RETURNING *', array( + $r_resource_vars['boards'], + $authUser['id'] + )); + } else { + $result = pg_query_params($db_lnk, 'UPDATE ' . $table_name . ' SET is_subscribed = True Where board_id = $1 and user_id = $2 RETURNING *', array( + $r_resource_vars['boards'], + $authUser['id'] + )); + } + } + $response = pg_fetch_assoc($result); + break; + + case '/boards/?/copy': //boards copy + $table_name = 'boards'; + $sql = true; + $copied_board_id = $r_resource_vars['boards']; + $board_visibility = $r_post['board_visibility']; + if (!empty($r_post['organization_id'])) { + $organization_id = $r_post['organization_id']; + } + $keepcards = false; + if (!empty($r_post['keepCards'])) { + $keepcards = true; + unset($r_post['keepCards']); + } + $sresult = pg_query_params($db_lnk, 'SELECT * FROM boards WHERE id = $1', array( + $copied_board_id + )); + $srow = pg_fetch_assoc($sresult); + unset($srow['id']); + unset($srow['created']); + unset($srow['modified']); + unset($srow['user_id']); + unset($srow['name']); + if ($srow['commenting_permissions'] === NULL) { + $srow['commenting_permissions'] = 0; + } + if ($srow['voting_permissions'] === NULL) { + $srow['voting_permissions'] = 0; + } + if ($srow['inivitation_permissions'] === NULL) { + $srow['inivitation_permissions'] = 0; + } + $r_post = array_merge($r_post, $srow); + $r_post['board_visibility'] = $board_visibility; + if (!empty($organization_id)) { + $r_post['organization_id'] = $organization_id; + } + break; + + case '/users/?/changepassword': + $user = executeQuery('SELECT * FROM users WHERE id = $1', array( + $r_resource_vars['users'] + )); + if ($user) { + $cry_old_pass = crypt($r_post['old_password'], $user['password']); + if ((($authUser['role_id'] == 2) && ($user['password'] == $cry_old_pass)) || ($authUser['role_id'] == 1)) { + $result = pg_query_params($db_lnk, 'UPDATE users SET (password) = ($1) WHERE id = $2', array( + getCryptHash($r_post['password']) , + $r_resource_vars['users'] + )); + if ($authUser['role_id'] == 1) { + $emailFindReplace = array( + 'to' => $user['email'], + 'mail' => 'changepassword', + '##PASSWORD##' => $r_post['password'] + ); + sendMail($emailFindReplace); + $response = array( + 'success' => 'Password change successfully. Please login.' + ); + } + } else { + $response = array( + 'error' => 'Invalid old password.' + ); + } + } else { + $response = array( + 'error' => 'Unable to change password. Please try again.' + ); + } + break; + + case '/organizations/?/upload_logo': // organizations logo upload + $sql = false; + $json = true; + $organization_id = $r_resource_vars['organizations']; + if (!empty($_FILES['attachment']) && $_FILES['attachment']['error'] == 0) { + $mediadir = APP_PATH . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'Organization' . DIRECTORY_SEPARATOR . $r_resource_vars['organizations']; + $save_path = 'media' . DIRECTORY_SEPARATOR . 'Organization' . DIRECTORY_SEPARATOR . $r_resource_vars['organizations']; + if (!file_exists($mediadir)) { + mkdir($mediadir, 0777, true); + } + $file = $_FILES['attachment']; + $file['name'] = preg_replace('/[^A-Za-z0-9\-.]/', '', $file['name']); + if (move_uploaded_file($file['tmp_name'], $mediadir . DIRECTORY_SEPARATOR . $file['name'])) { + $logo_url = $save_path . DIRECTORY_SEPARATOR . $file['name']; + foreach ($thumbsizes['Organization'] as $key => $value) { + $list = glob(APP_PATH . DIRECTORY_SEPARATOR . 'img' . DIRECTORY_SEPARATOR . $key . DIRECTORY_SEPARATOR . 'Organization' . DIRECTORY_SEPARATOR . $r_resource_vars['organizations'] . '.*'); + @unlink($list[0]); + } + foreach ($thumbsizes['Organization'] as $key => $value) { + $mediadir = dirname(dirname(dirname(dirname(__FILE__)))) . '/client/img/' . $key . '/Organization/' . $r_resource_vars['organizations']; + $list = glob($mediadir . '.*'); + @unlink($list[0]); + } + pg_query_params($db_lnk, 'UPDATE organizations SET logo_url = $1 WHERE id = $2', array( + $logo_url, + $r_resource_vars['organizations'] + )); + $response['logo_url'] = $logo_url; + } + } + break; + + case '/boards/?/lists': + $table_name = 'lists'; + $r_post['board_id'] = $r_resource_vars['boards']; + $r_post['user_id'] = $authUser['id']; + $sql = true; + if (isset($r_post['clone_list_id'])) { + $clone_list_id = $r_post['clone_list_id']; + unset($r_post['clone_list_id']); + unset($r_post['list_cards']); + } + break; + + case '/boards/?/lists/?/list_subscribers': + $table_name = 'list_subscribers'; + $r_post['user_id'] = $authUser['id']; + $s_result = pg_query_params($db_lnk, 'SELECT is_subscribed FROM list_subscribers WHERE list_id = $1 and user_id = $2', array( + $r_resource_vars['lists'], + $r_post['user_id'] + )); + $check_subscribed = pg_fetch_assoc($s_result); + if (!empty($check_subscribed)) { + $is_subscribed = ($r_post['is_subscribed']) ? true : false; + $s_result = pg_query_params($db_lnk, 'UPDATE list_subscribers SET is_subscribed = $1 WHERE list_id = $2 and user_id = $3', array( + $is_subscribed, + $r_resource_vars['lists'], + $r_post['user_id'] + )); + } else { + $r_post['list_id'] = $r_resource_vars['lists']; + $sql = true; + } + break; + + case '/boards/?/lists/?/cards/?/comments': + $is_return_vlaue = true; + $table_name = 'activities'; + $sql = true; + $prev_message = array(); + if (isset($r_post['root']) && !empty($r_post['root'])) { + $prev_message = executeQuery('SELECT ac.*, u,username, u.profile_picture_path, u.initials FROM activities ac LEFT JOIN users u ON ac.user_id = u.id WHERE ac.id = $1', array( + $r_post['root'] + )); + } + $r_post['freshness_ts'] = date('Y-m-d h:i:s'); + $r_post['type'] = 'add_comment'; + break; + + case '/boards/?/lists/?/cards/?/card_subscribers': + $table_name = 'card_subscribers'; + $json = true; + $r_post['user_id'] = $authUser['id']; + unset($r_post['list_id']); + unset($r_post['board_id']); + $s_result = pg_query_params($db_lnk, 'SELECT is_subscribed FROM card_subscribers WHERE card_id = $1 and user_id = $2', array( + $r_resource_vars['cards'], + $r_post['user_id'] + )); + $check_subscribed = pg_fetch_assoc($s_result); + if (!empty($check_subscribed)) { + $is_subscribed = ($r_post['is_subscribed']) ? true : false; + $s_result = pg_query_params($db_lnk, 'UPDATE card_subscribers SET is_subscribed = $1 WHERE card_id = $2 and user_id = $3 RETURNING id', array( + $is_subscribed, + $r_resource_vars['cards'], + $r_post['user_id'] + )); + $subscribe = pg_fetch_assoc($s_result); + $response['id'] = $subscribe['id']; + } else { + $r_post['card_id'] = $r_resource_vars['cards']; + $r_post['user_id'] = $r_post['user_id']; + $sql = true; + } + break; + + case '/boards/?/lists/?/cards/?/card_voters': + $table_name = 'card_voters'; + $r_post['card_id'] = $r_resource_vars['cards']; + $r_post['user_id'] = $authUser['id']; + $sql = true; + break; + + case '/boards/?/lists/?/cards': + $table_name = 'cards'; + $r_post['user_id'] = $authUser['id']; + $pos_res = pg_query_params($db_lnk, 'SELECT position FROM cards WHERE board_id = $1 AND list_id = $2 ORDER BY position DESC LIMIT 1', array( + $r_post['board_id'], + $r_post['list_id'] + )); + $position = pg_fetch_array($pos_res); + if (empty($r_post['due_date'])) { + unset($r_post['due_date']); + } + if (!empty($r_post['user_ids'])) { + $r_post['members'] = explode(',', $r_post['user_ids']); + } + if (!isset($r_post['position'])) { + $r_post['position'] = $position[0] + 1; + } + $sql = true; + break; + + case '/boards/?/lists/?/cards/?/attachments': + $is_return_vlaue = true; + $table_name = 'card_attachments'; + $r_post['card_id'] = $r_resource_vars['cards']; + $r_post['list_id'] = $r_resource_vars['lists']; + $r_post['board_id'] = $r_resource_vars['boards']; + $mediadir = APP_PATH . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'Card' . DIRECTORY_SEPARATOR . $r_resource_vars['cards']; + $save_path = 'media' . DIRECTORY_SEPARATOR . 'Card' . DIRECTORY_SEPARATOR . $r_resource_vars['cards']; + $save_path = str_replace('\\', '/', $save_path); + if (!empty($_FILES['attachment']) && $_FILES['attachment']['error'] == 0) { + if (!file_exists($mediadir)) { + mkdir($mediadir, 0777, true); + } + $file = $_FILES['attachment']; + if (move_uploaded_file($file['tmp_name'], $mediadir . DIRECTORY_SEPARATOR . $file['name'])) { + $r_post['path'] = $save_path . '/' . $file['name']; + $r_post['name'] = $file['name']; + $r_post['mimetype'] = $file['type']; + $s_result = pg_query_params($db_lnk, 'INSERT INTO card_attachments (created, modified, card_id, name, path, list_id, board_id, mimetype) VALUES (now(), now(), $1, $2, $3, $4, $5, $6) RETURNING *', array( + $r_post['card_id'], + $r_post['name'], + $r_post['path'], + $r_post['list_id'], + $r_post['board_id'], + $r_post['mimetype'] + )); + $response['card_attachments'][] = pg_fetch_assoc($s_result); + } + foreach ($thumbsizes['CardAttachment'] as $key => $value) { + $mediadir = dirname(dirname(dirname(dirname(__FILE__)))) . '/client/img/' . $key . '/CardAttachment/' . $response['card_attachments'][0]['id']; + $list = glob($mediadir . '.*'); + @unlink($list[0]); + } + $foreign_ids['board_id'] = $r_resource_vars['boards']; + $foreign_ids['list_id'] = $r_resource_vars['lists']; + $foreign_ids['card_id'] = $r_resource_vars['cards']; + $comment = $authUser['username'] . ' added attachment to this card ##CARD_LINK##'; + $response['activity'] = insertActivity($authUser['id'], $comment, 'add_card_attachment', $foreign_ids, NULL, $response['card_attachments'][0]['id']); + } else if (!empty($_FILES['attachment']) && is_array($_FILES['attachment']['name']) && $_FILES['attachment']['error'][0] == 0) { + $file = $_FILES['attachment']; + for ($i = 0; $i < count($file['name']); $i++) { + if (!file_exists($mediadir)) { + mkdir($mediadir, 0777, true); + } + if (move_uploaded_file($file['tmp_name'][$i], $mediadir . DIRECTORY_SEPARATOR . $file['name'][$i])) { + $r_post[$i]['path'] = $save_path . DIRECTORY_SEPARATOR . $file['name'][$i]; + $r_post[$i]['name'] = $file['name'][$i]; + $r_post[$i]['mimetype'] = $file['type'][$i]; + $s_result = pg_query_params($db_lnk, 'INSERT INTO card_attachments (created, modified, card_id, name, path, list_id, board_id, mimetype) VALUES (now(), now(), $1, $2, $3, $4, $5, $6) RETURNING *', array( + $r_post['card_id'], + $r_post[$i]['name'], + $r_post[$i]['path'], + $r_post['list_id'], + $r_post['board_id'], + $r_post[$i]['mimetype'] + )); + $response['card_attachments'][] = pg_fetch_assoc($s_result); + $foreign_ids['board_id'] = $r_resource_vars['boards']; + $foreign_ids['list_id'] = $r_resource_vars['lists']; + $foreign_ids['card_id'] = $r_resource_vars['cards']; + $comment = $authUser['username'] . ' added attachment to this card ##CARD_LINK##'; + $response['activity'] = insertActivity($authUser['id'], $comment, 'add_card_attachment', $foreign_ids, NULL, $response['card_attachments'][$i]['id']); + foreach ($thumbsizes['CardAttachment'] as $key => $value) { + $mediadir = dirname(dirname(dirname(dirname(__FILE__)))) . '/client/img/' . $key . '/CardAttachment/' . $response['card_attachments'][$i]['id']; + $list = glob($mediadir . '.*'); + @unlink($list[0]); + } + } + } + } else if (isset($r_post['image_link']) && !empty($r_post['image_link'])) { + $filename = curlExecute($r_post['image_link'], 'get', $mediadir, 'image'); + $sql = true; + unset($r_post['image_link']); + $r_post['path'] = $save_path . '/' . $filename; + $r_post['name'] = $filename; + } + break; + + case '/boards/?/lists/?/cards/?/labels': + $is_return_vlaue = true; + $table_name = 'cards_labels'; + $r_post['card_id'] = $r_resource_vars['cards']; + $r_post['list_id'] = $r_resource_vars['lists']; + $r_post['board_id'] = $r_resource_vars['boards']; + $delete_labels = pg_query_params($db_lnk, 'DELETE FROM ' . $table_name . ' WHERE card_id = $1', array( + $r_resource_vars['cards'] + )); + $delete_labels_count = pg_affected_rows($delete_labels); + if (!empty($r_post['name'])) { + $label_names = explode(',', $r_post['name']); + unset($r_post['name']); + foreach ($label_names as $label_name) { + $s_result = pg_query_params($db_lnk, 'SELECT id FROM labels WHERE name = $1', array( + $label_name + )); + $label = pg_fetch_assoc($s_result); + if (empty($label)) { + $s_result = pg_query_params($db_lnk, 'INSERT INTO labels (created, modified, name) VALUES (now(), now(), $1) RETURNING id', array( + $label_name + )); + $label = pg_fetch_assoc($s_result); + } + $r_post['label_id'] = $label['id']; + pg_query_params($db_lnk, 'INSERT INTO ' . $table_name . ' (created, modified, card_id, label_id, board_id, list_id) VALUES (now(), now(), $1, $2, $3, $4) RETURNING *', array( + $r_post['card_id'], + $r_post['label_id'], + $r_post['board_id'], + $r_post['list_id'] + )); + } + $s_result = pg_query_params($db_lnk, 'SELECT * FROM cards_labels_listing WHERE card_id = $1', array( + $r_post['card_id'] + )); + $cards_labels = pg_fetch_all($s_result); + $response['cards_labels'] = $cards_labels; + $comment = $authUser['username'] . ' added label(s) to this card ##CARD_LINK## - ##LABEL_NAME##'; + } else { + $response['cards_labels'] = array(); + $comment = $authUser['username'] . ' removed label(s) in this card ##CARD_LINK## - ##LABEL_NAME##'; + } + $foreign_ids['board_id'] = $r_post['board_id']; + $foreign_ids['list_id'] = $r_post['list_id']; + $foreign_ids['card_id'] = $r_post['card_id']; + if (!empty($delete_labels_count)) { + $response['activity'] = insertActivity($authUser['id'], $comment, 'add_card_label', $foreign_ids, NULL, $r_post['label_id']); + } + break; + + case '/boards/?/lists/?/cards/?/checklists': + $sql = true; + $table_name = 'checklists'; + $r_post['user_id'] = $authUser['id']; + $r_post['card_id'] = $r_resource_vars['cards']; + if (isset($r_post['checklist_id'])) { + $checklist_id = $r_post['checklist_id']; + unset($r_post['checklist_id']); + } + break; + + case '/boards/?/lists/?/cards/?/checklists/?/items': + $table_name = 'checklist_items'; + $is_return_vlaue = true; + $r_post['user_id'] = $authUser['id']; + $r_post['card_id'] = $r_resource_vars['cards']; + $r_post['checklist_id'] = $r_resource_vars['checklists']; + unset($r_post['created']); + unset($r_post['modified']); + unset($r_post['is_offline']); + unset($r_post['list_id']); + unset($r_post['board_id']); + $names = explode("\n", $r_post['name']); + foreach ($names as $name) { + $r_post['name'] = trim($name); + if (!empty($r_post['name'])) { + $position = executeQuery('SELECT max(position) as position FROM checklist_items WHERE checklist_id = $1', array( + $r_post['checklist_id'] + )); + $r_post['position'] = $position['position']; + if (empty($r_post['position'])) { + $r_post['position'] = 0; + } + $r_post['position']+= 1; + $result = pg_execute_insert($table_name, $r_post); + $item = pg_fetch_assoc($result); + $response[$table_name][] = $item; + $foreign_ids['board_id'] = $r_resource_vars['boards']; + $foreign_ids['list_id'] = $r_resource_vars['lists']; + $foreign_ids['card_id'] = $r_post['card_id']; + $comment = '##USER_NAME## added item ##CHECKLIST_ITEM_NAME## in checklist ##CHECKLIST_ITEM_PARENT_NAME## of card ##CARD_LINK##'; + $response['activities'][] = insertActivity($authUser['id'], $comment, 'add_checklist_item', $foreign_ids, '', $item['id']); + } + } + break; + + case '/boards/?/lists/?/cards/?/checklists/?/items/?/convert_to_card': + $is_return_vlaue = true; + $table_name = 'cards'; + $result = pg_query_params($db_lnk, 'SELECT name FROM checklist_items WHERE id = $1', array( + $r_resource_vars['items'] + )); + $row = pg_fetch_assoc($result); + $r_post['board_id'] = $r_resource_vars['boards']; + $r_post['list_id'] = $r_resource_vars['lists']; + $r_post['name'] = $row['name']; + $sresult = pg_query_params($db_lnk, 'SELECT max(position) as position FROM cards WHERE list_id = $1', array( + $r_post['list_id'] + )); + $srow = pg_fetch_assoc($sresult); + $r_post['position'] = $srow['position']; + $r_post['user_id'] = $authUser['id']; + $sql = true; + break; + + case '/users/?': + $is_return_vlaue = true; + $profile_picture_path = 'NULL'; + $no_error = true; + if (!empty($_FILES['attachment']['name']) && $_FILES['attachment']['error'] == 0) { + $mediadir = APP_PATH . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'User' . DIRECTORY_SEPARATOR . $r_resource_vars['users']; + $save_path = 'media' . DIRECTORY_SEPARATOR . 'User' . DIRECTORY_SEPARATOR . $r_resource_vars['users']; + if (!file_exists($mediadir)) { + mkdir($mediadir, 0777, true); + } + $file = $_FILES['attachment']; + $file['name'] = preg_replace('/[^A-Za-z0-9\-.]/', '', $file['name']); + if (move_uploaded_file($file['tmp_name'], $mediadir . DIRECTORY_SEPARATOR . $file['name'])) { + $profile_picture_path = $save_path . DIRECTORY_SEPARATOR . $file['name']; + foreach ($thumbsizes['User'] as $key => $value) { + $mediadir = dirname(dirname(dirname(dirname(__FILE__)))) . '/client/img/' . $key . '/User/' . $r_resource_vars['users']; + $list = glob($mediadir . '.*'); + @unlink($list[0]); + } + $authUser['profile_picture_path'] = $profile_picture_path; + $response['profile_picture_path'] = $profile_picture_path; + } + pg_query_params($db_lnk, 'UPDATE users SET profile_picture_path = $1 WHERE id = $2', array( + $profile_picture_path, + $r_resource_vars['users'] + )); + } else { + if (!empty($_POST['email'])) { + $user = executeQuery('SELECT * FROM users WHERE email = $1', array( + $_POST['email'] + )); + if ($user['id'] != $r_resource_vars['users'] && $user['email'] == $_POST['email']) { + $no_error = false; + $msg = 'Email address is already exist. User Profile could not be updated. Please, try again.'; + } + } + if ($no_error) { + $_POST['initials'] = strtoupper($_POST['initials']); + pg_query_params($db_lnk, 'UPDATE users SET full_name = $1, about_me = $2, initials = $3 WHERE id = $4', array( + $_POST['full_name'], + $_POST['about_me'], + $_POST['initials'], + $r_resource_vars['users'] + )); + if (!empty($_POST['email'])) { + pg_query_params($db_lnk, 'UPDATE users SET email= $1 WHERE id = $2', array( + $_POST['email'], + $r_resource_vars['users'] + )); + } + } + } + if ($no_error) { + $response['success'] = 'User Profile has been updated.'; + } else { + $response['error'] = $msg; + } + break; + + case '/boards/?/custom_backgrounds': + $is_return_vlaue = true; + if (!empty($_FILES['attachment']) && $_FILES['attachment']['error'] == 0) { + $mediadir = APP_PATH . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'Board' . DIRECTORY_SEPARATOR . $r_resource_vars['boards']; + $save_path = 'media' . DIRECTORY_SEPARATOR . 'Board' . DIRECTORY_SEPARATOR . $r_resource_vars['boards']; + if (!file_exists($mediadir)) { + mkdir($mediadir, 0777, true); + } + $file = $_FILES['attachment']; + $file['name'] = preg_replace('/[^A-Za-z0-9\-.]/', '', $file['name']); + if (move_uploaded_file($file['tmp_name'], $mediadir . DIRECTORY_SEPARATOR . $file['name'])) { + $r_post['name'] = $file['name']; + foreach ($thumbsizes['Board'] as $key => $value) { + $mediadir = dirname(dirname(dirname(dirname(__FILE__)))) . DIRECTORY_SEPARATOR . 'client' . DIRECTORY_SEPARATOR . 'img' . DIRECTORY_SEPARATOR . $key . DIRECTORY_SEPARATOR . 'Board' . DIRECTORY_SEPARATOR . $r_resource_vars['boards']; + $list = glob($mediadir . '.*'); + @unlink($list[0]); + } + $hash = md5(SecuritySalt . 'Board' . $r_resource_vars['boards'] . 'jpg' . 'extra_large_thumb' . SITE_NAME); + $background_picture_url = $_server_domain_url . '/img/extra_large_thumb/Board/' . $r_resource_vars['boards'] . '.' . $hash . '.jpg'; + $r_post['background_picture_path'] = $save_path . DIRECTORY_SEPARATOR . $file['name']; + $r_post['path'] = $background_picture_url; + $response['background_picture_url'] = $background_picture_url; + } + pg_query_params($db_lnk, 'UPDATE boards SET background_picture_url = $1,background_picture_path = $2 WHERE id = $3', array( + $r_post['path'], + $r_post['background_picture_path'], + $r_resource_vars['boards'] + )); + } + break; + + case '/boards/?/lists/?/cards/?/users/?': + $is_return_vlaue = true; + $table_name = 'cards_users'; + unset($r_post['board_id']); + unset($r_post['list_id']); + unset($r_post['is_offline']); + unset($r_post['profile_picture_path']); + unset($r_post['username']); + unset($r_post['initials']); + $check_already_added = executeQuery('SELECT * FROM cards_users WHERE card_id = $1 AND user_id = $2', array( + $r_resource_vars['cards'], + $r_resource_vars['users'] + )); + if (!empty($check_already_added)) { + $response['id'] = $check_already_added['id']; + $response['cards_users'] = $check_already_added; + } else { + $sql = true; + } + break; + + case '/boards/?/lists/?/cards/?/copy': + $is_return_vlaue = true; + $r_post['user_id'] = $authUser['id']; + $table_name = 'cards'; + $is_keep_attachment = $is_keep_user = $is_keep_label = $is_keep_activity = $is_keep_checklist = 0; + if (isset($r_post['keep_attachments'])) { + $is_keep_attachment = $r_post['keep_attachments']; + unset($r_post['keep_attachments']); + } + if (isset($r_post['keep_activities'])) { + $is_keep_activity = $r_post['keep_activities']; + unset($r_post['keep_activities']); + } + if (isset($r_post['keep_labels'])) { + $is_keep_label = $r_post['keep_labels']; + unset($r_post['keep_labels']); + } + if (isset($r_post['keep_users'])) { + $is_keep_user = $r_post['keep_users']; + unset($r_post['keep_users']); + } + if (isset($r_post['keep_checklists'])) { + $is_keep_checklist = $r_post['keep_checklists']; + unset($r_post['keep_checklists']); + } + $copied_card_id = $r_resource_vars['cards']; + unset($r_post['copied_card_id']); + $sresult = pg_query_params($db_lnk, 'SELECT * FROM cards WHERE id = $1', array( + $copied_card_id + )); + $srow = pg_fetch_assoc($sresult); + unset($srow['id']); + $card_name = $r_post['name']; + $r_post = array_merge($srow, $r_post); + $r_post['name'] = $card_name; + $sql = true; + break; + + case '/acl_links': + $table_name = 'acl_links_roles'; + $acl = executeQuery('SELECT * FROM ' . $table_name . ' WHERE acl_link_id = $1 AND role_id = $2', array( + $r_post['acl_link_id'], + $r_post['role_id'] + )); + if ($acl) { + pg_query_params($db_lnk, 'DELETE FROM ' . $table_name . ' WHERE acl_link_id = $1 AND role_id = $2', array( + $r_post['acl_link_id'], + $r_post['role_id'] + )); + } else { + pg_query_params($db_lnk, 'INSERT INTO ' . $table_name . ' (created, modified, acl_link_id, role_id) VALUES(now(), now(), $1, $2)', array( + $r_post['acl_link_id'], + $r_post['role_id'] + )); + } + break; + + case '/boards/?/users': + $is_return_vlaue = true; + $table_name = 'boards_users'; + $boards_user = executeQuery('SELECT * FROM boards_users WHERE board_id = $1 AND user_id = $2', array( + $r_resource_vars['boards'], + $r_post['user_id'] + )); + if (empty($boards_user)) { + $sql = true; + } + break; + + default: + header($_SERVER['SERVER_PROTOCOL'] . ' 501 Not Implemented', true, 501); + break; + } + if (!empty($sql)) { + $post = getbindValues($table_name, $r_post); + $result = pg_execute_insert($table_name, $post); + if ($result) { + $row = pg_fetch_assoc($result); + $response['id'] = $row['id']; + if ($is_return_vlaue) { + $response[$table_name] = $row; + } + if (!empty($uuid)) { + $response['uuid'] = $uuid; + } + if ($r_resource_cmd == '/users/register') { + $emailFindReplace['##USERNAME##'] = $r_post['username']; + $emailFindReplace['##ACTIVATION_URL##'] = 'http://' . $_SERVER['HTTP_HOST'] . '/#/users/activation/' . $row['id'] . '/' . md5($r_post['username']); + $emailFindReplace['to'] = $r_post['email']; + $emailFindReplace['mail'] = 'activation'; + sendMail($emailFindReplace); + } else if ($r_resource_cmd == '/boards') { + if (!$is_import_board) { + $foreign_id['board_id'] = $response['id']; + $comment = $authUser['username'] . ' created board'; + $response['activity'] = insertActivity($authUser['id'], $comment, 'add_board', $foreign_id); + $result = pg_query_params($db_lnk, 'INSERT INTO boards_users (created, modified, board_id , user_id, is_admin) VALUES (now(), now(), $1, $2, true)', array( + $row['id'], + $r_post['user_id'] + )); + if (isset($lists) && !empty($lists)) { + $position = 1; + $total_list = count($lists); + $s_sql = 'INSERT INTO lists (created, modified, board_id, name, user_id, position) VALUES'; + foreach ($lists as $list) { + $s_sql = 'INSERT INTO lists (created, modified, board_id, name, user_id, position) VALUES'; + $s_sql.= '(now(), now(), $1, $2, $3, $4)'; + pg_query_params($db_lnk, $s_sql, array( + $response['id'], + $list, + $authUser['id'], + $position + )); + $position++; + } + } + $response['simple_board'] = executeQuery('SELECT row_to_json(d) FROM (SELECT * FROM simple_board_listing sbl WHERE id = $1 ORDER BY id ASC) as d', array( + $row['id'] + )); + $response['simple_board'] = json_decode($response['simple_board']['row_to_json'], true); + } + } else if ($r_resource_cmd == '/organizations') { + $result = pg_query_params($db_lnk, 'INSERT INTO organizations_users (created, modified, organization_id , user_id, is_admin) VALUES (now(), now(), $1, $2, true)', array( + $row['id'], + $r_post['user_id'] + )); + } else if ($r_resource_cmd == '/boards/?/lists') { + $foreign_ids['board_id'] = $r_post['board_id']; + $foreign_ids['list_id'] = $response['id']; + $comment = $authUser['username'] . ' added list "' . $r_post['name'] . '".'; + $response['activity'] = insertActivity($authUser['id'], $comment, 'add_list', $foreign_ids); + $copy_checklists = array(); + $copy_checklists_items = array(); + if (!empty($clone_list_id)) { + $s_result = pg_query_params($db_lnk, 'SELECT name, board_id, position FROM lists WHERE id = $1', array( + $clone_list_id + )); + $previous_list = pg_fetch_assoc($s_result); + $new_list_id = $response['id']; + // Copy cards + $card_fields = 'board_id, name, description, position, due_date, is_archived, attachment_count, checklist_count, checklist_item_count, checklist_item_completed_count, label_count, cards_user_count, cards_subscriber_count, card_voter_count, activity_count, user_id, comment_count'; + $card_fields = 'list_id, ' . $card_fields; + $cards = pg_query_params($db_lnk, 'SELECT id, ' . $card_fields . ' FROM cards WHERE list_id = $1 ORDER BY id', array( + $clone_list_id + )); + if (pg_num_rows($cards)) { + copyCards($card_fields, $cards, $new_list_id, $post['name'], $foreign_ids['board_id']); + } + } + $s_result = pg_query_params($db_lnk, 'SELECT * FROM lists_listing WHERE id = $1', array( + $foreign_ids['list_id'] + )); + $list = pg_fetch_assoc($s_result); + $response['list'] = $list; + $attachments = pg_query_params($db_lnk, 'SELECT * FROM card_attachments WHERE list_id = $1', array( + $foreign_ids['list_id'] + )); + while ($attachment = pg_fetch_assoc($attachments)) { + $response['list']['attachments'][] = $attachment; + } + $activities = pg_query_params($db_lnk, 'SELECT * FROM activities_listing WHERE list_id = $1', array( + $foreign_ids['list_id'] + )); + while ($activity = pg_fetch_assoc($activities)) { + $response['list']['activities'][] = $activity; + } + $response['list']['checklists'] = $copy_checklists; + $response['list']['checklists_items'] = $copy_checklists_items; + $labels = pg_query_params($db_lnk, 'SELECT * FROM cards_labels_listing WHERE list_id = $1', array( + $foreign_ids['list_id'] + )); + while ($label = pg_fetch_assoc($labels)) { + $response['list']['labels'][] = $label; + } + $response['list']['cards'] = json_decode($response['list']['cards'], true); + $response['list']['lists_subscribers'] = json_decode($response['list']['lists_subscribers'], true); + } else if ($r_resource_cmd == '/boards/?/lists/?/cards' || $r_resource_cmd == '/boards/?/lists/?/cards/?/checklists/?/items/?/convert_to_card') { + $s_result = pg_query_params($db_lnk, 'SELECT name FROM lists WHERE id = $1', array( + $r_post['list_id'] + )); + $list = pg_fetch_assoc($s_result); + $foreign_ids['board_id'] = $r_post['board_id']; + $foreign_ids['card_id'] = $response['id']; + $foreign_ids['list_id'] = $r_post['list_id']; + $comment = $authUser['username'] . ' added card ##CARD_LINK## to list "' . $list['name'] . '".'; + $response['activity'] = insertActivity($authUser['id'], $comment, 'add_card', $foreign_ids); + if (!empty($r_post['members'])) { + $s_usql = ''; + foreach ($r_post['members'] as $member) { + $s_usql = 'INSERT INTO cards_users (created, modified, card_id, user_id) VALUES(now(), now(), ' . $response['id'] . ', ' . $member . ') RETURNING id'; + $s_result = pg_query_params($db_lnk, $s_usql, array()); + $card_user = pg_fetch_assoc($s_result); + $_user = executeQuery('SELECT username FROM users WHERE id = $1', array( + $member + )); + $comment = $authUser['username'] . ' added "' . $_user['username'] . '" as member to this card ##CARD_LINK##'; + $response['activity'] = insertActivity($authUser['id'], $comment, 'add_card_user', $foreign_ids, '', $card_user['id']); + } + } + $cards_users = pg_query_params($db_lnk, 'SELECT * FROM cards_users_listing WHERE card_id = $1', array( + $response['id'] + )); + while ($cards_user = pg_fetch_assoc($cards_users)) { + $response['cards_users'][] = $cards_user; + } + if (!empty($r_post['labels'])) { + $r_post['card_labels'] = $r_post['labels']; + } + if (!empty($r_post['card_labels'])) { + $label_names = explode(',', $r_post['card_labels']); + foreach ($label_names as $label_name) { + $s_result = pg_query_params($db_lnk, 'SELECT id FROM labels WHERE name = $1', array( + $label_name + )); + $label = pg_fetch_assoc($s_result); + if (empty($label)) { + $s_result = pg_query_params($db_lnk, $s_sql = 'INSERT INTO labels (created, modified, name) VALUES (now(), now(), $1) RETURNING id', array( + $label_name + )); + $label = pg_fetch_assoc($s_result); + } + $r_post['label_id'] = $label['id']; + $r_post['card_id'] = $row['id']; + $r_post['list_id'] = $row['list_id']; + $r_post['board_id'] = $row['board_id']; + pg_query_params($db_lnk, 'INSERT INTO cards_labels (created, modified, card_id, label_id, board_id, list_id) VALUES (now(), now(), $1, $2, $3, $4) RETURNING *', array( + $r_post['card_id'], + $r_post['label_id'], + $r_post['board_id'], + $r_post['list_id'] + )); + } + $comment = $authUser['username'] . ' added label(s) to this card ##CARD_LINK## - ##LABEL_NAME##'; + insertActivity($authUser['id'], $comment, 'add_card_label', $foreign_ids); + } + $cards_labels = pg_query_params($db_lnk, 'SELECT * FROM cards_labels_listing WHERE card_id = $1', array( + $response['id'] + )); + while ($cards_label = pg_fetch_assoc($cards_labels)) { + $response['cards_labels'][] = $cards_label; + } + if (!empty($clone_card_id)) { + pg_query_params($db_lnk, 'INSERT INTO card_attachments (created, modified, card_id, name, path, mimetype) SELECT created, modified, $1, name, path, mimetype FROM card_attachments WHERE card_id = $2', array( + $response['id'], + $clone_card_id + )); + $s_result = pg_query_params($db_lnk, 'SELECT name, list_id, board_id, position FROM lists WHERE id = $1', array( + $clone_card_id + )); + $previous_value = pg_fetch_assoc($s_result); + $comment = $authUser['username'] . ' copied card "' . $r_post['name'] . '". from "' . $previous_value['name'] . '"'; + $response['activity'] = insertActivity($authUser['id'], $comment, 'copy_card', $foreign_id); + } + } else if ($r_resource_cmd == '/boards/?/copy') { + $new_board_id = $row['id']; + //Copy board users + $boards_user_fields = 'user_id, is_admin'; + $boards_users = pg_query_params($db_lnk, 'SELECT id, ' . $boards_user_fields . ' FROM boards_users WHERE board_id = $1', array( + $r_resource_vars['boards'] + )); + if ($boards_users && pg_num_rows($boards_users)) { + $boards_user_fields = 'created, modified, board_id, ' . $boards_user_fields; + while ($boards_user = pg_fetch_object($boards_users)) { + $boards_user_values = array(); + array_push($boards_user_values, 'now()', 'now()', $new_board_id); + foreach ($boards_user as $key => $value) { + if ($key != 'id') { + if ($value === false) { + array_push($boards_user_values, 'false'); + } else if ($value === NULL) { + array_push($boards_user_values, NULL); + } else { + array_push($boards_user_values, $value); + } + } + } + $boards_user_val = ''; + for ($i = 1, $len = count($boards_user_values); $i <= $len; $i++) { + $boards_user_val.= '$' . $i; + $boards_user_val.= ($i != $len) ? ', ' : ''; + } + $boards_user_result = pg_query_params($db_lnk, 'INSERT INTO boards_users (' . $boards_user_fields . ') VALUES (' . $boards_user_val . ') RETURNING id', $boards_user_values); + } + } + //Copy board subscribers + $boards_subscriber_fields = 'user_id, is_subscribed'; + $boards_subscribers = pg_query_params($db_lnk, 'SELECT id, ' . $boards_subscriber_fields . ' FROM board_subscribers WHERE board_id = $1', array( + $r_resource_vars['boards'] + )); + if ($boards_subscribers && pg_num_rows($boards_subscribers)) { + $boards_subscriber_fields = 'created, modified, board_id, ' . $boards_subscriber_fields; + while ($boards_subscriber = pg_fetch_object($boards_subscribers)) { + $boards_subscriber_values = array(); + array_push($boards_subscriber_values, 'now()', 'now()', $new_board_id); + foreach ($boards_subscriber as $key => $value) { + if ($key != 'id') { + if ($value === false) { + array_push($boards_subscriber_values, 'false'); + } else if ($value === NULL) { + array_push($boards_subscriber_values, NULL); + } else { + array_push($boards_subscriber_values, $value); + } + } + } + $boards_subscriber_val = ''; + for ($i = 1, $len = count($boards_subscriber_values); $i <= $len; $i++) { + $boards_subscriber_val.= '$' . $i; + $boards_subscriber_val.= ($i != $len) ? ', ' : ''; + } + $boards_subscriber_result = pg_query_params($db_lnk, 'INSERT INTO board_subscribers (' . $boards_subscriber_fields . ') VALUES (' . $boards_subscriber_val . ') RETURNING id', $boards_subscriber_values); + } + } + //Copy board star + $boards_star_fields = 'user_id, is_starred'; + $boards_stars = pg_query_params($db_lnk, 'SELECT id, ' . $boards_star_fields . ' FROM board_stars WHERE board_id = $1', array( + $r_resource_vars['boards'] + )); + if ($boards_stars && pg_num_rows($boards_stars)) { + $boards_star_fields = 'created, modified, board_id, ' . $boards_star_fields; + while ($boards_star = pg_fetch_object($boards_stars)) { + $boards_star_values = array(); + array_push($boards_star_values, 'now()', 'now()', $new_board_id); + foreach ($boards_star as $key => $value) { + if ($key != 'id') { + if ($value === false) { + array_push($boards_star_values, 'false'); + } else if ($value === NULL) { + array_push($boards_star_values, NULL); + } else { + array_push($boards_star_values, $value); + } + } + } + $boards_star_val = ''; + for ($i = 1, $len = count($boards_star_values); $i <= $len; $i++) { + $boards_star_val.= '$' . $i; + $boards_star_val.= ($i != $len) ? ', ' : ''; + } + $boards_star_result = pg_query_params($db_lnk, 'INSERT INTO board_stars (' . $boards_star_fields . ') VALUES (' . $boards_star_val . ') RETURNING id', $boards_star_values); + } + } + if ($keepcards) { + $lists = pg_query_params($db_lnk, 'SELECT id, name, position, is_archived, card_count, lists_subscriber_count FROM lists WHERE board_id = $1', array( + $r_resource_vars['boards'] + )); + } else { + $lists = pg_query_params($db_lnk, 'SELECT id, name, position, is_archived, lists_subscriber_count FROM lists WHERE board_id = $1', array( + $r_resource_vars['boards'] + )); + } + if ($lists) { + // Copy lists + while ($list = pg_fetch_object($lists)) { + $list_id = $list->id; + $list_fields = 'created, modified, board_id, user_id'; + $list_values = array(); + array_push($list_values, 'now()', 'now()', $new_board_id, $authUser['id']); + foreach ($list as $key => $value) { + if ($key != 'id') { + $list_fields.= ', ' . $key; + if ($value === false) { + array_push($list_values, 'false'); + } else { + array_push($list_values, $value); + } + } + } + $list_val = ''; + for ($i = 1, $len = count($list_values); $i <= $len; $i++) { + $list_val.= '$' . $i; + $list_val.= ($i != $len) ? ', ' : ''; + } + $lists_result = pg_query_params($db_lnk, 'INSERT INTO lists (' . $list_fields . ') VALUES (' . $list_val . ') RETURNING id', $list_values); + if ($lists_result) { + $list_result = pg_fetch_assoc($lists_result); + $new_list_id = $list_result['id']; + //Copy list subscribers + $lists_subscriber_fields = 'user_id, is_subscribed'; + $lists_subscribers = pg_query_params($db_lnk, 'SELECT id, ' . $lists_subscriber_fields . ' FROM list_subscribers WHERE list_id = $1', array( + $list_id + )); + if ($lists_subscribers && pg_num_rows($lists_subscribers)) { + $lists_subscriber_fields = 'created, modified, list_id, ' . $lists_subscriber_fields; + while ($lists_subscriber = pg_fetch_object($lists_subscribers)) { + $lists_subscriber_values = array(); + array_push($lists_subscriber_values, 'now()', 'now()', $new_list_id); + foreach ($lists_subscriber as $key => $value) { + if ($key != 'id') { + if ($value === false) { + array_push($lists_subscriber_values, 'false'); + } else if ($value === NULL) { + array_push($lists_subscriber_values, NULL); + } else { + array_push($lists_subscriber_values, $value); + } + } + } + $lists_subscriber_val = ''; + for ($i = 1, $len = count($lists_subscriber_values); $i <= $len; $i++) { + $lists_subscriber_val.= '$' . $i; + $lists_subscriber_val.= ($i != $len) ? ', ' : ''; + } + $lists_subscriber_result = pg_query_params($db_lnk, 'INSERT INTO list_subscribers (' . $lists_subscriber_fields . ') VALUES (' . $lists_subscriber_val . ') RETURNING id', $lists_subscriber_values); + } + } + // Copy cards + $card_fields = 'name, description, due_date, position, is_archived, attachment_count, checklist_count, checklist_item_count, checklist_item_completed_count, label_count, cards_user_count, cards_subscriber_count, card_voter_count, activity_count, user_id, comment_count'; + if ($keepcards) { + $cards = pg_query_params($db_lnk, 'SELECT id, ' . $card_fields . ' FROM cards WHERE list_id = $1', array( + $list_id + )); + } + if ($keepcards && pg_num_rows($cards)) { + $card_fields = 'created, modified, board_id, list_id, ' . $card_fields; + while ($card = pg_fetch_object($cards)) { + $card_id = $card->id; + $card_values = array(); + array_push($card_values, 'now()', 'now()', $new_board_id, $new_list_id); + foreach ($card as $key => $value) { + if ($key != 'id') { + if ($value === false) { + array_push($card_values, 'false'); + } else if ($value === NULL) { + array_push($card_values, NULL); + } else { + array_push($card_values, $value); + } + } + } + $card_val = ''; + for ($i = 1, $len = count($card_values); $i <= $len; $i++) { + $card_val.= '$' . $i; + $card_val.= ($i != $len) ? ', ' : ''; + } + $card_result = pg_query_params($db_lnk, 'INSERT INTO cards (' . $card_fields . ') VALUES (' . $card_val . ') RETURNING id', $card_values); + if ($card_result) { + $card_result = pg_fetch_assoc($card_result); + $new_card_id = $card_result['id']; + //Copy card attachments + $attachment_fields = 'name, path, mimetype'; + $attachments = pg_query_params($db_lnk, 'SELECT id, ' . $attachment_fields . ' FROM card_attachments WHERE card_id = $1', array( + $card_id + )); + if ($attachments && pg_num_rows($attachments)) { + $attachment_fields = 'created, modified, board_id, list_id, card_id, ' . $attachment_fields; + while ($attachment = pg_fetch_object($attachments)) { + $attachment_values = array(); + array_push($attachment_values, 'now()', 'now()', $new_board_id, $new_list_id, $new_card_id); + foreach ($attachment as $key => $value) { + if ($key != 'id') { + if ($value === false) { + array_push($attachment_values, 'false'); + } else if ($value === NULL) { + array_push($attachment_values, NULL); + } else { + array_push($attachment_values, $value); + } + } + } + $attachment_val = ''; + for ($i = 1, $len = count($attachment_values); $i <= $len; $i++) { + $attachment_val.= '$' . $i; + $attachment_val.= ($i != $len) ? ', ' : ''; + } + $card_result = pg_query_params($db_lnk, 'INSERT INTO card_attachments (' . $attachment_fields . ') VALUES (' . $attachment_val . ') RETURNING id', $attachment_values); + } + } + //Copy checklists + $checklist_fields = 'user_id, name, checklist_item_count, checklist_item_completed_count, position'; + $checklists = pg_query_params($db_lnk, 'SELECT id, ' . $checklist_fields . ' FROM checklists WHERE card_id = $1', array( + $card_id + )); + if ($checklists && pg_num_rows($checklists)) { + $checklist_fields = 'created, modified, card_id, ' . $checklist_fields; + while ($checklist = pg_fetch_object($checklists)) { + $checklist_values = array(); + array_push($checklist_values, 'now()', 'now()', $new_card_id); + $checklist_id = $checklist->id; + foreach ($checklist as $key => $value) { + if ($key != 'id') { + if ($value === false) { + array_push($checklist_values, 'false'); + } else if ($value === NULL) { + array_push($checklist_values, NULL); + } else { + array_push($checklist_values, $value); + } + } + } + $checklist_val = ''; + for ($i = 1, $len = count($checklist_values); $i <= $len; $i++) { + $checklist_val.= '$' . $i; + $checklist_val.= ($i != $len) ? ', ' : ''; + } + $checklist_result = pg_query_params($db_lnk, 'INSERT INTO checklists (' . $checklist_fields . ') VALUES (' . $checklist_val . ') RETURNING id', $checklist_values); + if ($checklist_result) { + $checklist_result = pg_fetch_assoc($checklist_result); + $new_checklist_id = $checklist_result['id']; + //Copy checklist items + $checklist_item_fields = 'user_id, name, position'; + $checklist_items = pg_query_params($db_lnk, 'SELECT id, ' . $checklist_item_fields . ' FROM checklist_items WHERE checklist_id = $1', array( + $checklist_id + )); + if ($checklist_items && pg_num_rows($checklist_items)) { + $checklist_item_fields = 'created, modified, card_id, checklist_id, ' . $checklist_item_fields; + while ($checklist_item = pg_fetch_object($checklist_items)) { + $checklist_item_values = array(); + array_push($checklist_item_values, 'now()', 'now()', $new_card_id, $new_checklist_id); + foreach ($checklist_item as $key => $value) { + if ($key != 'id') { + if ($value === false) { + array_push($checklist_item_values, 'false'); + } else if ($value === NULL) { + array_push($checklist_item_values, NULL); + } else { + array_push($checklist_item_values, $value); + } + } + } + $checklist_item_val = ''; + for ($i = 1, $len = count($checklist_item_values); $i <= $len; $i++) { + $checklist_item_val.= '$' . $i; + $checklist_item_val.= ($i != $len) ? ', ' : ''; + } + $checklist_item_result = pg_query_params($db_lnk, 'INSERT INTO checklist_items (' . $checklist_item_fields . ') VALUES (' . $checklist_item_val . ') RETURNING id', $checklist_item_values); + } + } + } + } + } + //Copy card voters + $card_voter_fields = 'user_id'; + $card_voters = pg_query_params($db_lnk, 'SELECT id, ' . $card_voter_fields . ' FROM card_voters WHERE card_id = $1', array( + $card_id + )); + if ($card_voters && pg_num_rows($card_voters)) { + $card_voter_fields = 'created, modified, card_id, ' . $card_voter_fields; + while ($card_voter = pg_fetch_object($card_voters)) { + $card_voter_values = array(); + array_push($card_voter_values, 'now()', 'now()', $new_card_id); + foreach ($card_voter as $key => $value) { + if ($key != 'id') { + if ($value === false) { + array_push($card_voter_values, 'false'); + } else if ($value === NULL) { + array_push($card_voter_values, NULL); + } else { + array_push($card_voter_values, $value); + } + } + } + $card_voter_val = ''; + for ($i = 1, $len = count($card_voter_values); $i <= $len; $i++) { + $card_voter_val.= '$' . $i; + $card_voter_val.= ($i != $len) ? ', ' : ''; + } + $card_voter_result = pg_query_params($db_lnk, 'INSERT INTO card_voters (' . $card_voter_fields . ') VALUES (' . $card_voter_val . ') RETURNING id', $card_voter_values); + } + } + //Copy card labels + $cards_label_fields = 'label_id'; + $cards_labels = pg_query_params($db_lnk, 'SELECT id, ' . $cards_label_fields . ' FROM cards_labels WHERE card_id = $1', array( + $card_id + )); + if ($cards_labels && pg_num_rows($cards_labels)) { + $cards_label_fields = 'created, modified, board_id, list_id, card_id, ' . $cards_label_fields; + while ($cards_label = pg_fetch_object($cards_labels)) { + $cards_label_values = array(); + array_push($cards_label_values, 'now()', 'now()', $new_board_id, $new_list_id, $new_card_id); + foreach ($cards_label as $key => $value) { + if ($key != 'id') { + if ($value === false) { + array_push($cards_label_values, 'false'); + } else if ($value === NULL) { + array_push($cards_label_values, NULL); + } else { + array_push($cards_label_values, $value); + } + } + } + $cards_label_val = ''; + for ($i = 1, $len = count($cards_label_values); $i <= $len; $i++) { + $cards_label_val.= '$' . $i; + $cards_label_val.= ($i != $len) ? ', ' : ''; + } + $cards_label_result = pg_query_params($db_lnk, 'INSERT INTO cards_labels (' . $cards_label_fields . ') VALUES (' . $cards_label_val . ') RETURNING id', $cards_label_values); + } + } + //Copy card subscribers + $cards_subscriber_fields = 'user_id, is_subscribed'; + $cards_subscribers = pg_query_params($db_lnk, 'SELECT id, ' . $cards_subscriber_fields . ' FROM card_subscribers WHERE card_id = $1', array( + $card_id + )); + if ($cards_subscribers && pg_num_rows($cards_subscribers)) { + $cards_subscriber_fields = 'created, modified, card_id, ' . $cards_subscriber_fields; + while ($cards_subscriber = pg_fetch_object($cards_subscribers)) { + $cards_subscriber_values = array(); + array_push($cards_subscriber_values, 'now()', 'now()', $new_card_id); + foreach ($cards_subscriber as $key => $value) { + if ($key != 'id') { + if ($value === false) { + array_push($cards_subscriber_values, 'false'); + } else if ($value === NULL) { + array_push($cards_subscriber_values, NULL); + } else { + array_push($cards_subscriber_values, $value); + } + } + } + $cards_subscriber_val = ''; + for ($i = 1, $len = count($cards_subscriber_values); $i <= $len; $i++) { + $cards_subscriber_val.= '$' . $i; + $cards_subscriber_val.= ($i != $len) ? ', ' : ''; + } + $cards_subscriber_result = pg_query_params($db_lnk, 'INSERT INTO card_subscribers (' . $cards_subscriber_fields . ') VALUES (' . $cards_subscriber_val . ') RETURNING id', $cards_subscriber_values); + } + } + //Copy card users + $cards_user_fields = 'user_id'; + $cards_users = pg_query_params($db_lnk, 'SELECT id, ' . $cards_user_fields . ' FROM cards_users WHERE card_id = $1', array( + $card_id + )); + if ($cards_users && pg_num_rows($cards_users)) { + $cards_user_fields = 'created, modified, card_id, ' . $cards_user_fields; + while ($cards_user = pg_fetch_object($cards_users)) { + $cards_user_values = array(); + array_push($cards_user_values, 'now()', 'now()', $new_card_id); + foreach ($cards_user as $key => $value) { + if ($key != 'id') { + if ($value === false) { + array_push($cards_user_values, 'false'); + } else if ($value === NULL) { + array_push($cards_user_values, NULL); + } else { + array_push($cards_user_values, $value); + } + } + } + $cards_user_val = ''; + for ($i = 1, $len = count($cards_user_values); $i <= $len; $i++) { + $cards_user_val.= '$' . $i; + $cards_user_val.= ($i != $len) ? ', ' : ''; + } + $cards_user_result = pg_query_params($db_lnk, 'INSERT INTO cards_users (' . $cards_user_fields . ') VALUES (' . $cards_user_val . ') RETURNING id', $cards_user_values); + } + } + } + } + } + } + } + } + } else if ($r_resource_cmd == '/boards/?/lists/?/cards/?/checklists') { + if (isset($checklist_id) && !empty($checklist_id)) { + pg_query_params($db_lnk, 'INSERT INTO checklist_items (created, modified, user_id, card_id, checklist_id, name, is_completed, position) SELECT created, modified, $1, card_id, $2, name, false, position FROM checklist_items WHERE checklist_id = $3', array( + $r_post['user_id'], + $response['id'], + $checklist_id + )); + } + $result = pg_query_params($db_lnk, 'SELECT * FROM checklists_listing WHERE id = $1', array( + $response['id'] + )); + $response['checklist'] = pg_fetch_assoc($result); + $response['checklist']['checklists_items'] = json_decode($response['checklist']['checklists_items'], true); + $foreign_ids['board_id'] = $r_resource_vars['boards']; + $foreign_ids['list_id'] = $r_resource_vars['lists']; + $foreign_ids['card_id'] = $r_resource_vars['cards']; + $comment = '##USER_NAME## added checklist ##CHECKLIST_NAME## to this card ##CARD_LINK##'; + $response['activity'] = insertActivity($authUser['id'], $comment, 'add_card_checklist', $foreign_ids, '', $response['id']); + } else if ($r_resource_cmd == '/boards/?/lists/?/cards/?/comments') { + $id_converted = base_convert($response['id'], 10, 36); + $materialized_path = sprintf("%08s", $id_converted); + if (!empty($prev_message['materialized_path'])) { + $materialized_path = $prev_message['materialized_path'] . '-' . $materialized_path; + } + if (!empty($prev_message['path'])) { + $path = $prev_message['path'] . '.P' . $response['id']; + $depth = $prev_message['depth'] + 1; + $root = $prev_message['root']; + $response['activities']['depth'] = $depth; + } else { + $path = 'P' . $response['id']; + $depth = 0; + $root = $response['id']; + } + pg_query_params($db_lnk, 'UPDATE activities SET materialized_path = $1, path = $2, depth = $3, root = $4 WHERE id = $5', array( + $materialized_path, + $path, + $depth, + $root, + $response['id'] + )); + pg_query_params($db_lnk, 'UPDATE activities SET freshness_ts = $1 WHERE root = $2', array( + $r_post['freshness_ts'], + $root + )); + $act_res = pg_query_params($db_lnk, 'SELECT * FROM activities WHERE root = $1', array( + $root + )); + $response['activity'] = pg_fetch_assoc($act_res); + } else if ($r_resource_cmd == '/boards/?/lists/?/cards/?/copy') { + if ($is_keep_attachment) { + pg_query_params($db_lnk, 'INSERT INTO card_attachments (created, modified, card_id, name, path, mimetype, list_id, board_id) SELECT created, modified, $1, name, path, mimetype, $2, $3 FROM card_attachments WHERE card_id = $4 ORDER BY id', array( + $response['id'], + $r_post['list_id'], + $r_post['board_id'], + $copied_card_id + )); + } + if ($is_keep_user) { + pg_query_params($db_lnk, 'INSERT INTO cards_users (created, modified, card_id, user_id) SELECT created, modified, $1, user_id FROM cards_users WHERE card_id = $2 ORDER BY id', array( + $response['id'], + $copied_card_id + )); + } + if ($is_keep_label) { + pg_query_params($db_lnk, 'INSERT INTO cards_labels (created, modified, card_id, label_id, list_id, board_id) SELECT created, modified, $1, label_id, $2, $3 FROM cards_labels WHERE card_id = $4 ORDER BY id', array( + $response['id'], + $r_post['list_id'], + $r_post['board_id'], + $copied_card_id + )); + } + if ($is_keep_activity) { + pg_query_params($db_lnk, 'INSERT INTO activities (created, modified, card_id, user_id, list_id, board_id, foreign_id, type, comment, revisions, root, freshness_ts, depth, path, materialized_path) SELECT created, modified, $1, $2, $3, $4, foreign_id, type, comment, revisions, root, freshness_ts, depth, path, materialized_path FROM activities WHERE type = \'add_comment\' AND card_id = $5 ORDER BY id', array( + $response['id'], + $r_post['user_id'], + $r_post['list_id'], + $r_post['board_id'], + $copied_card_id + )); + } + if ($is_keep_checklist) { + pg_query_params($db_lnk, 'INSERT INTO checklists (created, modified, user_id, card_id, name, checklist_item_count, checklist_item_completed_count, position) SELECT created, modified, user_id, $1, name, checklist_item_count, checklist_item_completed_count, position FROM checklists WHERE card_id = $2 ORDER BY id', array( + $response['id'], + $copied_card_id + )); + $checklists = pg_query_params($db_lnk, 'SELECT id FROM checklists WHERE card_id = $1', array( + $response['id'] + )); + $prev_checklists = pg_query_params($db_lnk, 'SELECT id FROM checklists WHERE card_id = $1', array( + $copied_card_id + )); + $prev_checklist_ids = array(); + while ($prev_checklist_id = pg_fetch_assoc($prev_checklists)) { + $prev_checklist_ids[] = $prev_checklist_id['id']; + } + $i = 0; + while ($checklist_id = pg_fetch_assoc($checklists)) { + pg_query_params($db_lnk, 'INSERT INTO checklist_items (created, modified, user_id, card_id, name, checklist_id, is_completed, position) SELECT created, modified, user_id, $1, name , $2, is_completed, position FROM checklist_items WHERE checklist_id = $3 ORDER BY id', array( + $response['id'], + $checklist_id['id'], + $prev_checklist_ids[$i] + )); + $i++; + } + } + $foreign_ids['board_id'] = $r_post['board_id']; + $foreign_ids['list_id'] = $r_post['list_id']; + $foreign_ids['card_id'] = $response['id']; + $comment = $authUser['username'] . ' copied this card "' . $srow['name'] . '" to ##CARD_NAME##'; + $response['activity'] = insertActivity($authUser['id'], $comment, 'copy_card', $foreign_ids, NULL, $response['id']); + $response['cards'] = executeQuery('SELECT * FROM cards_listing WHERE id = $1', array( + $response['id'] + )); + if (!empty($response['cards']['cards_checklists'])) { + $response['cards']['cards_checklists'] = json_decode($response['cards']['cards_checklists'], true); + } + if (!empty($response['cards']['cards_users'])) { + $response['cards']['cards_users'] = json_decode($response['cards']['cards_users'], true); + } + if (!empty($response['cards']['cards_voters'])) { + $response['cards']['cards_voters'] = json_decode($response['cards']['cards_voters'], true); + } + if (!empty($response['cards']['cards_subscribers'])) { + $response['cards']['cards_subscribers'] = json_decode($response['cards']['cards_subscribers'], true); + } + if (!empty($response['cards']['cards_labels'])) { + $response['cards']['cards_labels'] = json_decode($response['cards']['cards_labels'], true); + } + $activities = executeQuery('SELECT ( SELECT array_to_json(array_agg(row_to_json(cl.*))) AS array_to_json FROM ( SELECT activities_listing.* FROM activities_listing activities_listing WHERE (activities_listing.card_id = cards.id) ORDER BY activities_listing.id DESC) cl) AS activities FROM cards cards WHERE id = $1', array( + $response['id'] + )); + if (!empty($activities)) { + $response['cards']['activities'] = json_decode($activities['activities'], true); + } + $attachments = pg_query_params($db_lnk, 'SELECT * FROM card_attachments WHERE card_id = $1', array( + $response['id'] + )); + while ($attachment = pg_fetch_assoc($attachments)) { + $response['cards']['attachments'][] = $attachment; + } + } else if ($r_resource_cmd == '/boards/?/lists/?/cards/?/users/?') { + $sel_query = 'SELECT cu.card_id, cu.user_id, users.username, c.board_id, c.list_id, b.name as board_name FROM cards_users cu LEFT JOIN cards c ON cu.card_id = c.id LEFT JOIN users ON cu.user_id = users.id LEFT JOIN boards b ON c.board_id = b.id WHERE cu.card_id = $1 AND cu.user_id = $2'; + $get_details = pg_query_params($db_lnk, $sel_query, array( + $r_post['card_id'], + $r_post['user_id'] + )); + $sel_details = pg_fetch_assoc($get_details); + $foreign_ids['board_id'] = $sel_details['board_id']; + $foreign_ids['list_id'] = $sel_details['list_id']; + $foreign_ids['card_id'] = $r_post['card_id']; + $user = executeQuery('SELECT * FROM users WHERE id = $1', array( + $r_post['user_id'] + )); + if ($user) { + $emailFindReplace = array( + 'mail' => 'newprojectuser', + '##USERNAME##' => $user['username'], + '##CURRENT_USER##' => $authUser['username'], + '##BOARD_NAME##' => $sel_details['board_name'], + '##BOARD_URL##' => 'http://' . $_SERVER['HTTP_HOST'] . '/#/board/' . $foreign_ids['board_id'] . '/card/' . $foreign_ids['card_id'], + 'to' => $user['email'] + ); + sendMail($emailFindReplace); + } + $comment = $authUser['username'] . ' added "' . $sel_details['username'] . '" as member to this card ##CARD_LINK##'; + $response['activity'] = insertActivity($authUser['id'], $comment, 'add_card_user', $foreign_ids, '', $response['id']); + } else if ($r_resource_cmd == '/boards/?/lists/?/cards/?/attachments') { + $foreign_ids['board_id'] = $r_post['board_id']; + $foreign_ids['list_id'] = $r_post['list_id']; + $foreign_ids['card_id'] = $r_post['card_id']; + $comment = $authUser['username'] . ' added attachment to this card ##CARD_LINK##'; + $response['activity'] = insertActivity($authUser['id'], $comment, 'add_card_attachment', $foreign_ids, NULL, $response['id']); + foreach ($thumbsizes['CardAttachment'] as $key => $value) { + $mediadir = dirname(dirname(dirname(dirname(__FILE__)))) . '/client/img/' . $key . '/CardAttachment/' . $response['id']; + $list = glob($mediadir . '.*'); + @unlink($list[0]); + } + } else if ($r_resource_cmd == '/boards/?/lists/?/cards/?/card_voters') { + $previous_value = executeQuery('SELECT name FROM cards WHERE id = $1', array( + $r_resource_vars['cards'] + )); + $foreign_ids['board_id'] = $r_resource_vars['boards']; + $foreign_ids['list_id'] = $r_resource_vars['lists']; + $foreign_ids['card_id'] = $r_post['card_id']; + $comment = $authUser['username'] . ' voted on ##CARD_LINK##'; + $response['activity'] = insertActivity($authUser['id'], $comment, 'add_card_voter', $foreign_ids, '', $response['id']); + $s_result = pg_query_params($db_lnk, 'SELECT * FROM card_voters_listing WHERE id = $1', array( + $response['id'] + )); + $user = pg_fetch_assoc($s_result); + $response['card_voters'] = $user; + } else if ($r_resource_cmd == '/boards/?/users') { + $s_result = pg_query_params($db_lnk, 'SELECT name FROM boards WHERE id = $1', array( + $r_post['board_id'] + )); + $previous_value = pg_fetch_assoc($s_result); + $foreign_ids['board_id'] = $r_resource_vars['boards']; + $foreign_ids['board_id'] = $r_post['board_id']; + $user = executeQuery('SELECT * FROM users WHERE id = $1', array( + $r_post['user_id'] + )); + if ($user) { + $emailFindReplace = array( + 'mail' => 'newprojectuser', + '##USERNAME##' => $user['username'], + '##CURRENT_USER##' => $authUser['username'], + '##BOARD_NAME##' => $previous_value['name'], + '##BOARD_URL##' => 'http://' . $_SERVER['HTTP_HOST'] . '/#/board/' . $r_post['board_id'], + 'to' => $user['email'] + ); + sendMail($emailFindReplace); + } + $comment = $authUser['username'] . ' added member to board'; + $response['activity'] = insertActivity($authUser['id'], $comment, 'add_board_user', $foreign_ids, '', $response['id']); + } else if ($r_resource_cmd == '/organizations/?/users/?') { + $response['organizations_users'] = executeQuery('SELECT * FROM organizations_users_listing WHERE id = $1', array( + $response['id'] + )); + $response['organizations_users']['boards_users'] = json_decode($response['organizations_users']['boards_users'], true); + } + } + } + // todo: $sql set as true query not execute, so add condition ($sql !== true) + if ($sql && ($sql !== true) && !empty($json) && !empty($response['id'])) { + if ($result = pg_query_params($db_lnk, $sql, array())) { + $data = array(); + $count = pg_num_rows($result); + $i = 0; + while ($row = pg_fetch_row($result)) { + if ($i == 0 && $count > 1) { + echo '['; + } + echo $row[0]; + $i++; + if ($i < $count) { + echo ','; + } else { + if ($count > 1) { + echo ']'; + } + } + } + pg_free_result($result); + } + } else { + echo json_encode($response); + } +} +/** + * Common method to handle PUT method + * + * @param $r_resource_cmd + * @param $r_resource_vars + * @param $r_resource_filters + * @param $r_put + * @return mixed + */ +function r_put($r_resource_cmd, $r_resource_vars, $r_resource_filters, $r_put) +{ + global $r_debug, $db_lnk, $authUser, $thumbsizes, $_server_domain_url; + $fields = 'modified'; + $values = array( + 'now()' + ); + $sfields = ''; + $pg_params = array(); + $emailFindReplace = $response = array(); + $res_status = true; + $sql = $json = false; + $table_name = ''; + $id = ''; + unset($r_put['temp_id']); + switch ($r_resource_cmd) { + case '/users/activation/?': //users activation + $user = executeQuery('SELECT * FROM users WHERE id = $1 AND is_email_confirmed = $2', array( + $r_put['id'], + 'false' + )); + if ($user && (md5($user['username']) == $r_put['hash'])) { + $sql = pg_query_params($db_lnk, "UPDATE users SET is_email_confirmed = $1, is_active = $2 WHERE id = $3", array( + 'true', + 'true', + $r_put['id'] + )); + if ($sql) { + $emailFindReplace = array( + 'mail' => 'welcome', + '##USERNAME##' => $user['username'], + 'to' => $user['email'] + ); + sendMail($emailFindReplace); + $response['success'] = 'Your activation has been confirmed . You can now login to the site'; + } else { + $response['error'] = 'Invalid Activation URL'; + } + } else { + $response['error'] = 'Invalid Activation URL'; + } + break; + + case '/organizations/?': + $json = true; + $table_name = 'organizations'; + $id = $r_resource_vars['organizations']; + if (isset($r_put['logo_url']) && $r_put['logo_url'] == 'NULL') { + foreach ($thumbsizes['Organization'] as $key => $value) { + $mediadir = dirname(dirname(dirname(dirname(__FILE__)))) . '/client/img/' . $key . '/Organization/' . $id; + $list = glob($mediadir . '.*'); + @unlink($list[0]); + } + } + $organization = executeQuery('SELECT id FROM ' . $table_name . ' WHERE id = $1', array( + $r_resource_vars['organizations'] + )); + break; + + case '/organizations_users/?': + $json = true; + $table_name = 'organizations_users'; + $id = $r_resource_vars['organizations_users']; + $organizations_user = executeQuery('SELECT id FROM ' . $table_name . ' WHERE id = $1', array( + $r_resource_vars['organizations_users'] + )); + break; + + case '/boards_users/?': + $json = true; + $table_name = 'boards_users'; + $id = $r_resource_vars['boards_users']; + $boards_users = executeQuery('SELECT id FROM ' . $table_name . ' WHERE id = $1', array( + $r_resource_vars['boards_users'] + )); + break; + + case '/boards/?': + $table_name = 'boards'; + $id = $r_resource_vars['boards']; + $previous_value = executeQuery('SELECT * FROM ' . $table_name . ' WHERE id = $1', array( + $r_resource_vars['boards'] + )); + $board_visibility = array( + 'Private', + 'Organization', + 'Public' + ); + $foreign_ids['board_id'] = $r_resource_vars['boards']; + if (isset($r_put['board_visibility'])) { + $comment = $authUser['username'] . ' changed visibility to ' . $board_visibility[$r_put['board_visibility']]; + $activity_type = 'change_visibility'; + } else if (!empty($r_put['is_closed'])) { + $comment = $authUser['username'] . ' closed ##BOARD_NAME## board.'; + $activity_type = 'reopen_board'; + } else if (isset($r_put['is_closed'])) { + $comment = $authUser['username'] . ' reopened ##BOARD_NAME## board.'; + $activity_type = 'reopen_board'; + } else if (isset($r_put['name'])) { + $comment = $authUser['username'] . ' renamed ##BOARD_NAME## board.'; + $activity_type = 'edit_board'; + } else if (isset($r_put['background_picture_url']) || isset($r_put['background_pattern_url']) || isset($r_put['background_color'])) { + if (empty($previous_value['background_picture_url']) && empty($previous_value['background_pattern_url']) && empty($previous_value['background_color'])) { + $comment = $authUser['username'] . ' added background to board "' . $previous_value['name'] . '"'; + $activity_type = 'add_background'; + } else { + $comment = $authUser['username'] . ' changed backgound to board "' . $previous_value['name'] . '"'; + $activity_type = 'change_background'; + } + } + break; + + case '/boards/?/lists/?': //lists update + $json = true; + $table_name = 'lists'; + $id = $r_resource_vars['lists']; + if (isset($r_put['position']) || isset($r_put['is_archived'])) { + $s_sql = 'SELECT name, board_id, position FROM ' . $table_name . ' WHERE id = $1'; + $s_result = pg_query_params($db_lnk, $s_sql, array( + $r_resource_vars['lists'] + )); + $previous_value = pg_fetch_assoc($s_result); + } + $foreign_ids['board_id'] = $r_resource_vars['boards']; + $foreign_ids['list_id'] = $r_resource_vars['lists']; + if (isset($r_put['board_id']) && !empty($r_put['board_id'])) { + pg_query_params($db_lnk, 'UPDATE cards SET board_id = $1 WHERE list_id = $2', array( + $r_put['board_id'], + $r_resource_vars['lists'] + )); + pg_query_params($db_lnk, 'UPDATE card_attachments SET board_id = $1 WHERE list_id = $2', array( + $r_put['board_id'], + $r_resource_vars['lists'] + )); + } + if (isset($r_put['position'])) { + $comment = $authUser['username'] . ' changed list ' . $previous_value['name'] . ' position.'; + $activity_type = 'change_list_position'; + $start = $end = 0; + if ($previous_value['position'] > $r_put['position']) { + $start = $r_put['position']; + $end = $previous_value['position']; + $postion = ' position + 1'; + } else { + $start = $previous_value['position']; + $end = $r_put['position']; + $postion = ' position - 1'; + } + } else if (isset($previous_value) && isset($r_put['is_archived'])) { + $id = $r_resource_vars['lists']; + $foreign_ids['board_id'] = $r_resource_vars['boards']; + $foreign_ids['list_id'] = $r_resource_vars['lists']; + $comment = $authUser['username'] . ' archived ##LIST_NAME##'; + $activity_type = 'archive_list'; + } else { + $id = $r_resource_vars['lists']; + $comment = $authUser['username'] . ' renamed this list.'; + $activity_type = 'edit_list'; + } + break; + + case '/boards/?/lists/?/cards': //card list_id(move cards all in this list) update + $json = true; + $table_name = 'cards'; + $id = $r_resource_vars['lists']; + $foreign_ids['board_id'] = $r_resource_vars['boards']; + $foreign_ids['list_id'] = $r_resource_vars['lists']; + $old_list = executeQuery('SELECT name FROM lists WHERE id = $1', array( + $foreign_ids['list_id'] + )); + if (!empty($r_put['list_id'])) { + pg_query_params($db_lnk, 'UPDATE card_attachments SET list_id = $1 WHERE list_id = $2', array( + $r_put['list_id'], + $foreign_ids['list_id'] + )); + pg_query_params($db_lnk, 'UPDATE cards_labels SET list_id = $1 WHERE list_id = $2', array( + $r_put['list_id'], + $foreign_ids['list_id'] + )); + $new_list = executeQuery('SELECT name FROM lists WHERE id = $1', array( + $r_put['list_id'] + )); + $comment = $authUser['username'] . ' moved cards FROM ' . $old_list['name'] . ' to ' . $new_list['name']; + $activity_type = 'moved_list_card'; + $revisions['old_value']['list_id'] = $foreign_ids['list_id']; + $revisions['new_value'] = $r_put; + } else if (isset($r_put['is_archived']) && !empty($r_put['is_archived'])) { + $comment = $authUser['username'] . ' archived cards in ' . $old_list['name']; + $activity_type = 'archived_card'; + } else { + $comment = $authUser['username'] . ' edited ' . $old_list['name'] . ' card in this board.'; + $activity_type = 'edit_card'; + } + break; + + case '/boards/?/lists/?/cards/?': //cards update + $table_name = 'cards'; + $id = $r_resource_vars['cards']; + $foreign_ids['board_id'] = $r_resource_vars['boards']; + $foreign_ids['list_id'] = $r_resource_vars['lists']; + $foreign_ids['card_id'] = $r_resource_vars['cards']; + $activity_type = 'edit_card'; + $id = $r_resource_vars['cards']; + $s_result = pg_query_params($db_lnk, 'SELECT name, board_id, list_id, position, description, due_date FROM ' . $table_name . ' WHERE id = $1', array( + $r_resource_vars['cards'] + )); + $previous_value = pg_fetch_assoc($s_result); + if (isset($r_put['position'])) { + $start = $end = 0; + if ($previous_value['position'] > $r_put['position']) { + $start = $r_put['position']; + $end = $previous_value['position']; + $postion = ' position + 1'; + } else { + $start = $previous_value['position']; + $end = $r_put['position']; + $postion = ' position - 1'; + } + if (!empty($r_put['list_id'])) { + $foreign_ids['list_id'] = $r_put['list_id']; + pg_query_params($db_lnk, 'UPDATE card_attachments SET list_id = $1 WHERE list_id = $2', array( + $r_put['list_id'], + $r_resource_vars['lists'] + )); + } + $comment = '##USER_NAME## moved this card to different position.'; + $activity_type = 'change_card_position'; + } + if (isset($previous_value) && isset($r_put['is_archived'])) { + if ($r_put['is_archived']) { + $comment = '##USER_NAME## archived ##CARD_LINK##'; + } else { + $comment = '##USER_NAME## send back ' . $previous_value['name'] . ' to board'; + } + $foreign_ids['board_id'] = $r_resource_vars['boards']; + $foreign_ids['list_id'] = $r_resource_vars['lists']; + } + if (isset($r_put['due_date']) && ($r_put['due_date'] != 'NULL' && $r_put['due_date'] != '')) { + if (isset($previous_value['due_date']) && ($previous_value['due_date'] != 'NULL' && $previous_value['due_date'] != '')) { + $comment = '##USER_NAME## updated due date to this card ##CARD_LINK##'; + $activity_type = 'edit_card_duedate'; + } else { + $comment = '##USER_NAME## SET due date to this card ##CARD_LINK##'; + $activity_type = 'add_card_duedate'; + } + } else if (isset($r_put['due_date']) && ($r_put['due_date'] == 'NULL' || $r_put['due_date'] == '')) { + $comment = '##USER_NAME## deleted due date FROM this card ##CARD_LINK##'; + $activity_type = 'delete_card_duedate'; + } + if (isset($previous_value['board_id']) && isset($r_put['board_id']) && $r_put['board_id'] != $previous_value['board_id']) { + $comment = '##USER_NAME## moved this card to different board.'; + } + if (isset($previous_value['name']) && isset($r_put['name']) && $r_put['name'] != $previous_value['name']) { + $comment = '##USER_NAME## renamed ##CARD_LINK##'; + } + if (!isset($previous_value['description']) && isset($r_put['description'])) { + $comment = '##USER_NAME## added card description in ##CARD_LINK## - ##DESCRIPTION##'; + $activity_type = 'add_card_desc'; + } else if (isset($previous_value) && isset($r_put['description']) && $r_put['description'] != $previous_value['description']) { + if (empty($r_put['description'])) { + $comment = '##USER_NAME## removed description from ##CARD_LINK##'; + } else { + $comment = '##USER_NAME## updated description on ##CARD_LINK## - ##DESCRIPTION##'; + } + $activity_type = 'edit_card_desc'; + } + if (isset($previous_value['list_id']) && isset($r_put['list_id']) && $r_put['list_id'] != $previous_value['list_id']) { + $s_result = pg_query_params($db_lnk, 'SELECT name FROM lists WHERE id = $1', array( + $r_put['list_id'] + )); + $list_value = pg_fetch_assoc($s_result); + $comment = '##USER_NAME## moved this card (' . $previous_value['name'] . ') to different list (' . $list_value['name'] . ').'; + } + unset($r_put['start']); + break; + + case '/boards/?/lists/?/cards/?/comments/?': // comment update + $table_name = 'activities'; + $id = $r_resource_vars['comments']; + $foreign_ids['board_id'] = $r_resource_vars['boards']; + $foreign_ids['list_id'] = $r_resource_vars['lists']; + $foreign_ids['card_id'] = $r_resource_vars['cards']; + $comment = '##USER_NAME## updated comment to this card ##CARD_LINK##'; + $activity_type = 'update_card_comment'; + break; + + case '/boards/?/lists/?/cards/?/checklists/?': + $table_name = 'checklists'; + $id = $r_resource_vars['checklists']; + $foreign_ids['board_id'] = $r_resource_vars['boards']; + $foreign_ids['list_id'] = $r_resource_vars['lists']; + $foreign_ids['card_id'] = $r_resource_vars['cards']; + $comment = '##USER_NAME## updated checklist of card "##CARD_LINK##"'; + unset($r_put['checklists_items']); + unset($r_put['created']); + unset($r_put['modified']); + unset($r_put['checklist_item_completed_count']); + unset($r_put['checklist_item_count']); + unset($r_put['is_offline']); + unset($r_put['list_id']); + unset($r_put['board_id']); + if (isset($r_put['position']) && !empty($r_put['position'])) { + $comment.= ' position'; + } + $activity_type = 'update_card_checklist'; + break; + + case '/boards/?/lists/?/cards/?/checklists/?/items/?': + $table_name = 'checklist_items'; + $id = $r_resource_vars['items']; + $foreign_ids['board_id'] = $r_resource_vars['boards']; + $foreign_ids['list_id'] = $r_resource_vars['lists']; + $foreign_ids['card_id'] = $r_resource_vars['cards']; + unset($r_put['created']); + unset($r_put['modified']); + unset($r_put['is_offline']); + unset($r_put['list_id']); + unset($r_put['board_id']); + $prev_value = executeQuery('SELECT * FROM ' . $table_name . ' WHERE id = $1', array( + $r_resource_vars['items'] + )); + $activity_type = 'update_card_checklist_item'; + if (isset($r_put['is_completed']) && $r_put['is_completed'] == 'true') { + $comment = '##USER_NAME## updated ##CHECKLIST_ITEM_NAME## as completed on card ##CARD_LINK##'; + } else if (isset($r_put['position'])) { + $comment = $authUser['username'] . ' moved checklist item on card ##CARD_LINK##'; + if (isset($r_put['checklist_id']) && $r_put['checklist_id'] != $prev_value['checklist_id']) { + $activity_type = 'moved_card_checklist_item'; + } + } else if (isset($r_put['is_completed']) && $r_put['is_completed'] == 'false') { + $comment = '##USER_NAME## updated ##CHECKLIST_ITEM_NAME## as incomplete on card ##CARD_LINK##'; + } else { + $comment = '##USER_NAME## updated item name as ##CHECKLIST_ITEM_NAME## in card ##CARD_LINK##'; + } + break; + + case '/activities/undo/?': + $activity = executeQuery('SELECT * FROM activities WHERE id = $1', array( + $r_resource_vars['undo'] + )); + if (!empty($activity['revisions']) && trim($activity['revisions']) != '') { + $revisions = unserialize($activity['revisions']); + if ($activity['type'] == 'update_card_checklist_item') { + $table_name = 'checklist_items'; + $id = $activity['foreign_id']; + $r_put = $revisions['old_value']; + $foreign_ids['board_id'] = $activity['board_id']; + $foreign_ids['list_id'] = $activity['list_id']; + $foreign_ids['card_id'] = $activity['card_id']; + $comment = '##USER_NAME## undo this card ##CARD_LINK## checklist item ##CHECKLIST_ITEM_NAME##'; + $activity_type = 'update_card_checklist_item'; + $response['undo']['checklist_item'] = $r_put; + $response['undo']['checklist_item']['id'] = $id; + } else if ($activity['type'] == 'update_card_checklist') { + $table_name = 'checklists'; + $id = $activity['foreign_id']; + $r_put = $revisions['old_value']; + $foreign_ids['board_id'] = $activity['board_id']; + $foreign_ids['list_id'] = $activity['list_id']; + $foreign_ids['card_id'] = $activity['card_id']; + $comment = '##USER_NAME## undo this card ##CARD_LINK## checklist ##CHECKLIST_NAME##'; + $activity_type = 'update_card_checklist'; + $response['undo']['checklist'] = $r_put; + $response['undo']['checklist']['id'] = $id; + } else if (!empty($activity['card_id'])) { + $table_name = 'cards'; + $id = $activity['card_id']; + $r_put = $revisions['old_value']; + $foreign_ids['board_id'] = $activity['board_id']; + $foreign_ids['list_id'] = $activity['list_id']; + $foreign_ids['card_id'] = $activity['card_id']; + $comment = '##USER_NAME## undo this card ##CARD_LINK##'; + $activity_type = 'edit_card'; + $response['undo']['card'] = $r_put; + $response['undo']['card']['id'] = $id; + } else if (!empty($activity['list_id'])) { + $table_name = 'lists'; + $id = $activity['list_id']; + $r_put = $revisions['old_value']; + $foreign_ids['board_id'] = $activity['board_id']; + $foreign_ids['list_id'] = $activity['list_id']; + $comment = '##USER_NAME## undo this list.'; + $activity_type = 'edit_list'; + $response['undo']['list'] = $r_put; + $response['undo']['list']['id'] = $id; + } else if (!empty($activity['board_id'])) { + $table_name = 'boards'; + $id = $activity['board_id']; + $r_put = $revisions['old_value']; + $foreign_ids['board_id'] = $activity['board_id']; + $comment = '##USER_NAME## undo this board.'; + $activity_type = 'edit_board'; + $response['undo']['board'] = $r_put; + $response['undo']['board']['id'] = $id; + } + } + break; + + case '/users/?': //users + $table_name = 'users'; + $id = $r_resource_vars['users']; + break; + + case '/email_templates/?': //email template update + $json = true; + $table_name = 'email_templates'; + $id = $r_resource_vars['email_templates']; + $response['success'] = 'Email Template has been updated successfully.'; + break; + + case '/boards/?/board_subscribers/?': //boards subscribers update + $json = true; + $table_name = 'board_subscribers'; + $id = $r_resource_vars['board_subscribers']; + $response['success'] = 'Updated successfully.'; + $response['id'] = $id; + break; + + case '/boards/?/lists/?/list_subscribers/?': //lists update + $json = true; + $table_name = 'list_subscribers'; + $id = $r_resource_vars['list_subscribers']; + break; + + default: + header($_SERVER['SERVER_PROTOCOL'] . ' 501 Not Implemented', true, 501); + break; + } + if (!empty($table_name) && !empty($id)) { + $put = getbindValues($table_name, $r_put); + if ($table_name == 'users') { + unset($put['ip_id']); + } + foreach ($put as $key => $value) { + if ($key != 'id') { + $fields.= ', ' . $key; + if ($value === false) { + array_push($values, 'false'); + } elseif ($value === 'NULL' || $value === 'NULL') { + array_push($values, NULL); + } else { + array_push($values, $value); + } + } + if ($key != 'id' && $key != 'position') { + $sfields.= (empty($sfields)) ? $key : ", " . $key; + } + } + if (!empty($comment)) { + $revision = ''; + if ($activity_type != 'reopen_board' && $activity_type != 'moved_list_card' && $activity_type != 'moved_card_checklist_item') { + $revisions['old_value'] = executeQuery('SELECT ' . $sfields . ' FROM ' . $table_name . ' WHERE id = $1', array( + $id + )); + unset($r_put['position']); + unset($r_put['id']); + $revisions['new_value'] = $r_put; + $revision = serialize($revisions); + } + $foreign_id = $id; + if ($activity_type == 'moved_list_card') { + $foreign_id = $r_put['list_id']; + } + $response['activity'] = insertActivity($authUser['id'], $comment, $activity_type, $foreign_ids, $revision, $foreign_id); + if (!empty($response['activity']['revisions']) && trim($response['activity']['revisions']) != '') { + $revisions = unserialize($response['activity']['revisions']); + } + if (!empty($revisions) && !empty($revisions['new_value']) && $response['activity']['type'] != 'moved_card_checklist_item') { + foreach ($revisions['new_value'] as $key => $value) { + if ($key != 'is_archived' && $key != 'is_deleted' && $key != 'created' && $key != 'modified' && $key != 'is_offline' && $key != 'uuid' && $key != 'to_date' && $key != 'temp_id' && $activity_type != 'moved_card_checklist_item' && $activity_type != 'add_card_desc' && $activity_type != 'add_card_duedate' && $activity_type != 'delete_card_duedate' && $activity_type != 'add_background' && $activity_type != 'change_background' && $activity_type != 'change_visibility') { + $old_val = (isset($revisions['old_value'][$key])) ? $revisions['old_value'][$key] : ''; + $new_val = (isset($revisions['new_value'][$key])) ? $revisions['new_value'][$key] : ''; + $dif[] = nl2br(getRevisiondifference($old_val, $new_val)); + } + if ($activity_type == 'add_card_desc' || $activity_type == 'edit_card_duedate' || $activity_type == 'add_background' || $activity_type == 'change_background' || $activity_type == 'change_visibility') { + $dif[] = $revisions['new_value'][$key]; + } + } + } + if (isset($dif)) { + $response['activity']['difference'] = $dif; + } + if (isset($r_put['description'])) { + $response['activity']['description'] = $r_put['description']; + } + } + if ($r_resource_cmd == '/users/?') { + $user = executeQuery('SELECT boards_users FROM users_listing WHERE id = $1', array( + $r_resource_vars['users'] + )); + $board_ids = array(); + if (!empty($user['boards_users'])) { + $boards_users = json_decode($user['boards_users'], true); + foreach ($boards_users as $boards_user) { + $board_ids[] = $boards_user['board_id']; + } + } + $board_id = implode(',', $board_ids); + $last_activity_status = executeQuery('SELECT * FROM activities_listing al WHERE board_id IN ( $1 ) ORDER BY id DESC LIMIT 1', array( + $board_id + )); + } + $val = ''; + for ($i = 1, $len = count($values); $i <= $len; $i++) { + $val.= '$' . $i; + $val.= ($i != $len) ? ', ' : ''; + } + array_push($values, $id); + $query = 'UPDATE ' . $table_name . ' SET (' . $fields . ') = (' . $val . ') WHERE id = ' . '$' . $i; + if ($r_resource_cmd == '/boards/?/lists/?/cards') { + $query = 'UPDATE ' . $table_name . ' SET (' . $fields . ') = (' . $val . ') WHERE list_id = ' . '$' . $i; + } + $result = pg_query_params($db_lnk, $query, $values); + } + if (!empty($sql) && !empty($json)) { + if ($table_name == 'organizations') { + $sql = 'SELECT row_to_json(d) FROM (SELECT * FROM organizations_listing ul WHERE id = $1) as d '; + array_push($pg_params, $r_resource_vars['organizations']); + } elseif ($table_name == 'organizations_users') { + $sql = 'SELECT row_to_json(d) FROM (SELECT * FROM organizations_users_listing ul WHERE id = $1) as d '; + array_push($pg_params, $r_resource_vars['organizations_users']); + } elseif ($table_name == 'lists') { + $sql = 'SELECT row_to_json(d) FROM (SELECT * FROM lists_listing WHERE id = $1) as d '; + array_push($pg_params, $r_resource_vars['lists']); + } elseif ($table_name == 'cards' && !empty($r_resource_vars['cards'])) { + $sql = 'SELECT row_to_json(d) FROM (SELECT * FROM cards_listing WHERE id = $1) as d '; + array_push($pg_params, $r_resource_vars['cards']); + } elseif ($table_name == 'cards' && !empty($r_resource_vars['lists'])) { + $sql = 'SELECT row_to_json(d) FROM (SELECT * FROM cards_listing WHERE list_id = $1) as d '; + array_push($pg_params, $r_resource_vars['lists']); + } + if ($result = pg_query_params($db_lnk, $sql, $pg_params)) { + $data = array(); + $count = pg_num_rows($result); + $i = 0; + while ($row = pg_fetch_row($result)) { + if ($i == 0 && $count > 1) { + echo '['; + } + echo $row[0]; + $i++; + if ($i < $count) { + echo ','; + } else { + if ($count > 1) { + echo ']'; + } + } + } + pg_free_result($result); + } + } else { + echo json_encode($response); + } +} +/** + * Common method to handle DELETE method + * + * @param $r_resource_cmd + * @param $r_resource_vars + * @param $r_resource_filters + * @return mixed + */ +function r_delete($r_resource_cmd, $r_resource_vars, $r_resource_filters) +{ + global $r_debug, $db_lnk, $authUser, $_server_domain_url; + $sql = false; + $pg_params = array(); + $response = array(); + switch ($r_resource_cmd) { + case '/organizations/?': //organizations delete + $sql = 'DELETE FROM organizations WHERE id= $1'; + array_push($pg_params, $r_resource_vars['organizations']); + pg_query_params($db_lnk, 'UPDATE boards SET organization_id = $1, board_visibility = $2 WHERE organization_id= $3', array( + 0, + 0, + $r_resource_vars['organizations'] + )); + break; + + case '/organizations_users/?': //organizations delete + $sql = 'DELETE FROM organizations_users WHERE id= $1'; + array_push($pg_params, $r_resource_vars['organizations_users']); + break; + + case '/boards_users/?': //board user delete + $s_result = pg_query_params($db_lnk, 'SELECT username, board_id, board_name FROM boards_users_listing WHERE id = $1', array( + $r_resource_vars['boards_users'] + )); + $previous_value = pg_fetch_assoc($s_result); + $foreign_ids['board_id'] = $previous_value['board_id']; + $comment = $authUser['username'] . ' removed member "' . $previous_value['username'] . '" from board'; + $response['activity'] = insertActivity($authUser['id'], $comment, 'delete_board_user', $foreign_ids, '', $r_resource_vars['boards_users']); + $sql = 'DELETE FROM boards_users WHERE id= $1'; + array_push($pg_params, $r_resource_vars['boards_users']); + break; + + case '/boards/?/lists/?': //lists delete + $s_result = pg_query_params($db_lnk, 'SELECT name, board_id, position FROM lists WHERE id = $1', array( + $r_resource_vars['lists'] + )); + $previous_value = pg_fetch_assoc($s_result); + $foreign_id['board_id'] = $r_resource_vars['boards']; + $foreign_id['list_id'] = $r_resource_vars['lists']; + $comment = $authUser['username'] . ' deleted "' . $previous_value['name'] . '"'; + $response['activity'] = insertActivity($authUser['id'], $comment, 'delete_list', $foreign_id); + $sql = 'DELETE FROM lists WHERE id= $1'; + array_push($pg_params, $r_resource_vars['lists']); + break; + + case '/boards/?/lists/?/cards/?/card_voters/?': + $sql = 'DELETE FROM card_voters WHERE id = $1'; + array_push($pg_params, $r_resource_vars['card_voters']); + $previous_value = executeQuery('SELECT name FROM cards WHERE id = $1', array( + $r_resource_vars['cards'] + )); + $foreign_ids['board_id'] = $r_resource_vars['boards']; + $foreign_ids['list_id'] = $r_resource_vars['lists']; + $foreign_ids['card_id'] = $r_resource_vars['cards']; + $comment = $authUser['username'] . ' unvoted this card ##CARD_LINK##'; + $response['activity'] = insertActivity($authUser['id'], $comment, 'unvote_card', $foreign_ids, NULL, $r_resource_vars['card_voters']); + break; + + case '/boards/?/lists/?/cards/?/comments/?': // comment DELETE + $sql = 'DELETE FROM activities WHERE id = $1'; + array_push($pg_params, $r_resource_vars['comments']); + $previous_value = executeQuery('SELECT name FROM cards WHERE id = $1', array( + $r_resource_vars['cards'] + )); + $foreign_ids['board_id'] = $r_resource_vars['boards']; + $foreign_ids['list_id'] = $r_resource_vars['lists']; + $foreign_ids['card_id'] = $r_resource_vars['cards']; + $comment = $authUser['username'] . ' deleted comment in card ##CARD_LINK##'; + $response['activity'] = insertActivity($authUser['id'], $comment, 'delete_card_comment', $foreign_ids, NULL, $r_resource_vars['comments']); + break; + + case '/boards/?/lists/?/cards/?': + $s_result = pg_query_params($db_lnk, 'SELECT name, board_id, position FROM cards WHERE id = $1', array( + $r_resource_vars['cards'] + )); + $previous_value = pg_fetch_assoc($s_result); + $foreign_id['board_id'] = $r_resource_vars['boards']; + $foreign_id['list_id'] = $r_resource_vars['lists']; + $foreign_id['card_id'] = $r_resource_vars['cards']; + $comment = $authUser['username'] . ' deleted card ' . $previous_value['name']; + $response['activity'] = insertActivity($authUser['id'], $comment, 'delete_card', $foreign_id); + $sql = 'DELETE FROM cards WHERE id = $1'; + array_push($pg_params, $r_resource_vars['cards']); + break; + + case '/boards/?/lists/?/cards/?/attachments/?': //card view + $sql = 'DELETE FROM card_attachments WHERE id = $1'; + array_push($pg_params, $r_resource_vars['attachments']); + $foreign_ids['board_id'] = $r_resource_vars['boards']; + $foreign_ids['list_id'] = $r_resource_vars['lists']; + $foreign_ids['card_id'] = $r_resource_vars['cards']; + $comment = $authUser['username'] . ' deleted attachment from card ##CARD_LINK##'; + $response['activity'] = insertActivity($authUser['id'], $comment, 'delete_card_attachment', $foreign_ids, NULL, $r_resource_vars['attachments']); + break; + + case '/boards/?/lists/?/cards/?/checklists/?': + pg_query_params($db_lnk, 'DELETE FROM checklist_items WHERE checklist_id = $1', array( + $r_resource_vars['checklists'] + )); + $foreign_ids['board_id'] = $r_resource_vars['boards']; + $foreign_ids['list_id'] = $r_resource_vars['lists']; + $foreign_ids['card_id'] = $r_resource_vars['cards']; + $comment = $authUser['username'] . ' deleted checklist from card ##CARD_LINK##'; + $response['activity'] = insertActivity($authUser['id'], $comment, 'delete_checklist', $foreign_ids, NULL, $r_resource_vars['checklists']); + $sql = 'DELETE FROM checklists WHERE id = $1'; + array_push($pg_params, $r_resource_vars['checklists']); + break; + + case '/boards/?/lists/?/cards/?/checklists/?/items/?': + $foreign_ids['board_id'] = $r_resource_vars['boards']; + $foreign_ids['list_id'] = $r_resource_vars['lists']; + $foreign_ids['card_id'] = $r_resource_vars['cards']; + $comment = $authUser['username'] . ' deleted checklist item from card ##CARD_LINK##'; + $response['activity'] = insertActivity($authUser['id'], $comment, 'delete_checklist_item', $foreign_ids, NULL, $r_resource_vars['items']); + $sql = 'DELETE FROM checklist_items WHERE id = $1'; + array_push($pg_params, $r_resource_vars['items']); + break; + + case '/boards/?/lists/?/cards/?/cards_users/?': + $foreign_ids['board_id'] = $r_resource_vars['boards']; + $foreign_ids['list_id'] = $r_resource_vars['lists']; + $foreign_ids['card_id'] = $r_resource_vars['cards']; + $comment = $authUser['username'] . ' deleted member from card ##CARD_LINK##'; + $response['activity'] = insertActivity($authUser['id'], $comment, 'delete_card_users', $foreign_ids, NULL, $r_resource_vars['cards_users']); + $sql = 'DELETE FROM cards_users WHERE id = $1'; + array_push($pg_params, $r_resource_vars['cards_users']); + break; + + case '/users/?': //users delete + $sql = 'DELETE FROM users WHERE id= $1'; + array_push($pg_params, $r_resource_vars['users']); + break; + + case '/boards/?/lists/?/cards/?': + $foreign_id['board_id'] = $r_resource_vars['boards']; + $foreign_id['list_id'] = $r_resource_vars['lists']; + $foreign_id['card_id'] = $r_resource_vars['cards']; + $comment = $authUser['username'] . ' deleted card ##CARD_NAME##'; + $response['activity'] = insertActivity($authUser['id'], $comment, 'delete_card', $foreign_id); + $sql = 'UPDATE cards SET is_deleted = true WHERE id= $1'; + array_push($pg_params, $r_resource_vars['cards']); + break; + + default: + header($_SERVER['SERVER_PROTOCOL'] . ' 501 Not Implemented', true, 501); + break; + } + if (!empty($sql)) { + $result = pg_query_params($db_lnk, $sql, $pg_params); + $response['error'] = array( + 'code' => (!$result) ? 1 : 0 + ); + } + echo json_encode($response); +} +$exception_url = array( + '/users/forgotpassword', + '/users/register', + '/users/login', + '/users/activation/?', + '/settings', + '/boards/?' +); +if (!empty($_GET['_url']) && $db_lnk) { + $r_debug.= __LINE__ . ': ' . $_GET['_url'] . "\n"; + $url = '/' . $_GET['_url']; + $url = str_replace('/v' . R_API_VERSION, '', $url); + // routes... + // Samples: 1. /products.json + // 2. /products.json?page=1&key1=val1 + // 3. /users/5/products/10.json + // 4. /products/10.json + $_url_parts_with_querystring = explode('?', $url); + $_url_parts_with_ext = explode('.', $_url_parts_with_querystring[0]); + $r_resource_type = @$_url_parts_with_ext[1]; // 'json' + $r_resource_filters = $_GET; + unset($r_resource_filters['_url']); // page=1&key1=val1 + // /users/5/products/10 -> /users/?/products/? ... + $r_resource_cmd = preg_replace('/\/\d+/', '/?', $_url_parts_with_ext[0]); + header('Content-Type: application/json'); + if ($r_resource_cmd != '/users/login') { + if (!empty($_GET['token'])) { + // Generate full URL using PHP_SELF server variable + $post_url = $_server_domain_url . str_replace('r.php', 'resource.php', $_SERVER['PHP_SELF']); + $response = doPost($post_url, array( + 'access_token' => $_GET['token'] + )); + if (!empty($response['error'])) { + $response['error']['type'] = 'OAuth'; + echo json_encode($response); + exit; + } + $notify_count = $user = $role_links = array(); + if (!empty($response['username'])) { + $user = executeQuery('SELECT * FROM users WHERE username = $1', array( + $response['username'] + )); + $notify_count = executeQuery('SELECT count(a.*) AS notify_count FROM activities a LEFT JOIN boards_users bu ON bu.board_id = a.board_id WHERE bu.user_id = $1', array( + $user['id'] + )); + $role_links = executeQuery('SELECT * FROM role_links_listing WHERE id = $1', array( + $user['role_id'] + )); + } + $authUser = array_merge($notify_count, $role_links, $user); + } else if (!empty($_GET['refresh_token'])) { + // Generate full URL using PHP_SELF server variable + $post_url = $_server_domain_url . str_replace('r.php', 'token.php', $_SERVER['PHP_SELF']); + $response = doPost($post_url, array( + 'grant_type' => 'refresh_token', + 'refresh_token' => $_GET['refresh_token'], + 'client_id' => OAUTH_CLIENTID, + 'client_secret' => OAUTH_CLIENT_SECRET + )); + echo json_encode($response); + exit; + } else if ($r_resource_cmd != '/settings') { + // Generate full URL using PHP_SELF server variable + $post_url = $_server_domain_url . str_replace('r.php', 'token.php', $_SERVER['PHP_SELF']); + $response = doPost($post_url, array( + 'grant_type' => 'client_credentials', + 'client_id' => OAUTH_CLIENTID, + 'client_secret' => OAUTH_CLIENT_SECRET + )); + $role_links = executeQuery('SELECT * FROM role_links_listing WHERE id = $1', array( + 3 + )); + $response = array_merge($response, $role_links); + echo json_encode($response); + exit; + } + } + if ($r_resource_cmd == '/users/logout' || checkAclLinks($_SERVER['REQUEST_METHOD'], $r_resource_cmd)) { + // /users/5/products/10 -> array('users' => 5, 'products' => 10) ... + $r_resource_vars = array(); + if (preg_match_all('/([^\/]+)\/(\d+)/', $_url_parts_with_ext[0], $matches)) { + for ($i = 0, $len = count($matches[0]); $i < $len; ++$i) { + $r_resource_vars[$matches[1][$i]] = $matches[2][$i]; + } + } + if ($r_resource_type == 'json') { + $is_valid_req = false; + // Server... + switch ($_SERVER['REQUEST_METHOD']) { + case 'GET': + r_get($r_resource_cmd, $r_resource_vars, $r_resource_filters); + $is_valid_req = true; + break; + + case 'POST': + if ((!empty($authUser)) || (in_array($r_resource_cmd, $exception_url) && empty($authUser))) { + $r_post = json_decode(file_get_contents('php://input')); + $r_post = (array)$r_post; + r_post($r_resource_cmd, $r_resource_vars, $r_resource_filters, $r_post); + $is_valid_req = true; + } + break; + + case 'PUT': + if ((!empty($authUser)) || (in_array($r_resource_cmd, $exception_url) && empty($authUser))) { + $r_put = json_decode(file_get_contents('php://input')); + $r_put = (array)$r_put; + r_put($r_resource_cmd, $r_resource_vars, $r_resource_filters, $r_put); + $is_valid_req = true; + } + break; + + case 'DELETE': + if ((!empty($authUser)) || (in_array($r_resource_cmd, $exception_url) && empty($authUser))) { + r_delete($r_resource_cmd, $r_resource_vars, $r_resource_filters); + $is_valid_req = true; + } + break; + + default: + header($_SERVER['SERVER_PROTOCOL'] . ' 501 Not Implemented', true, 501); + break; + } + } + } +} else { + header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found', true, 404); +} +if (R_DEBUG) { + @header('X-RDebug: ' . $r_debug); +} diff --git a/server/php/R/resource.php b/server/php/R/resource.php new file mode 100644 index 000000000..735293fdb --- /dev/null +++ b/server/php/R/resource.php @@ -0,0 +1,24 @@ + + * @copyright 2014 Restya + * @license http://www.restya.com/ Restya Licence + * @link http://www.restya.com + */ +require_once ('server.php'); +if (!$server->verifyResourceRequest(OAuth2\Request::createFromGlobals())) { + $server->getResponse()->send(); + die; +} +$token = $server->getAccessTokenData(OAuth2\Request::createFromGlobals()); +echo json_encode(array( + 'success' => true, + 'username' => $token['user_id'] +)); diff --git a/server/php/R/server.php b/server/php/R/server.php new file mode 100644 index 000000000..93c27ca69 --- /dev/null +++ b/server/php/R/server.php @@ -0,0 +1,49 @@ + + * @copyright 2014 Restya + * @license http://www.restya.com/ Restya Licence + * @link http://www.restya.com + */ +require_once ('config.inc.php'); +require_once ('libs/vendors/OAuth2/Autoloader.php'); +OAuth2\Autoloader::register(); +$oauth_config = array( + 'user_table' => 'users' +); +$storage = new OAuth2\Storage\Pdo(array( + 'dsn' => 'pgsql:host=' . R_DB_HOST . ';dbname=' . R_DB_NAME . ';port=' . R_DB_PORT, + 'username' => R_DB_USER, + 'password' => R_DB_PASSWORD +) , $oauth_config); +$server = new OAuth2\Server($storage); +if (isset($_POST['grant_type']) && $_POST['grant_type'] == 'password') { + $users = array( + $_POST['username'] => array( + 'password' => $_POST['password'] + ) + ); + $storage = new OAuth2\Storage\Memory(array( + 'user_credentials' => $users + )); + $server->addGrantType(new OAuth2\GrantType\UserCredentials($storage)); +} elseif (isset($_POST['grant_type']) && $_POST['grant_type'] == 'refresh_token') { + $server->addGrantType(new OAuth2\GrantType\RefreshToken($storage)); +} else { + $clients = array( + OAUTH_CLIENTID => array( + 'client_secret' => OAUTH_CLIENT_SECRET + ) + ); + $storage = new OAuth2\Storage\Memory(array( + 'client_credentials' => $clients + )); + $server->addGrantType(new OAuth2\GrantType\ClientCredentials($storage)); +} diff --git a/server/php/R/shell/cron.php b/server/php/R/shell/cron.php new file mode 100644 index 000000000..8056931b7 --- /dev/null +++ b/server/php/R/shell/cron.php @@ -0,0 +1,163 @@ + + * @copyright 2014 Restya + * @license http://www.restya.com/ Restya Licence + * @link http://www.restya.com + */ +if (!empty($argv[0])) { + $app_path = str_replace('shell/cron.php', '', $argv[0]); + require_once $app_path . 'config.inc.php'; +} else { + require_once '../config.inc.php'; +} +if ($db_lnk) { + $result = pg_query_params($db_lnk, 'SELECT value FROM settings WHERE name = $1', array( + 'elasticsearch.last_processed_activtiy_id' + )); + $row = pg_fetch_assoc($result); + if (!empty($row)) { + $result = pg_query_params($db_lnk, "SELECT * FROM activities WHERE (type = 'add_card' OR type = 'add_list' OR type = 'add_board' OR type = 'edit_card' OR type = 'edit_list' OR type = 'edit_board') AND id > $1", array( + $row['value'] + )); + $count = pg_num_rows($result); + if ($count > 0) { + $params = array(); + $ch = curl_init(); + while ($row = pg_fetch_assoc($result)) { + $revisions = unserialize($row['revisions']); + $id = ''; + $type = ''; + $doc_fileds = array(); + if (!empty($row['card_id'])) { + $id = $row['card_id']; + $user_result = pg_query_params($db_lnk, 'SELECT username FROM users WHERE id = $1', array( + $row['user_id'] + )); + $user_row = pg_fetch_assoc($user_result); + $doc_fileds['user_name'] = $user_row['username']; + $list_result = pg_query_params($db_lnk, 'SELECT name FROM lists WHERE id = $1', array( + $row['list_id'] + )); + $list_row = pg_fetch_assoc($list_result); + $doc_fileds['list_name'] = $list_row['name']; + $doc_fileds['list_id'] = $row['list_id']; + $board_result = pg_query_params($db_lnk, 'SELECT name FROM boards WHERE id = $1', array( + $row['board_id'] + )); + $board_row = pg_fetch_assoc($board_result); + $doc_fileds['board_name'] = $board_row['name']; + $doc_fileds['board_id'] = $row['board_id']; + $card_result = pg_query_params($db_lnk, 'SELECT name, description, due_date FROM cards WHERE id = $1', array( + $row['card_id'] + )); + $card_row = pg_fetch_assoc($card_result); + $doc_fileds['card_name'] = $card_row['name']; + $doc_fileds['card_description'] = $card_row['description']; + $doc_fileds['due_date'] = $card_row['due_date']; + if (!empty($revisions)) { + foreach ($revisions['new_value'] as $key => $value) { + if ($key == 'name') { + $doc_fileds['card_name'] = $value; + } else { + $doc_fileds[$key] = $value; + } + } + } + $type = 'cards'; + } else if (!empty($row['list_id'])) { + $id = $row['list_id']; + $user_result = pg_query_params($db_lnk, 'SELECT username FROM users WHERE id = $1', array( + $row['user_id'] + )); + $user_row = pg_fetch_assoc($user_result); + $doc_fileds['user_name'] = $user_row['username']; + $board_result = pg_query_params($db_lnk, 'SELECT name FROM boards WHERE id = $1', array( + $row['board_id'] + )); + $board_row = pg_fetch_assoc($board_result); + $doc_fileds['board_name'] = $board_row['name']; + $doc_fileds['board_id'] = $row['board_id']; + $list_result = pg_query_params($db_lnk, 'SELECT name FROM lists WHERE id = $1', array( + $row['list_id'] + )); + $list_row = pg_fetch_assoc($list_result); + $doc_fileds['list_name'] = $list_row['name']; + if (!empty($revisions)) { + foreach ($revisions['new_value'] as $key => $value) { + if ($key == 'name') { + $doc_fileds['list_name'] = $value; + } else { + $doc_fileds[$key] = $value; + } + } + } + $type = 'lists'; + } else if (!empty($row['board_id'])) { + $id = $row['board_id']; + $user_result = pg_query_params($db_lnk, 'SELECT username FROM users WHERE id = $1', array( + $row['user_id'] + )); + $user_row = pg_fetch_assoc($user_result); + $doc_fileds['user_name'] = $user_row['username']; + if (!empty($revisions)) { + foreach ($revisions['new_value'] as $key => $value) { + if ($key == 'name') { + $doc_fileds['board_name'] = $value; + } else { + $doc_fileds[$key] = $value; + } + } + } + $type = 'boards'; + } + $url = ELASTICSEARCH_URL . ELASTICSEARCH_INDEX . '/' . $type . '/' . $id; + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 300); // 300 seconds (5min) + unset($doc_fileds['activity']); + $post_string = json_encode($doc_fileds); + curl_setopt($ch, CURLOPT_HTTPHEADER, array( + 'Content-Type: application/json', + 'Content-Length: ' . strlen($post_string) + )); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $post_string); + curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if (curl_errno($ch)) { + echo 'Error:: ' . curl_error($ch) . PHP_EOL; + } + switch ($http_code) { + case 201: + case 200: + echo 'Saved Successfully.' . PHP_EOL; + break; + + case 401: + echo 'Unauthorized.' . PHP_EOL; + break; + + default: + echo 'Not Found.' . PHP_EOL; + } + pg_query_params($db_lnk, 'UPDATE settings SET value = $1 WHERE name = $2', array( + $row['id'], + 'elasticsearch.last_processed_activtiy_id' + )); + } + curl_close($ch); + } + } +} diff --git a/server/php/R/token.php b/server/php/R/token.php new file mode 100644 index 000000000..d6f62ccbf --- /dev/null +++ b/server/php/R/token.php @@ -0,0 +1,16 @@ + + * @copyright 2014 Restya + * @license http://www.restya.com/ Restya Licence + * @link http://www.restya.com + */ +require_once ('server.php'); +$server->handleTokenRequest(OAuth2\Request::createFromGlobals())->send(); diff --git a/server/py/Bottle/.gitignore b/server/py/Bottle/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/sql/restyaboard_with_empty_data.sql b/sql/restyaboard_with_empty_data.sql new file mode 100644 index 000000000..f8d429570 --- /dev/null +++ b/sql/restyaboard_with_empty_data.sql @@ -0,0 +1,5956 @@ +-- +-- PostgreSQL database dump +-- + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SET check_function_bodies = false; +SET client_min_messages = warning; + +-- +-- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: +-- + +CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; + + +-- +-- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner: +-- + +COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language'; + + +SET search_path = public, pg_catalog; + +-- +-- Name: label_card_count_update(); Type: FUNCTION; Schema: public; Owner: postgres +-- + +CREATE FUNCTION label_card_count_update() RETURNS trigger + LANGUAGE plpgsql + AS $$ + +BEGIN + IF (TG_OP = 'DELETE') THEN + UPDATE "labels" SET "card_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "cards_labels" WHERE "label_id" = OLD."label_id") t WHERE "id" = OLD."label_id"; + UPDATE "cards" SET "label_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "cards_labels" WHERE "label_id" = OLD."card_id") t WHERE "id" = OLD."card_id"; + RETURN OLD; + ELSIF (TG_OP = 'UPDATE') THEN + UPDATE "labels" SET "card_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "cards_labels" WHERE "label_id" = OLD."label_id") t WHERE "id" = OLD."label_id"; + UPDATE "labels" SET "card_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "cards_labels" WHERE "label_id" = NEW."label_id") t WHERE "id" = NEW."label_id"; + UPDATE "cards" SET "label_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "cards_labels" WHERE "label_id" = OLD."card_id") t WHERE "id" = OLD."card_id"; + UPDATE "cards" SET "label_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "cards_labels" WHERE "label_id" = NEW."label_id") t WHERE "id" = NEW."card_id"; + RETURN OLD; + ELSIF (TG_OP = 'INSERT') THEN + UPDATE "labels" SET "card_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "cards_labels" WHERE "label_id" = NEW."label_id") t WHERE "id" = NEW."label_id"; + UPDATE "cards" SET "label_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "cards_labels" WHERE "label_id" = NEW."label_id") t WHERE "id" = NEW."card_id"; + RETURN NEW; + END IF; +END; + +$$; + + +ALTER FUNCTION public.label_card_count_update() OWNER TO postgres; + +-- +-- Name: update_board_count(); Type: FUNCTION; Schema: public; Owner: postgres +-- + +CREATE FUNCTION update_board_count() RETURNS trigger + LANGUAGE plpgsql + AS $$ + +BEGIN + IF (TG_OP = 'DELETE') THEN + UPDATE "organizations" SET "board_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "boards" WHERE "organization_id" = OLD."organization_id") t WHERE "id" = OLD."organization_id"; + UPDATE "users" SET "created_board_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "boards" WHERE "user_id" = OLD."user_id") t WHERE "id" = OLD."user_id"; + RETURN OLD; + ELSIF (TG_OP = 'UPDATE') THEN + UPDATE "organizations" SET "board_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "boards" WHERE "organization_id" = OLD."organization_id") t WHERE "id" = OLD."organization_id"; + UPDATE "organizations" SET "board_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "boards" WHERE "organization_id" = NEW."organization_id") t WHERE "id" = NEW."organization_id"; + UPDATE "users" SET "created_board_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "boards" WHERE "user_id" = OLD."user_id") t WHERE "id" = OLD."user_id"; + UPDATE "users" SET "created_board_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "boards" WHERE "user_id" = NEW."user_id") t WHERE "id" = NEW."user_id"; + RETURN OLD; + ELSIF (TG_OP = 'INSERT') THEN + UPDATE "organizations" SET "board_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "boards" WHERE "organization_id" = NEW."organization_id") t WHERE "id" = NEW."organization_id"; + UPDATE "users" SET "created_board_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "boards" WHERE "user_id" = NEW."user_id") t WHERE "id" = NEW."user_id"; + RETURN NEW; + END IF; +END; + +$$; + + +ALTER FUNCTION public.update_board_count() OWNER TO postgres; + +-- +-- Name: update_board_star_count(); Type: FUNCTION; Schema: public; Owner: postgres +-- + +CREATE FUNCTION update_board_star_count() RETURNS trigger + LANGUAGE plpgsql + AS $$ + +BEGIN + IF (TG_OP = 'DELETE') THEN + UPDATE "boards" SET "boards_star_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "board_stars" WHERE "board_id" = OLD."board_id") t WHERE "id" = OLD."board_id"; + RETURN OLD; + ELSIF (TG_OP = 'UPDATE') THEN + UPDATE "boards" SET "boards_star_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "board_stars" WHERE "board_id" = OLD."board_id") t WHERE "id" = OLD."board_id"; + UPDATE "boards" SET "boards_star_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "board_stars" WHERE "board_id" = NEW."board_id") t WHERE "id" = NEW."board_id"; + RETURN OLD; + ELSIF (TG_OP = 'INSERT') THEN + UPDATE "boards" SET "boards_star_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "board_stars" WHERE "board_id" = NEW."board_id") t WHERE "id" = NEW."board_id"; + RETURN NEW; + END IF; +END; +$$; + + +ALTER FUNCTION public.update_board_star_count() OWNER TO postgres; + +-- +-- Name: update_board_subscriber_count(); Type: FUNCTION; Schema: public; Owner: postgres +-- + +CREATE FUNCTION update_board_subscriber_count() RETURNS trigger + LANGUAGE plpgsql + AS $$ + +BEGIN + IF (TG_OP = 'DELETE') THEN + UPDATE "boards" SET "boards_subscriber_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "board_subscribers" WHERE "board_id" = OLD."board_id") t WHERE "id" = OLD."board_id"; + RETURN OLD; + ELSIF (TG_OP = 'UPDATE') THEN + UPDATE "boards" SET "boards_subscriber_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "board_subscribers" WHERE "board_id" = OLD."board_id") t WHERE "id" = OLD."board_id"; + UPDATE "boards" SET "boards_subscriber_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "board_subscribers" WHERE "board_id" = NEW."board_id") t WHERE "id" = NEW."board_id"; + RETURN OLD; + ELSIF (TG_OP = 'INSERT') THEN + UPDATE "boards" SET "boards_subscriber_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "board_subscribers" WHERE "board_id" = NEW."board_id") t WHERE "id" = NEW."board_id"; + RETURN NEW; + END IF; +END; +$$; + + +ALTER FUNCTION public.update_board_subscriber_count() OWNER TO postgres; + +-- +-- Name: update_board_user_count(); Type: FUNCTION; Schema: public; Owner: postgres +-- + +CREATE FUNCTION update_board_user_count() RETURNS trigger + LANGUAGE plpgsql + AS $$ + +BEGIN + IF (TG_OP = 'DELETE') THEN + UPDATE "boards" SET "boards_user_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "boards_users" WHERE "board_id" = OLD."board_id") t WHERE "id" = OLD."board_id"; + UPDATE "users" SET "joined_board_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "boards_users" WHERE "user_id" = OLD."user_id") t WHERE "id" = OLD."user_id"; + RETURN OLD; + ELSIF (TG_OP = 'UPDATE') THEN + UPDATE "boards" SET "boards_user_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "boards_users" WHERE "board_id" = OLD."board_id") t WHERE "id" = OLD."board_id"; + UPDATE "boards" SET "boards_user_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "boards_users" WHERE "board_id" = NEW."board_id") t WHERE "id" = NEW."board_id"; + UPDATE "users" SET "joined_board_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "boards_users" WHERE "user_id" = OLD."user_id") t WHERE "id" = OLD."user_id"; + UPDATE "users" SET "joined_board_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "boards_users" WHERE "user_id" = NEW."user_id") t WHERE "id" = NEW."user_id"; + RETURN OLD; + ELSIF (TG_OP = 'INSERT') THEN + UPDATE "boards" SET "boards_user_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "boards_users" WHERE "board_id" = NEW."board_id") t WHERE "id" = NEW."board_id"; + UPDATE "users" SET "joined_board_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "boards_users" WHERE "user_id" = NEW."user_id") t WHERE "id" = NEW."user_id"; + RETURN NEW; + END IF; +END; + +$$; + + +ALTER FUNCTION public.update_board_user_count() OWNER TO postgres; + +-- +-- Name: update_card_activity_count(); Type: FUNCTION; Schema: public; Owner: postgres +-- + +CREATE FUNCTION update_card_activity_count() RETURNS trigger + LANGUAGE plpgsql + AS $$ + +BEGIN + IF (TG_OP = 'DELETE') THEN + UPDATE "cards" SET "activity_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "activities" WHERE "card_id" = OLD."card_id") t WHERE "id" = OLD."card_id"; + UPDATE "users" SET "activity_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "activities" WHERE "user_id" = OLD."user_id") t WHERE "id" = OLD."user_id"; + RETURN OLD; + ELSIF (TG_OP = 'UPDATE') THEN + UPDATE "cards" SET "activity_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "activities" WHERE "card_id" = OLD."card_id") t WHERE "id" = OLD."card_id"; + UPDATE "cards" SET "activity_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "activities" WHERE "card_id" = NEW."card_id") t WHERE "id" = NEW."card_id"; + UPDATE "users" SET "activity_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "activities" WHERE "user_id" = OLD."user_id") t WHERE "id" = OLD."user_id"; + UPDATE "users" SET "activity_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "activities" WHERE "user_id" = NEW."user_id") t WHERE "id" = NEW."user_id"; + RETURN OLD; + ELSIF (TG_OP = 'INSERT') THEN + UPDATE "cards" SET "activity_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "activities" WHERE "card_id" = NEW."card_id") t WHERE "id" = NEW."card_id"; + UPDATE "users" SET "activity_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "activities" WHERE "user_id" = NEW."user_id") t WHERE "id" = NEW."user_id"; + RETURN NEW; + END IF; +END; + +$$; + + +ALTER FUNCTION public.update_card_activity_count() OWNER TO postgres; + +-- +-- Name: update_card_attachment_count(); Type: FUNCTION; Schema: public; Owner: postgres +-- + +CREATE FUNCTION update_card_attachment_count() RETURNS trigger + LANGUAGE plpgsql + AS $$ + +BEGIN + IF (TG_OP = 'DELETE') THEN + UPDATE "cards" SET "attachment_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "card_attachments" WHERE "card_id" = OLD."card_id") t WHERE "id" = OLD."card_id"; + RETURN OLD; + ELSIF (TG_OP = 'UPDATE') THEN + UPDATE "cards" SET "attachment_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "card_attachments" WHERE "card_id" = OLD."card_id") t WHERE "id" = OLD."card_id"; + UPDATE "cards" SET "attachment_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "card_attachments" WHERE "card_id" = NEW."card_id") t WHERE "id" = NEW."card_id"; + RETURN OLD; + ELSIF (TG_OP = 'INSERT') THEN + UPDATE "cards" SET "attachment_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "card_attachments" WHERE "card_id" = NEW."card_id") t WHERE "id" = NEW."card_id"; + RETURN NEW; + END IF; +END; + +$$; + + +ALTER FUNCTION public.update_card_attachment_count() OWNER TO postgres; + +-- +-- Name: update_card_checklist_count(); Type: FUNCTION; Schema: public; Owner: postgres +-- + +CREATE FUNCTION update_card_checklist_count() RETURNS trigger + LANGUAGE plpgsql + AS $$ + +BEGIN + IF (TG_OP = 'DELETE') THEN + UPDATE "cards" SET "checklist_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklists" WHERE "card_id" = OLD."card_id") t WHERE "id" = OLD."card_id"; + UPDATE "users" SET "checklist_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklists" WHERE "user_id" = OLD."user_id") t WHERE "id" = OLD."user_id"; + RETURN OLD; + ELSIF (TG_OP = 'UPDATE') THEN + UPDATE "cards" SET "checklist_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklists" WHERE "card_id" = OLD."card_id") t WHERE "id" = OLD."card_id"; + UPDATE "cards" SET "checklist_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklists" WHERE "card_id" = NEW."card_id") t WHERE "id" = NEW."card_id"; + UPDATE "users" SET "checklist_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklists" WHERE "user_id" = OLD."user_id") t WHERE "id" = OLD."user_id"; + UPDATE "users" SET "checklist_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklists" WHERE "user_id" = NEW."user_id") t WHERE "id" = NEW."user_id"; + RETURN OLD; + ELSIF (TG_OP = 'INSERT') THEN + UPDATE "cards" SET "checklist_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklists" WHERE "card_id" = NEW."card_id") t WHERE "id" = NEW."card_id"; + UPDATE "users" SET "checklist_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklists" WHERE "user_id" = NEW."user_id") t WHERE "id" = NEW."user_id"; + RETURN NEW; + END IF; +END; + +$$; + + +ALTER FUNCTION public.update_card_checklist_count() OWNER TO postgres; + +-- +-- Name: update_card_checklist_item_count(); Type: FUNCTION; Schema: public; Owner: postgres +-- + +CREATE FUNCTION update_card_checklist_item_count() RETURNS trigger + LANGUAGE plpgsql + AS $$ + +BEGIN + IF (TG_OP = 'DELETE') THEN + UPDATE "cards" SET "checklist_item_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklist_items" WHERE "card_id" = OLD."card_id") t WHERE "id" = OLD."card_id"; + UPDATE "cards" SET "checklist_item_completed_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklist_items" WHERE "card_id" = OLD."card_id" AND "is_completed" = TRUE) t WHERE "id" = OLD."card_id"; + UPDATE "checklists" SET "checklist_item_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklist_items" WHERE "checklist_id" = OLD."checklist_id") t WHERE "id" = OLD."checklist_id"; + UPDATE "checklists" SET "checklist_item_completed_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklist_items" WHERE "checklist_id" = OLD."checklist_id" AND "is_completed" = TRUE) t WHERE "id" = OLD."checklist_id"; + UPDATE "users" SET "checklist_item_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklist_items" WHERE "user_id" = OLD."user_id") t WHERE "id" = OLD."user_id"; + UPDATE "users" SET "checklist_item_completed_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklist_items" WHERE "user_id" = OLD."user_id" AND "is_completed" = TRUE) t WHERE "id" = OLD."user_id"; + RETURN OLD; + ELSIF (TG_OP = 'UPDATE') THEN + UPDATE "cards" SET "checklist_item_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklist_items" WHERE "card_id" = OLD."card_id") t WHERE "id" = OLD."card_id"; + UPDATE "cards" SET "checklist_item_completed_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklist_items" WHERE "card_id" = OLD."card_id" AND "is_completed" = TRUE) t WHERE "id" = OLD."card_id"; + UPDATE "cards" SET "checklist_item_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklist_items" WHERE "card_id" = NEW."card_id") t WHERE "id" = NEW."card_id"; + UPDATE "cards" SET "checklist_item_completed_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklist_items" WHERE "card_id" = NEW."card_id" AND "is_completed" = TRUE) t WHERE "id" = NEW."card_id"; + UPDATE "checklists" SET "checklist_item_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklist_items" WHERE "checklist_id" = OLD."checklist_id") t WHERE "id" = OLD."checklist_id"; + UPDATE "checklists" SET "checklist_item_completed_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklist_items" WHERE "checklist_id" = OLD."checklist_id" AND "is_completed" = TRUE) t WHERE "id" = OLD."checklist_id"; + UPDATE "checklists" SET "checklist_item_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklist_items" WHERE "checklist_id" = NEW."checklist_id") t WHERE "id" = NEW."checklist_id"; + UPDATE "checklists" SET "checklist_item_completed_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklist_items" WHERE "checklist_id" = NEW."checklist_id" AND "is_completed" = TRUE) t WHERE "id" = NEW."checklist_id"; + UPDATE "users" SET "checklist_item_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklist_items" WHERE "user_id" = OLD."user_id") t WHERE "id" = OLD."user_id"; + UPDATE "users" SET "checklist_item_completed_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklist_items" WHERE "user_id" = OLD."user_id" AND "is_completed" = TRUE) t WHERE "id" = OLD."user_id"; + UPDATE "users" SET "checklist_item_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklist_items" WHERE "user_id" = NEW."user_id") t WHERE "id" = NEW."user_id"; + UPDATE "users" SET "checklist_item_completed_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklist_items" WHERE "user_id" = NEW."user_id" AND "is_completed" = TRUE) t WHERE "id" = NEW."user_id"; + RETURN NEW; + ELSIF (TG_OP = 'INSERT') THEN + UPDATE "cards" SET "checklist_item_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklist_items" WHERE "card_id" = NEW."card_id") t WHERE "id" = NEW."card_id"; + UPDATE "cards" SET "checklist_item_completed_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklist_items" WHERE "card_id" = NEW."card_id" AND "is_completed" = TRUE) t WHERE "id" = NEW."card_id"; + UPDATE "checklists" SET "checklist_item_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklist_items" WHERE "checklist_id" = NEW."checklist_id") t WHERE "id" = NEW."checklist_id"; + UPDATE "checklists" SET "checklist_item_completed_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklist_items" WHERE "checklist_id" = NEW."checklist_id" AND "is_completed" = TRUE) t WHERE "id" = NEW."checklist_id"; + UPDATE "users" SET "checklist_item_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklist_items" WHERE "user_id" = NEW."user_id") t WHERE "id" = NEW."user_id"; + UPDATE "users" SET "checklist_item_completed_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "checklist_items" WHERE "user_id" = NEW."user_id" AND "is_completed" = TRUE) t WHERE "id" = NEW."user_id"; + RETURN NEW; + END IF; +END; + +$$; + + +ALTER FUNCTION public.update_card_checklist_item_count() OWNER TO postgres; + +-- +-- Name: update_card_count(); Type: FUNCTION; Schema: public; Owner: postgres +-- + +CREATE FUNCTION update_card_count() RETURNS trigger + LANGUAGE plpgsql + AS $$ + +BEGIN + IF (TG_OP = 'DELETE') THEN + UPDATE "lists" SET "card_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "cards" WHERE "list_id" = OLD."list_id" AND "is_archived" = false) t WHERE "id" = OLD."list_id"; + UPDATE "boards" SET "card_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "cards" WHERE "board_id" = OLD."board_id") t WHERE "id" = OLD."board_id"; + UPDATE "users" SET "created_card_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "cards" WHERE "user_id" = OLD."user_id") t WHERE "id" = OLD."user_id"; + RETURN OLD; + ELSIF (TG_OP = 'UPDATE') THEN + UPDATE "lists" SET "card_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "cards" WHERE "list_id" = OLD."list_id" AND "is_archived" = false) t WHERE "id" = OLD."list_id"; + UPDATE "lists" SET "card_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "cards" WHERE "list_id" = NEW."list_id" AND "is_archived" = false) t WHERE "id" = NEW."list_id"; + UPDATE "boards" SET "card_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "cards" WHERE "board_id" = OLD."board_id") t WHERE "id" = OLD."board_id"; + UPDATE "boards" SET "card_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "cards" WHERE "board_id" = NEW."board_id") t WHERE "id" = NEW."board_id"; + UPDATE "users" SET "created_card_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "cards" WHERE "user_id" = OLD."user_id") t WHERE "id" = OLD."user_id"; + UPDATE "users" SET "created_card_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "cards" WHERE "user_id" = NEW."user_id") t WHERE "id" = NEW."user_id"; + RETURN OLD; + ELSIF (TG_OP = 'INSERT') THEN + UPDATE "lists" SET "card_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "cards" WHERE "list_id" = NEW."list_id" AND "is_archived" = false) t WHERE "id" = NEW."list_id"; + UPDATE "boards" SET "card_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "cards" WHERE "board_id" = NEW."board_id") t WHERE "id" = NEW."list_id"; + UPDATE "users" SET "created_card_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "cards" WHERE "user_id" = NEW."user_id") t WHERE "id" = NEW."user_id"; + RETURN NEW; + END IF; +END; + +$$; + + +ALTER FUNCTION public.update_card_count() OWNER TO postgres; + +-- +-- Name: update_card_subscriber_count(); Type: FUNCTION; Schema: public; Owner: postgres +-- + +CREATE FUNCTION update_card_subscriber_count() RETURNS trigger + LANGUAGE plpgsql + AS $$ + +BEGIN + IF (TG_OP = 'DELETE') THEN + UPDATE "cards" SET "cards_subscriber_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "card_subscribers" WHERE "card_id" = OLD."card_id") t WHERE "id" = OLD."card_id"; + RETURN OLD; + ELSIF (TG_OP = 'UPDATE') THEN + UPDATE "cards" SET "cards_subscriber_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "card_subscribers" WHERE "card_id" = OLD."card_id") t WHERE "id" = OLD."card_id"; + UPDATE "cards" SET "cards_subscriber_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "card_subscribers" WHERE "card_id" = NEW."card_id") t WHERE "id" = NEW."card_id"; + RETURN OLD; + ELSIF (TG_OP = 'INSERT') THEN + UPDATE "cards" SET "cards_subscriber_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "card_subscribers" WHERE "card_id" = NEW."card_id") t WHERE "id" = NEW."card_id"; + RETURN NEW; + END IF; +END; +$$; + + +ALTER FUNCTION public.update_card_subscriber_count() OWNER TO postgres; + +-- +-- Name: update_card_user_count(); Type: FUNCTION; Schema: public; Owner: postgres +-- + +CREATE FUNCTION update_card_user_count() RETURNS trigger + LANGUAGE plpgsql + AS $$ + +BEGIN + IF (TG_OP = 'DELETE') THEN + UPDATE "cards" SET "cards_user_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "cards_users" WHERE "card_id" = OLD."card_id") t WHERE "id" = OLD."card_id"; + UPDATE "users" SET "joined_card_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "cards_users" WHERE "user_id" = OLD."user_id") t WHERE "id" = OLD."user_id"; + RETURN OLD; + ELSIF (TG_OP = 'UPDATE') THEN + UPDATE "cards" SET "cards_user_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "cards_users" WHERE "card_id" = OLD."card_id") t WHERE "id" = OLD."card_id"; + UPDATE "cards" SET "cards_user_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "cards_users" WHERE "card_id" = NEW."card_id") t WHERE "id" = NEW."card_id"; + UPDATE "users" SET "joined_card_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "cards_users" WHERE "user_id" = OLD."user_id") t WHERE "id" = OLD."user_id"; + UPDATE "users" SET "joined_card_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "cards_users" WHERE "user_id" = NEW."user_id") t WHERE "id" = NEW."user_id"; + RETURN OLD; + ELSIF (TG_OP = 'INSERT') THEN + UPDATE "cards" SET "cards_user_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "cards_users" WHERE "card_id" = NEW."card_id") t WHERE "id" = NEW."card_id"; + UPDATE "users" SET "joined_card_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "cards_users" WHERE "user_id" = NEW."user_id") t WHERE "id" = NEW."user_id"; + RETURN NEW; + END IF; +END; + +$$; + + +ALTER FUNCTION public.update_card_user_count() OWNER TO postgres; + +-- +-- Name: update_card_voters_count(); Type: FUNCTION; Schema: public; Owner: postgres +-- + +CREATE FUNCTION update_card_voters_count() RETURNS trigger + LANGUAGE plpgsql + AS $$ + +BEGIN + IF (TG_OP = 'DELETE') THEN + UPDATE "cards" SET "card_voter_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "card_voters" WHERE "card_id" = OLD."card_id") t WHERE "id" = OLD."card_id"; + UPDATE "users" SET "card_voter_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "card_voters" WHERE "user_id" = OLD."user_id") t WHERE "id" = OLD."user_id"; + RETURN OLD; + ELSIF (TG_OP = 'UPDATE') THEN + UPDATE "cards" SET "card_voter_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "card_voters" WHERE "card_id" = OLD."card_id") t WHERE "id" = OLD."card_id"; + UPDATE "cards" SET "card_voter_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "card_voters" WHERE "card_id" = NEW."card_id") t WHERE "id" = NEW."card_id"; + UPDATE "users" SET "card_voter_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "card_voters" WHERE "user_id" = OLD."user_id") t WHERE "id" = OLD."user_id"; + UPDATE "users" SET "card_voter_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "card_voters" WHERE "user_id" = NEW."user_id") t WHERE "id" = NEW."user_id"; + RETURN OLD; + ELSIF (TG_OP = 'INSERT') THEN + UPDATE "cards" SET "card_voter_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "card_voters" WHERE "card_id" = NEW."card_id") t WHERE "id" = NEW."card_id"; + UPDATE "users" SET "card_voter_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "card_voters" WHERE "user_id" = NEW."user_id") t WHERE "id" = NEW."user_id"; + RETURN NEW; + END IF; +END; + +$$; + + +ALTER FUNCTION public.update_card_voters_count() OWNER TO postgres; + +-- +-- Name: update_comment_count(); Type: FUNCTION; Schema: public; Owner: postgres +-- + +CREATE FUNCTION update_comment_count() RETURNS trigger + LANGUAGE plpgsql + AS $$ + +BEGIN + IF (TG_OP = 'DELETE') THEN + UPDATE "cards" SET "comment_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "activities" WHERE "type" = 'add_comment' AND "card_id" = OLD."card_id") t WHERE "id" = OLD."card_id"; + RETURN OLD; + ELSIF (TG_OP = 'UPDATE') THEN + UPDATE "cards" SET "comment_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "activities" WHERE "type" = 'add_comment' AND "card_id" = OLD."card_id") t WHERE "id" = OLD."card_id"; + UPDATE "cards" SET "comment_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "activities" WHERE "type" = 'add_comment' AND "card_id" = NEW."card_id") t WHERE "id" = NEW."card_id"; + RETURN OLD; + ELSIF (TG_OP = 'INSERT') THEN + UPDATE "cards" SET "comment_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "activities" WHERE "type" = 'add_comment' AND "card_id" = NEW."card_id") t WHERE "id" = NEW."card_id"; + RETURN NEW; + END IF; +END; + +$$; + + +ALTER FUNCTION public.update_comment_count() OWNER TO postgres; + +-- +-- Name: update_list_count(); Type: FUNCTION; Schema: public; Owner: postgres +-- + +CREATE FUNCTION update_list_count() RETURNS trigger + LANGUAGE plpgsql + AS $$ + +BEGIN + + IF (TG_OP = 'DELETE') THEN + UPDATE "boards" SET "list_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "lists" WHERE "board_id" = OLD."board_id") t WHERE "id" = OLD."board_id"; + UPDATE "users" SET "list_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "lists" WHERE "user_id" = OLD."user_id") t WHERE "id" = OLD."user_id"; + RETURN OLD; + ELSIF (TG_OP = 'UPDATE') THEN + UPDATE "boards" SET "list_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "lists" WHERE "board_id" = OLD."board_id") t WHERE "id" = OLD."board_id"; + UPDATE "boards" SET "list_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "lists" WHERE "board_id" = NEW."board_id") t WHERE "id" = NEW."board_id"; + UPDATE "users" SET "list_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "lists" WHERE "user_id" = OLD."user_id") t WHERE "id" = OLD."user_id"; + UPDATE "users" SET "list_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "lists" WHERE "user_id" = NEW."user_id") t WHERE "id" = NEW."user_id"; + RETURN OLD; + ELSIF (TG_OP = 'INSERT') THEN + UPDATE "boards" SET "list_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "lists" WHERE "board_id" = NEW."board_id") t WHERE "id" = NEW."board_id"; + UPDATE "users" SET "list_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "lists" WHERE "user_id" = NEW."user_id") t WHERE "id" = NEW."user_id"; + RETURN NEW; + END IF; +END; + +$$; + + +ALTER FUNCTION public.update_list_count() OWNER TO postgres; + +-- +-- Name: update_list_subscriber_count(); Type: FUNCTION; Schema: public; Owner: postgres +-- + +CREATE FUNCTION update_list_subscriber_count() RETURNS trigger + LANGUAGE plpgsql + AS $$ + +BEGIN + IF (TG_OP = 'DELETE') THEN + UPDATE "lists" SET "lists_subscriber_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "list_subscribers" WHERE "list_id" = OLD."list_id") t WHERE "id" = OLD."list_id"; + RETURN OLD; + ELSIF (TG_OP = 'UPDATE') THEN + UPDATE "lists" SET "lists_subscriber_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "list_subscribers" WHERE "list_id" = OLD."list_id") t WHERE "id" = OLD."list_id"; + UPDATE "lists" SET "lists_subscriber_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "list_subscribers" WHERE "list_id" = NEW."list_id") t WHERE "id" = NEW."list_id"; + RETURN OLD; + ELSIF (TG_OP = 'INSERT') THEN + UPDATE "lists" SET "lists_subscriber_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "list_subscribers" WHERE "list_id" = NEW."list_id") t WHERE "id" = NEW."list_id"; + RETURN NEW; + END IF; +END; +$$; + + +ALTER FUNCTION public.update_list_subscriber_count() OWNER TO postgres; + +-- +-- Name: update_organization_count(); Type: FUNCTION; Schema: public; Owner: postgres +-- + +CREATE FUNCTION update_organization_count() RETURNS trigger + LANGUAGE plpgsql + AS $$ + +BEGIN + IF (TG_OP = 'DELETE') THEN + UPDATE "users" SET "created_organization_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "organizations" WHERE "user_id" = OLD."user_id") t WHERE "id" = OLD."user_id"; + RETURN OLD; + ELSIF (TG_OP = 'UPDATE') THEN + UPDATE "users" SET "created_organization_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "organizations" WHERE "user_id" = OLD."user_id") t WHERE "id" = OLD."user_id"; + UPDATE "users" SET "created_organization_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "organizations" WHERE "user_id" = NEW."user_id") t WHERE "id" = NEW."user_id"; + RETURN OLD; + ELSIF (TG_OP = 'INSERT') THEN + UPDATE "users" SET "created_organization_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "organizations" WHERE "user_id" = NEW."user_id") t WHERE "id" = NEW."user_id"; + RETURN NEW; + END IF; +END; + +$$; + + +ALTER FUNCTION public.update_organization_count() OWNER TO postgres; + +-- +-- Name: update_organization_user_count(); Type: FUNCTION; Schema: public; Owner: postgres +-- + +CREATE FUNCTION update_organization_user_count() RETURNS trigger + LANGUAGE plpgsql + AS $$ + +BEGIN + IF (TG_OP = 'DELETE') THEN + UPDATE "organizations" SET "organizations_user_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "organizations_users" WHERE "organization_id" = OLD."organization_id") t WHERE "id" = OLD."organization_id"; + UPDATE "users" SET "joined_organization_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "organizations_users" WHERE "user_id" = OLD."user_id") t WHERE "id" = OLD."user_id"; + RETURN OLD; + ELSIF (TG_OP = 'UPDATE') THEN + UPDATE "organizations" SET "organizations_user_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "organizations_users" WHERE "organization_id" = OLD."organization_id") t WHERE "id" = OLD."organization_id"; + UPDATE "organizations" SET "organizations_user_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "organizations_users" WHERE "organization_id" = NEW."organization_id") t WHERE "id" = NEW."organization_id"; + UPDATE "users" SET "joined_organization_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "organizations_users" WHERE "user_id" = OLD."user_id") t WHERE "id" = OLD."user_id"; + UPDATE "users" SET "joined_organization_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "organizations_users" WHERE "user_id" = NEW."user_id") t WHERE "id" = NEW."user_id"; + RETURN OLD; + ELSIF (TG_OP = 'INSERT') THEN + UPDATE "organizations" SET "organizations_user_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "organizations_users" WHERE "organization_id" = NEW."organization_id") t WHERE "id" = NEW."organization_id"; + UPDATE "users" SET "joined_organization_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "organizations_users" WHERE "user_id" = NEW."user_id") t WHERE "id" = NEW."user_id"; + RETURN NEW; + END IF; +END; + +$$; + + +ALTER FUNCTION public.update_organization_user_count() OWNER TO postgres; + +-- +-- Name: update_user_delete(); Type: FUNCTION; Schema: public; Owner: postgres +-- + +CREATE FUNCTION update_user_delete() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + IF (TG_OP = 'DELETE') THEN + DELETE FROM "organizations_users" WHERE "user_id" = OLD."id"; + DELETE FROM "boards_users" WHERE "user_id" = OLD."id"; + DELETE FROM "cards_users" WHERE "user_id" = OLD."id"; + DELETE FROM "card_voters" WHERE "user_id" = OLD."id"; + RETURN OLD; + END IF; +END; +$$; + + +ALTER FUNCTION public.update_user_delete() OWNER TO postgres; + +-- +-- Name: update_users_user_login_count(); Type: FUNCTION; Schema: public; Owner: postgres +-- + +CREATE FUNCTION update_users_user_login_count() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + IF (TG_OP = 'DELETE') THEN + UPDATE "users" SET "user_login_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "user_logins" WHERE "user_id" = OLD."user_id") t WHERE "id" = OLD."user_id"; + RETURN OLD; + ELSIF (TG_OP = 'UPDATE') THEN + UPDATE "users" SET "user_login_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "user_logins" WHERE "user_id" = OLD."user_id") t WHERE "id" = OLD."user_id"; + RETURN OLD; + RETURN NEW; + ELSIF (TG_OP = 'INSERT') THEN + UPDATE "users" SET "user_login_count" = total_count FROM (SELECT COUNT(*) as total_count FROM "user_logins" WHERE "user_id" = NEW."user_id") t WHERE "id" = NEW."user_id"; + RETURN NEW; + END IF; +END; +$$; + + +ALTER FUNCTION public.update_users_user_login_count() OWNER TO postgres; + +-- +-- Name: acl_links_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE acl_links_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE acl_links_id_seq OWNER TO postgres; + +SET default_tablespace = ''; + +SET default_with_oids = false; + +-- +-- Name: acl_links; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE acl_links ( + id bigint DEFAULT nextval('acl_links_id_seq'::regclass) NOT NULL, + created timestamp without time zone NOT NULL, + modified timestamp without time zone NOT NULL, + name character varying(255) NOT NULL, + url character varying(255) NOT NULL, + method character varying(255) NOT NULL, + slug character varying(255) NOT NULL, + group_id smallint, + is_allow_only_to_admin smallint DEFAULT (0)::smallint NOT NULL, + is_allow_only_to_user smallint DEFAULT (0)::smallint NOT NULL +); + + +ALTER TABLE acl_links OWNER TO postgres; + +-- +-- Name: acl_links_roles_roles_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE acl_links_roles_roles_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE acl_links_roles_roles_id_seq OWNER TO postgres; + +-- +-- Name: acl_links_roles; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE acl_links_roles ( + id bigint DEFAULT nextval('acl_links_roles_roles_id_seq'::regclass) NOT NULL, + created timestamp without time zone NOT NULL, + modified timestamp without time zone NOT NULL, + acl_link_id bigint NOT NULL, + role_id bigint NOT NULL +); + + +ALTER TABLE acl_links_roles OWNER TO postgres; + +-- +-- Name: acl_links_listing; Type: VIEW; Schema: public; Owner: postgres +-- + +CREATE VIEW acl_links_listing AS + SELECT aclr.role_id, + acl.slug, + acl.url, + acl.method + FROM (acl_links_roles aclr + JOIN acl_links acl ON ((acl.id = aclr.acl_link_id))); + + +ALTER TABLE acl_links_listing OWNER TO postgres; + +-- +-- Name: activities_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE activities_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE activities_id_seq OWNER TO postgres; + +-- +-- Name: activities; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE activities ( + id bigint DEFAULT nextval('activities_id_seq'::regclass) NOT NULL, + created timestamp without time zone NOT NULL, + modified timestamp without time zone NOT NULL, + board_id bigint DEFAULT 0, + list_id bigint DEFAULT 0, + card_id bigint DEFAULT 0, + user_id bigint NOT NULL, + foreign_id bigint, + type character varying(255) NOT NULL, + comment text NOT NULL, + revisions text, + root bigint DEFAULT 0, + freshness_ts timestamp without time zone, + depth integer DEFAULT 0, + path text, + materialized_path character varying(255) DEFAULT NULL::character varying +); + + +ALTER TABLE activities OWNER TO postgres; + +-- +-- Name: boards_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE boards_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE boards_id_seq OWNER TO postgres; + +-- +-- Name: boards; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE boards ( + id bigint DEFAULT nextval('boards_id_seq'::regclass) NOT NULL, + created timestamp without time zone NOT NULL, + modified timestamp without time zone NOT NULL, + user_id bigint NOT NULL, + organization_id bigint DEFAULT 0, + name text NOT NULL, + board_visibility smallint, + background_color character varying(255), + background_picture_url text, + commenting_permissions smallint, + voting_permissions smallint, + inivitation_permissions smallint, + is_closed boolean DEFAULT false NOT NULL, + is_allow_organization_members_to_join boolean DEFAULT false NOT NULL, + boards_user_count bigint DEFAULT 0, + list_count bigint DEFAULT 0, + card_count bigint DEFAULT 0, + boards_subscriber_count bigint DEFAULT 0, + background_pattern_url character varying(255), + boards_star_count bigint DEFAULT 0, + is_show_image_front_of_card boolean DEFAULT true, + background_picture_path character varying(255), + music_name character varying(255), + music_content text +); + + +ALTER TABLE boards OWNER TO postgres; + +-- +-- Name: cards_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE cards_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE cards_id_seq OWNER TO postgres; + +-- +-- Name: cards; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE cards ( + id bigint DEFAULT nextval('cards_id_seq'::regclass) NOT NULL, + created timestamp without time zone NOT NULL, + modified timestamp without time zone NOT NULL, + board_id bigint NOT NULL, + list_id bigint NOT NULL, + name text NOT NULL, + description text, + due_date timestamp without time zone, + "position" double precision NOT NULL, + is_archived boolean DEFAULT false NOT NULL, + attachment_count bigint DEFAULT 0, + checklist_count bigint DEFAULT 0, + checklist_item_count bigint DEFAULT 0, + checklist_item_completed_count bigint DEFAULT 0, + label_count bigint DEFAULT 0, + cards_user_count bigint DEFAULT 0, + cards_subscriber_count bigint DEFAULT 0, + card_voter_count bigint DEFAULT 0, + activity_count bigint DEFAULT 0, + user_id bigint NOT NULL, + is_deleted boolean DEFAULT false NOT NULL, + comment_count bigint DEFAULT (0)::bigint +); + + +ALTER TABLE cards OWNER TO postgres; + +-- +-- Name: cards_labels_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE cards_labels_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE cards_labels_id_seq OWNER TO postgres; + +-- +-- Name: cards_labels; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE cards_labels ( + id bigint DEFAULT nextval('cards_labels_id_seq'::regclass) NOT NULL, + created timestamp without time zone NOT NULL, + modified timestamp without time zone NOT NULL, + label_id bigint NOT NULL, + card_id bigint NOT NULL, + list_id bigint, + board_id bigint +); + + +ALTER TABLE cards_labels OWNER TO postgres; + +-- +-- Name: labels_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE labels_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE labels_id_seq OWNER TO postgres; + +-- +-- Name: labels; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE labels ( + id bigint DEFAULT nextval('labels_id_seq'::regclass) NOT NULL, + created timestamp without time zone NOT NULL, + modified timestamp without time zone NOT NULL, + name character varying(255) NOT NULL, + card_count bigint DEFAULT 0 NOT NULL +); + + +ALTER TABLE labels OWNER TO postgres; + +-- +-- Name: cards_labels_listing; Type: VIEW; Schema: public; Owner: postgres +-- + +CREATE VIEW cards_labels_listing AS + SELECT cl.id, + cl.created, + cl.modified, + cl.label_id, + cl.card_id, + c.name AS card_name, + c.list_id, + l.name, + cl.board_id + FROM ((cards_labels cl + LEFT JOIN cards c ON ((c.id = cl.card_id))) + LEFT JOIN labels l ON ((l.id = cl.label_id))); + + +ALTER TABLE cards_labels_listing OWNER TO postgres; + +-- +-- Name: checklist_items_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE checklist_items_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE checklist_items_id_seq OWNER TO postgres; + +-- +-- Name: checklist_items; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE checklist_items ( + id bigint DEFAULT nextval('checklist_items_id_seq'::regclass) NOT NULL, + created timestamp without time zone NOT NULL, + modified timestamp without time zone NOT NULL, + user_id bigint NOT NULL, + card_id bigint NOT NULL, + checklist_id bigint NOT NULL, + name text NOT NULL, + is_completed boolean DEFAULT false NOT NULL, + "position" double precision NOT NULL +); + + +ALTER TABLE checklist_items OWNER TO postgres; + +-- +-- Name: checklists_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE checklists_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE checklists_id_seq OWNER TO postgres; + +-- +-- Name: checklists; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE checklists ( + id bigint DEFAULT nextval('checklists_id_seq'::regclass) NOT NULL, + created timestamp without time zone NOT NULL, + modified timestamp without time zone NOT NULL, + user_id bigint NOT NULL, + card_id bigint NOT NULL, + name character varying(255) NOT NULL, + checklist_item_count bigint DEFAULT 0, + checklist_item_completed_count bigint DEFAULT 0, + "position" double precision NOT NULL +); + + +ALTER TABLE checklists OWNER TO postgres; + +-- +-- Name: lists_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE lists_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE lists_id_seq OWNER TO postgres; + +-- +-- Name: lists; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE lists ( + id bigint DEFAULT nextval('lists_id_seq'::regclass) NOT NULL, + created timestamp without time zone NOT NULL, + modified timestamp without time zone NOT NULL, + board_id bigint NOT NULL, + user_id bigint NOT NULL, + name character varying(255) NOT NULL, + "position" double precision NOT NULL, + is_archived boolean DEFAULT false NOT NULL, + card_count bigint DEFAULT 0, + lists_subscriber_count bigint DEFAULT 0, + is_deleted boolean DEFAULT false NOT NULL +); + + +ALTER TABLE lists OWNER TO postgres; + +-- +-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE users_id_seq + START WITH 2 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE users_id_seq OWNER TO postgres; + +-- +-- Name: users; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE users ( + id bigint DEFAULT nextval('users_id_seq'::regclass) NOT NULL, + created timestamp without time zone NOT NULL, + modified timestamp without time zone NOT NULL, + role_id integer DEFAULT 0 NOT NULL, + username character varying(255) NOT NULL, + email character varying(255) NOT NULL, + password character varying(255) NOT NULL, + full_name character varying(255), + initials character varying(10), + about_me text, + profile_picture_path character varying(255), + notification_frequency smallint, + is_allow_desktop_notification boolean DEFAULT false NOT NULL, + is_active boolean DEFAULT false NOT NULL, + is_email_confirmed boolean DEFAULT false NOT NULL, + created_organization_count bigint DEFAULT 0, + created_board_count bigint DEFAULT 0, + joined_organization_count bigint DEFAULT 0, + list_count bigint DEFAULT 0, + joined_card_count bigint DEFAULT 0, + created_card_count bigint DEFAULT 0, + joined_board_count bigint DEFAULT 0, + checklist_count bigint DEFAULT 0, + checklist_item_completed_count bigint DEFAULT 0, + checklist_item_count bigint DEFAULT 0, + activity_count bigint DEFAULT 0, + card_voter_count bigint DEFAULT 0, + last_activity_id bigint, + last_login_date timestamp without time zone, + last_login_ip_id bigint, + ip_id bigint, + login_type_id smallint, + is_productivity_beats boolean DEFAULT false NOT NULL, + user_login_count bigint DEFAULT (0)::bigint NOT NULL +); + + +ALTER TABLE users OWNER TO postgres; + +-- +-- Name: activities_listing; Type: VIEW; Schema: public; Owner: postgres +-- + +CREATE VIEW activities_listing AS + SELECT activity.id, + activity.created, + activity.modified, + activity.board_id, + activity.list_id, + activity.card_id, + activity.user_id, + activity.foreign_id, + activity.type, + activity.comment, + activity.revisions, + activity.root, + activity.freshness_ts, + activity.depth, + activity.path, + activity.materialized_path, + board.name AS board_name, + list.name AS list_name, + card.name AS card_name, + users.username, + users.full_name, + users.profile_picture_path, + users.initials, + cll.name AS label_name, + card.description AS card_description, + users.role_id AS user_role_id, + checklist_item.name AS checklist_item_name, + checklist.name AS checklist_item_parent_name, + checklist1.name AS checklist_name + FROM ((((((((activities activity + LEFT JOIN boards board ON ((board.id = activity.board_id))) + LEFT JOIN lists list ON ((list.id = activity.list_id))) + LEFT JOIN cards card ON ((card.id = activity.card_id))) + LEFT JOIN cards_labels_listing cll ON ((cll.id = activity.card_id))) + LEFT JOIN checklist_items checklist_item ON ((checklist_item.id = activity.foreign_id))) + LEFT JOIN checklists checklist ON ((checklist.id = checklist_item.checklist_id))) + LEFT JOIN checklists checklist1 ON ((checklist1.id = activity.foreign_id))) + LEFT JOIN users users ON ((users.id = activity.user_id))); + + +ALTER TABLE activities_listing OWNER TO postgres; + +-- +-- Name: attachments_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE attachments_id_seq + START WITH 2 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE attachments_id_seq OWNER TO postgres; + +-- +-- Name: boards_stars_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE boards_stars_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE boards_stars_id_seq OWNER TO postgres; + +-- +-- Name: board_stars; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE board_stars ( + id bigint DEFAULT nextval('boards_stars_id_seq'::regclass) NOT NULL, + created timestamp without time zone NOT NULL, + modified timestamp without time zone NOT NULL, + board_id bigint NOT NULL, + user_id bigint NOT NULL, + is_starred boolean NOT NULL +); + + +ALTER TABLE board_stars OWNER TO postgres; + +-- +-- Name: boards_subscribers_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE boards_subscribers_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE boards_subscribers_id_seq OWNER TO postgres; + +-- +-- Name: board_subscribers; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE board_subscribers ( + id bigint DEFAULT nextval('boards_subscribers_id_seq'::regclass) NOT NULL, + created timestamp without time zone NOT NULL, + modified timestamp without time zone NOT NULL, + board_id bigint NOT NULL, + user_id bigint NOT NULL, + is_subscribed boolean NOT NULL +); + + +ALTER TABLE board_subscribers OWNER TO postgres; + +-- +-- Name: boards_labels_listing; Type: VIEW; Schema: public; Owner: postgres +-- + +CREATE VIEW boards_labels_listing AS + SELECT cards_labels.id, + cards_labels.created, + cards_labels.modified, + cards_labels.label_id, + cards_labels.card_id, + cards_labels.list_id, + cards_labels.board_id, + labels.name + FROM (cards_labels cards_labels + LEFT JOIN labels labels ON ((labels.id = cards_labels.label_id))); + + +ALTER TABLE boards_labels_listing OWNER TO postgres; + +-- +-- Name: boards_users_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE boards_users_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE boards_users_id_seq OWNER TO postgres; + +-- +-- Name: boards_users; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE boards_users ( + id bigint DEFAULT nextval('boards_users_id_seq'::regclass) NOT NULL, + created timestamp without time zone NOT NULL, + modified timestamp without time zone NOT NULL, + board_id bigint NOT NULL, + user_id bigint NOT NULL, + is_admin boolean NOT NULL +); + + +ALTER TABLE boards_users OWNER TO postgres; + +-- +-- Name: boards_users_listing; Type: VIEW; Schema: public; Owner: postgres +-- + +CREATE VIEW boards_users_listing AS + SELECT bu.id, + bu.created, + bu.modified, + bu.board_id, + bu.user_id, + bu.is_admin, + u.username, + u.email, + u.full_name, + u.is_active, + u.is_email_confirmed, + b.name AS board_name, + u.profile_picture_path, + u.initials + FROM ((boards_users bu + JOIN users u ON ((u.id = bu.user_id))) + JOIN boards b ON ((b.id = bu.board_id))); + + +ALTER TABLE boards_users_listing OWNER TO postgres; + +-- +-- Name: card_attachments_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE card_attachments_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE card_attachments_id_seq OWNER TO postgres; + +-- +-- Name: card_attachments; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE card_attachments ( + id bigint DEFAULT nextval('card_attachments_id_seq'::regclass) NOT NULL, + created timestamp without time zone NOT NULL, + modified timestamp without time zone NOT NULL, + card_id bigint, + name character varying(255) NOT NULL, + path character varying(255) NOT NULL, + list_id bigint, + board_id bigint DEFAULT 1, + mimetype character varying(255) +); + + +ALTER TABLE card_attachments OWNER TO postgres; + +-- +-- Name: cards_subscribers_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE cards_subscribers_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE cards_subscribers_id_seq OWNER TO postgres; + +-- +-- Name: card_subscribers; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE card_subscribers ( + id bigint DEFAULT nextval('cards_subscribers_id_seq'::regclass) NOT NULL, + created timestamp without time zone NOT NULL, + modified timestamp without time zone NOT NULL, + card_id bigint NOT NULL, + user_id bigint NOT NULL, + is_subscribed boolean NOT NULL +); + + +ALTER TABLE card_subscribers OWNER TO postgres; + +-- +-- Name: card_voters_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE card_voters_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE card_voters_id_seq OWNER TO postgres; + +-- +-- Name: card_voters; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE card_voters ( + id bigint DEFAULT nextval('card_voters_id_seq'::regclass) NOT NULL, + created timestamp without time zone NOT NULL, + modified timestamp without time zone NOT NULL, + card_id bigint NOT NULL, + user_id bigint NOT NULL +); + + +ALTER TABLE card_voters OWNER TO postgres; + +-- +-- Name: card_voters_listing; Type: VIEW; Schema: public; Owner: postgres +-- + +CREATE VIEW card_voters_listing AS + SELECT card_voters.id, + card_voters.created, + card_voters.modified, + card_voters.user_id, + card_voters.card_id, + users.username, + users.role_id, + users.profile_picture_path, + users.initials + FROM (card_voters card_voters + LEFT JOIN users users ON ((users.id = card_voters.user_id))); + + +ALTER TABLE card_voters_listing OWNER TO postgres; + +-- +-- Name: cards_users_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE cards_users_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE cards_users_id_seq OWNER TO postgres; + +-- +-- Name: cards_users; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE cards_users ( + id bigint DEFAULT nextval('cards_users_id_seq'::regclass) NOT NULL, + created timestamp without time zone NOT NULL, + modified timestamp without time zone NOT NULL, + card_id bigint NOT NULL, + user_id bigint NOT NULL +); + + +ALTER TABLE cards_users OWNER TO postgres; + +-- +-- Name: cards_users_listing; Type: VIEW; Schema: public; Owner: postgres +-- + +CREATE VIEW cards_users_listing AS + SELECT u.username, + u.profile_picture_path, + cu.id, + cu.created, + cu.modified, + cu.card_id, + cu.user_id, + u.initials + FROM (cards_users cu + LEFT JOIN users u ON ((u.id = cu.user_id))); + + +ALTER TABLE cards_users_listing OWNER TO postgres; + +-- +-- Name: checklists_listing; Type: VIEW; Schema: public; Owner: postgres +-- + +CREATE VIEW checklists_listing AS + SELECT checklists.id, + checklists.created, + checklists.modified, + checklists.user_id, + checklists.card_id, + checklists.name, + checklists.checklist_item_count, + checklists.checklist_item_completed_count, + ( SELECT array_to_json(array_agg(row_to_json(ci.*))) AS array_to_json + FROM ( SELECT checklist_items.id, + checklist_items.created, + checklist_items.modified, + checklist_items.user_id, + checklist_items.card_id, + checklist_items.checklist_id, + checklist_items.name, + checklist_items.is_completed, + checklist_items."position" + FROM checklist_items checklist_items + WHERE (checklist_items.checklist_id = checklists.id) + ORDER BY checklist_items."position") ci) AS checklists_items, + checklists."position" + FROM checklists checklists; + + +ALTER TABLE checklists_listing OWNER TO postgres; + +-- +-- Name: cards_listing; Type: VIEW; Schema: public; Owner: postgres +-- + +CREATE VIEW cards_listing AS + SELECT cards.id, + cards.created, + cards.modified, + cards.board_id, + cards.list_id, + cards.name, + cards.description, + cards.due_date, + to_date(to_char(cards.due_date, 'YYYY/MM/DD'::text), 'YYYY/MM/DD'::text) AS to_date, + cards."position", + cards.is_archived, + cards.attachment_count, + cards.checklist_count, + cards.checklist_item_count, + cards.checklist_item_completed_count, + cards.label_count, + cards.cards_user_count, + cards.cards_subscriber_count, + cards.card_voter_count, + cards.activity_count, + cards.user_id, + cards.name AS title, + cards.due_date AS start, + cards.due_date AS "end", + ( SELECT array_to_json(array_agg(row_to_json(cc.*))) AS array_to_json + FROM ( SELECT checklists_listing.id, + checklists_listing.created, + checklists_listing.modified, + checklists_listing.user_id, + checklists_listing.card_id, + checklists_listing.name, + checklists_listing.checklist_item_count, + checklists_listing.checklist_item_completed_count, + checklists_listing."position", + checklists_listing.checklists_items + FROM checklists_listing checklists_listing + WHERE (checklists_listing.card_id = cards.id) + ORDER BY checklists_listing.id) cc) AS cards_checklists, + ( SELECT array_to_json(array_agg(row_to_json(cc.*))) AS array_to_json + FROM ( SELECT cards_users_listing.username, + cards_users_listing.profile_picture_path, + cards_users_listing.id, + cards_users_listing.created, + cards_users_listing.modified, + cards_users_listing.card_id, + cards_users_listing.user_id, + cards_users_listing.initials + FROM cards_users_listing cards_users_listing + WHERE (cards_users_listing.card_id = cards.id) + ORDER BY cards_users_listing.id) cc) AS cards_users, + ( SELECT array_to_json(array_agg(row_to_json(cv.*))) AS array_to_json + FROM ( SELECT card_voters_listing.id, + card_voters_listing.created, + card_voters_listing.modified, + card_voters_listing.user_id, + card_voters_listing.card_id, + card_voters_listing.username, + card_voters_listing.role_id, + card_voters_listing.profile_picture_path, + card_voters_listing.initials + FROM card_voters_listing card_voters_listing + WHERE (card_voters_listing.card_id = cards.id) + ORDER BY card_voters_listing.id) cv) AS cards_voters, + ( SELECT array_to_json(array_agg(row_to_json(cs.*))) AS array_to_json + FROM ( SELECT cards_subscribers.id, + cards_subscribers.created, + cards_subscribers.modified, + cards_subscribers.card_id, + cards_subscribers.user_id, + cards_subscribers.is_subscribed + FROM card_subscribers cards_subscribers + WHERE (cards_subscribers.card_id = cards.id) + ORDER BY cards_subscribers.id) cs) AS cards_subscribers, + ( SELECT array_to_json(array_agg(row_to_json(cl.*))) AS array_to_json + FROM ( SELECT cards_labels.label_id, + cards_labels.card_id, + cards_labels.list_id, + cards_labels.board_id, + cards_labels.name + FROM cards_labels_listing cards_labels + WHERE (cards_labels.card_id = cards.id) + ORDER BY cards_labels.id) cl) AS cards_labels, + cards.comment_count + FROM cards cards; + + +ALTER TABLE cards_listing OWNER TO postgres; + +-- +-- Name: lists_subscribers_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE lists_subscribers_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE lists_subscribers_id_seq OWNER TO postgres; + +-- +-- Name: list_subscribers; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE list_subscribers ( + id bigint DEFAULT nextval('lists_subscribers_id_seq'::regclass) NOT NULL, + created timestamp without time zone NOT NULL, + modified timestamp without time zone, + list_id bigint NOT NULL, + user_id bigint NOT NULL, + is_subscribed boolean DEFAULT false NOT NULL +); + + +ALTER TABLE list_subscribers OWNER TO postgres; + +-- +-- Name: lists_listing; Type: VIEW; Schema: public; Owner: postgres +-- + +CREATE VIEW lists_listing AS + SELECT lists.id, + lists.created, + lists.modified, + lists.board_id, + lists.name, + lists."position", + lists.is_archived, + lists.card_count, + lists.lists_subscriber_count, + ( SELECT array_to_json(array_agg(row_to_json(lc.*))) AS array_to_json + FROM ( SELECT cards_listing.id, + cards_listing.created, + cards_listing.modified, + cards_listing.board_id, + cards_listing.list_id, + cards_listing.name, + cards_listing.description, + cards_listing.due_date, + cards_listing.to_date, + cards_listing."position", + cards_listing.is_archived, + cards_listing.attachment_count, + cards_listing.checklist_count, + cards_listing.checklist_item_count, + cards_listing.checklist_item_completed_count, + cards_listing.label_count, + cards_listing.cards_user_count, + cards_listing.cards_subscriber_count, + cards_listing.card_voter_count, + cards_listing.activity_count, + cards_listing.user_id, + cards_listing.title, + cards_listing.start, + cards_listing."end", + cards_listing.cards_checklists, + cards_listing.cards_users, + cards_listing.cards_voters, + cards_listing.cards_subscribers, + cards_listing.cards_labels, + cards_listing.comment_count + FROM cards_listing cards_listing + WHERE (cards_listing.list_id = lists.id) + ORDER BY cards_listing."position") lc) AS cards, + ( SELECT array_to_json(array_agg(row_to_json(ls.*))) AS array_to_json + FROM ( SELECT lists_subscribers.id, + lists_subscribers.created, + lists_subscribers.modified, + lists_subscribers.list_id, + lists_subscribers.user_id, + lists_subscribers.is_subscribed + FROM list_subscribers lists_subscribers + WHERE (lists_subscribers.list_id = lists.id) + ORDER BY lists_subscribers.id) ls) AS lists_subscribers + FROM lists lists; + + +ALTER TABLE lists_listing OWNER TO postgres; + +-- +-- Name: organizations_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE organizations_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE organizations_id_seq OWNER TO postgres; + +-- +-- Name: organizations; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE organizations ( + id bigint DEFAULT nextval('organizations_id_seq'::regclass) NOT NULL, + created timestamp without time zone NOT NULL, + modified timestamp without time zone NOT NULL, + user_id bigint NOT NULL, + name character varying(255) NOT NULL, + website_url character varying(255), + description text, + logo_url character varying(255), + organization_visibility smallint DEFAULT 1, + organizations_user_count bigint DEFAULT 0, + board_count bigint DEFAULT 0 +); + + +ALTER TABLE organizations OWNER TO postgres; + +-- +-- Name: boards_listing; Type: VIEW; Schema: public; Owner: postgres +-- + +CREATE VIEW boards_listing AS + SELECT board.id, + board.name, + board.user_id, + board.organization_id, + board.board_visibility, + board.background_color, + board.background_picture_url, + board.commenting_permissions, + board.voting_permissions, + board.is_closed, + board.is_allow_organization_members_to_join, + board.boards_user_count, + board.list_count, + board.card_count, + board.boards_subscriber_count, + board.background_pattern_url, + board.is_show_image_front_of_card, + board.music_name, + board.music_content, + organizations.name AS organization_name, + organizations.website_url AS organization_website_url, + organizations.description AS organization_description, + organizations.logo_url AS organization_logo_url, + organizations.organization_visibility, + ( SELECT array_to_json(array_agg(row_to_json(ba.*))) AS array_to_json + FROM ( SELECT activities.id, + activities.created, + activities.modified, + activities.board_id, + activities.list_id, + activities.card_id, + activities.user_id, + activities.foreign_id AS attachment_id, + activities.type, + activities.comment, + activities.revisions, + activities.root, + activities.freshness_ts, + activities.depth, + activities.path, + activities.materialized_path, + users.username, + users.role_id, + users.profile_picture_path, + users.initials + FROM (activities activities + LEFT JOIN users users ON ((users.id = activities.user_id))) + WHERE (activities.board_id = board.id) + ORDER BY activities.freshness_ts DESC, activities.materialized_path + OFFSET 0 + LIMIT 20) ba) AS activities, + ( SELECT array_to_json(array_agg(row_to_json(bs.*))) AS array_to_json + FROM ( SELECT boards_subscribers.id, + boards_subscribers.created, + boards_subscribers.modified, + boards_subscribers.board_id, + boards_subscribers.user_id, + boards_subscribers.is_subscribed + FROM board_subscribers boards_subscribers + WHERE (boards_subscribers.board_id = board.id) + ORDER BY boards_subscribers.id) bs) AS boards_subscribers, + ( SELECT array_to_json(array_agg(row_to_json(bs.*))) AS array_to_json + FROM ( SELECT boards_stars.id, + boards_stars.created, + boards_stars.modified, + boards_stars.board_id, + boards_stars.user_id, + boards_stars.is_starred + FROM board_stars boards_stars + WHERE (boards_stars.board_id = board.id) + ORDER BY boards_stars.id) bs) AS boards_stars, + ( SELECT array_to_json(array_agg(row_to_json(batt.*))) AS array_to_json + FROM ( SELECT card_attachments.id, + card_attachments.created, + card_attachments.modified, + card_attachments.card_id, + card_attachments.name, + card_attachments.path, + card_attachments.mimetype, + card_attachments.list_id, + card_attachments.board_id + FROM card_attachments card_attachments + WHERE (card_attachments.board_id = board.id) + ORDER BY card_attachments.id DESC) batt) AS attachments, + ( SELECT array_to_json(array_agg(row_to_json(bl.*))) AS array_to_json + FROM ( SELECT lists_listing.id, + lists_listing.created, + lists_listing.modified, + lists_listing.board_id, + lists_listing.name, + lists_listing."position", + lists_listing.is_archived, + lists_listing.card_count, + lists_listing.lists_subscriber_count, + lists_listing.cards, + lists_listing.lists_subscribers + FROM lists_listing lists_listing + WHERE (lists_listing.board_id = board.id) + ORDER BY lists_listing."position") bl) AS lists, + ( SELECT array_to_json(array_agg(row_to_json(bu.*))) AS array_to_json + FROM ( SELECT boards_users.id, + boards_users.created, + boards_users.modified, + boards_users.board_id, + boards_users.user_id, + boards_users.is_admin, + boards_users.username, + boards_users.email, + boards_users.full_name, + boards_users.is_active, + boards_users.is_email_confirmed, + boards_users.board_name, + boards_users.profile_picture_path, + boards_users.initials + FROM boards_users_listing boards_users + WHERE (boards_users.board_id = board.id) + ORDER BY boards_users.id) bu) AS boards_users + FROM (boards board + LEFT JOIN organizations organizations ON ((organizations.id = board.organization_id))); + + +ALTER TABLE boards_listing OWNER TO postgres; + +-- +-- Name: checklist_add_listing; Type: VIEW; Schema: public; Owner: postgres +-- + +CREATE VIEW checklist_add_listing AS + SELECT c.id, + c.name, + c.board_id, + cl.checklist_item_count, + cl.name AS checklist_name, + cl.id AS checklist_id + FROM (cards c + LEFT JOIN checklists cl ON ((cl.card_id = c.id))) + WHERE (c.checklist_item_count > 0) + ORDER BY c.id; + + +ALTER TABLE checklist_add_listing OWNER TO postgres; + +-- +-- Name: cities; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE cities ( + id bigint NOT NULL, + created timestamp without time zone, + modified timestamp without time zone, + country_id bigint, + state_id bigint, + latitude character varying(255), + longitude character varying(255), + name character varying(255), + is_active boolean DEFAULT false +); + + +ALTER TABLE cities OWNER TO postgres; + +-- +-- Name: cities_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE cities_id_seq + START WITH 15178 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE cities_id_seq OWNER TO postgres; + +-- +-- Name: cities_id_seq1; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE cities_id_seq1 + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE cities_id_seq1 OWNER TO postgres; + +-- +-- Name: cities_id_seq1; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE cities_id_seq1 OWNED BY cities.id; + + +-- +-- Name: countries; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE countries ( + id bigint NOT NULL, + iso_alpha2 character(2) DEFAULT NULL::bpchar, + iso_alpha3 character(3) DEFAULT NULL::bpchar, + iso_numeric bigint, + fips_code character varying(3) DEFAULT NULL::character varying, + name character varying(200) DEFAULT NULL::character varying, + capital character varying(200) DEFAULT NULL::character varying, + areainsqkm double precision, + population bigint, + continent character(2) DEFAULT NULL::bpchar, + tld character(3) DEFAULT NULL::bpchar, + currency character(3) DEFAULT NULL::bpchar, + currencyname character(20) DEFAULT NULL::bpchar, + phone character(10) DEFAULT NULL::bpchar, + postalcodeformat character(20) DEFAULT NULL::bpchar, + postalcoderegex character(20) DEFAULT NULL::bpchar, + languages character varying(200) DEFAULT NULL::character varying, + geonameid bigint, + neighbours character(20) DEFAULT NULL::bpchar, + equivalentfipscode character(10) DEFAULT NULL::bpchar, + created timestamp without time zone, + iso2 character varying(2), + iso3 character varying(3), + modified timestamp without time zone +); + + +ALTER TABLE countries OWNER TO postgres; + +-- +-- Name: countries_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE countries_id_seq + START WITH 262 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE countries_id_seq OWNER TO postgres; + +-- +-- Name: countries_id_seq1; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE countries_id_seq1 + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE countries_id_seq1 OWNER TO postgres; + +-- +-- Name: countries_id_seq1; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE countries_id_seq1 OWNED BY countries.id; + + +-- +-- Name: email_templates_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE email_templates_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE email_templates_id_seq OWNER TO postgres; + +-- +-- Name: email_templates; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE email_templates ( + id bigint DEFAULT nextval('email_templates_id_seq'::regclass) NOT NULL, + created timestamp without time zone, + modified timestamp without time zone, + from_email character varying(500) DEFAULT NULL::character varying, + reply_to_email character varying(500) DEFAULT NULL::character varying, + name character varying(150) DEFAULT NULL::character varying, + description text, + subject character varying(255) DEFAULT NULL::character varying, + email_text_content text, + email_variables character varying(1000) DEFAULT NULL::character varying, + display_name text +); + + +ALTER TABLE email_templates OWNER TO postgres; + +-- +-- Name: gadget_users_listing; Type: VIEW; Schema: public; Owner: postgres +-- + +CREATE VIEW gadget_users_listing AS + SELECT checklists.id, + checklists.created, + checklists.modified, + checklists.user_id, + checklists.card_id, + checklists.name, + checklists.checklist_item_count, + checklists.checklist_item_completed_count, + ( SELECT array_to_json(array_agg(row_to_json(ci.*))) AS array_to_json + FROM ( SELECT checklist_items.id, + checklist_items.created, + checklist_items.modified, + checklist_items.user_id, + checklist_items.card_id, + checklist_items.checklist_id, + checklist_items.name, + checklist_items.is_completed + FROM checklist_items checklist_items + WHERE (checklist_items.checklist_id = checklists.id) + ORDER BY checklist_items.id) ci) AS checklist_items + FROM checklists checklists; + + +ALTER TABLE gadget_users_listing OWNER TO postgres; + +-- +-- Name: ips_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE ips_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE ips_id_seq OWNER TO postgres; + +-- +-- Name: ips; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE ips ( + id bigint DEFAULT nextval('ips_id_seq'::regclass) NOT NULL, + created timestamp without time zone, + modified timestamp without time zone, + ip character varying(255) NOT NULL, + host character varying(255) NOT NULL, + user_agent character varying(255) NOT NULL, + "order" integer DEFAULT 0, + city_id bigint, + state_id bigint, + country_id bigint, + latitude double precision, + longitude double precision +); + + +ALTER TABLE ips OWNER TO postgres; + +-- +-- Name: list_subscribers_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE list_subscribers_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE list_subscribers_id_seq OWNER TO postgres; + +-- +-- Name: login_types_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE login_types_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE login_types_id_seq OWNER TO postgres; + +-- +-- Name: login_types; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE login_types ( + id bigint DEFAULT nextval('login_types_id_seq'::regclass) NOT NULL, + created timestamp without time zone NOT NULL, + modified timestamp without time zone NOT NULL, + name character varying(255) NOT NULL +); + + +ALTER TABLE login_types OWNER TO postgres; + +-- +-- Name: oauth_access_tokens; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE oauth_access_tokens ( + access_token character varying(40) NOT NULL, + client_id character varying(80), + user_id character varying(255), + expires timestamp without time zone, + scope character varying(2000) +); + + +ALTER TABLE oauth_access_tokens OWNER TO postgres; + +-- +-- Name: oauth_authorization_codes; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE oauth_authorization_codes ( + authorization_code character varying(40) NOT NULL, + client_id character varying(80), + user_id character varying(255), + redirect_uri character varying(2000), + expires timestamp without time zone, + scope character varying(2000) +); + + +ALTER TABLE oauth_authorization_codes OWNER TO postgres; + +-- +-- Name: oauth_clients; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE oauth_clients ( + client_id character varying(80) NOT NULL, + client_secret character varying(80), + redirect_uri character varying(2000), + grant_types character varying(80), + scope character varying(100), + user_id character varying(80) +); + + +ALTER TABLE oauth_clients OWNER TO postgres; + +-- +-- Name: oauth_jwt; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE oauth_jwt ( + client_id character varying(80) NOT NULL, + subject character varying(80), + public_key character varying(2000) +); + + +ALTER TABLE oauth_jwt OWNER TO postgres; + +-- +-- Name: oauth_refresh_tokens; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE oauth_refresh_tokens ( + refresh_token character varying(40) NOT NULL, + client_id character varying(80), + user_id character varying(255), + expires timestamp without time zone, + scope character varying(2000) +); + + +ALTER TABLE oauth_refresh_tokens OWNER TO postgres; + +-- +-- Name: oauth_scopes; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE oauth_scopes ( + scope text NOT NULL, + is_default boolean +); + + +ALTER TABLE oauth_scopes OWNER TO postgres; + +-- +-- Name: organizations_users_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE organizations_users_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE organizations_users_id_seq OWNER TO postgres; + +-- +-- Name: organizations_users; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE organizations_users ( + id bigint DEFAULT nextval('organizations_users_id_seq'::regclass) NOT NULL, + created timestamp without time zone NOT NULL, + modified timestamp without time zone NOT NULL, + organization_id bigint NOT NULL, + user_id bigint NOT NULL, + is_admin boolean NOT NULL +); + + +ALTER TABLE organizations_users OWNER TO postgres; + +-- +-- Name: organizations_users_listing; Type: VIEW; Schema: public; Owner: postgres +-- + +CREATE VIEW organizations_users_listing AS + SELECT organizations_users.id, + organizations_users.created, + organizations_users.modified, + organizations_users.user_id, + organizations_users.organization_id, + organizations_users.is_admin, + users.role_id, + users.username, + users.email, + users.full_name, + users.initials, + users.about_me, + users.created_organization_count, + users.created_board_count, + users.joined_organization_count, + users.list_count, + users.joined_card_count, + users.created_card_count, + users.joined_board_count, + users.checklist_count, + users.checklist_item_completed_count, + users.checklist_item_count, + users.activity_count, + users.card_voter_count, + organizations.name, + organizations.website_url, + organizations.description, + organizations.logo_url, + organizations.organization_visibility, + users.profile_picture_path, + ( SELECT array_to_json(array_agg(row_to_json(o.*))) AS array_to_json + FROM ( SELECT boards_users.id, + boards_users.board_id, + boards_users.user_id, + boards_users.is_admin, + boards.name + FROM (boards_users boards_users + JOIN boards ON ((boards.id = boards_users.board_id))) + WHERE ((boards_users.user_id = organizations_users.user_id) AND (boards_users.board_id IN ( SELECT boards_1.id + FROM boards boards_1 + WHERE (boards_1.organization_id = organizations_users.organization_id)))) + ORDER BY boards_users.id) o) AS boards_users, + ( SELECT count(boards.id) AS count + FROM (boards + JOIN boards_users bu ON ((bu.board_id = boards.id))) + WHERE ((boards.organization_id = organizations_users.organization_id) AND (bu.user_id = organizations_users.user_id))) AS user_board_count + FROM ((organizations_users organizations_users + LEFT JOIN users users ON ((users.id = organizations_users.user_id))) + LEFT JOIN organizations organizations ON ((organizations.id = organizations_users.organization_id))); + + +ALTER TABLE organizations_users_listing OWNER TO postgres; + +-- +-- Name: organizations_listing; Type: VIEW; Schema: public; Owner: postgres +-- + +CREATE VIEW organizations_listing AS + SELECT organizations.id, + organizations.created, + organizations.modified, + organizations.user_id, + organizations.name, + organizations.website_url, + organizations.description, + organizations.logo_url, + organizations.organization_visibility, + organizations.organizations_user_count, + organizations.board_count, + ( SELECT array_to_json(array_agg(row_to_json(b.*))) AS array_to_json + FROM ( SELECT boards_listing.id, + boards_listing.name, + boards_listing.user_id, + boards_listing.organization_id, + boards_listing.board_visibility, + boards_listing.background_color, + boards_listing.background_picture_url, + boards_listing.commenting_permissions, + boards_listing.voting_permissions, + boards_listing.is_closed, + boards_listing.is_allow_organization_members_to_join, + boards_listing.boards_user_count, + boards_listing.list_count, + boards_listing.card_count, + boards_listing.boards_subscriber_count, + boards_listing.background_pattern_url, + boards_listing.is_show_image_front_of_card, + boards_listing.organization_name, + boards_listing.organization_website_url, + boards_listing.organization_description, + boards_listing.organization_logo_url, + boards_listing.organization_visibility, + boards_listing.activities, + boards_listing.boards_subscribers, + boards_listing.boards_stars, + boards_listing.attachments, + boards_listing.lists, + boards_listing.boards_users + FROM boards_listing boards_listing + WHERE (boards_listing.organization_id = organizations.id) + ORDER BY boards_listing.id) b) AS boards_listing, + ( SELECT array_to_json(array_agg(row_to_json(c.*))) AS array_to_json + FROM ( SELECT organizations_users_listing.id, + organizations_users_listing.created, + organizations_users_listing.modified, + organizations_users_listing.user_id, + organizations_users_listing.organization_id, + organizations_users_listing.is_admin, + organizations_users_listing.role_id, + organizations_users_listing.username, + organizations_users_listing.email, + organizations_users_listing.full_name, + organizations_users_listing.initials, + organizations_users_listing.about_me, + organizations_users_listing.created_organization_count, + organizations_users_listing.created_board_count, + organizations_users_listing.joined_organization_count, + organizations_users_listing.list_count, + organizations_users_listing.joined_card_count, + organizations_users_listing.created_card_count, + organizations_users_listing.joined_board_count, + organizations_users_listing.checklist_count, + organizations_users_listing.checklist_item_completed_count, + organizations_users_listing.checklist_item_count, + organizations_users_listing.activity_count, + organizations_users_listing.card_voter_count, + organizations_users_listing.name, + organizations_users_listing.website_url, + organizations_users_listing.description, + organizations_users_listing.logo_url, + organizations_users_listing.organization_visibility, + organizations_users_listing.profile_picture_path, + organizations_users_listing.boards_users, + organizations_users_listing.user_board_count + FROM organizations_users_listing organizations_users_listing + WHERE (organizations_users_listing.organization_id = organizations.id) + ORDER BY organizations_users_listing.id) c) AS organizations_users, + u.username, + u.full_name, + u.initials, + u.profile_picture_path + FROM (organizations organizations + LEFT JOIN users u ON ((u.id = organizations.user_id))); + + +ALTER TABLE organizations_listing OWNER TO postgres; + +-- +-- Name: roles_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE roles_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE roles_id_seq OWNER TO postgres; + +-- +-- Name: roles; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE roles ( + id bigint DEFAULT nextval('roles_id_seq'::regclass) NOT NULL, + created timestamp without time zone NOT NULL, + modified timestamp without time zone NOT NULL, + name character varying(255) NOT NULL +); + + +ALTER TABLE roles OWNER TO postgres; + +-- +-- Name: role_links_listing; Type: VIEW; Schema: public; Owner: postgres +-- + +CREATE VIEW role_links_listing AS + SELECT role.id, + ( SELECT array_to_json(array_agg(link.*)) AS array_to_json + FROM ( SELECT alls.slug + FROM acl_links_listing alls + WHERE (alls.role_id = role.id)) link) AS links + FROM roles role; + + +ALTER TABLE role_links_listing OWNER TO postgres; + +-- +-- Name: setting_categories; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE setting_categories ( + id integer NOT NULL, + created timestamp without time zone, + modified timestamp without time zone, + parent_id bigint, + name character varying(255), + description text, + "order" integer DEFAULT 0 NOT NULL +); + + +ALTER TABLE setting_categories OWNER TO postgres; + +-- +-- Name: setting_categories_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE setting_categories_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE setting_categories_id_seq OWNER TO postgres; + +-- +-- Name: setting_categories_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE setting_categories_id_seq OWNED BY setting_categories.id; + + +-- +-- Name: settings_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE settings_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE settings_id_seq OWNER TO postgres; + +-- +-- Name: settings; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE settings ( + id bigint DEFAULT nextval('settings_id_seq'::regclass) NOT NULL, + setting_category_id bigint NOT NULL, + setting_category_parent_id bigint DEFAULT 0, + name character varying(255), + value text, + description text, + type character varying(8), + options text, + label character varying(22), + "order" integer DEFAULT 0 NOT NULL +); + + +ALTER TABLE settings OWNER TO postgres; + +-- +-- Name: settings_listing; Type: VIEW; Schema: public; Owner: postgres +-- + +CREATE VIEW settings_listing AS + SELECT setting_categories.id, + setting_categories.created, + setting_categories.modified, + setting_categories.parent_id, + setting_categories.name, + setting_categories.description, + ( SELECT array_to_json(array_agg(row_to_json(o.*))) AS array_to_json + FROM ( SELECT settings.id, + settings.name, + settings.setting_category_id, + settings.setting_category_parent_id, + settings.value, + settings.type, + settings.options, + settings.label, + settings."order" + FROM settings settings + WHERE (settings.setting_category_id = setting_categories.id) + ORDER BY settings."order") o) AS settings + FROM setting_categories setting_categories; + + +ALTER TABLE settings_listing OWNER TO postgres; + +-- +-- Name: simple_board_listing; Type: VIEW; Schema: public; Owner: postgres +-- + +CREATE VIEW simple_board_listing AS + SELECT board.id, + board.name, + board.user_id, + board.organization_id, + board.board_visibility, + board.background_color, + board.background_picture_url, + board.commenting_permissions, + board.voting_permissions, + board.is_closed, + board.is_allow_organization_members_to_join, + board.boards_user_count, + board.list_count, + board.card_count, + board.boards_subscriber_count, + board.background_pattern_url, + ( SELECT array_to_json(array_agg(row_to_json(l.*))) AS array_to_json + FROM ( SELECT lists.id, + lists.created, + lists.modified, + lists.board_id, + lists.user_id, + lists.name, + lists."position", + lists.is_archived, + lists.card_count, + lists.lists_subscriber_count, + lists.is_deleted + FROM lists lists + WHERE (lists.board_id = board.id) + ORDER BY lists."position") l) AS lists, + ( SELECT array_to_json(array_agg(row_to_json(l.*))) AS array_to_json + FROM ( SELECT cll.label_id, + cll.name + FROM cards_labels_listing cll + WHERE (cll.board_id = board.id) + ORDER BY cll.name) l) AS labels, + ( SELECT array_to_json(array_agg(row_to_json(l.*))) AS array_to_json + FROM ( SELECT bs.id, + bs.board_id, + bs.user_id, + bs.is_starred + FROM board_stars bs + WHERE (bs.board_id = board.id) + ORDER BY bs.id) l) AS stars, + org.name AS organization_name, + ( SELECT array_to_json(array_agg(row_to_json(l.*))) AS array_to_json + FROM ( SELECT bu.id, + bu.board_id, + bu.user_id, + bu.is_admin + FROM boards_users bu + WHERE (bu.board_id = board.id) + ORDER BY bu.id) l) AS users + FROM (boards board + LEFT JOIN organizations org ON ((org.id = board.organization_id))) + ORDER BY board.id; + + +ALTER TABLE simple_board_listing OWNER TO postgres; + +-- +-- Name: states; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE states ( + id bigint NOT NULL, + created timestamp without time zone, + modified timestamp without time zone, + country_id bigint, + name character varying(45), + is_active boolean DEFAULT false +); + + +ALTER TABLE states OWNER TO postgres; + +-- +-- Name: states_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE states_id_seq + START WITH 15138 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE states_id_seq OWNER TO postgres; + +-- +-- Name: states_id_seq1; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE states_id_seq1 + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE states_id_seq1 OWNER TO postgres; + +-- +-- Name: states_id_seq1; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE states_id_seq1 OWNED BY states.id; + + +-- +-- Name: user_logins; Type: TABLE; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE TABLE user_logins ( + id integer NOT NULL, + created timestamp without time zone NOT NULL, + modified timestamp without time zone NOT NULL, + user_id bigint DEFAULT (0)::bigint NOT NULL, + ip_id bigint DEFAULT (0)::bigint NOT NULL, + user_agent character varying(255) +); + + +ALTER TABLE user_logins OWNER TO postgres; + +-- +-- Name: user_logins_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE user_logins_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE user_logins_id_seq OWNER TO postgres; + +-- +-- Name: user_logins_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE user_logins_id_seq OWNED BY user_logins.id; + + +-- +-- Name: users_cards_listing; Type: VIEW; Schema: public; Owner: postgres +-- + +CREATE VIEW users_cards_listing AS + SELECT b.name AS board_name, + l.name AS list_name, + c.id, + c.created, + c.modified, + c.board_id, + c.list_id, + c.name, + c.description, + c.due_date, + c."position", + c.is_archived, + c.attachment_count, + c.checklist_count, + c.checklist_item_count, + c.checklist_item_completed_count, + c.label_count, + c.cards_user_count, + c.cards_subscriber_count, + c.card_voter_count, + c.activity_count, + c.user_id AS created_user_id, + c.is_deleted, + cu.user_id, + c.comment_count + FROM (((cards_users cu + JOIN cards c ON ((c.id = cu.card_id))) + JOIN boards b ON ((b.id = c.board_id))) + JOIN lists l ON ((l.id = c.list_id))); + + +ALTER TABLE users_cards_listing OWNER TO postgres; + +-- +-- Name: users_listing; Type: VIEW; Schema: public; Owner: postgres +-- + +CREATE VIEW users_listing AS + SELECT users.id, + users.role_id, + users.username, + users.password, + users.email, + users.full_name, + users.initials, + users.about_me, + users.profile_picture_path, + users.notification_frequency, + users.is_allow_desktop_notification, + users.is_active, + users.is_email_confirmed, + users.created_organization_count, + users.created_board_count, + users.joined_organization_count, + users.list_count, + users.joined_card_count, + users.created_card_count, + users.joined_board_count, + users.checklist_count, + users.checklist_item_completed_count, + users.checklist_item_count, + users.activity_count, + users.card_voter_count, + users.is_productivity_beats, + ( SELECT array_to_json(array_agg(row_to_json(o.*))) AS array_to_json + FROM ( SELECT organizations_users_listing.organization_id AS id, + organizations_users_listing.name, + organizations_users_listing.description, + organizations_users_listing.website_url, + organizations_users_listing.logo_url, + organizations_users_listing.organization_visibility + FROM organizations_users_listing organizations_users_listing + WHERE (organizations_users_listing.user_id = users.id) + ORDER BY organizations_users_listing.id) o) AS organizations, + users.last_activity_id, + ( SELECT array_to_json(array_agg(row_to_json(o.*))) AS array_to_json + FROM ( SELECT boards_stars.id, + boards_stars.board_id, + boards_stars.user_id, + boards_stars.is_starred + FROM board_stars boards_stars + WHERE (boards_stars.user_id = users.id) + ORDER BY boards_stars.id) o) AS boards_stars, + ( SELECT array_to_json(array_agg(row_to_json(o.*))) AS array_to_json + FROM ( SELECT boards_users.id, + boards_users.board_id, + boards_users.user_id, + boards_users.is_admin, + boards.name, + boards.background_picture_url, + boards.background_pattern_url, + boards.background_color + FROM (boards_users boards_users + JOIN boards ON ((boards.id = boards_users.board_id))) + WHERE (boards_users.user_id = users.id) + ORDER BY boards_users.id) o) AS boards_users, + users.last_login_date, + li.ip AS last_login_ip, + lci.name AS log_city_name, + lst.name AS log_state_name, + lco.name AS log_country_name, + lower((lco.iso_alpha2)::text) AS log_country_iso2, + i.ip AS registered_ip, + rci.name AS reg_city_name, + rst.name AS reg_state_name, + rco.name AS reg_country_name, + lower((rco.iso_alpha2)::text) AS reg_country_iso2, + lt.name AS login_type, + users.created, + users.user_login_count + FROM (((((((((users users + LEFT JOIN ips i ON ((i.id = users.ip_id))) + LEFT JOIN cities rci ON ((rci.id = i.city_id))) + LEFT JOIN states rst ON ((rst.id = i.state_id))) + LEFT JOIN countries rco ON ((rco.id = i.country_id))) + LEFT JOIN ips li ON ((li.id = users.last_login_ip_id))) + LEFT JOIN cities lci ON ((lci.id = li.city_id))) + LEFT JOIN states lst ON ((lst.id = li.state_id))) + LEFT JOIN countries lco ON ((lco.id = li.country_id))) + LEFT JOIN login_types lt ON ((lt.id = users.login_type_id))); + + +ALTER TABLE users_listing OWNER TO postgres; + +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY cities ALTER COLUMN id SET DEFAULT nextval('cities_id_seq1'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY countries ALTER COLUMN id SET DEFAULT nextval('countries_id_seq1'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY setting_categories ALTER COLUMN id SET DEFAULT nextval('setting_categories_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY states ALTER COLUMN id SET DEFAULT nextval('states_id_seq1'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY user_logins ALTER COLUMN id SET DEFAULT nextval('user_logins_id_seq'::regclass); + + +-- +-- Data for Name: acl_links; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY acl_links (id, created, modified, name, url, method, slug, group_id, is_allow_only_to_admin, is_allow_only_to_user) FROM stdin; +1 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Forgot password /users/forgotpassword POST users_forgotpassword 1 0 0 +2 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Register /users/register POST users_register 1 0 0 +3 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Login /users/login POST users_login 1 0 0 +58 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 View board /boards/? GET view_board 2 0 0 +5 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Add organization /organizations POST add_organization 5 0 1 +6 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Add board /boards POST add_board 2 0 1 +7 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Subscribe board /boards/?/board_subscribers POST subscribe_board 2 0 1 +8 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Copy board /boards/?/copy POST copy_board 2 0 1 +10 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Change password /users/?/changepassword POST user_changepassword 1 0 1 +11 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Upload organization logo /organizations/?/upload_logo POST upload_organization_logo 5 0 1 +12 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Add list /boards/?/lists POST add_list 3 0 1 +13 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Subscribe list /boards/?/lists/?/list_subscribers POST subscribe_list 3 0 1 +16 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Vote card /boards/?/lists/?/cards/?/card_voters POST vote_card 4 0 1 +17 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Add card /boards/?/lists/?/cards POST add_card 4 0 1 +18 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Upload attachment to card /boards/?/lists/?/cards/?/attachments POST add_card_attachment 4 0 1 +19 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Assign labels to card /boards/?/lists/?/cards/?/labels POST add_labels 4 0 1 +20 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Add checklist to card /boards/?/lists/?/cards/?/checklists POST add_checklists 4 0 1 +21 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Add item to checklist /boards/?/lists/?/cards/?/checklists/?/items POST add_checklist_item 4 0 1 +22 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Convert item to card /boards/?/lists/?/cards/?/checklists/?/items/?/convert_to_card POST convert_item_to_card 4 0 1 +23 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Upload profile picture /users/? POST add_user_profile_picture 1 0 1 +24 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Upload custom background to board /boards/?/custom_backgrounds POST add_custom_background 2 0 1 +25 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Assign member to card /boards/?/lists/?/cards/?/users/? POST add_card_user 4 0 1 +26 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Copy card /boards/?/lists/?/cards/?/copy POST copy_card 4 0 1 +28 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Edit organization /organizations/? PUT edit_organization 5 0 1 +57 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Users management /users GET view_user_listing 1 1 0 +31 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Edit board /boards/? PUT edit_board 2 0 1 +32 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Unsubscribe board /subscriber/?/edit PUT unsubscribe_board 2 0 1 +33 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Edit list /boards/?/lists/? PUT edit_list 3 0 1 +34 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Edit card /boards/?/lists/?/cards/? PUT edit_card 4 0 1 +35 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Unsubscribe list /boards/?/lists/?/list_subscribers/? PUT unsubscribe_list 3 0 1 +36 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Unsubscribe card /boards/?/lists/?/cards/?/card_subscribers/? PUT unsubscribe_card 4 0 1 +37 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Unvote card /boards/?/lists/?/cards/?/card_voters/? PUT unvote_card 4 0 1 +39 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Edit checklist /boards/?/lists/?/cards/?/checklists/? PUT edit_checklist 4 0 1 +41 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Undo activity /activities/undo/? PUT undo_activity 4 0 1 +42 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Edit user details /users/? PUT edit_user_details 1 0 1 +43 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Delete board /boards/? DELETE delete_board 2 0 1 +44 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Delete organization /organizations/? DELETE delete_organization 5 0 1 +45 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Remove organization member /organizations_users/? DELETE remove_organization_user 5 0 1 +46 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Remove board member /boards_users/? DELETE remove_board_user 2 0 1 +47 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Delete list /boards/?/lists/? DELETE delete_list 3 0 1 +62 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Organization members listing /organizations_users/? GET view_organization_user_listing 5 0 1 +50 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Delete card /boards/?/lists/?/cards/? DELETE delete_card 4 0 1 +51 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Remove attachment from card /boards/?/lists/?/cards/?/attachments/? DELETE remove_card_attachment 4 0 1 +52 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Delete checklist /boards/?/lists/?/cards/?/checklists/? DELETE delete_checklist 4 0 1 +53 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Delete item in checklist /boards/?/lists/?/cards/?/checklists/?/items/? DELETE delete_checklist_item 4 0 1 +54 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Remove card member /boards/?/lists/?/cards/?/cards_users/? DELETE remove_card_user 4 0 1 +72 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Archived lists /boards/?/archived_lists GET view_archived_lists 3 0 1 +61 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 View organization /organizations/? GET view_organization 5 0 1 +101 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 Setting view /settings/? GET setting_list 6 1 0 +55 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Delete user /users/? DELETE delete_user 1 0 1 +70 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Board search /boards/search GET view_board_search 2 0 1 +56 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 View boards listing /boards GET view_board_listing 2 0 1 +29 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Update organization member permission /organizations_users/? PUT edit_organization_user 5 0 1 +111 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Settings management /settings GET load_settings 6 1 0 +109 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Email templates management /email_templates/? GET view_email_template_listing 6 1 0 +99 2014-11-21 02:52:08.822706 2014-11-21 02:52:08.822706 Setting update /settings POST setting_update 6 1 0 +110 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Edit email template /email_templates/? PUT edit_email_template 6 1 0 +64 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 View user activities /users/?/activities GET view_user_activities 1 0 1 +115 2014-08-25 13:14:18.2 2014-08-25 13:14:18.2 All activities /activities GET activities_listing 2 1 1 +4 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Add organization member /organizations/?/users/? POST add_organization_user 5 0 1 +9 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Add board member /boards/?/users POST add_board_users 2 0 1 +14 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Post comment to card /boards/?/lists/?/cards/?/comments POST comment_card 4 0 1 +15 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Subscribe card /boards/?/lists/?/cards/?/card_subscribers POST subscribe_card 4 0 1 +68 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Starred boards listing /boards/starred GET view_stared_boards 2 0 1 +38 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Edit comment /boards/?/lists/?/cards/?/comments/? PUT edit_comment 4 0 1 +40 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Edit item in checklist /boards/?/lists/?/cards/?/checklists/?/items/? PUT edit_checklist_item 4 0 1 +49 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Delete comment /boards/?/lists/?/cards/?/comments/? DELETE delete_comment 4 0 1 +69 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Board subscribers /boards/?/board_subscribers GET view_board_subscribers 2 0 1 +63 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 View board activities /boards/?/activities GET view_board_activities 2 0 1 +71 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Archived cards /boards/?/archived_cards GET view_archived_cards 4 0 1 +73 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Cards listing /boards/?/lists/?/cards/? GET view_card_isting 4 0 1 +75 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Checklist listing /boards/?/lists/?/cards/?/checklists GET view_checklist_listing 4 0 1 +85 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 View user assigned cards /users/?/cards GET view_user_cards 4 0 1 +86 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 View user assigned boards /users/?/boards GET view_user_board 2 0 1 +87 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Search card to add in comment /boards/?/lists/?/cards/?/search GET view_card_search 4 0 1 +74 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Card activities /boards/?/lists/?/cards/?/activities GET view_card_activities 4 0 0 +30 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Update board member permission /boards_users/? PUT edit_board_user 2 0 1 +117 2015-05-09 13:14:18.2 2015-05-09 13:14:18.2 Create user /users/admin_user_add POST users 1 1 0 +81 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 View user /users/? GET view_user 1 0 1 +89 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 View card labels /boards/?/lists/?/cards/?/labels GET view_card_labels 4 0 1 +91 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Archived card send back to board /boards/?/lists/?/cards POST send_back_to_archived_card 4 0 1 +92 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Archived list send back to board /lists/? PUT send_back_to_archived_list 2 0 1 +77 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Board visibility /boards/?/visibility GET view_board_visibility 2 0 1 +104 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Starred board /boards/?/boards_stars POST starred_board 2 0 1 +105 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Unstarred board /starred/?/edit PUT unstarred_board 2 0 1 +60 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Board members listing /board_users/? GET view_board_listing 2 0 1 +67 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 My boards listing /boards/my_boards GET view_my_boards 2 0 1 +97 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Move list cards /boards/?/lists/?/cards PUT move_list_cards 4 0 1 +98 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Search card /cards/search GET search_card 4 0 1 +103 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 View starred boards listing /boards/?/boards_stars GET view_board_star 2 0 1 +106 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 View closed boards /boards/closed_boards GET view_closed_boards 2 0 1 +108 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 View organizations listing /organizations GET view_organization_listing 5 0 1 +112 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Remove card member /boards/?/lists/?/cards/?/users/? DELETE delete_card_user 4 0 1 +113 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Unstar board /boards/?/boards_stars/? PUT board_star 2 0 1 +114 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Unsubscribe board /boards/?/board_subscribers/? PUT board_subscriber 2 0 1 +78 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Organization visibility /organizations/?/visibility GET view_organization_visibility 5 0 1 +116 2014-08-25 13:14:18.2 2014-08-25 13:14:18.2 Download attachment from card /download/? GET activities_listing 4 1 1 +76 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 View user search /users/search GET view_user_search 1 0 1 +27 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 User activation /users/activation/? PUT user_activation 1 0 0 +79 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Load workflow templates /workflow_templates GET view_workflow_templates 2 0 1 +94 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Roles listing /acl_links GET roles 6 1 0 +80 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Search /search GET view_search 2 0 1 +82 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Board sync Google calendar URL /boards/?/sync_calendar GET view_sync_calendar 2 0 1 +118 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 Roles Update /acl_links POST roles 6 1 0 +\. + + +-- +-- Name: acl_links_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('acl_links_id_seq', 118, true); + + +-- +-- Data for Name: acl_links_roles; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY acl_links_roles (id, created, modified, acl_link_id, role_id) FROM stdin; +1164 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 101 1 +1165 2014-11-21 02:52:08.822706 2014-11-21 02:52:08.822706 102 1 +1166 2014-11-21 02:52:08.822706 2014-11-21 02:52:08.822706 103 1 +1167 2014-11-21 02:52:08.822706 2014-11-21 02:52:08.822706 104 1 +1168 2014-11-21 02:52:08.822706 2014-11-21 02:52:08.822706 103 2 +1169 2014-11-21 02:52:08.822706 2014-11-21 02:52:08.822706 104 2 +1170 2014-11-21 02:52:08.822706 2014-11-21 02:52:08.822706 105 1 +1171 2014-11-21 02:52:08.822706 2014-11-21 02:52:08.822706 105 2 +870 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 1 1 +871 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 1 2 +872 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 1 3 +873 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 2 1 +874 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 2 2 +875 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 2 3 +876 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 3 1 +877 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 3 2 +878 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 3 3 +879 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 4 1 +880 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 4 2 +881 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 4 3 +882 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 5 1 +883 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 5 2 +884 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 5 3 +885 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 6 1 +888 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 7 1 +889 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 7 2 +890 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 7 3 +891 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 8 1 +892 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 8 2 +893 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 8 3 +894 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 9 1 +895 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 9 2 +896 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 9 3 +897 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 10 1 +898 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 10 2 +899 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 10 3 +900 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 11 1 +901 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 11 2 +902 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 11 3 +903 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 12 1 +904 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 12 2 +905 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 12 3 +906 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 13 1 +907 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 13 2 +908 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 13 3 +909 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 14 1 +910 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 14 2 +911 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 14 3 +912 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 15 1 +913 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 15 2 +914 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 15 3 +915 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 16 1 +916 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 16 2 +917 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 16 3 +918 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 17 1 +919 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 17 2 +920 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 17 3 +921 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 18 1 +922 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 18 2 +923 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 18 3 +924 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 19 1 +925 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 19 2 +926 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 19 3 +927 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 20 1 +928 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 20 2 +929 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 20 3 +930 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 21 1 +931 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 21 2 +932 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 21 3 +933 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 22 1 +934 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 22 2 +935 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 22 3 +936 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 23 1 +937 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 23 2 +938 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 23 3 +939 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 24 1 +940 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 24 2 +941 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 24 3 +942 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 25 1 +943 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 25 2 +944 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 25 3 +945 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 26 1 +946 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 26 2 +947 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 26 3 +948 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 27 1 +949 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 27 2 +950 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 27 3 +951 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 28 1 +952 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 28 2 +953 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 28 3 +954 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 29 1 +955 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 29 2 +956 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 29 3 +957 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 30 1 +958 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 30 2 +959 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 30 3 +960 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 31 1 +961 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 31 2 +962 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 31 3 +963 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 32 1 +964 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 32 2 +965 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 32 3 +966 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 33 1 +967 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 33 2 +968 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 33 3 +969 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 34 1 +970 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 34 2 +971 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 34 3 +972 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 35 1 +973 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 35 2 +974 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 35 3 +975 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 36 1 +976 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 36 2 +977 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 36 3 +978 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 37 1 +979 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 37 2 +980 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 37 3 +981 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 38 1 +982 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 38 2 +983 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 38 3 +984 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 39 1 +985 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 39 2 +986 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 39 3 +987 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 40 1 +988 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 40 2 +989 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 40 3 +990 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 41 1 +991 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 41 2 +992 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 41 3 +993 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 42 1 +994 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 42 2 +995 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 42 3 +996 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 43 1 +997 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 43 2 +998 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 43 3 +999 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 44 1 +1000 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 44 2 +1001 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 44 3 +1002 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 45 1 +1003 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 45 2 +1004 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 45 3 +1005 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 46 1 +1006 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 46 2 +1007 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 46 3 +1008 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 47 1 +1009 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 47 2 +1010 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 47 3 +1011 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 48 1 +1012 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 48 2 +1013 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 48 3 +1014 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 49 1 +1015 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 49 2 +1016 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 49 3 +1017 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 50 1 +1018 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 50 2 +1019 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 50 3 +1020 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 51 1 +1021 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 51 2 +1022 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 51 3 +1023 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 52 1 +1024 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 52 2 +1025 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 52 3 +1026 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 53 1 +1027 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 53 2 +1028 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 53 3 +1029 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 54 1 +1030 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 54 2 +1031 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 54 3 +1032 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 55 1 +1033 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 55 2 +1034 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 55 3 +1035 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 56 1 +1036 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 56 2 +1037 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 56 3 +1038 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 57 1 +1041 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 58 1 +1042 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 58 2 +1043 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 58 3 +1044 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 59 1 +1045 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 59 2 +1046 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 59 3 +1047 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 60 1 +1048 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 60 2 +1049 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 60 3 +1050 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 61 1 +1051 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 61 2 +1052 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 61 3 +1053 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 62 1 +1054 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 62 2 +1055 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 62 3 +1056 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 63 1 +1057 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 63 2 +1058 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 63 3 +1059 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 64 1 +1060 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 64 2 +1061 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 64 3 +1062 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 65 1 +1063 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 65 2 +1064 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 65 3 +1065 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 66 1 +1066 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 66 2 +1067 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 66 3 +1068 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 67 1 +1069 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 67 2 +1070 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 67 3 +1071 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 68 1 +1072 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 68 2 +1073 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 68 3 +1074 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 69 1 +1075 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 69 2 +1076 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 69 3 +1077 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 70 1 +1078 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 70 2 +1079 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 70 3 +1080 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 71 1 +1081 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 71 2 +1082 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 71 3 +1083 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 72 1 +1084 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 72 2 +1085 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 72 3 +1086 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 73 1 +1087 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 73 2 +1088 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 73 3 +1089 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 74 1 +1090 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 74 2 +1091 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 74 3 +1092 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 75 1 +1093 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 75 2 +1094 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 75 3 +1095 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 76 1 +1096 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 76 2 +1097 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 76 3 +1098 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 77 1 +1099 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 77 2 +1100 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 77 3 +1101 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 78 1 +1102 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 78 2 +1103 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 78 3 +1104 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 79 1 +1105 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 79 2 +1106 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 79 3 +1107 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 80 1 +1108 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 80 2 +1109 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 80 3 +1110 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 81 1 +1111 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 81 2 +1112 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 81 3 +1113 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 82 1 +1114 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 82 2 +1115 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 82 3 +1116 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 83 1 +1117 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 83 2 +1118 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 83 3 +1119 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 84 1 +1120 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 84 2 +1121 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 84 3 +1122 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 85 1 +1123 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 85 2 +1124 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 85 3 +1125 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 86 1 +1126 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 86 2 +1127 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 86 3 +1128 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 87 1 +1129 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 87 2 +1130 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 87 3 +1131 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 88 1 +1132 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 88 2 +1133 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 88 3 +1134 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 89 1 +1135 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 89 2 +1136 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 89 3 +1137 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 90 1 +1138 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 90 2 +1139 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 90 3 +1140 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 91 1 +1141 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 91 2 +1142 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 91 3 +1143 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 92 1 +1144 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 92 2 +1145 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 92 3 +1146 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 93 1 +1147 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 93 2 +1148 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 93 3 +1149 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 94 1 +1152 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 95 1 +1153 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 95 2 +1154 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 95 3 +1155 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 96 1 +1156 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 96 2 +1157 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 96 3 +1158 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 97 1 +1159 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 97 2 +1160 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 97 3 +1161 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 98 1 +1162 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 98 2 +1163 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 98 3 +1172 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 106 1 +1173 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 106 2 +1174 2014-11-14 16:23:16.77598 2014-11-14 16:23:16.77598 106 3 +1175 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 106 1 +1176 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 106 2 +1177 2014-08-25 13:14:18.247 2014-08-25 13:14:18.247 106 3 +1178 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 108 1 +1179 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 109 1 +1180 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 110 1 +1181 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 108 2 +1182 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 108 2 +1183 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 108 3 +1184 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 108 3 +1185 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 111 1 +1186 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 111 2 +1187 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 111 3 +1188 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 99 1 +1189 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 112 1 +1190 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 112 2 +1191 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 112 3 +1192 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 113 1 +1193 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 113 2 +1194 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 113 3 +1195 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 114 1 +1196 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 114 2 +1197 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 114 3 +1198 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 24 1 +1199 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 24 2 +1200 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 24 3 +1201 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 115 1 +1202 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 115 2 +1203 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 115 3 +1204 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 116 1 +1205 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 116 2 +1206 2014-11-21 06:46:53.094432 2014-11-21 06:46:53.094432 116 3 +1207 2015-05-09 06:46:53.094432 2015-05-09 06:46:53.094432 117 1 +1208 2015-05-09 06:46:53.094432 2015-05-09 06:46:53.094432 117 2 +1209 2015-05-09 06:46:53.094432 2015-05-09 06:46:53.094432 117 3 +1210 2013-02-07 10:11:00 2015-04-25 19:58:48.8 118 1 +\. + + +-- +-- Name: acl_links_roles_roles_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('acl_links_roles_roles_id_seq', 1210, true); + + +-- +-- Data for Name: activities; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY activities (id, created, modified, board_id, list_id, card_id, user_id, foreign_id, type, comment, revisions, root, freshness_ts, depth, path, materialized_path) FROM stdin; +\. + + +-- +-- Name: activities_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('activities_id_seq', 2, true); + + +-- +-- Name: attachments_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('attachments_id_seq', 1, false); + + +-- +-- Data for Name: board_stars; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY board_stars (id, created, modified, board_id, user_id, is_starred) FROM stdin; +\. + + +-- +-- Data for Name: board_subscribers; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY board_subscribers (id, created, modified, board_id, user_id, is_subscribed) FROM stdin; +\. + + +-- +-- Data for Name: boards; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY boards (id, created, modified, user_id, organization_id, name, board_visibility, background_color, background_picture_url, commenting_permissions, voting_permissions, inivitation_permissions, is_closed, is_allow_organization_members_to_join, boards_user_count, list_count, card_count, boards_subscriber_count, background_pattern_url, boards_star_count, is_show_image_front_of_card, background_picture_path, music_name, music_content) FROM stdin; +\. + + +-- +-- Name: boards_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('boards_id_seq', 2, true); + + +-- +-- Name: boards_stars_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('boards_stars_id_seq', 1, false); + + +-- +-- Name: boards_subscribers_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('boards_subscribers_id_seq', 1, true); + + +-- +-- Data for Name: boards_users; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY boards_users (id, created, modified, board_id, user_id, is_admin) FROM stdin; +\. + + +-- +-- Name: boards_users_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('boards_users_id_seq', 2, true); + + +-- +-- Data for Name: card_attachments; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY card_attachments (id, created, modified, card_id, name, path, list_id, board_id, mimetype) FROM stdin; +\. + + +-- +-- Name: card_attachments_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('card_attachments_id_seq', 1, true); + + +-- +-- Data for Name: card_subscribers; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY card_subscribers (id, created, modified, card_id, user_id, is_subscribed) FROM stdin; +\. + + +-- +-- Data for Name: card_voters; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY card_voters (id, created, modified, card_id, user_id) FROM stdin; +\. + + +-- +-- Name: card_voters_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('card_voters_id_seq', 1, true); + + +-- +-- Data for Name: cards; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY cards (id, created, modified, board_id, list_id, name, description, due_date, "position", is_archived, attachment_count, checklist_count, checklist_item_count, checklist_item_completed_count, label_count, cards_user_count, cards_subscriber_count, card_voter_count, activity_count, user_id, is_deleted, comment_count) FROM stdin; +\. + + +-- +-- Name: cards_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('cards_id_seq', 1, true); + + +-- +-- Data for Name: cards_labels; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY cards_labels (id, created, modified, label_id, card_id, list_id, board_id) FROM stdin; +\. + + +-- +-- Name: cards_labels_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('cards_labels_id_seq', 1, true); + + +-- +-- Name: cards_subscribers_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('cards_subscribers_id_seq', 1, true); + + +-- +-- Data for Name: cards_users; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY cards_users (id, created, modified, card_id, user_id) FROM stdin; +\. + + +-- +-- Name: cards_users_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('cards_users_id_seq', 1, true); + + +-- +-- Data for Name: checklist_items; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY checklist_items (id, created, modified, user_id, card_id, checklist_id, name, is_completed, "position") FROM stdin; +\. + + +-- +-- Name: checklist_items_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('checklist_items_id_seq', 1, true); + + +-- +-- Data for Name: checklists; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY checklists (id, created, modified, user_id, card_id, name, checklist_item_count, checklist_item_completed_count, "position") FROM stdin; +\. + + +-- +-- Name: checklists_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('checklists_id_seq', 1, true); + + +-- +-- Data for Name: cities; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY cities (id, created, modified, country_id, state_id, latitude, longitude, name, is_active) FROM stdin; +1 2015-05-21 11:45:47.245 2015-05-21 11:45:47.245 102 1 20 77 undefined f +\. + + +-- +-- Name: cities_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('cities_id_seq', 15178, false); + + +-- +-- Name: cities_id_seq1; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('cities_id_seq1', 1, true); + + +-- +-- Data for Name: countries; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY countries (id, iso_alpha2, iso_alpha3, iso_numeric, fips_code, name, capital, areainsqkm, population, continent, tld, currency, currencyname, phone, postalcodeformat, postalcoderegex, languages, geonameid, neighbours, equivalentfipscode, created, iso2, iso3, modified) FROM stdin; +1 AF AFG 4 AF Afghanistan Kabul 647500 29121286 AS .af AFN Afghani 93 fa-AF,ps,uz-AF,tk 1149361 TM,CN,IR,TJ,PK,UZ 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +2 AX ALA 248 Aland Islands Mariehamn 0 26711 EU .ax EUR Euro +358-18 sv-AX 661882 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +4 DZ DZA 12 AG Algeria Algiers 2381740 34586184 AF .dz DZD Dinar 213 ##### ^(d{5})$ ar-DZ 2589581 NE,EH,LY,MR,TN,MA,ML 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +5 AS ASM 16 AQ American Samoa Pago Pago 199 57881 OC .as USD Dollar +1-684 en-AS,sm,to 5880801 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +6 AD AND 20 AN Andorra Andorra la Vella 468 84000 EU .ad EUR Euro 376 AD### ^(?:AD)*(d{3})$ ca 3041565 ES,FR 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +7 AO AGO 24 AO Angola Luanda 1246700 13068161 AF .ao AOA Kwanza 244 pt-AO 3351879 CD,NA,ZM,CG 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +8 AI AIA 660 AV Anguilla The Valley 102 13254 NA .ai XCD Dollar +1-264 en-AI 3573511 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +9 AQ ATA 10 AY Antarctica 14000000 0 AN .aq 6697173 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +10 AG ATG 28 AC Antigua and Barbuda St. John's 443 86754 NA .ag XCD Dollar +1-268 en-AG 3576396 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +11 AR ARG 32 AR Argentina Buenos Aires 2766890 41343201 SA .ar ARS Peso 54 @####@@@ ^([A-Z]d{4}[A-Z]{3}) es-AR,en,it,de,fr,gn 3865483 CL,BO,UY,PY,BR 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +12 AM ARM 51 AM Armenia Yerevan 29800 2968000 AS .am AMD Dram 374 ###### ^(d{6})$ hy 174982 GE,IR,AZ,TR 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +13 AW ABW 533 AA Aruba Oranjestad 193 71566 NA .aw AWG Guilder 297 nl-AW,es,en 3577279 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +14 AU AUS 36 AS Australia Canberra 7686850 21515754 OC .au AUD Dollar 61 #### ^(d{4})$ en-AU 2077456 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +15 AT AUT 40 AU Austria Vienna 83858 8205000 EU .at EUR Euro 43 #### ^(d{4})$ de-AT,hr,hu,sl 2782113 CH,DE,HU,SK,CZ,IT,SI 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +16 AZ AZE 31 AJ Azerbaijan Baku 86600 8303512 AS .az AZN Manat 994 AZ #### ^(?:AZ)*(d{4})$ az,ru,hy 587116 GE,IR,AM,TR,RU 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +17 BS BHS 44 BF Bahamas Nassau 13940 301790 NA .bs BSD Dollar +1-242 en-BS 3572887 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +18 BH BHR 48 BA Bahrain Manama 665 738004 AS .bh BHD Dinar 973 ####|### ^(d{3}d?)$ ar-BH,en,fa,ur 290291 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +19 BD BGD 50 BG Bangladesh Dhaka 144000 156118464 AS .bd BDT Taka 880 #### ^(d{4})$ bn-BD,en 1210997 MM,IN 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +20 BB BRB 52 BB Barbados Bridgetown 431 285653 NA .bb BBD Dollar +1-246 BB##### ^(?:BB)*(d{5})$ en-BB 3374084 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +21 BY BLR 112 BO Belarus Minsk 207600 9685000 EU .by BYR Ruble 375 ###### ^(d{6})$ be,ru 630336 PL,LT,UA,RU,LV 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +22 BE BEL 56 BE Belgium Brussels 30510 10403000 EU .be EUR Euro 32 #### ^(d{4})$ nl-BE,fr-BE,de-BE 2802361 DE,NL,LU,FR 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +23 BZ BLZ 84 BH Belize Belmopan 22966 314522 NA .bz BZD Dollar 501 en-BZ,es 3582678 GT,MX 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +24 BJ BEN 204 BN Benin Porto-Novo 112620 9056010 AF .bj XOF Franc 229 fr-BJ 2395170 NE,TG,BF,NG 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +25 BM BMU 60 BD Bermuda Hamilton 53 65365 NA .bm BMD Dollar +1-441 @@ ## ^([A-Z]{2}d{2})$ en-BM,pt 3573345 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +26 BT BTN 64 BT Bhutan Thimphu 47000 699847 AS .bt BTN Ngultrum 975 dz 1252634 CN,IN 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +27 BO BOL 68 BL Bolivia Sucre 1098580 9947418 SA .bo BOB Boliviano 591 es-BO,qu,ay 3923057 PE,CL,PY,BR,AR 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +28 BQ BES 535 Bonaire, Saint Eustatius and Saba 0 18012 NA .bq USD Dollar 599 nl,pap,en 7626844 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +29 BA BIH 70 BK Bosnia and Herzegovina Sarajevo 51129 4590000 EU .ba BAM Marka 387 ##### ^(d{5})$ bs,hr-BA,sr-BA 3277605 CS,HR,ME,RS 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +30 BW BWA 72 BC Botswana Gaborone 600370 2029307 AF .bw BWP Pula 267 en-BW,tn-BW 933860 ZW,ZA,NA 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +31 BV BVT 74 BV Bouvet Island 0 0 AN .bv NOK Krone 3371123 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +33 IO IOT 86 IO British Indian Ocean Territory Diego Garcia 60 4000 AS .io USD Dollar 246 en-IO 1282588 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +34 VG VGB 92 VI British Virgin Islands Road Town 153 21730 NA .vg USD Dollar +1-284 en-VG 3577718 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +35 BN BRN 96 BX Brunei Bandar Seri Begawan 5770 395027 AS .bn BND Dollar 673 @@#### ^([A-Z]{2}d{4})$ ms-BN,en-BN 1820814 MY 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +36 BG BGR 100 BU Bulgaria Sofia 110910 7148785 EU .bg BGN Lev 359 #### ^(d{4})$ bg,tr-BG 732800 MK,GR,RO,CS,TR,RS 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +37 BF BFA 854 UV Burkina Faso Ouagadougou 274200 16241811 AF .bf XOF Franc 226 fr-BF 2361809 NE,BJ,GH,CI,TG,ML 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +38 BI BDI 108 BY Burundi Bujumbura 27830 9863117 AF .bi BIF Franc 257 fr-BI,rn 433561 TZ,CD,RW 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +39 KH KHM 116 CB Cambodia Phnom Penh 181040 14453680 AS .kh KHR Riels 855 ##### ^(d{5})$ km,fr,en 1831722 LA,TH,VN 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +40 CM CMR 120 CM Cameroon Yaounde 475440 19294149 AF .cm XAF Franc 237 en-CM,fr-CM 2233387 TD,CF,GA,GQ,CG,NG 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +41 CA CAN 124 CA Canada Ottawa 9984670 33679000 NA .ca CAD Dollar 1 @#@ #@# ^([a-zA-Z]d[a-zA-Z]d en-CA,fr-CA,iu 6251999 US 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +42 CV CPV 132 CV Cape Verde Praia 4033 508659 AF .cv CVE Escudo 238 #### ^(d{4})$ pt-CV 3374766 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +43 KY CYM 136 CJ Cayman Islands George Town 262 44270 NA .ky KYD Dollar +1-345 en-KY 3580718 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +44 CF CAF 140 CT Central African Republic Bangui 622984 4844927 AF .cf XAF Franc 236 fr-CF,sg,ln,kg 239880 TD,SD,CD,SS,CM,CG 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +45 TD TCD 148 CD Chad N'Djamena 1284000 10543464 AF .td XAF Franc 235 fr-TD,ar-TD,sre 2434508 NE,LY,CF,SD,CM,NG 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +46 CL CHL 152 CI Chile Santiago 756950 16746491 SA .cl CLP Peso 56 ####### ^(d{7})$ es-CL 3895114 PE,BO,AR 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +47 CN CHN 156 CH China Beijing 9596960 1330044000 AS .cn CNY Yuan Renminbi 86 ###### ^(d{6})$ zh-CN,yue,wuu,dta,ug,za 1814991 LA,BT,TJ,KZ,MN,AF,NP 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +48 CX CXR 162 KT Christmas Island Flying Fish Cove 135 1500 AS .cx AUD Dollar 61 #### ^(d{4})$ en,zh,ms-CC 2078138 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +49 CC CCK 166 CK Cocos Islands West Island 14 628 AS .cc AUD Dollar 61 ms-CC,en 1547376 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +50 CO COL 170 CO Colombia Bogota 1138910 44205293 SA .co COP Peso 57 es-CO 3686110 EC,PE,PA,BR,VE 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +51 KM COM 174 CN Comoros Moroni 2170 773407 AF .km KMF Franc 269 ar,fr-KM 921929 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +52 CK COK 184 CW Cook Islands Avarua 240 21388 OC .ck NZD Dollar 682 en-CK,mi 1899402 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +53 CR CRI 188 CS Costa Rica San Jose 51100 4516220 NA .cr CRC Colon 506 #### ^(d{4})$ es-CR,en 3624060 PA,NI 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +54 HR HRV 191 HR Croatia Zagreb 56542 4491000 EU .hr HRK Kuna 385 HR-##### ^(?:HR)*(d{5})$ hr-HR,sr 3202326 HU,SI,CS,BA,ME,RS 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +55 CU CUB 192 CU Cuba Havana 110860 11423000 NA .cu CUP Peso 53 CP ##### ^(?:CP)*(d{5})$ es-CU 3562981 US 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +56 CW CUW 531 UC Curacao Willemstad 0 141766 NA .cw ANG Guilder 599 nl,pap 7626836 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +57 CY CYP 196 CY Cyprus Nicosia 9250 1102677 EU .cy EUR Euro 357 #### ^(d{4})$ el-CY,tr-CY,en 146669 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +58 CZ CZE 203 EZ Czech Republic Prague 78866 10476000 EU .cz CZK Koruna 420 ### ## ^(d{5})$ cs,sk 3077311 PL,DE,SK,AT 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +59 CD COD 180 CG Democratic Republic of the Congo Kinshasa 2345410 70916439 AF .cd CDF Franc 243 fr-CD,ln,kg 203312 TZ,CF,SS,RW,ZM,BI,UG 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +60 DK DNK 208 DA Denmark Copenhagen 43094 5484000 EU .dk DKK Krone 45 #### ^(d{4})$ da-DK,en,fo,de-DK 2623032 DE 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +61 DJ DJI 262 DJ Djibouti Djibouti 23000 740528 AF .dj DJF Franc 253 fr-DJ,ar,so-DJ,aa 223816 ER,ET,SO 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +62 DM DMA 212 DO Dominica Roseau 754 72813 NA .dm XCD Dollar +1-767 en-DM 3575830 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +63 DO DOM 214 DR Dominican Republic Santo Domingo 48730 9823821 NA .do DOP Peso +1-809 and ##### ^(d{5})$ es-DO 3508796 HT 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +64 TL TLS 626 TT East Timor Dili 15007 1154625 OC .tl USD Dollar 670 tet,pt-TL,id,en 1966436 ID 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +65 EC ECU 218 EC Ecuador Quito 283560 14790608 SA .ec USD Dollar 593 @####@ ^([a-zA-Z]d{4}[a-zA- es-EC 3658394 PE,CO 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +66 EG EGY 818 EG Egypt Cairo 1001450 80471869 AF .eg EGP Pound 20 ##### ^(d{5})$ ar-EG,en,fr 357994 LY,SD,IL 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +67 SV SLV 222 ES El Salvador San Salvador 21040 6052064 NA .sv USD Dollar 503 CP #### ^(?:CP)*(d{4})$ es-SV 3585968 GT,HN 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +68 GQ GNQ 226 EK Equatorial Guinea Malabo 28051 1014999 AF .gq XAF Franc 240 es-GQ,fr 2309096 GA,CM 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +69 ER ERI 232 ER Eritrea Asmara 121320 5792984 AF .er ERN Nakfa 291 aa-ER,ar,tig,kun,ti-ER 338010 ET,SD,DJ 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +70 EE EST 233 EN Estonia Tallinn 45226 1291170 EU .ee EUR Euro 372 ##### ^(d{5})$ et,ru 453733 RU,LV 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +71 ET ETH 231 ET Ethiopia Addis Ababa 1127127 88013491 AF .et ETB Birr 251 #### ^(d{4})$ am,en-ET,om-ET,ti-ET,so-ET,sid 337996 ER,KE,SD,SS,SO,DJ 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +72 FK FLK 238 FK Falkland Islands Stanley 12173 2638 SA .fk FKP Pound 500 en-FK 3474414 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +73 FO FRO 234 FO Faroe Islands Torshavn 1399 48228 EU .fo DKK Krone 298 FO-### ^(?:FO)*(d{3})$ fo,da-FO 2622320 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +74 FJ FJI 242 FJ Fiji Suva 18270 875983 OC .fj FJD Dollar 679 en-FJ,fj 2205218 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +75 FI FIN 246 FI Finland Helsinki 337030 5244000 EU .fi EUR Euro 358 ##### ^(?:FI)*(d{5})$ fi-FI,sv-FI,smn 660013 NO,RU,SE 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +76 FR FRA 250 FR France Paris 547030 64768389 EU .fr EUR Euro 33 ##### ^(d{5})$ fr-FR,frp,br,co,ca,eu,oc 3017382 CH,DE,BE,LU,IT,AD,MC 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +77 GF GUF 254 FG French Guiana Cayenne 91000 195506 SA .gf EUR Euro 594 ##### ^((97)|(98)3d{2})$ fr-GF 3381670 SR,BR 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +78 PF PYF 258 FP French Polynesia Papeete 4167 270485 OC .pf XPF Franc 689 ##### ^((97)|(98)7d{2})$ fr-PF,ty 4030656 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +79 TF ATF 260 FS French Southern Territories Port-aux-Francais 7829 140 AN .tf EUR Euro fr 1546748 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +80 GA GAB 266 GB Gabon Libreville 267667 1545255 AF .ga XAF Franc 241 fr-GA 2400553 CM,GQ,CG 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +81 GM GMB 270 GA Gambia Banjul 11300 1593256 AF .gm GMD Dalasi 220 en-GM,mnk,wof,wo,ff 2413451 SN 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +82 GE GEO 268 GG Georgia Tbilisi 69700 4630000 AS .ge GEL Lari 995 #### ^(d{4})$ ka,ru,hy,az 614540 AM,AZ,TR,RU 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +83 DE DEU 276 GM Germany Berlin 357021 81802257 EU .de EUR Euro 49 ##### ^(d{5})$ de 2921044 CH,PL,NL,DK,BE,CZ,LU 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +84 GH GHA 288 GH Ghana Accra 239460 24339838 AF .gh GHS Cedi 233 en-GH,ak,ee,tw 2300660 CI,TG,BF 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +85 GI GIB 292 GI Gibraltar Gibraltar 6.5 27884 EU .gi GIP Pound 350 en-GI,es,it,pt 2411586 ES 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +86 GR GRC 300 GR Greece Athens 131940 11000000 EU .gr EUR Euro 30 ### ## ^(d{5})$ el-GR,en,fr 390903 AL,MK,TR,BG 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +87 GL GRL 304 GL Greenland Nuuk 2166086 56375 NA .gl DKK Krone 299 #### ^(d{4})$ kl,da-GL,en 3425505 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +88 GD GRD 308 GJ Grenada St. George's 344 107818 NA .gd XCD Dollar +1-473 en-GD 3580239 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +89 GP GLP 312 GP Guadeloupe Basse-Terre 1780 443000 NA .gp EUR Euro 590 ##### ^((97)|(98)d{3})$ fr-GP 3579143 AN 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +90 GU GUM 316 GQ Guam Hagatna 549 159358 OC .gu USD Dollar +1-671 969## ^(969d{2})$ en-GU,ch-GU 4043988 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +91 GT GTM 320 GT Guatemala Guatemala City 108890 13550440 NA .gt GTQ Quetzal 502 ##### ^(d{5})$ es-GT 3595528 MX,HN,BZ,SV 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +92 GG GGY 831 GK Guernsey St Peter Port 78 65228 EU .gg GBP Pound +44-1481 @# #@@|@## #@@|@@# # ^(([A-Z]d{2}[A-Z]{2} en,fr 3042362 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +93 GN GIN 324 GV Guinea Conakry 245857 10324025 AF .gn GNF Franc 224 fr-GN 2420477 LR,SN,SL,CI,GW,ML 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +94 GW GNB 624 PU Guinea-Bissau Bissau 36120 1565126 AF .gw XOF Franc 245 #### ^(d{4})$ pt-GW,pov 2372248 SN,GN 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +95 GY GUY 328 GY Guyana Georgetown 214970 748486 SA .gy GYD Dollar 592 en-GY 3378535 SR,BR,VE 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +96 HT HTI 332 HA Haiti Port-au-Prince 27750 9648924 NA .ht HTG Gourde 509 HT#### ^(?:HT)*(d{4})$ ht,fr-HT 3723988 DO 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +97 HM HMD 334 HM Heard Island and McDonald Islands 412 0 AN .hm AUD Dollar 1547314 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +98 HN HND 340 HO Honduras Tegucigalpa 112090 7989415 NA .hn HNL Lempira 504 @@#### ^([A-Z]{2}d{4})$ es-HN 3608932 GT,NI,SV 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +99 HK HKG 344 HK Hong Kong Hong Kong 1092 6898686 AS .hk HKD Dollar 852 zh-HK,yue,zh,en 1819730 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +100 HU HUN 348 HU Hungary Budapest 93030 9930000 EU .hu HUF Forint 36 #### ^(d{4})$ hu-HU 719819 SK,SI,RO,UA,CS,HR,AT 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +101 IS ISL 352 IC Iceland Reykjavik 103000 308910 EU .is ISK Krona 354 ### ^(d{3})$ is,en,de,da,sv,no 2629691 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +102 IN IND 356 IN India New Delhi 3287590 1173108018 AS .in INR Rupee 91 ###### ^(d{6})$ en-IN,hi,bn,te,mr,ta,ur,gu,kn,ml,or,pa,as,bh,sat,ks,ne,sd,kok,doi,mni,sit,sa,fr,lus,inc 1269750 CN,NP,MM,BT,PK,BD 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +103 ID IDN 360 ID Indonesia Jakarta 1919440 242968342 AS .id IDR Rupiah 62 ##### ^(d{5})$ id,en,nl,jv 1643084 PG,TL,MY 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +104 IR IRN 364 IR Iran Tehran 1648000 76923300 AS .ir IRR Rial 98 ########## ^(d{10})$ fa-IR,ku 130758 TM,AF,IQ,AM,PK,AZ,TR 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +105 IQ IRQ 368 IZ Iraq Baghdad 437072 29671605 AS .iq IQD Dinar 964 ##### ^(d{5})$ ar-IQ,ku,hy 99237 SY,SA,IR,JO,TR,KW 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +106 IE IRL 372 EI Ireland Dublin 70280 4622917 EU .ie EUR Euro 353 en-IE,ga-IE 2963597 GB 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +107 IM IMN 833 IM Isle of Man Douglas, Isle of Man 572 75049 EU .im GBP Pound +44-1624 @# #@@|@## #@@|@@# # ^(([A-Z]d{2}[A-Z]{2} en,gv 3042225 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +108 IL ISR 376 IS Israel Jerusalem 20770 7353985 AS .il ILS Shekel 972 ##### ^(d{5})$ he,ar-IL,en-IL, 294640 SY,JO,LB,EG,PS 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +109 IT ITA 380 IT Italy Rome 301230 60340328 EU .it EUR Euro 39 ##### ^(d{5})$ it-IT,de-IT,fr-IT,sc,ca,co,sl 3175395 CH,VA,SI,SM,FR,AT 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +110 CI CIV 384 IV Ivory Coast Yamoussoukro 322460 21058798 AF .ci XOF Franc 225 fr-CI 2287781 LR,GH,GN,BF,ML 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +111 JM JAM 388 JM Jamaica Kingston 10991 2847232 NA .jm JMD Dollar +1-876 en-JM 3489940 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +113 JE JEY 832 JE Jersey Saint Helier 116 90812 EU .je GBP Pound +44-1534 @# #@@|@## #@@|@@# # ^(([A-Z]d{2}[A-Z]{2} en,pt 3042142 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +114 JO JOR 400 JO Jordan Amman 92300 6407085 AS .jo JOD Dinar 962 ##### ^(d{5})$ ar-JO,en 248816 SY,SA,IQ,IL,PS 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +115 KZ KAZ 398 KZ Kazakhstan Astana 2717300 15340000 AS .kz KZT Tenge 7 ###### ^(d{6})$ kk,ru 1522867 TM,CN,KG,UZ,RU 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +116 KE KEN 404 KE Kenya Nairobi 582650 40046566 AF .ke KES Shilling 254 ##### ^(d{5})$ en-KE,sw-KE 192950 ET,TZ,SS,SO,UG 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +117 KI KIR 296 KR Kiribati Tarawa 811 92533 OC .ki AUD Dollar 686 en-KI,gil 4030945 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +118 XK XKX 0 KV Kosovo Pristina 0 1800000 EU EUR Euro sq,sr 831053 RS,AL,MK,ME 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +119 KW KWT 414 KU Kuwait Kuwait City 17820 2789132 AS .kw KWD Dinar 965 ##### ^(d{5})$ ar-KW,en 285570 SA,IQ 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +120 KG KGZ 417 KG Kyrgyzstan Bishkek 198500 5508626 AS .kg KGS Som 996 ###### ^(d{6})$ ky,uz,ru 1527747 CN,TJ,UZ,KZ 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +121 LA LAO 418 LA Laos Vientiane 236800 6368162 AS .la LAK Kip 856 ##### ^(d{5})$ lo,fr,en 1655842 CN,MM,KH,TH,VN 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +122 LV LVA 428 LG Latvia Riga 64589 2217969 EU .lv LVL Lat 371 LV-#### ^(?:LV)*(d{4})$ lv,ru,lt 458258 LT,EE,BY,RU 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +123 LB LBN 422 LE Lebanon Beirut 10400 4125247 AS .lb LBP Pound 961 #### ####|#### ^(d{4}(d{4})?)$ ar-LB,fr-LB,en,hy 272103 SY,IL 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +124 LS LSO 426 LT Lesotho Maseru 30355 1919552 AF .ls LSL Loti 266 ### ^(d{3})$ en-LS,st,zu,xh 932692 ZA 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +125 LR LBR 430 LI Liberia Monrovia 111370 3685076 AF .lr LRD Dollar 231 #### ^(d{4})$ en-LR 2275384 SL,CI,GN 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +126 LY LBY 434 LY Libya Tripolis 1759540 6461454 AF .ly LYD Dinar 218 ar-LY,it,en 2215636 TD,NE,DZ,SD,TN,EG 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +127 LI LIE 438 LS Liechtenstein Vaduz 160 35000 EU .li CHF Franc 423 #### ^(d{4})$ de-LI 3042058 CH,AT 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +128 LT LTU 440 LH Lithuania Vilnius 65200 3565000 EU .lt LTL Litas 370 LT-##### ^(?:LT)*(d{5})$ lt,ru,pl 597427 PL,BY,RU,LV 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +129 LU LUX 442 LU Luxembourg Luxembourg 2586 497538 EU .lu EUR Euro 352 #### ^(d{4})$ lb,de-LU,fr-LU 2960313 DE,BE,FR 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +130 MO MAC 446 MC Macao Macao 254 449198 AS .mo MOP Pataca 853 zh,zh-MO,pt 1821275 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +131 MK MKD 807 MK Macedonia Skopje 25333 2061000 EU .mk MKD Denar 389 #### ^(d{4})$ mk,sq,tr,rmm,sr 718075 AL,GR,CS,BG,RS,XK 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +132 MG MDG 450 MA Madagascar Antananarivo 587040 21281844 AF .mg MGA Ariary 261 ### ^(d{3})$ fr-MG,mg 1062947 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +133 MW MWI 454 MI Malawi Lilongwe 118480 15447500 AF .mw MWK Kwacha 265 ny,yao,tum,swk 927384 TZ,MZ,ZM 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +134 MY MYS 458 MY Malaysia Kuala Lumpur 329750 28274729 AS .my MYR Ringgit 60 ##### ^(d{5})$ ms-MY,en,zh,ta,te,ml,pa,th 1733045 BN,TH,ID 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +135 MV MDV 462 MV Maldives Male 300 395650 AS .mv MVR Rufiyaa 960 ##### ^(d{5})$ dv,en 1282028 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +136 ML MLI 466 ML Mali Bamako 1240000 13796354 AF .ml XOF Franc 223 fr-ML,bm 2453866 SN,NE,DZ,CI,GN,MR,BF 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +137 MT MLT 470 MT Malta Valletta 316 403000 EU .mt EUR Euro 356 @@@ ###|@@@ ## ^([A-Z]{3}d{2}d?)$ mt,en-MT 2562770 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +138 MH MHL 584 RM Marshall Islands Majuro 181.30000000000001 65859 OC .mh USD Dollar 692 mh,en-MH 2080185 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +139 MQ MTQ 474 MB Martinique Fort-de-France 1100 432900 NA .mq EUR Euro 596 ##### ^(d{5})$ fr-MQ 3570311 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +140 MR MRT 478 MR Mauritania Nouakchott 1030700 3205060 AF .mr MRO Ouguiya 222 ar-MR,fuc,snk,fr,mey,wo 2378080 SN,DZ,EH,ML 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +141 MU MUS 480 MP Mauritius Port Louis 2040 1294104 AF .mu MUR Rupee 230 en-MU,bho,fr 934292 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +142 YT MYT 175 MF Mayotte Mamoudzou 374 159042 AF .yt EUR Euro 262 ##### ^(d{5})$ fr-YT 1024031 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +143 MX MEX 484 MX Mexico Mexico City 1972550 112468855 NA .mx MXN Peso 52 ##### ^(d{5})$ es-MX 3996063 GT,US,BZ 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +144 FM FSM 583 FM Micronesia Palikir 702 107708 OC .fm USD Dollar 691 ##### ^(d{5})$ en-FM,chk,pon,yap,kos,uli,woe,nkr,kpg 2081918 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +145 MD MDA 498 MD Moldova Chisinau 33843 4324000 EU .md MDL Leu 373 MD-#### ^(?:MD)*(d{4})$ ro,ru,gag,tr 617790 RO,UA 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +146 MC MCO 492 MN Monaco Monaco 1.95 32965 EU .mc EUR Euro 377 ##### ^(d{5})$ fr-MC,en,it 2993457 FR 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +147 MN MNG 496 MG Mongolia Ulan Bator 1565000 3086918 AS .mn MNT Tugrik 976 ###### ^(d{6})$ mn,ru 2029969 CN,RU 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +148 ME MNE 499 MJ Montenegro Podgorica 14026 666730 EU .me EUR Euro 382 ##### ^(d{5})$ sr,hu,bs,sq,hr,rom 3194884 AL,HR,BA,RS,XK 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +149 MS MSR 500 MH Montserrat Plymouth 102 9341 NA .ms XCD Dollar +1-664 en-MS 3578097 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +150 MA MAR 504 MO Morocco Rabat 446550 31627428 AF .ma MAD Dirham 212 ##### ^(d{5})$ ar-MA,fr 2542007 DZ,EH,ES 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +151 MZ MOZ 508 MZ Mozambique Maputo 801590 22061451 AF .mz MZN Metical 258 #### ^(d{4})$ pt-MZ,vmw 1036973 ZW,TZ,SZ,ZA,ZM,MW 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +152 MM MMR 104 BM Myanmar Nay Pyi Taw 678500 53414374 AS .mm MMK Kyat 95 ##### ^(d{5})$ my 1327865 CN,LA,TH,BD,IN 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +153 NA NAM 516 WA Namibia Windhoek 825418 2128471 AF .na NAD Dollar 264 en-NA,af,de,hz,naq 3355338 ZA,BW,ZM,AO 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +154 NR NRU 520 NR Nauru Yaren 21 10065 OC .nr AUD Dollar 674 na,en-NR 2110425 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +155 NP NPL 524 NP Nepal Kathmandu 140800 28951852 AS .np NPR Rupee 977 ##### ^(d{5})$ ne,en 1282988 CN,IN 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +156 NL NLD 528 NL Netherlands Amsterdam 41526 16645000 EU .nl EUR Euro 31 #### @@ ^(d{4}[A-Z]{2})$ nl-NL,fy-NL 2750405 DE,BE 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +157 AN ANT 530 NT Netherlands Antilles Willemstad 960 136197 NA .an ANG Guilder 599 nl-AN,en,es 0 GP 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +158 NC NCL 540 NC New Caledonia Noumea 19060 216494 OC .nc XPF Franc 687 ##### ^(d{5})$ fr-NC 2139685 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +159 NZ NZL 554 NZ New Zealand Wellington 268680 4252277 OC .nz NZD Dollar 64 #### ^(d{4})$ en-NZ,mi 2186224 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +160 NI NIC 558 NU Nicaragua Managua 129494 5995928 NA .ni NIO Cordoba 505 ###-###-# ^(d{7})$ es-NI,en 3617476 CR,HN 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +161 NE NER 562 NG Niger Niamey 1267000 15878271 AF .ne XOF Franc 227 #### ^(d{4})$ fr-NE,ha,kr,dje 2440476 TD,BJ,DZ,LY,BF,NG,ML 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +162 NG NGA 566 NI Nigeria Abuja 923768 154000000 AF .ng NGN Naira 234 ###### ^(d{6})$ en-NG,ha,yo,ig,ff 2328926 TD,NE,BJ,CM 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +163 NU NIU 570 NE Niue Alofi 260 2166 OC .nu NZD Dollar 683 niu,en-NU 4036232 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +164 NF NFK 574 NF Norfolk Island Kingston 34.600000000000001 1828 OC .nf AUD Dollar 672 en-NF 2155115 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +165 KP PRK 408 KN North Korea Pyongyang 120540 22912177 AS .kp KPW Won 850 ###-### ^(d{6})$ ko-KP 1873107 CN,KR,RU 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +166 MP MNP 580 CQ Northern Mariana Islands Saipan 477 53883 OC .mp USD Dollar +1-670 fil,tl,zh,ch-MP,en-MP 4041468 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +167 NO NOR 578 NO Norway Oslo 324220 4985870 EU .no NOK Krone 47 #### ^(d{4})$ no,nb,nn,se,fi 3144096 FI,RU,SE 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +168 OM OMN 512 MU Oman Muscat 212460 2967717 AS .om OMR Rial 968 ### ^(d{3})$ ar-OM,en,bal,ur 286963 SA,YE,AE 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +169 PK PAK 586 PK Pakistan Islamabad 803940 184404791 AS .pk PKR Rupee 92 ##### ^(d{5})$ ur-PK,en-PK,pa,sd,ps,brh 1168579 CN,AF,IR,IN 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +170 PW PLW 585 PS Palau Melekeok 458 19907 OC .pw USD Dollar 680 96940 ^(96940)$ pau,sov,en-PW,tox,ja,fil,zh 1559582 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +171 PS PSE 275 WE Palestinian Territory East Jerusalem 5970 3800000 AS .ps ILS Shekel 970 ar-PS 6254930 JO,IL 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +172 PA PAN 591 PM Panama Panama City 78200 3410676 NA .pa PAB Balboa 507 es-PA,en 3703430 CR,CO 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +173 PG PNG 598 PP Papua New Guinea Port Moresby 462840 6064515 OC .pg PGK Kina 675 ### ^(d{3})$ en-PG,ho,meu,tpi 2088628 ID 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +174 PY PRY 600 PA Paraguay Asuncion 406750 6375830 SA .py PYG Guarani 595 #### ^(d{4})$ es-PY,gn 3437598 BO,BR,AR 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +175 PE PER 604 PE Peru Lima 1285220 29907003 SA .pe PEN Sol 51 es-PE,qu,ay 3932488 EC,CL,BO,BR,CO 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +176 PH PHL 608 RP Philippines Manila 300000 99900177 AS .ph PHP Peso 63 #### ^(d{4})$ tl,en-PH,fil 1694008 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +177 PN PCN 612 PC Pitcairn Adamstown 47 46 OC .pn NZD Dollar 870 en-PN 4030699 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +178 PL POL 616 PL Poland Warsaw 312685 38500000 EU .pl PLN Zloty 48 ##-### ^(d{5})$ pl 798544 DE,LT,SK,CZ,BY,UA,RU 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +179 PT PRT 620 PO Portugal Lisbon 92391 10676000 EU .pt EUR Euro 351 ####-### ^(d{7})$ pt-PT,mwl 2264397 ES 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +180 PR PRI 630 RQ Puerto Rico San Juan 9104 3916632 NA .pr USD Dollar +1-787 and #####-#### ^(d{9})$ en-PR,es-PR 4566966 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +181 QA QAT 634 QA Qatar Doha 11437 840926 AS .qa QAR Rial 974 ar-QA,es 289688 SA 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +182 CG COG 178 CF Republic of the Congo Brazzaville 342000 3039126 AF .cg XAF Franc 242 fr-CG,kg,ln-CG 2260494 CF,GA,CD,CM,AO 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +183 RE REU 638 RE Reunion Saint-Denis 2517 776948 AF .re EUR Euro 262 ##### ^((97)|(98)(4|7|8)d{ fr-RE 935317 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +184 RO ROU 642 RO Romania Bucharest 237500 21959278 EU .ro RON Leu 40 ###### ^(d{6})$ ro,hu,rom 798549 MD,HU,UA,CS,BG,RS 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +185 RU RUS 643 RS Russia Moscow 17100000 140702000 EU .ru RUB Ruble 7 ###### ^(d{6})$ ru,tt,xal,cau,ady,kv,ce,tyv,cv,udm,tut,mns,bua,myv,mdf,chm,ba,inh,tut,kbd,krc,ava,sah,nog 2017370 GE,CN,BY,UA,KZ,LV,PL 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +186 RW RWA 646 RW Rwanda Kigali 26338 11055976 AF .rw RWF Franc 250 rw,en-RW,fr-RW,sw 49518 TZ,CD,BI,UG 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +187 BL BLM 652 TB Saint Barthelemy Gustavia 21 8450 NA .gp EUR Euro 590 ### ### fr 3578476 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +188 SH SHN 654 SH Saint Helena Jamestown 410 7460 AF .sh SHP Pound 290 STHL 1ZZ ^(STHL1ZZ)$ en-SH 3370751 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +189 KN KNA 659 SC Saint Kitts and Nevis Basseterre 261 49898 NA .kn XCD Dollar +1-869 en-KN 3575174 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +190 LC LCA 662 ST Saint Lucia Castries 616 160922 NA .lc XCD Dollar +1-758 en-LC 3576468 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +191 MF MAF 663 RN Saint Martin Marigot 53 35925 NA .gp EUR Euro 590 ### ### fr 3578421 SX 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +192 PM SPM 666 SB Saint Pierre and Miquelon Saint-Pierre 242 7012 NA .pm EUR Euro 508 ##### ^(97500)$ fr-PM 3424932 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +193 VC VCT 670 VC Saint Vincent and the Grenadines Kingstown 389 104217 NA .vc XCD Dollar +1-784 en-VC,fr 3577815 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +194 WS WSM 882 WS Samoa Apia 2944 192001 OC .ws WST Tala 685 sm,en-WS 4034894 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +195 SM SMR 674 SM San Marino San Marino 61.200000000000003 31477 EU .sm EUR Euro 378 4789# ^(4789d)$ it-SM 3168068 IT 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +196 ST STP 678 TP Sao Tome and Principe Sao Tome 1001 175808 AF .st STD Dobra 239 pt-ST 2410758 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +197 SA SAU 682 SA Saudi Arabia Riyadh 1960582 25731776 AS .sa SAR Rial 966 ##### ^(d{5})$ ar-SA 102358 QA,OM,IQ,YE,JO,AE,KW 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +198 SN SEN 686 SG Senegal Dakar 196190 12323252 AF .sn XOF Franc 221 ##### ^(d{5})$ fr-SN,wo,fuc,mnk 2245662 GN,MR,GW,GM,ML 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +199 RS SRB 688 RI Serbia Belgrade 88361 7344847 EU .rs RSD Dinar 381 ###### ^(d{6})$ sr,hu,bs,rom 6290252 AL,HU,MK,RO,HR,BA,BG 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +200 CS SCG 891 YI Serbia and Montenegro Belgrade 102350 10829175 EU .cs RSD Dinar 381 ##### ^(d{5})$ cu,hu,sq,sr 0 AL,HU,MK,RO,HR,BA,BG 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +201 SC SYC 690 SE Seychelles Victoria 455 88340 AF .sc SCR Rupee 248 en-SC,fr-SC 241170 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +202 SL SLE 694 SL Sierra Leone Freetown 71740 5245695 AF .sl SLL Leone 232 en-SL,men,tem 2403846 LR,GN 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +203 SG SGP 702 SN Singapore Singapur 692.70000000000005 4701069 AS .sg SGD Dollar 65 ###### ^(d{6})$ cmn,en-SG,ms-SG,ta-SG,zh-SG 1880251 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +204 SX SXM 534 NN Sint Maarten Philipsburg 0 37429 NA .sx ANG Guilder 599 nl,en 7609695 MF 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +205 SK SVK 703 LO Slovakia Bratislava 48845 5455000 EU .sk EUR Euro 421 ### ## ^(d{5})$ sk,hu 3057568 PL,HU,CZ,UA,AT 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +206 SI SVN 705 SI Slovenia Ljubljana 20273 2007000 EU .si EUR Euro 386 SI- #### ^(?:SI)*(d{4})$ sl,sh 3190538 HU,IT,HR,AT 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +207 SB SLB 90 BP Solomon Islands Honiara 28450 559198 OC .sb SBD Dollar 677 en-SB,tpi 2103350 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +208 SO SOM 706 SO Somalia Mogadishu 637657 10112453 AF .so SOS Shilling 252 @@ ##### ^([A-Z]{2}d{5})$ so-SO,ar-SO,it,en-SO 51537 ET,KE,DJ 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +209 ZA ZAF 710 SF South Africa Pretoria 1219912 49000000 AF .za ZAR Rand 27 #### ^(d{4})$ zu,xh,af,nso,en-ZA,tn,st,ts,ss,ve,nr 953987 ZW,SZ,MZ,BW,NA,LS 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +210 GS SGS 239 SX South Georgia and the South Sandwich Islands Grytviken 3903 30 AN .gs GBP Pound en 3474415 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +211 KR KOR 410 KS South Korea Seoul 98480 48422644 AS .kr KRW Won 82 SEOUL ###-### ^(?:SEOUL)*(d{6})$ ko-KR,en 1835841 KP 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +212 SS SSD 728 OD South Sudan Juba 644329 8260490 AF SSP Pound 211 en 7909807 CD,CF,ET,KE,SD,UG, 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +213 ES ESP 724 SP Spain Madrid 504782 46505963 EU .es EUR Euro 34 ##### ^(d{5})$ es-ES,ca,gl,eu,oc 2510769 AD,PT,GI,FR,MA 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +214 LK LKA 144 CE Sri Lanka Colombo 65610 21513990 AS .lk LKR Rupee 94 ##### ^(d{5})$ si,ta,en 1227603 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +215 SD SDN 729 SU Sudan Khartoum 1861484 35000000 AF .sd SDG Pound 249 ##### ^(d{5})$ ar-SD,en,fia 366755 SS,TD,EG,ET,ER,LY,CF 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +216 SR SUR 740 NS Suriname Paramaribo 163270 492829 SA .sr SRD Dollar 597 nl-SR,en,srn,hns,jv 3382998 GY,BR,GF 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +217 SJ SJM 744 SV Svalbard and Jan Mayen Longyearbyen 62049 2550 EU .sj NOK Krone 47 no,ru 607072 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +218 SZ SWZ 748 WZ Swaziland Mbabane 17363 1354051 AF .sz SZL Lilangeni 268 @### ^([A-Z]d{3})$ en-SZ,ss-SZ 934841 ZA,MZ 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +219 SE SWE 752 SW Sweden Stockholm 449964 9045000 EU .se SEK Krona 46 SE-### ## ^(?:SE)*(d{5})$ sv-SE,se,sma,fi-SE 2661886 NO,FI 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +220 CH CHE 756 SZ Switzerland Berne 41290 7581000 EU .ch CHF Franc 41 #### ^(d{4})$ de-CH,fr-CH,it-CH,rm 2658434 DE,IT,LI,FR,AT 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +221 SY SYR 760 SY Syria Damascus 185180 22198110 AS .sy SYP Pound 963 ar-SY,ku,hy,arc,fr,en 163843 IQ,JO,IL,TR,LB 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +222 TW TWN 158 TW Taiwan Taipei 35980 22894384 AS .tw TWD Dollar 886 ##### ^(d{5})$ zh-TW,zh,nan,hak 1668284 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +223 TJ TJK 762 TI Tajikistan Dushanbe 143100 7487489 AS .tj TJS Somoni 992 ###### ^(d{6})$ tg,ru 1220409 CN,AF,KG,UZ 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +224 TZ TZA 834 TZ Tanzania Dodoma 945087 41892895 AF .tz TZS Shilling 255 sw-TZ,en,ar 149590 MZ,KE,CD,RW,ZM,BI,UG 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +225 TH THA 764 TH Thailand Bangkok 514000 67089500 AS .th THB Baht 66 ##### ^(d{5})$ th,en 1605651 LA,MM,KH,MY 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +226 TG TGO 768 TO Togo Lome 56785 6587239 AF .tg XOF Franc 228 fr-TG,ee,hna,kbp,dag,ha 2363686 BJ,GH,BF 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +227 TK TKL 772 TL Tokelau 10 1466 OC .tk NZD Dollar 690 tkl,en-TK 4031074 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +228 TO TON 776 TN Tonga Nuku'alofa 748 122580 OC .to TOP Pa'anga 676 to,en-TO 4032283 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +229 TT TTO 780 TD Trinidad and Tobago Port of Spain 5128 1228691 NA .tt TTD Dollar +1-868 en-TT,hns,fr,es,zh 3573591 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +230 TN TUN 788 TS Tunisia Tunis 163610 10589025 AF .tn TND Dinar 216 #### ^(d{4})$ ar-TN,fr 2464461 DZ,LY 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +231 TR TUR 792 TU Turkey Ankara 780580 77804122 AS .tr TRY Lira 90 ##### ^(d{5})$ tr-TR,ku,diq,az,av 298795 SY,GE,IQ,IR,GR,AM,AZ 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +232 TM TKM 795 TX Turkmenistan Ashgabat 488100 4940916 AS .tm TMT Manat 993 ###### ^(d{6})$ tk,ru,uz 1218197 AF,IR,UZ,KZ 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +234 TV TUV 798 TV Tuvalu Funafuti 26 10472 OC .tv AUD Dollar 688 tvl,en,sm,gil 2110297 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +235 VI VIR 850 VQ U.S. Virgin Islands Charlotte Amalie 352 108708 NA .vi USD Dollar +1-340 en-VI 4796775 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +236 UG UGA 800 UG Uganda Kampala 236040 33398682 AF .ug UGX Shilling 256 en-UG,lg,sw,ar 226074 TZ,KE,SS,CD,RW 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +237 UA UKR 804 UP Ukraine Kiev 603700 45415596 EU .ua UAH Hryvnia 380 ##### ^(d{5})$ uk,ru-UA,rom,pl,hu 690791 PL,MD,HU,SK,BY,RO,RU 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +238 AE ARE 784 AE United Arab Emirates Abu Dhabi 82880 4975593 AS .ae AED Dirham 971 ar-AE,fa,en,hi,ur 290557 SA,OM 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +241 UM UMI 581 United States Minor Outlying Islands 0 0 OC .um USD Dollar 1 en-UM 5854968 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +242 UY URY 858 UY Uruguay Montevideo 176220 3477000 SA .uy UYU Peso 598 ##### ^(d{5})$ es-UY 3439705 BR,AR 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +243 UZ UZB 860 UZ Uzbekistan Tashkent 447400 27865738 AS .uz UZS Som 998 ###### ^(d{6})$ uz,ru,tg 1512440 TM,AF,KG,TJ,KZ 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +244 VU VUT 548 NH Vanuatu Port Vila 12200 221552 OC .vu VUV Vatu 678 bi,en-VU,fr-VU 2134431 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +245 VA VAT 336 VT Vatican Vatican City 0.44 921 EU .va EUR Euro 379 la,it,fr 3164670 IT 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +246 VE VEN 862 VE Venezuela Caracas 912050 27223228 SA .ve VEF Bolivar 58 #### ^(d{4})$ es-VE 3625428 GY,BR,CO 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +247 VN VNM 704 VM Vietnam Hanoi 329560 89571130 AS .vn VND Dong 84 ###### ^(d{6})$ vi,en,fr,zh,km 1562822 CN,LA,KH 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +248 WF WLF 876 WF Wallis and Futuna Mata Utu 274 16025 OC .wf XPF Franc 681 ##### ^(986d{2})$ wls,fud,fr-WF 4034749 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +240 US USA 840 US UnitedStates Washington 9629091 310232863 NA .us USD Dollar 1 #####-#### ^(d{9})$ en-US,es-US,haw,fr 6252001 CA,MX,CU 2013-02-07 10:11:00 US USA 2013-09-19 16:10:20.878 +250 YE YEM 887 YM Yemen Sanaa 527970 23495361 AS .ye YER Rial 967 ar-YE 69543 SA,OM 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +251 ZM ZMB 894 ZA Zambia Lusaka 752614 13460305 AF .zm ZMK Kwacha 260 ##### ^(d{5})$ en-ZM,bem,loz,lun,lue,ny,toi 895949 ZW,TZ,MZ,CD,NA,MW,AO 2013-02-07 10:11:00 \N \N 2013-02-07 10:11:00 +3 AL ALB 8 AL Albania Tirana 28748 2986952 EU .al ALL Lek 355 sq,el 783754 MK,GR,CS,ME,RS,XK 2013-02-07 10:11:00 2 3 2013-04-11 16:15:53.73 +32 BR BRA 76 BR Brazil Brasilia 8511965 201103330 SA .br BRL Real 55 #####-### ^(d{8})$ pt-BR,es,en,fr 3469034 SR,PE,BO,UY,GY,PY,GF 2013-02-07 10:11:00 BR BRZ 2013-09-19 14:52:25.866 +112 JP JPN 392 JA Japan Tokyo 377835 127288000 AS .jp JPY Yen 81 ###-#### ^(d{7})$ ja 1861060 2013-02-07 10:11:00 JA JAP 2013-09-19 14:53:54.835 +239 GB GBR 826 UK United Kingdom London 244820 62348447 EU .uk GBP Pound 44 @# #@@|@## #@@|@@# # ^(([A-Z]d{2}[A-Z]{2} en-GB,cy-GB,gd 2635167 IE 2013-02-07 10:11:00 UK UKS 2013-09-19 14:55:04.538 +252 ZW ZWE 716 ZI Zimbabwe Harare 390580 0 AF .zw ZWL Dollar 263 en-ZW,sn,nr,nd 878675 ZA,MZ,BW,ZM 2013-02-07 10:11:00 \N \N 2013-11-19 14:03:16.283 +\. + + +-- +-- Name: countries_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('countries_id_seq', 262, false); + + +-- +-- Name: countries_id_seq1; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('countries_id_seq1', 1, false); + + +-- +-- Data for Name: email_templates; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY email_templates (id, created, modified, from_email, reply_to_email, name, description, subject, email_text_content, email_variables, display_name) FROM stdin; +1 2014-05-08 12:13:37.268 2014-05-08 12:13:37.268 ##FROM_EMAIL## ##REPLY_TO_EMAIL## activation We will send this mail, when user registering an account he/she will get an activation request. Please activate your ##SITE_NAME## account Hi ##USERNAME##,\r\n\r\nYour account has been created. Please visit the following URL to activate your account.\r\n\r\n##ACTIVATION_URL##\r\n\r\nThanks,\r\n##SITE_NAME##\r\n##SITE_URL## SITE_URL, SITE_NAME, ACTIVATION_URL, USERNAME Activation +2 2014-05-08 12:14:07.472 2014-05-08 12:14:07.472 ##FROM_EMAIL## ##REPLY_TO_EMAIL## welcome We will send this mail, when user register in this site and get activate. Welcome to ##SITE_NAME## Hi ##USERNAME##,\r\n\r\nWe wish to say a quick hello and thanks for registering at ##SITE_NAME##.\r\n\r\nIf you did not request this account and feel this is an error, please contact us at ##CONTACT_MAIL##\r\n\r\nThanks,\r\n##SITE_NAME##\r\n##SITE_URL## SITE_NAME, SITE_URL, CONTACT_MAIL, USERNAME Welcome +3 2014-05-08 12:13:59.784 2014-05-08 12:13:59.784 ##FROM_EMAIL## ##REPLY_TO_EMAIL## forgetpassword We will send this mail, when user submit the forgot password form Forgot Password Hi ##USERNAME##,\r\n\r\nA password request has been made for your user account at ##SITE_NAME##.\r\n\r\nNew password: ##PASSWORD##\r\n\r\nIf you did not request this action and feel this is in error, please contact us at ##CONTACT_MAIL##.\r\n\r\nThanks,\r\n##SITE_NAME##\r\n##SITE_URL## SITE_NAME, SITE_URL, CONTACT_MAIL, PASSWORD, USERNAME Forgot Password +4 2014-05-08 12:13:50.69 2014-05-08 12:13:50.69 ##FROM_EMAIL## ##REPLY_TO_EMAIL## changepassword We will send this mail to user, when admin change users password. Password changed Hi,\r\n\r\nAdmin reset your password for your ##SITE_NAME## account.\r\n\r\nYour new password: ##PASSWORD##\r\n\r\nThanks,\r\n##SITE_NAME##\r\n##SITE_URL## SITE_NAME, SITE_URL, PASSWORD Change Password +5 2014-05-08 12:14:07.472 2014-05-08 12:14:07.472 ##FROM_EMAIL## ##REPLY_TO_EMAIL## newprojectuser We will send this mail, when user added for board. Welcome to ##SITE_NAME## Hi ##USERNAME##,\r\n\r\n You have been invited by ##CURRENT_USER## to the ##SITE_NAME## board: ##BOARD_NAME##\r\n\r\nPlease visit the following URL.\r\n\r\n##BOARD_URL##\r\n\r\nThanks,\r\n##SITE_NAME##\r\n##SITE_URL## SITE_NAME, SITE_URL, CONTACT_MAIL, USERNAME, CURRENT_USER New Board User +\. + + +-- +-- Name: email_templates_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('email_templates_id_seq', 1, true); + + +-- +-- Data for Name: ips; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY ips (id, created, modified, ip, host, user_agent, "order", city_id, state_id, country_id, latitude, longitude) FROM stdin; +1 2015-05-21 11:45:47.262 2015-05-21 11:45:47.262 ::1 115.111.183.202 Mozilla/5.0 (Windows NT 6.3; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0 0 1 1 102 20 77 +\. + + +-- +-- Name: ips_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('ips_id_seq', 1, true); + + +-- +-- Data for Name: labels; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY labels (id, created, modified, name, card_count) FROM stdin; +\. + + +-- +-- Name: labels_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('labels_id_seq', 1, true); + + +-- +-- Data for Name: list_subscribers; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY list_subscribers (id, created, modified, list_id, user_id, is_subscribed) FROM stdin; +\. + + +-- +-- Name: list_subscribers_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('list_subscribers_id_seq', 1, false); + + +-- +-- Data for Name: lists; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY lists (id, created, modified, board_id, user_id, name, "position", is_archived, card_count, lists_subscriber_count, is_deleted) FROM stdin; +\. + + +-- +-- Name: lists_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('lists_id_seq', 196, true); + + +-- +-- Name: lists_subscribers_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('lists_subscribers_id_seq', 1, true); + + +-- +-- Data for Name: login_types; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY login_types (id, created, modified, name) FROM stdin; +1 2015-04-07 18:42:59.514 2015-04-07 18:42:59.514 LDAP +2 2015-04-07 18:42:59.515 2015-04-07 18:42:59.515 Normal +\. + + +-- +-- Name: login_types_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('login_types_id_seq', 2, true); + + +-- +-- Data for Name: oauth_access_tokens; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY oauth_access_tokens (access_token, client_id, user_id, expires, scope) FROM stdin; +\. + + +-- +-- Data for Name: oauth_authorization_codes; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY oauth_authorization_codes (authorization_code, client_id, user_id, redirect_uri, expires, scope) FROM stdin; +\. + + +-- +-- Data for Name: oauth_clients; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY oauth_clients (client_id, client_secret, redirect_uri, grant_types, scope, user_id) FROM stdin; +7742632501382313 4g7C4l1Y2b0S6a7L8c1E7B3K0e http://localhost/restyaboard/server/php/R/r.php client_credentials password refresh_token 2 +\. + + +-- +-- Data for Name: oauth_jwt; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY oauth_jwt (client_id, subject, public_key) FROM stdin; +\. + + +-- +-- Data for Name: oauth_refresh_tokens; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY oauth_refresh_tokens (refresh_token, client_id, user_id, expires, scope) FROM stdin; +8adf4daa06961f18d2afda535b2f4463193c62f5 7742632501382313 admin 2015-04-16 12:55:32 \N +b43d289f47100a9c70ebd21f31c15db059ef82bb 7742632501382313 admin 2015-06-04 08:15:47 \N +52831802ce6fbd12bfbe34f1def7b679a0822a18 7742632501382313 admin 2015-06-20 07:23:34 \N +\. + + +-- +-- Data for Name: oauth_scopes; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY oauth_scopes (scope, is_default) FROM stdin; +\. + + +-- +-- Data for Name: organizations; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY organizations (id, created, modified, user_id, name, website_url, description, logo_url, organization_visibility, organizations_user_count, board_count) FROM stdin; +\. + + +-- +-- Name: organizations_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('organizations_id_seq', 1, true); + + +-- +-- Data for Name: organizations_users; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY organizations_users (id, created, modified, organization_id, user_id, is_admin) FROM stdin; +\. + + +-- +-- Name: organizations_users_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('organizations_users_id_seq', 1, true); + + +-- +-- Data for Name: roles; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY roles (id, created, modified, name) FROM stdin; +1 2014-09-02 19:43:15.815 2014-09-02 19:43:15.815 admin +2 2014-09-02 19:43:15.815 2014-09-02 19:43:15.815 user +3 2014-09-02 19:43:15.815 2014-09-02 19:43:15.815 guest +\. + + +-- +-- Name: roles_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('roles_id_seq', 3, true); + + +-- +-- Data for Name: setting_categories; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY setting_categories (id, created, modified, parent_id, name, description, "order") FROM stdin; +4 2014-11-21 02:52:08.822706 2014-11-21 02:52:08.822706 2 Server Details \N 0 +5 2014-11-21 02:52:08.822706 2014-11-21 02:52:08.822706 2 Connection Details \N 0 +7 2015-04-25 19:58:48.845 2015-04-25 19:58:48.845 6 Dropbox \N 0 +3 2014-11-21 02:52:08.822706 2014-11-21 02:52:08.822706 \N System \N 1 +2 2014-11-08 02:52:08.822706 2014-04-28 17:01:11 \N LDAP \N 2 +6 2015-04-25 19:58:48.845 2015-04-25 19:58:48.845 \N Third Party API \N 3 +1 2014-04-23 16:30:20.121 2014-04-23 16:30:20.121 \N ElasticSearch 4 +\. + + +-- +-- Name: setting_categories_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('setting_categories_id_seq', 5, true); + + +-- +-- Data for Name: settings; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY settings (id, setting_category_id, setting_category_parent_id, name, value, description, type, options, label, "order") FROM stdin; +2 4 2 LDAP_SERVER \N \N text \N Server 1 +5 4 2 LDAP_PORT \N \N text \N Port 2 +4 4 2 LDAP_PROTOCOL_VERSION \N \N text \N Protocol Version 3 +6 4 2 LDAP_ROOT_DN \N \N text \N User DNS 4 +7 4 2 LDAP_ORGANISATION \N \N text \N Organization 5 +8 5 2 LDAP_UID_FIELD \N \N text \N Domain 6 +9 5 2 LDAP_BIND_DN \N \N text \N Username 7 +10 5 2 LDAP_BIND_PASSWD \N \N text \N Password 8 +3 5 2 LDAP_LOGIN_ENABLED false \N checkbox \N LDAP Login Enabled 9 +11 3 0 SITE_NAME Restyaboard \N text \N Site Name 1 +13 3 0 DEFAULT_FROM_EMAIL board@restya.com \N text \N From Email 2 +19 3 0 LABEL_ICON icon-circle Font\r\nAwesome class name. Recommended: icon-circle, icon-bullhorn,\r\nicon-tag, icon-bookmark, icon-pushpin, icon-star text \N Label Icon 3 +12 3 0 PAGING_COUNT 20 \N text \N Paging Count 4 +21 3 0 SITE_TIMEZONE +0200 \N text \N Site Timezone 5 +18 6 0 DROPBOX_APPKEY \N text \N Dropbox App Key 1 +20 6 0 FLICKR_API_KEY \N text \N Flickr API Key 2 +14 1 0 ELASTICSEARCH_HOST \N text \N Host 1 +15 1 0 SECRET_KEY \N text \N Secret Key 2 +16 1 0 ELASTICSEARCH_URL \N text \N URL 3 +17 1 0 ELASTICSEARCH_INDEX \N text \N Index 4 +23 0 0 elasticsearch.last_processed_activtiy_id 0 \N hidden \N Last Activity ID 3 +\. + + +-- +-- Name: settings_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('settings_id_seq', 21, true); + + +-- +-- Data for Name: states; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY states (id, created, modified, country_id, name, is_active) FROM stdin; +1 2015-05-21 11:45:47.229 2015-05-21 11:45:47.229 102 undefined f +\. + + +-- +-- Name: states_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('states_id_seq', 15138, false); + + +-- +-- Name: states_id_seq1; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('states_id_seq1', 1, true); + + +-- +-- Data for Name: user_logins; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY user_logins (id, created, modified, user_id, ip_id, user_agent) FROM stdin; +1 2015-05-21 11:45:47.266 2015-05-21 11:45:47.266 1 1 Mozilla/5.0 (Windows NT 6.3; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0 +2 2015-06-06 10:53:34.529 2015-06-06 10:53:34.529 1 1 Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.81 Safari/537.36 +\. + + +-- +-- Name: user_logins_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('user_logins_id_seq', 2, true); + + +-- +-- Data for Name: users; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY users (id, created, modified, role_id, username, email, password, full_name, initials, about_me, profile_picture_path, notification_frequency, is_allow_desktop_notification, is_active, is_email_confirmed, created_organization_count, created_board_count, joined_organization_count, list_count, joined_card_count, created_card_count, joined_board_count, checklist_count, checklist_item_completed_count, checklist_item_count, activity_count, card_voter_count, last_activity_id, last_login_date, last_login_ip_id, ip_id, login_type_id, is_productivity_beats, user_login_count) FROM stdin; +2 2014-07-05 11:46:40.804 2014-07-05 11:46:40.804 2 user board+user@restya.com $2y$12$QiJW6TjPKzDZPAuoWEex9OjPHQF33YzfkdC09FhasgPO.MjZ5btKe User U \N \N \N f t t 0 0 0 0 0 0 0 0 0 0 0 0 \N \N \N \N \N f 0 +1 2014-06-03 12:40:41.189 2015-04-02 16:26:03.939 1 admin board@restya.com $2y$12$QiJW6TjPKzDZPAuoWEex9OjPHQF33YzfkdC09FhasgPO.MjZ5btKe New Admin PA Added About Me media/User/1/default-admin-user.png \N f t t 0 0 0 0 0 0 0 0 0 0 0 0 2 2015-06-06 10:53:34.46 1 \N 2 t 2 +\. + + +-- +-- Name: users_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres +-- + +SELECT pg_catalog.setval('users_id_seq', 2, true); + + +-- +-- Name: acl_links_id; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY acl_links + ADD CONSTRAINT acl_links_id PRIMARY KEY (id); + + +-- +-- Name: acl_links_roles_id; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY acl_links_roles + ADD CONSTRAINT acl_links_roles_id PRIMARY KEY (id); + + +-- +-- Name: activities_id; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY activities + ADD CONSTRAINT activities_id PRIMARY KEY (id); + + +-- +-- Name: board_subscribers_id; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY board_subscribers + ADD CONSTRAINT board_subscribers_id PRIMARY KEY (id); + + +-- +-- Name: board_users_id; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY boards_users + ADD CONSTRAINT board_users_id PRIMARY KEY (id); + + +-- +-- Name: boards_id; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY boards + ADD CONSTRAINT boards_id PRIMARY KEY (id); + + +-- +-- Name: card_attachments_id; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY card_attachments + ADD CONSTRAINT card_attachments_id PRIMARY KEY (id); + + +-- +-- Name: card_subscribers_id; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY card_subscribers + ADD CONSTRAINT card_subscribers_id PRIMARY KEY (id); + + +-- +-- Name: card_users_id; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY cards_users + ADD CONSTRAINT card_users_id PRIMARY KEY (id); + + +-- +-- Name: card_voters_id; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY card_voters + ADD CONSTRAINT card_voters_id PRIMARY KEY (id); + + +-- +-- Name: cards_id; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY cards + ADD CONSTRAINT cards_id PRIMARY KEY (id); + + +-- +-- Name: cards_labels_id; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY cards_labels + ADD CONSTRAINT cards_labels_id PRIMARY KEY (id); + + +-- +-- Name: checklist_items_id; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY checklist_items + ADD CONSTRAINT checklist_items_id PRIMARY KEY (id); + + +-- +-- Name: checklists_id; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY checklists + ADD CONSTRAINT checklists_id PRIMARY KEY (id); + + +-- +-- Name: cities_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY cities + ADD CONSTRAINT cities_pkey PRIMARY KEY (id); + + +-- +-- Name: countries_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY countries + ADD CONSTRAINT countries_pkey PRIMARY KEY (id); + + +-- +-- Name: email_templates_id; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY email_templates + ADD CONSTRAINT email_templates_id PRIMARY KEY (id); + + +-- +-- Name: ips_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY ips + ADD CONSTRAINT ips_pkey PRIMARY KEY (id); + + +-- +-- Name: labels_id; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY labels + ADD CONSTRAINT labels_id PRIMARY KEY (id); + + +-- +-- Name: lists_id; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY lists + ADD CONSTRAINT lists_id PRIMARY KEY (id); + + +-- +-- Name: lists_subscribers_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY list_subscribers + ADD CONSTRAINT lists_subscribers_pkey PRIMARY KEY (id); + + +-- +-- Name: oauth_access_tokens_access_token; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY oauth_access_tokens + ADD CONSTRAINT oauth_access_tokens_access_token PRIMARY KEY (access_token); + + +-- +-- Name: oauth_authorization_codes_authorization_code; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY oauth_authorization_codes + ADD CONSTRAINT oauth_authorization_codes_authorization_code PRIMARY KEY (authorization_code); + + +-- +-- Name: oauth_clients_client_id; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY oauth_clients + ADD CONSTRAINT oauth_clients_client_id PRIMARY KEY (client_id); + + +-- +-- Name: oauth_jwt_client_id; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY oauth_jwt + ADD CONSTRAINT oauth_jwt_client_id PRIMARY KEY (client_id); + + +-- +-- Name: oauth_refresh_tokens_refresh_token; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY oauth_refresh_tokens + ADD CONSTRAINT oauth_refresh_tokens_refresh_token PRIMARY KEY (refresh_token); + + +-- +-- Name: organization_users_id; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY organizations_users + ADD CONSTRAINT organization_users_id PRIMARY KEY (id); + + +-- +-- Name: organizations_id; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY organizations + ADD CONSTRAINT organizations_id PRIMARY KEY (id); + + +-- +-- Name: roles_id; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY roles + ADD CONSTRAINT roles_id PRIMARY KEY (id); + + +-- +-- Name: setting_categories_id; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY setting_categories + ADD CONSTRAINT setting_categories_id PRIMARY KEY (id); + + +-- +-- Name: settings_id; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY settings + ADD CONSTRAINT settings_id PRIMARY KEY (id); + + +-- +-- Name: states_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY states + ADD CONSTRAINT states_pkey PRIMARY KEY (id); + + +-- +-- Name: users_id; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: +-- + +ALTER TABLE ONLY users + ADD CONSTRAINT users_id PRIMARY KEY (id); + + +-- +-- Name: acl_links_roles_acl_link_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX acl_links_roles_acl_link_id ON acl_links_roles USING btree (acl_link_id); + + +-- +-- Name: acl_links_roles_role_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX acl_links_roles_role_id ON acl_links_roles USING btree (role_id); + + +-- +-- Name: acl_links_slug; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX acl_links_slug ON acl_links USING btree (slug); + + +-- +-- Name: activities_attachment_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX activities_attachment_id ON activities USING btree (foreign_id); + + +-- +-- Name: activities_board_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX activities_board_id ON activities USING btree (board_id); + + +-- +-- Name: activities_card_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX activities_card_id ON activities USING btree (card_id); + + +-- +-- Name: activities_depth; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX activities_depth ON activities USING btree (depth); + + +-- +-- Name: activities_freshness_ts; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX activities_freshness_ts ON activities USING btree (freshness_ts); + + +-- +-- Name: activities_list_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX activities_list_id ON activities USING btree (list_id); + + +-- +-- Name: activities_materialized_path; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX activities_materialized_path ON activities USING btree (materialized_path); + + +-- +-- Name: activities_path; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX activities_path ON activities USING btree (path); + + +-- +-- Name: activities_root; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX activities_root ON activities USING btree (root); + + +-- +-- Name: activities_type; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX activities_type ON activities USING btree (type); + + +-- +-- Name: activities_user_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX activities_user_id ON activities USING btree (user_id); + + +-- +-- Name: attachments_card_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX attachments_card_id ON card_attachments USING btree (card_id); + + +-- +-- Name: board_stars_board_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX board_stars_board_id ON board_stars USING btree (board_id); + + +-- +-- Name: board_stars_user_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX board_stars_user_id ON board_stars USING btree (user_id); + + +-- +-- Name: board_subscribers_board_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX board_subscribers_board_id ON board_subscribers USING btree (board_id); + + +-- +-- Name: board_subscribers_user_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX board_subscribers_user_id ON board_subscribers USING btree (user_id); + + +-- +-- Name: board_users_board_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX board_users_board_id ON boards_users USING btree (board_id); + + +-- +-- Name: board_users_user_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX board_users_user_id ON boards_users USING btree (user_id); + + +-- +-- Name: boards_organization_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX boards_organization_id ON boards USING btree (organization_id); + + +-- +-- Name: boards_user_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX boards_user_id ON boards USING btree (user_id); + + +-- +-- Name: card_attachments_board_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX card_attachments_board_id ON card_attachments USING btree (board_id); + + +-- +-- Name: card_attachments_list_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX card_attachments_list_id ON card_attachments USING btree (list_id); + + +-- +-- Name: card_subscribers_card_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX card_subscribers_card_id ON card_subscribers USING btree (card_id); + + +-- +-- Name: card_subscribers_user_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX card_subscribers_user_id ON card_subscribers USING btree (user_id); + + +-- +-- Name: card_users_card_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX card_users_card_id ON cards_users USING btree (card_id); + + +-- +-- Name: card_users_user_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX card_users_user_id ON cards_users USING btree (user_id); + + +-- +-- Name: card_voters_card_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX card_voters_card_id ON card_voters USING btree (card_id); + + +-- +-- Name: card_voters_user_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX card_voters_user_id ON card_voters USING btree (user_id); + + +-- +-- Name: cards_board_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX cards_board_id ON cards USING btree (board_id); + + +-- +-- Name: cards_labels_board_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX cards_labels_board_id ON cards_labels USING btree (board_id); + + +-- +-- Name: cards_labels_card_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX cards_labels_card_id ON cards_labels USING btree (card_id); + + +-- +-- Name: cards_labels_label_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX cards_labels_label_id ON cards_labels USING btree (label_id); + + +-- +-- Name: cards_labels_list_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX cards_labels_list_id ON cards_labels USING btree (list_id); + + +-- +-- Name: cards_list_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX cards_list_id ON cards USING btree (list_id); + + +-- +-- Name: cards_user_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX cards_user_id ON cards USING btree (user_id); + + +-- +-- Name: checklist_items_card_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX checklist_items_card_id ON checklist_items USING btree (card_id); + + +-- +-- Name: checklist_items_checklist_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX checklist_items_checklist_id ON checklist_items USING btree (checklist_id); + + +-- +-- Name: checklist_items_user_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX checklist_items_user_id ON checklist_items USING btree (user_id); + + +-- +-- Name: checklists_card_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX checklists_card_id ON checklists USING btree (card_id); + + +-- +-- Name: checklists_user_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX checklists_user_id ON checklists USING btree (user_id); + + +-- +-- Name: email_templates_name; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX email_templates_name ON email_templates USING btree (name); + + +-- +-- Name: labels_name; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX labels_name ON labels USING btree (name); + + +-- +-- Name: list_subscribers_list_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX list_subscribers_list_id ON list_subscribers USING btree (list_id); + + +-- +-- Name: list_subscribers_user_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX list_subscribers_user_id ON list_subscribers USING btree (user_id); + + +-- +-- Name: lists_board_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX lists_board_id ON lists USING btree (board_id); + + +-- +-- Name: lists_user_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX lists_user_id ON lists USING btree (user_id); + + +-- +-- Name: oauth_access_tokens_client_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX oauth_access_tokens_client_id ON oauth_access_tokens USING btree (client_id); + + +-- +-- Name: oauth_access_tokens_user_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX oauth_access_tokens_user_id ON oauth_access_tokens USING btree (user_id); + + +-- +-- Name: oauth_authorization_codes_client_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX oauth_authorization_codes_client_id ON oauth_authorization_codes USING btree (client_id); + + +-- +-- Name: oauth_authorization_codes_user_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX oauth_authorization_codes_user_id ON oauth_authorization_codes USING btree (user_id); + + +-- +-- Name: oauth_clients_user_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX oauth_clients_user_id ON oauth_clients USING btree (user_id); + + +-- +-- Name: oauth_refresh_tokens_client_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX oauth_refresh_tokens_client_id ON oauth_refresh_tokens USING btree (client_id); + + +-- +-- Name: oauth_refresh_tokens_user_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX oauth_refresh_tokens_user_id ON oauth_refresh_tokens USING btree (user_id); + + +-- +-- Name: organization_users_organization_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX organization_users_organization_id ON organizations_users USING btree (organization_id); + + +-- +-- Name: organization_users_user_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX organization_users_user_id ON organizations_users USING btree (user_id); + + +-- +-- Name: organizations_user_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX organizations_user_id ON organizations USING btree (user_id); + + +-- +-- Name: roles_name; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX roles_name ON roles USING btree (name); + + +-- +-- Name: setting_categories_parent_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX setting_categories_parent_id ON setting_categories USING btree (parent_id); + + +-- +-- Name: settings_setting_category_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX settings_setting_category_id ON settings USING btree (setting_category_id); + + +-- +-- Name: settings_setting_category_parent_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX settings_setting_category_parent_id ON settings USING btree (setting_category_parent_id); + + +-- +-- Name: users_email; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX users_email ON users USING btree (email); + + +-- +-- Name: users_last_activity_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX users_last_activity_id ON users USING btree (last_activity_id); + + +-- +-- Name: users_role_id; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX users_role_id ON users USING btree (role_id); + + +-- +-- Name: users_username; Type: INDEX; Schema: public; Owner: postgres; Tablespace: +-- + +CREATE INDEX users_username ON users USING btree (username); + + +-- +-- Name: label_card_count_update; Type: TRIGGER; Schema: public; Owner: postgres +-- + +CREATE TRIGGER label_card_count_update AFTER INSERT OR DELETE OR UPDATE ON cards_labels FOR EACH ROW EXECUTE PROCEDURE label_card_count_update(); + + +-- +-- Name: update_board_count; Type: TRIGGER; Schema: public; Owner: postgres +-- + +CREATE TRIGGER update_board_count AFTER INSERT OR DELETE OR UPDATE ON boards FOR EACH ROW EXECUTE PROCEDURE update_board_count(); + + +-- +-- Name: update_board_star_count; Type: TRIGGER; Schema: public; Owner: postgres +-- + +CREATE TRIGGER update_board_star_count AFTER INSERT OR DELETE OR UPDATE ON board_stars FOR EACH ROW EXECUTE PROCEDURE update_board_star_count(); + + +-- +-- Name: update_board_subscriber_count; Type: TRIGGER; Schema: public; Owner: postgres +-- + +CREATE TRIGGER update_board_subscriber_count AFTER INSERT OR DELETE OR UPDATE ON board_subscribers FOR EACH ROW EXECUTE PROCEDURE update_board_subscriber_count(); + + +-- +-- Name: update_board_user_count; Type: TRIGGER; Schema: public; Owner: postgres +-- + +CREATE TRIGGER update_board_user_count AFTER INSERT OR DELETE OR UPDATE ON boards_users FOR EACH ROW EXECUTE PROCEDURE update_board_user_count(); + + +-- +-- Name: update_card_activity_count; Type: TRIGGER; Schema: public; Owner: postgres +-- + +CREATE TRIGGER update_card_activity_count AFTER INSERT OR DELETE OR UPDATE ON activities FOR EACH ROW EXECUTE PROCEDURE update_card_activity_count(); + + +-- +-- Name: update_card_attachment_count; Type: TRIGGER; Schema: public; Owner: postgres +-- + +CREATE TRIGGER update_card_attachment_count AFTER INSERT OR DELETE OR UPDATE ON card_attachments FOR EACH ROW EXECUTE PROCEDURE update_card_attachment_count(); + + +-- +-- Name: update_card_checklist_count; Type: TRIGGER; Schema: public; Owner: postgres +-- + +CREATE TRIGGER update_card_checklist_count AFTER INSERT OR DELETE OR UPDATE ON checklists FOR EACH ROW EXECUTE PROCEDURE update_card_checklist_count(); + + +-- +-- Name: update_card_checklist_item_count; Type: TRIGGER; Schema: public; Owner: postgres +-- + +CREATE TRIGGER update_card_checklist_item_count AFTER INSERT OR DELETE OR UPDATE ON checklist_items FOR EACH ROW EXECUTE PROCEDURE update_card_checklist_item_count(); + + +-- +-- Name: update_card_count; Type: TRIGGER; Schema: public; Owner: postgres +-- + +CREATE TRIGGER update_card_count AFTER INSERT OR DELETE OR UPDATE ON cards FOR EACH ROW EXECUTE PROCEDURE update_card_count(); + + +-- +-- Name: update_card_subscriber_count; Type: TRIGGER; Schema: public; Owner: postgres +-- + +CREATE TRIGGER update_card_subscriber_count AFTER INSERT OR DELETE OR UPDATE ON card_subscribers FOR EACH ROW EXECUTE PROCEDURE update_card_subscriber_count(); + + +-- +-- Name: update_card_user_count; Type: TRIGGER; Schema: public; Owner: postgres +-- + +CREATE TRIGGER update_card_user_count AFTER INSERT OR DELETE OR UPDATE ON cards_users FOR EACH ROW EXECUTE PROCEDURE update_card_user_count(); + + +-- +-- Name: update_card_voters_count; Type: TRIGGER; Schema: public; Owner: postgres +-- + +CREATE TRIGGER update_card_voters_count AFTER INSERT OR DELETE OR UPDATE ON card_voters FOR EACH ROW EXECUTE PROCEDURE update_card_voters_count(); + + +-- +-- Name: update_comment_count; Type: TRIGGER; Schema: public; Owner: postgres +-- + +CREATE TRIGGER update_comment_count AFTER INSERT OR DELETE OR UPDATE ON activities FOR EACH ROW EXECUTE PROCEDURE update_comment_count(); + + +-- +-- Name: update_list_count; Type: TRIGGER; Schema: public; Owner: postgres +-- + +CREATE TRIGGER update_list_count AFTER INSERT OR DELETE OR UPDATE ON lists FOR EACH ROW EXECUTE PROCEDURE update_list_count(); + + +-- +-- Name: update_list_subscriber_count; Type: TRIGGER; Schema: public; Owner: postgres +-- + +CREATE TRIGGER update_list_subscriber_count AFTER INSERT OR DELETE OR UPDATE ON list_subscribers FOR EACH ROW EXECUTE PROCEDURE update_list_subscriber_count(); + + +-- +-- Name: update_organization_count; Type: TRIGGER; Schema: public; Owner: postgres +-- + +CREATE TRIGGER update_organization_count AFTER INSERT OR DELETE OR UPDATE ON organizations FOR EACH ROW EXECUTE PROCEDURE update_organization_count(); + + +-- +-- Name: update_organization_user_count; Type: TRIGGER; Schema: public; Owner: postgres +-- + +CREATE TRIGGER update_organization_user_count AFTER INSERT OR DELETE OR UPDATE ON organizations_users FOR EACH ROW EXECUTE PROCEDURE update_organization_user_count(); + + +-- +-- Name: update_user_delete; Type: TRIGGER; Schema: public; Owner: postgres +-- + +CREATE TRIGGER update_user_delete AFTER DELETE ON users FOR EACH ROW EXECUTE PROCEDURE update_user_delete(); + + +-- +-- Name: update_users_user_login_count; Type: TRIGGER; Schema: public; Owner: postgres +-- + +CREATE TRIGGER update_users_user_login_count AFTER INSERT OR DELETE OR UPDATE ON user_logins FOR EACH ROW EXECUTE PROCEDURE update_users_user_login_count(); + + +-- +-- Name: cities_country_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY cities + ADD CONSTRAINT cities_country_id_fkey FOREIGN KEY (country_id) REFERENCES countries(id) ON DELETE CASCADE; + + +-- +-- Name: cities_state_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY cities + ADD CONSTRAINT cities_state_id_fkey FOREIGN KEY (state_id) REFERENCES states(id) ON DELETE CASCADE; + + +-- +-- Name: states_country_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY states + ADD CONSTRAINT states_country_id_fkey FOREIGN KEY (country_id) REFERENCES countries(id) ON DELETE CASCADE; + + +-- +-- Name: public; Type: ACL; Schema: -; Owner: postgres +-- + +REVOKE ALL ON SCHEMA public FROM PUBLIC; +REVOKE ALL ON SCHEMA public FROM postgres; +GRANT ALL ON SCHEMA public TO postgres; +GRANT ALL ON SCHEMA public TO PUBLIC; + + +-- +-- Name: acl_links_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE acl_links_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE acl_links_id_seq FROM postgres; +GRANT ALL ON SEQUENCE acl_links_id_seq TO postgres; +GRANT ALL ON SEQUENCE acl_links_id_seq TO restya; + + +-- +-- Name: acl_links; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE acl_links FROM PUBLIC; +REVOKE ALL ON TABLE acl_links FROM postgres; +GRANT ALL ON TABLE acl_links TO postgres; +GRANT ALL ON TABLE acl_links TO restya; + + +-- +-- Name: acl_links_roles_roles_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE acl_links_roles_roles_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE acl_links_roles_roles_id_seq FROM postgres; +GRANT ALL ON SEQUENCE acl_links_roles_roles_id_seq TO postgres; +GRANT ALL ON SEQUENCE acl_links_roles_roles_id_seq TO restya; + + +-- +-- Name: acl_links_roles; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE acl_links_roles FROM PUBLIC; +REVOKE ALL ON TABLE acl_links_roles FROM postgres; +GRANT ALL ON TABLE acl_links_roles TO postgres; +GRANT ALL ON TABLE acl_links_roles TO restya; + + +-- +-- Name: acl_links_listing; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE acl_links_listing FROM PUBLIC; +REVOKE ALL ON TABLE acl_links_listing FROM postgres; +GRANT ALL ON TABLE acl_links_listing TO postgres; +GRANT ALL ON TABLE acl_links_listing TO restya; + + +-- +-- Name: activities_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE activities_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE activities_id_seq FROM postgres; +GRANT ALL ON SEQUENCE activities_id_seq TO postgres; +GRANT ALL ON SEQUENCE activities_id_seq TO restya; + + +-- +-- Name: activities; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE activities FROM PUBLIC; +REVOKE ALL ON TABLE activities FROM postgres; +GRANT ALL ON TABLE activities TO postgres; +GRANT ALL ON TABLE activities TO restya; + + +-- +-- Name: boards_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE boards_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE boards_id_seq FROM postgres; +GRANT ALL ON SEQUENCE boards_id_seq TO postgres; +GRANT ALL ON SEQUENCE boards_id_seq TO restya; + + +-- +-- Name: boards; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE boards FROM PUBLIC; +REVOKE ALL ON TABLE boards FROM postgres; +GRANT ALL ON TABLE boards TO postgres; +GRANT ALL ON TABLE boards TO restya; + + +-- +-- Name: cards_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE cards_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE cards_id_seq FROM postgres; +GRANT ALL ON SEQUENCE cards_id_seq TO postgres; +GRANT ALL ON SEQUENCE cards_id_seq TO restya; + + +-- +-- Name: cards; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE cards FROM PUBLIC; +REVOKE ALL ON TABLE cards FROM postgres; +GRANT ALL ON TABLE cards TO postgres; +GRANT ALL ON TABLE cards TO restya; + + +-- +-- Name: cards_labels_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE cards_labels_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE cards_labels_id_seq FROM postgres; +GRANT ALL ON SEQUENCE cards_labels_id_seq TO postgres; +GRANT ALL ON SEQUENCE cards_labels_id_seq TO restya; + + +-- +-- Name: cards_labels; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE cards_labels FROM PUBLIC; +REVOKE ALL ON TABLE cards_labels FROM postgres; +GRANT ALL ON TABLE cards_labels TO postgres; +GRANT ALL ON TABLE cards_labels TO restya; + + +-- +-- Name: labels_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE labels_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE labels_id_seq FROM postgres; +GRANT ALL ON SEQUENCE labels_id_seq TO postgres; +GRANT ALL ON SEQUENCE labels_id_seq TO restya; + + +-- +-- Name: labels; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE labels FROM PUBLIC; +REVOKE ALL ON TABLE labels FROM postgres; +GRANT ALL ON TABLE labels TO postgres; +GRANT ALL ON TABLE labels TO restya; + + +-- +-- Name: cards_labels_listing; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE cards_labels_listing FROM PUBLIC; +REVOKE ALL ON TABLE cards_labels_listing FROM postgres; +GRANT ALL ON TABLE cards_labels_listing TO postgres; +GRANT ALL ON TABLE cards_labels_listing TO restya; + + +-- +-- Name: checklist_items_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE checklist_items_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE checklist_items_id_seq FROM postgres; +GRANT ALL ON SEQUENCE checklist_items_id_seq TO postgres; +GRANT ALL ON SEQUENCE checklist_items_id_seq TO restya; + + +-- +-- Name: checklist_items; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE checklist_items FROM PUBLIC; +REVOKE ALL ON TABLE checklist_items FROM postgres; +GRANT ALL ON TABLE checklist_items TO postgres; +GRANT ALL ON TABLE checklist_items TO restya; + + +-- +-- Name: checklists_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE checklists_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE checklists_id_seq FROM postgres; +GRANT ALL ON SEQUENCE checklists_id_seq TO postgres; +GRANT ALL ON SEQUENCE checklists_id_seq TO restya; + + +-- +-- Name: checklists; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE checklists FROM PUBLIC; +REVOKE ALL ON TABLE checklists FROM postgres; +GRANT ALL ON TABLE checklists TO postgres; +GRANT ALL ON TABLE checklists TO restya; + + +-- +-- Name: lists_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE lists_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE lists_id_seq FROM postgres; +GRANT ALL ON SEQUENCE lists_id_seq TO postgres; +GRANT ALL ON SEQUENCE lists_id_seq TO restya; + + +-- +-- Name: lists; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE lists FROM PUBLIC; +REVOKE ALL ON TABLE lists FROM postgres; +GRANT ALL ON TABLE lists TO postgres; +GRANT ALL ON TABLE lists TO restya; + + +-- +-- Name: users_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE users_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE users_id_seq FROM postgres; +GRANT ALL ON SEQUENCE users_id_seq TO postgres; +GRANT ALL ON SEQUENCE users_id_seq TO restya; + + +-- +-- Name: users; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE users FROM PUBLIC; +REVOKE ALL ON TABLE users FROM postgres; +GRANT ALL ON TABLE users TO postgres; +GRANT ALL ON TABLE users TO restya; + + +-- +-- Name: activities_listing; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE activities_listing FROM PUBLIC; +REVOKE ALL ON TABLE activities_listing FROM postgres; +GRANT ALL ON TABLE activities_listing TO postgres; +GRANT ALL ON TABLE activities_listing TO restya; + + +-- +-- Name: attachments_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE attachments_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE attachments_id_seq FROM postgres; +GRANT ALL ON SEQUENCE attachments_id_seq TO postgres; +GRANT ALL ON SEQUENCE attachments_id_seq TO restya; + + +-- +-- Name: boards_stars_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE boards_stars_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE boards_stars_id_seq FROM postgres; +GRANT ALL ON SEQUENCE boards_stars_id_seq TO postgres; +GRANT ALL ON SEQUENCE boards_stars_id_seq TO restya; + + +-- +-- Name: board_stars; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE board_stars FROM PUBLIC; +REVOKE ALL ON TABLE board_stars FROM postgres; +GRANT ALL ON TABLE board_stars TO postgres; +GRANT ALL ON TABLE board_stars TO restya; + + +-- +-- Name: boards_subscribers_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE boards_subscribers_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE boards_subscribers_id_seq FROM postgres; +GRANT ALL ON SEQUENCE boards_subscribers_id_seq TO postgres; +GRANT ALL ON SEQUENCE boards_subscribers_id_seq TO restya; + + +-- +-- Name: board_subscribers; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE board_subscribers FROM PUBLIC; +REVOKE ALL ON TABLE board_subscribers FROM postgres; +GRANT ALL ON TABLE board_subscribers TO postgres; +GRANT ALL ON TABLE board_subscribers TO restya; + + +-- +-- Name: boards_labels_listing; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE boards_labels_listing FROM PUBLIC; +REVOKE ALL ON TABLE boards_labels_listing FROM postgres; +GRANT ALL ON TABLE boards_labels_listing TO postgres; +GRANT ALL ON TABLE boards_labels_listing TO restya; + + +-- +-- Name: boards_users_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE boards_users_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE boards_users_id_seq FROM postgres; +GRANT ALL ON SEQUENCE boards_users_id_seq TO postgres; +GRANT ALL ON SEQUENCE boards_users_id_seq TO restya; + + +-- +-- Name: boards_users; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE boards_users FROM PUBLIC; +REVOKE ALL ON TABLE boards_users FROM postgres; +GRANT ALL ON TABLE boards_users TO postgres; +GRANT ALL ON TABLE boards_users TO restya; + + +-- +-- Name: boards_users_listing; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE boards_users_listing FROM PUBLIC; +REVOKE ALL ON TABLE boards_users_listing FROM postgres; +GRANT ALL ON TABLE boards_users_listing TO postgres; +GRANT ALL ON TABLE boards_users_listing TO restya; + + +-- +-- Name: card_attachments_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE card_attachments_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE card_attachments_id_seq FROM postgres; +GRANT ALL ON SEQUENCE card_attachments_id_seq TO postgres; +GRANT ALL ON SEQUENCE card_attachments_id_seq TO restya; + + +-- +-- Name: card_attachments; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE card_attachments FROM PUBLIC; +REVOKE ALL ON TABLE card_attachments FROM postgres; +GRANT ALL ON TABLE card_attachments TO postgres; +GRANT ALL ON TABLE card_attachments TO restya; + + +-- +-- Name: cards_subscribers_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE cards_subscribers_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE cards_subscribers_id_seq FROM postgres; +GRANT ALL ON SEQUENCE cards_subscribers_id_seq TO postgres; +GRANT ALL ON SEQUENCE cards_subscribers_id_seq TO restya; + + +-- +-- Name: card_subscribers; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE card_subscribers FROM PUBLIC; +REVOKE ALL ON TABLE card_subscribers FROM postgres; +GRANT ALL ON TABLE card_subscribers TO postgres; +GRANT ALL ON TABLE card_subscribers TO restya; + + +-- +-- Name: card_voters_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE card_voters_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE card_voters_id_seq FROM postgres; +GRANT ALL ON SEQUENCE card_voters_id_seq TO postgres; +GRANT ALL ON SEQUENCE card_voters_id_seq TO restya; + + +-- +-- Name: card_voters; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE card_voters FROM PUBLIC; +REVOKE ALL ON TABLE card_voters FROM postgres; +GRANT ALL ON TABLE card_voters TO postgres; +GRANT ALL ON TABLE card_voters TO restya; + + +-- +-- Name: card_voters_listing; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE card_voters_listing FROM PUBLIC; +REVOKE ALL ON TABLE card_voters_listing FROM postgres; +GRANT ALL ON TABLE card_voters_listing TO postgres; +GRANT ALL ON TABLE card_voters_listing TO restya; + + +-- +-- Name: cards_users_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE cards_users_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE cards_users_id_seq FROM postgres; +GRANT ALL ON SEQUENCE cards_users_id_seq TO postgres; +GRANT ALL ON SEQUENCE cards_users_id_seq TO restya; + + +-- +-- Name: cards_users; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE cards_users FROM PUBLIC; +REVOKE ALL ON TABLE cards_users FROM postgres; +GRANT ALL ON TABLE cards_users TO postgres; +GRANT ALL ON TABLE cards_users TO restya; + + +-- +-- Name: cards_users_listing; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE cards_users_listing FROM PUBLIC; +REVOKE ALL ON TABLE cards_users_listing FROM postgres; +GRANT ALL ON TABLE cards_users_listing TO postgres; +GRANT ALL ON TABLE cards_users_listing TO restya; + + +-- +-- Name: checklists_listing; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE checklists_listing FROM PUBLIC; +REVOKE ALL ON TABLE checklists_listing FROM postgres; +GRANT ALL ON TABLE checklists_listing TO postgres; +GRANT ALL ON TABLE checklists_listing TO restya; + + +-- +-- Name: cards_listing; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE cards_listing FROM PUBLIC; +REVOKE ALL ON TABLE cards_listing FROM postgres; +GRANT ALL ON TABLE cards_listing TO postgres; +GRANT ALL ON TABLE cards_listing TO restya; + + +-- +-- Name: lists_subscribers_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE lists_subscribers_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE lists_subscribers_id_seq FROM postgres; +GRANT ALL ON SEQUENCE lists_subscribers_id_seq TO postgres; +GRANT ALL ON SEQUENCE lists_subscribers_id_seq TO restya; + + +-- +-- Name: list_subscribers; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE list_subscribers FROM PUBLIC; +REVOKE ALL ON TABLE list_subscribers FROM postgres; +GRANT ALL ON TABLE list_subscribers TO postgres; +GRANT ALL ON TABLE list_subscribers TO restya; + + +-- +-- Name: lists_listing; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE lists_listing FROM PUBLIC; +REVOKE ALL ON TABLE lists_listing FROM postgres; +GRANT ALL ON TABLE lists_listing TO postgres; +GRANT ALL ON TABLE lists_listing TO restya; + + +-- +-- Name: organizations_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE organizations_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE organizations_id_seq FROM postgres; +GRANT ALL ON SEQUENCE organizations_id_seq TO postgres; +GRANT ALL ON SEQUENCE organizations_id_seq TO restya; + + +-- +-- Name: organizations; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE organizations FROM PUBLIC; +REVOKE ALL ON TABLE organizations FROM postgres; +GRANT ALL ON TABLE organizations TO postgres; +GRANT ALL ON TABLE organizations TO restya; + + +-- +-- Name: boards_listing; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE boards_listing FROM PUBLIC; +REVOKE ALL ON TABLE boards_listing FROM postgres; +GRANT ALL ON TABLE boards_listing TO postgres; +GRANT ALL ON TABLE boards_listing TO restya; + + +-- +-- Name: checklist_add_listing; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE checklist_add_listing FROM PUBLIC; +REVOKE ALL ON TABLE checklist_add_listing FROM postgres; +GRANT ALL ON TABLE checklist_add_listing TO postgres; +GRANT ALL ON TABLE checklist_add_listing TO restya; + + +-- +-- Name: cities; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE cities FROM PUBLIC; +REVOKE ALL ON TABLE cities FROM postgres; +GRANT ALL ON TABLE cities TO postgres; +GRANT ALL ON TABLE cities TO restya; + + +-- +-- Name: cities_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE cities_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE cities_id_seq FROM postgres; +GRANT ALL ON SEQUENCE cities_id_seq TO postgres; +GRANT ALL ON SEQUENCE cities_id_seq TO restya; + + +-- +-- Name: cities_id_seq1; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE cities_id_seq1 FROM PUBLIC; +REVOKE ALL ON SEQUENCE cities_id_seq1 FROM postgres; +GRANT ALL ON SEQUENCE cities_id_seq1 TO postgres; +GRANT ALL ON SEQUENCE cities_id_seq1 TO restya; + + +-- +-- Name: countries; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE countries FROM PUBLIC; +REVOKE ALL ON TABLE countries FROM postgres; +GRANT ALL ON TABLE countries TO postgres; +GRANT ALL ON TABLE countries TO restya; + + +-- +-- Name: countries_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE countries_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE countries_id_seq FROM postgres; +GRANT ALL ON SEQUENCE countries_id_seq TO postgres; +GRANT ALL ON SEQUENCE countries_id_seq TO restya; + + +-- +-- Name: countries_id_seq1; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE countries_id_seq1 FROM PUBLIC; +REVOKE ALL ON SEQUENCE countries_id_seq1 FROM postgres; +GRANT ALL ON SEQUENCE countries_id_seq1 TO postgres; +GRANT ALL ON SEQUENCE countries_id_seq1 TO restya; + + +-- +-- Name: email_templates_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE email_templates_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE email_templates_id_seq FROM postgres; +GRANT ALL ON SEQUENCE email_templates_id_seq TO postgres; +GRANT ALL ON SEQUENCE email_templates_id_seq TO restya; + + +-- +-- Name: email_templates; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE email_templates FROM PUBLIC; +REVOKE ALL ON TABLE email_templates FROM postgres; +GRANT ALL ON TABLE email_templates TO postgres; +GRANT ALL ON TABLE email_templates TO restya; + + +-- +-- Name: gadget_users_listing; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE gadget_users_listing FROM PUBLIC; +REVOKE ALL ON TABLE gadget_users_listing FROM postgres; +GRANT ALL ON TABLE gadget_users_listing TO postgres; +GRANT ALL ON TABLE gadget_users_listing TO restya; + + +-- +-- Name: ips_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE ips_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE ips_id_seq FROM postgres; +GRANT ALL ON SEQUENCE ips_id_seq TO postgres; +GRANT ALL ON SEQUENCE ips_id_seq TO restya; + + +-- +-- Name: ips; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE ips FROM PUBLIC; +REVOKE ALL ON TABLE ips FROM postgres; +GRANT ALL ON TABLE ips TO postgres; +GRANT ALL ON TABLE ips TO restya; + + +-- +-- Name: list_subscribers_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE list_subscribers_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE list_subscribers_id_seq FROM postgres; +GRANT ALL ON SEQUENCE list_subscribers_id_seq TO postgres; +GRANT ALL ON SEQUENCE list_subscribers_id_seq TO restya; + + +-- +-- Name: login_types_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE login_types_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE login_types_id_seq FROM postgres; +GRANT ALL ON SEQUENCE login_types_id_seq TO postgres; +GRANT ALL ON SEQUENCE login_types_id_seq TO restya; + + +-- +-- Name: login_types; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE login_types FROM PUBLIC; +REVOKE ALL ON TABLE login_types FROM postgres; +GRANT ALL ON TABLE login_types TO postgres; +GRANT ALL ON TABLE login_types TO restya; + + +-- +-- Name: oauth_access_tokens; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE oauth_access_tokens FROM PUBLIC; +REVOKE ALL ON TABLE oauth_access_tokens FROM postgres; +GRANT ALL ON TABLE oauth_access_tokens TO postgres; +GRANT ALL ON TABLE oauth_access_tokens TO restya; + + +-- +-- Name: oauth_authorization_codes; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE oauth_authorization_codes FROM PUBLIC; +REVOKE ALL ON TABLE oauth_authorization_codes FROM postgres; +GRANT ALL ON TABLE oauth_authorization_codes TO postgres; +GRANT ALL ON TABLE oauth_authorization_codes TO restya; + + +-- +-- Name: oauth_clients; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE oauth_clients FROM PUBLIC; +REVOKE ALL ON TABLE oauth_clients FROM postgres; +GRANT ALL ON TABLE oauth_clients TO postgres; +GRANT ALL ON TABLE oauth_clients TO restya; + + +-- +-- Name: oauth_jwt; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE oauth_jwt FROM PUBLIC; +REVOKE ALL ON TABLE oauth_jwt FROM postgres; +GRANT ALL ON TABLE oauth_jwt TO postgres; +GRANT ALL ON TABLE oauth_jwt TO restya; + + +-- +-- Name: oauth_refresh_tokens; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE oauth_refresh_tokens FROM PUBLIC; +REVOKE ALL ON TABLE oauth_refresh_tokens FROM postgres; +GRANT ALL ON TABLE oauth_refresh_tokens TO postgres; +GRANT ALL ON TABLE oauth_refresh_tokens TO restya; + + +-- +-- Name: oauth_scopes; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE oauth_scopes FROM PUBLIC; +REVOKE ALL ON TABLE oauth_scopes FROM postgres; +GRANT ALL ON TABLE oauth_scopes TO postgres; +GRANT ALL ON TABLE oauth_scopes TO restya; + + +-- +-- Name: organizations_users_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE organizations_users_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE organizations_users_id_seq FROM postgres; +GRANT ALL ON SEQUENCE organizations_users_id_seq TO postgres; +GRANT ALL ON SEQUENCE organizations_users_id_seq TO restya; + + +-- +-- Name: organizations_users; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE organizations_users FROM PUBLIC; +REVOKE ALL ON TABLE organizations_users FROM postgres; +GRANT ALL ON TABLE organizations_users TO postgres; +GRANT ALL ON TABLE organizations_users TO restya; + + +-- +-- Name: organizations_users_listing; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE organizations_users_listing FROM PUBLIC; +REVOKE ALL ON TABLE organizations_users_listing FROM postgres; +GRANT ALL ON TABLE organizations_users_listing TO postgres; +GRANT ALL ON TABLE organizations_users_listing TO restya; + + +-- +-- Name: organizations_listing; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE organizations_listing FROM PUBLIC; +REVOKE ALL ON TABLE organizations_listing FROM postgres; +GRANT ALL ON TABLE organizations_listing TO postgres; +GRANT ALL ON TABLE organizations_listing TO restya; + + +-- +-- Name: roles_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE roles_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE roles_id_seq FROM postgres; +GRANT ALL ON SEQUENCE roles_id_seq TO postgres; +GRANT ALL ON SEQUENCE roles_id_seq TO restya; + + +-- +-- Name: roles; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE roles FROM PUBLIC; +REVOKE ALL ON TABLE roles FROM postgres; +GRANT ALL ON TABLE roles TO postgres; +GRANT ALL ON TABLE roles TO restya; + + +-- +-- Name: role_links_listing; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE role_links_listing FROM PUBLIC; +REVOKE ALL ON TABLE role_links_listing FROM postgres; +GRANT ALL ON TABLE role_links_listing TO postgres; +GRANT ALL ON TABLE role_links_listing TO restya; + + +-- +-- Name: setting_categories; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE setting_categories FROM PUBLIC; +REVOKE ALL ON TABLE setting_categories FROM postgres; +GRANT ALL ON TABLE setting_categories TO postgres; +GRANT ALL ON TABLE setting_categories TO restya; + + +-- +-- Name: setting_categories_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE setting_categories_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE setting_categories_id_seq FROM postgres; +GRANT ALL ON SEQUENCE setting_categories_id_seq TO postgres; +GRANT ALL ON SEQUENCE setting_categories_id_seq TO restya; + + +-- +-- Name: settings_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE settings_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE settings_id_seq FROM postgres; +GRANT ALL ON SEQUENCE settings_id_seq TO postgres; +GRANT ALL ON SEQUENCE settings_id_seq TO restya; + + +-- +-- Name: settings; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE settings FROM PUBLIC; +REVOKE ALL ON TABLE settings FROM postgres; +GRANT ALL ON TABLE settings TO postgres; +GRANT ALL ON TABLE settings TO restya; + + +-- +-- Name: settings_listing; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE settings_listing FROM PUBLIC; +REVOKE ALL ON TABLE settings_listing FROM postgres; +GRANT ALL ON TABLE settings_listing TO postgres; +GRANT ALL ON TABLE settings_listing TO restya; + + +-- +-- Name: simple_board_listing; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE simple_board_listing FROM PUBLIC; +REVOKE ALL ON TABLE simple_board_listing FROM postgres; +GRANT ALL ON TABLE simple_board_listing TO postgres; +GRANT ALL ON TABLE simple_board_listing TO restya; + + +-- +-- Name: states; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE states FROM PUBLIC; +REVOKE ALL ON TABLE states FROM postgres; +GRANT ALL ON TABLE states TO postgres; +GRANT ALL ON TABLE states TO restya; + + +-- +-- Name: states_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE states_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE states_id_seq FROM postgres; +GRANT ALL ON SEQUENCE states_id_seq TO postgres; +GRANT ALL ON SEQUENCE states_id_seq TO restya; + + +-- +-- Name: states_id_seq1; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE states_id_seq1 FROM PUBLIC; +REVOKE ALL ON SEQUENCE states_id_seq1 FROM postgres; +GRANT ALL ON SEQUENCE states_id_seq1 TO postgres; +GRANT ALL ON SEQUENCE states_id_seq1 TO restya; + + +-- +-- Name: user_logins; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE user_logins FROM PUBLIC; +REVOKE ALL ON TABLE user_logins FROM postgres; +GRANT ALL ON TABLE user_logins TO postgres; +GRANT ALL ON TABLE user_logins TO restya; + + +-- +-- Name: user_logins_id_seq; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON SEQUENCE user_logins_id_seq FROM PUBLIC; +REVOKE ALL ON SEQUENCE user_logins_id_seq FROM postgres; +GRANT ALL ON SEQUENCE user_logins_id_seq TO postgres; +GRANT ALL ON SEQUENCE user_logins_id_seq TO restya; + + +-- +-- Name: users_cards_listing; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE users_cards_listing FROM PUBLIC; +REVOKE ALL ON TABLE users_cards_listing FROM postgres; +GRANT ALL ON TABLE users_cards_listing TO postgres; +GRANT ALL ON TABLE users_cards_listing TO restya; + + +-- +-- Name: users_listing; Type: ACL; Schema: public; Owner: postgres +-- + +REVOKE ALL ON TABLE users_listing FROM PUBLIC; +REVOKE ALL ON TABLE users_listing FROM postgres; +GRANT ALL ON TABLE users_listing TO postgres; +GRANT ALL ON TABLE users_listing TO restya; + + +-- +-- PostgreSQL database dump complete +-- +