├── .git-blame-ignore-revs ├── .github ├── assets │ ├── gameplan-hero-dark.png │ └── gameplan-hero-light.png ├── helper │ ├── install.sh │ ├── install_dependencies.sh │ ├── redisearch.so │ └── site_config.json └── workflows │ └── ui-test.yml ├── .gitignore ├── .gitmodules ├── .pre-commit-config.yaml ├── LICENSE ├── MANIFEST.in ├── README.md ├── docker ├── docker-compose.yml └── init.sh ├── frontend ├── .gitignore ├── .prettierrc.json ├── README.md ├── cypress.config.js ├── cypress │ ├── e2e │ │ ├── comment.cy.js │ │ ├── discussion.cy.js │ │ ├── onboarding.cy.js │ │ ├── project.cy.js │ │ ├── task.cy.js │ │ └── team.cy.js │ └── support │ │ ├── commands.js │ │ └── e2e.js ├── index.html ├── lucideIcons.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public │ ├── favicon.png │ └── gameplan-logo.svg ├── src │ ├── App.vue │ ├── components │ │ ├── AboutDialog.vue │ │ ├── ActionSheet.vue │ │ ├── Activity.vue │ │ ├── AddMemberDialog.vue │ │ ├── AddTeamDialog.vue │ │ ├── AppLink.vue │ │ ├── AppSidebar.vue │ │ ├── AppSidebarLink.vue │ │ ├── AssignUser.vue │ │ ├── ChangeSpaceCategoryDialog.vue │ │ ├── ColorPicker.vue │ │ ├── CommandPalette │ │ │ ├── CommandPalette.vue │ │ │ ├── Item.vue │ │ │ ├── ItemProject.vue │ │ │ ├── ItemTeam.vue │ │ │ └── commandPalette.ts │ │ ├── Comment.vue │ │ ├── CommentEditor.vue │ │ ├── CommentsArea.vue │ │ ├── CommentsList.vue │ │ ├── CoverImage.vue │ │ ├── DesktopLayout.vue │ │ ├── DiscussionBreadcrumbs.vue │ │ ├── DiscussionList.vue │ │ ├── DiscussionMeta.vue │ │ ├── DiscussionRow.vue │ │ ├── DiscussionView.vue │ │ ├── DraftDiscussions.vue │ │ ├── DragHandleIcon.vue │ │ ├── DropdownMoreOptions.vue │ │ ├── EditSpaceDialog.vue │ │ ├── EmptyStateBox.vue │ │ ├── GameplanLogo.vue │ │ ├── GameplanLogoType.vue │ │ ├── HomePageSettingsDialog.vue │ │ ├── IconPicker.vue │ │ ├── ImagePreview.vue │ │ ├── InputWithPills.vue │ │ ├── InviteGuestDialog.vue │ │ ├── KeyboardShortcut.vue │ │ ├── LastPostReminder.vue │ │ ├── Link.vue │ │ ├── Links.vue │ │ ├── ManageMembersDialog.vue │ │ ├── MergeSpaceDialog.vue │ │ ├── MobileLayout.vue │ │ ├── NewSpaceDialog.vue │ │ ├── NewTaskDialog │ │ │ ├── NewTaskDialog.vue │ │ │ ├── TaskStatusIcon.vue │ │ │ ├── index.ts │ │ │ └── state.ts │ │ ├── PageHeader.vue │ │ ├── PageList.vue │ │ ├── Pie.vue │ │ ├── Poll.vue │ │ ├── PollEditor.vue │ │ ├── ProfileImageEditor.vue │ │ ├── ReactionFaceIcon.vue │ │ ├── Reactions.vue │ │ ├── ReactionsDesktop.vue │ │ ├── ReactionsMobile.vue │ │ ├── ReadmeEditor.vue │ │ ├── RevisionsDialog.vue │ │ ├── RichQuoteExtension │ │ │ ├── RichQuoteNodeView.vue │ │ │ ├── floating-quote-button.ts │ │ │ ├── rich-quote-node-extension.ts │ │ │ └── useRichQuoteHandler.ts │ │ ├── ScrollBar.vue │ │ ├── ScrollContainer.vue │ │ ├── Settings │ │ │ ├── ArchivedTeams.vue │ │ │ ├── InvitePeople.vue │ │ │ ├── Members.vue │ │ │ ├── SettingsDialog.vue │ │ │ └── SettingsTab.vue │ │ ├── SpaceBreadcrumbs.vue │ │ ├── SpaceOptions.vue │ │ ├── SpaceTabs.vue │ │ ├── Tabs.vue │ │ ├── TaskDetail.vue │ │ ├── TaskList.vue │ │ ├── TeamMembers.vue │ │ ├── TextEditor.vue │ │ ├── TextEditorTaskExtension │ │ │ ├── Component.vue │ │ │ └── index.js │ │ ├── UnsplashImageBrowser.vue │ │ ├── UserAvatar.vue │ │ ├── UserAvatarWithHover.vue │ │ ├── UserDropdown.vue │ │ ├── UserImage.vue │ │ ├── UserInfo.vue │ │ ├── UserProfileLink.vue │ │ └── icons │ │ │ ├── ChevronTriangle.vue │ │ │ ├── Pin.vue │ │ │ └── TaskPriorityIcon.vue │ ├── data │ │ ├── discussions.ts │ │ ├── groupedSpaces.ts │ │ ├── newDoc.js │ │ ├── notifications.ts │ │ ├── projects.js │ │ ├── session.ts │ │ ├── spaces.ts │ │ ├── tags.ts │ │ ├── tasks.ts │ │ ├── teams.ts │ │ └── users.ts │ ├── directives │ │ ├── focus.ts │ │ └── index.ts │ ├── globals.d.ts │ ├── index.css │ ├── main.js │ ├── pages │ │ ├── ComingSoon.vue │ │ ├── Discussions.vue │ │ ├── Home.vue │ │ ├── HomeOverview.vue │ │ ├── Login.vue │ │ ├── MyPages.vue │ │ ├── MyTasks.vue │ │ ├── NewDiscussion.vue │ │ ├── Notifications.vue │ │ ├── Onboarding.vue │ │ ├── Page.vue │ │ ├── PageGrid.vue │ │ ├── People.vue │ │ ├── PersonProfile.vue │ │ ├── PersonProfileAboutMe.vue │ │ ├── PersonProfileBookmarks.vue │ │ ├── PersonProfilePosts.vue │ │ ├── PersonProfileReplies.vue │ │ ├── Project.vue │ │ ├── ProjectDiscussion.vue │ │ ├── ProjectDiscussionNew.vue │ │ ├── ProjectDiscussions.vue │ │ ├── ProjectLayout.vue │ │ ├── ProjectOverview.vue │ │ ├── ProjectOverviewReadme.vue │ │ ├── ProjectPages.vue │ │ ├── ProjectTaskDetail.vue │ │ ├── ProjectTasks.vue │ │ ├── Search.vue │ │ ├── Search2.vue │ │ ├── Space.vue │ │ ├── SpaceDiscussion.vue │ │ ├── SpaceDiscussions.vue │ │ ├── SpaceList.vue │ │ ├── SpacePages.vue │ │ ├── SpaceTasks.vue │ │ ├── Task.vue │ │ ├── Team.vue │ │ ├── TeamDiscussions.vue │ │ ├── TeamLayout.vue │ │ ├── TeamOverview.vue │ │ └── Teams.vue │ ├── router.js │ ├── socket.js │ ├── types │ │ └── doctypes.ts │ └── utils │ │ ├── composables.ts │ │ ├── dayjs.ts │ │ ├── dialogs.tsx │ │ ├── formatters.js │ │ ├── index.ts │ │ ├── resetDataMixin.js │ │ ├── scrollContainer.ts │ │ ├── sidebarResize.ts │ │ └── theme.js ├── tailwind.config.js ├── tsconfig.json ├── vite.config.js └── yarn.lock ├── gameplan ├── __init__.py ├── api.py ├── command_palette.py ├── config │ ├── __init__.py │ ├── desktop.py │ └── docs.py ├── demo │ ├── __init__.py │ ├── demo.py │ ├── discussions_comments.py │ ├── team_projects.py │ └── user.py ├── extends │ └── client.py ├── fixtures │ └── role.json ├── gameplan │ ├── __init__.py │ └── doctype │ │ ├── __init__.py │ │ ├── discourse_id_map │ │ ├── __init__.py │ │ ├── discourse_id_map.js │ │ ├── discourse_id_map.json │ │ ├── discourse_id_map.py │ │ └── test_discourse_id_map.py │ │ ├── gp_activity │ │ ├── __init__.py │ │ ├── gp_activity.js │ │ ├── gp_activity.json │ │ ├── gp_activity.py │ │ └── test_gp_activity.py │ │ ├── gp_bookmark │ │ ├── __init__.py │ │ ├── gp_bookmark.js │ │ ├── gp_bookmark.json │ │ ├── gp_bookmark.py │ │ └── test_gp_bookmark.py │ │ ├── gp_comment │ │ ├── __init__.py │ │ ├── gp_comment.js │ │ ├── gp_comment.json │ │ ├── gp_comment.py │ │ └── test_gp_comment.py │ │ ├── gp_discussion │ │ ├── __init__.py │ │ ├── api.py │ │ ├── gp_discussion.js │ │ ├── gp_discussion.json │ │ ├── gp_discussion.py │ │ ├── patches │ │ │ ├── add_full_text_search_index.py │ │ │ ├── migrate_gp_bookmark_child.py │ │ │ ├── rename_team_project_discussion_to_team_discussion.py │ │ │ ├── rename_team_project_status_update_doctype.py │ │ │ ├── set_last_post.py │ │ │ ├── set_title_slug.py │ │ │ └── update_participants_count.py │ │ └── test_gp_discussion.py │ │ ├── gp_discussion_visit │ │ ├── __init__.py │ │ ├── gp_discussion_visit.js │ │ ├── gp_discussion_visit.json │ │ ├── gp_discussion_visit.py │ │ ├── patches │ │ │ └── add_unique_constraint.py │ │ └── test_gp_discussion_visit.py │ │ ├── gp_draft │ │ ├── __init__.py │ │ ├── gp_draft.js │ │ ├── gp_draft.json │ │ ├── gp_draft.py │ │ └── test_gp_draft.py │ │ ├── gp_followed_project │ │ ├── __init__.py │ │ ├── gp_followed_project.js │ │ ├── gp_followed_project.json │ │ ├── gp_followed_project.py │ │ └── test_gp_followed_project.py │ │ ├── gp_guest_access │ │ ├── __init__.py │ │ ├── gp_guest_access.js │ │ ├── gp_guest_access.json │ │ ├── gp_guest_access.py │ │ └── test_gp_guest_access.py │ │ ├── gp_invitation │ │ ├── __init__.py │ │ ├── gp_invitation.js │ │ ├── gp_invitation.json │ │ ├── gp_invitation.py │ │ └── test_gp_invitation.py │ │ ├── gp_member │ │ ├── __init__.py │ │ ├── gp_member.json │ │ └── gp_member.py │ │ ├── gp_notification │ │ ├── __init__.py │ │ ├── gp_notification.js │ │ ├── gp_notification.json │ │ ├── gp_notification.py │ │ └── test_gp_notification.py │ │ ├── gp_page │ │ ├── __init__.py │ │ ├── gp_page.js │ │ ├── gp_page.json │ │ ├── gp_page.py │ │ └── test_gp_page.py │ │ ├── gp_pinned_project │ │ ├── __init__.py │ │ ├── gp_pinned_project.js │ │ ├── gp_pinned_project.json │ │ ├── gp_pinned_project.py │ │ └── test_gp_pinned_project.py │ │ ├── gp_poll │ │ ├── __init__.py │ │ ├── gp_poll.js │ │ ├── gp_poll.json │ │ ├── gp_poll.py │ │ ├── gp_poll_attributes.py │ │ └── test_gp_poll.py │ │ ├── gp_poll_option │ │ ├── __init__.py │ │ ├── gp_poll_option.json │ │ └── gp_poll_option.py │ │ ├── gp_poll_vote │ │ ├── __init__.py │ │ ├── gp_poll_vote.json │ │ └── gp_poll_vote.py │ │ ├── gp_project │ │ ├── __init__.py │ │ ├── gp_project.js │ │ ├── gp_project.json │ │ ├── gp_project.py │ │ ├── patches │ │ │ ├── __init__.py │ │ │ └── migrate_members_from_team.py │ │ └── test_gp_project.py │ │ ├── gp_project_visit │ │ ├── __init__.py │ │ ├── gp_project_visit.js │ │ ├── gp_project_visit.json │ │ ├── gp_project_visit.py │ │ └── test_gp_project_visit.py │ │ ├── gp_reaction │ │ ├── __init__.py │ │ ├── gp_reaction.js │ │ ├── gp_reaction.json │ │ ├── gp_reaction.py │ │ └── test_gp_reaction.py │ │ ├── gp_search_feedback │ │ ├── __init__.py │ │ ├── gp_search_feedback.js │ │ ├── gp_search_feedback.json │ │ ├── gp_search_feedback.py │ │ └── test_gp_search_feedback.py │ │ ├── gp_tag │ │ ├── __init__.py │ │ ├── gp_tag.js │ │ ├── gp_tag.json │ │ ├── gp_tag.py │ │ └── test_gp_tag.py │ │ ├── gp_tag_link │ │ ├── __init__.py │ │ ├── gp_tag_link.json │ │ └── gp_tag_link.py │ │ ├── gp_task │ │ ├── __init__.py │ │ ├── gp_task.js │ │ ├── gp_task.json │ │ ├── gp_task.py │ │ ├── patches │ │ │ └── set_status.py │ │ └── test_gp_task.py │ │ ├── gp_team │ │ ├── __init__.py │ │ ├── gp_team.js │ │ ├── gp_team.json │ │ ├── gp_team.py │ │ ├── patches │ │ │ └── remove_invited_members.py │ │ └── test_gp_team.py │ │ └── gp_user_profile │ │ ├── __init__.py │ │ ├── gp_user_profile.js │ │ ├── gp_user_profile.json │ │ ├── gp_user_profile.py │ │ ├── patches │ │ ├── create_user_profile.py │ │ ├── set_image.py │ │ ├── set_name.py │ │ └── setup_rembg.py │ │ ├── profile_photo.py │ │ └── test_gp_user_profile.py ├── gemoji.py ├── hooks.py ├── install.py ├── migrate_from_discourse │ ├── __init__.py │ └── emojis.py ├── mixins │ ├── activity.py │ ├── archivable.py │ ├── manage_members.py │ ├── mentions.py │ ├── on_delete.py │ ├── reactions.py │ └── tags.py ├── modules.txt ├── patches.txt ├── patches │ ├── rename_doctypes_with_gp_prefix.py │ └── update_gameplan_roles.py ├── public │ ├── .gitkeep │ └── manifest │ │ ├── apple-icon-180.png │ │ ├── apple-splash-1125-2436.jpg │ │ ├── apple-splash-1136-640.jpg │ │ ├── apple-splash-1170-2532.jpg │ │ ├── apple-splash-1179-2556.jpg │ │ ├── apple-splash-1242-2208.jpg │ │ ├── apple-splash-1242-2688.jpg │ │ ├── apple-splash-1284-2778.jpg │ │ ├── apple-splash-1290-2796.jpg │ │ ├── apple-splash-1334-750.jpg │ │ ├── apple-splash-1536-2048.jpg │ │ ├── apple-splash-1620-2160.jpg │ │ ├── apple-splash-1668-2224.jpg │ │ ├── apple-splash-1668-2388.jpg │ │ ├── apple-splash-1792-828.jpg │ │ ├── apple-splash-2048-1536.jpg │ │ ├── apple-splash-2048-2732.jpg │ │ ├── apple-splash-2160-1620.jpg │ │ ├── apple-splash-2208-1242.jpg │ │ ├── apple-splash-2224-1668.jpg │ │ ├── apple-splash-2388-1668.jpg │ │ ├── apple-splash-2436-1125.jpg │ │ ├── apple-splash-2532-1170.jpg │ │ ├── apple-splash-2556-1179.jpg │ │ ├── apple-splash-2688-1242.jpg │ │ ├── apple-splash-2732-2048.jpg │ │ ├── apple-splash-2778-1284.jpg │ │ ├── apple-splash-2796-1290.jpg │ │ ├── apple-splash-640-1136.jpg │ │ ├── apple-splash-750-1334.jpg │ │ ├── apple-splash-828-1792.jpg │ │ ├── favicon-180.png │ │ ├── favicon-196.png │ │ ├── manifest-icon-192.maskable.png │ │ ├── manifest-icon-512.maskable.png │ │ └── site.webmanifest ├── search.py ├── search2.py ├── templates │ ├── __init__.py │ ├── emails │ │ └── gameplan_invitation.html │ └── pages │ │ └── __init__.py ├── test_api.py ├── unsplash.py ├── utils │ ├── __init__.py │ ├── fts.py │ ├── search.py │ └── utils.py └── www │ ├── __init__.py │ └── g.py ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml.disabled ├── pyproject.toml ├── scripts ├── install-pnpm.sh └── manage_workspaces.sh └── yarn.lock /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # prettier formatting with printWidth 100 and tailwindcss plugin 2 | 46be8a13f45b5b60caee4f2023931beee7691af8 3 | 4 | # format using pre-commit 5 | e0dece22c8d85b8495692c7efd64f3afc1b6b7ad -------------------------------------------------------------------------------- /.github/assets/gameplan-hero-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/.github/assets/gameplan-hero-dark.png -------------------------------------------------------------------------------- /.github/assets/gameplan-hero-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/.github/assets/gameplan-hero-light.png -------------------------------------------------------------------------------- /.github/helper/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | cd ~ || exit 4 | 5 | echo "Setting Up Bench..." 6 | 7 | pip install frappe-bench 8 | bench -v init frappe-bench --skip-assets --python "$(which python)" 9 | cd ./frappe-bench || exit 10 | 11 | bench -v setup requirements 12 | 13 | echo "Setting Up Gameplan App..." 14 | bench get-app gameplan "${GITHUB_WORKSPACE}" 15 | 16 | echo "Setting Up Sites & Database..." 17 | 18 | mkdir ~/frappe-bench/sites/gameplan.test 19 | cp "${GITHUB_WORKSPACE}/.github/helper/site_config.json" ~/frappe-bench/sites/gameplan.test/site_config.json 20 | 21 | mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "SET GLOBAL character_set_server = 'utf8mb4'"; 22 | mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"; 23 | 24 | mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "CREATE DATABASE test_gameplan"; 25 | mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "CREATE USER 'test_gameplan'@'localhost' IDENTIFIED BY 'test_gameplan'"; 26 | mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "GRANT ALL PRIVILEGES ON \`test_gameplan\`.* TO 'test_gameplan'@'localhost'"; 27 | 28 | mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "FLUSH PRIVILEGES"; 29 | 30 | 31 | echo "Setting Up Procfile..." 32 | 33 | sed -i 's/^watch:/# watch:/g' Procfile 34 | sed -i 's/^schedule:/# schedule:/g' Procfile 35 | 36 | echo "Setting up redisearch module..." 37 | echo "loadmodule ${GITHUB_WORKSPACE}/.github/helper/redisearch.so" >> ./config/redis_cache.conf 38 | chmod +x "${GITHUB_WORKSPACE}/.github/helper/redisearch.so" 39 | cat ./config/redis_cache.conf 40 | 41 | echo "Starting Bench..." 42 | 43 | bench start &> bench_start.log & 44 | 45 | CI=Yes bench build & 46 | build_pid=$! 47 | 48 | bench --site gameplan.test reinstall --yes 49 | bench --site gameplan.test install-app gameplan 50 | bench --site gameplan.test execute gameplan.search2.build_index_if_not_exists 51 | 52 | # wait till assets are built succesfully 53 | wait $build_pid 54 | -------------------------------------------------------------------------------- /.github/helper/install_dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | echo "Setting Up System Dependencies..." 5 | 6 | # redis repository 7 | curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg 8 | echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list 9 | 10 | sudo apt update 11 | sudo apt install libcups2-dev redis mariadb-client-10.6 12 | 13 | install_wkhtmltopdf() { 14 | wget -q https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.focal_amd64.deb 15 | sudo apt install ./wkhtmltox_0.12.6-1.focal_amd64.deb 16 | } 17 | install_wkhtmltopdf & 18 | -------------------------------------------------------------------------------- /.github/helper/redisearch.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/.github/helper/redisearch.so -------------------------------------------------------------------------------- /.github/helper/site_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_host": "127.0.0.1", 3 | "db_port": 3306, 4 | "db_name": "test_gameplan", 5 | "db_password": "test_gameplan", 6 | "allow_tests": true, 7 | "enable_ui_tests": true, 8 | "db_type": "mariadb", 9 | "auto_email_id": "test@example.com", 10 | "mail_server": "smtp.example.com", 11 | "mail_login": "test@example.com", 12 | "mail_password": "test", 13 | "admin_password": "admin", 14 | "root_login": "root", 15 | "root_password": "123", 16 | "host_name": "http://gameplan.test:8000", 17 | "monitor": 1, 18 | "server_script_enabled": true, 19 | "mute_emails": true 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.egg-info 4 | *.swp 5 | __pycache__ 6 | tags 7 | node_modules 8 | gameplan/docs/current 9 | gameplan/public/frontend 10 | gameplan/www/teams.html 11 | gameplan/www/g.html 12 | build 13 | frontend/components.d.ts -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "frappe-ui"] 2 | path = frappe-ui 3 | url = https://github.com/frappe/frappe-ui 4 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: 'node_modules|.git' 2 | default_stages: [pre-commit] 3 | fail_fast: false 4 | 5 | 6 | repos: 7 | - repo: https://github.com/pre-commit/pre-commit-hooks 8 | rev: v5.0.0 9 | hooks: 10 | - id: trailing-whitespace 11 | exclude: ".*json$|.*txt$|.*csv|.*md|.*svg" 12 | - id: check-merge-conflict 13 | - id: check-ast 14 | - id: check-json 15 | - id: check-toml 16 | - id: check-yaml 17 | - id: debug-statements 18 | 19 | - repo: https://github.com/astral-sh/ruff-pre-commit 20 | rev: v0.8.1 21 | hooks: 22 | - id: ruff 23 | name: "Run ruff import sorter" 24 | args: ["--select=I", "--fix"] 25 | 26 | - id: ruff 27 | name: "Run ruff linter" 28 | 29 | - id: ruff-format 30 | name: "Run ruff formatter" 31 | 32 | - repo: https://github.com/pre-commit/mirrors-prettier 33 | rev: v3.1.0 34 | hooks: 35 | - id: prettier 36 | types_or: [javascript, vue, scss] 37 | exclude_types: [yaml] 38 | 39 | 40 | ci: 41 | autoupdate_schedule: weekly 42 | skip: [] 43 | submodules: false 44 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | include requirements.txt 3 | include *.json 4 | include *.md 5 | include *.py 6 | include *.txt 7 | recursive-include gameplan *.css 8 | recursive-include gameplan *.csv 9 | recursive-include gameplan *.html 10 | recursive-include gameplan *.ico 11 | recursive-include gameplan *.js 12 | recursive-include gameplan *.json 13 | recursive-include gameplan *.md 14 | recursive-include gameplan *.png 15 | recursive-include gameplan *.py 16 | recursive-include gameplan *.svg 17 | recursive-include gameplan *.txt 18 | recursive-exclude gameplan *.pyc -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | name: gameplan 3 | services: 4 | mariadb: 5 | image: mariadb:10.6 6 | command: 7 | - --character-set-server=utf8mb4 8 | - --collation-server=utf8mb4_unicode_ci 9 | - --skip-character-set-client-handshake 10 | - --skip-innodb-read-only-compressed # Temporary fix for MariaDB 10.6 11 | environment: 12 | MYSQL_ROOT_PASSWORD: 123 13 | volumes: 14 | - mariadb-data:/var/lib/mysql 15 | 16 | redis: 17 | image: redis:alpine 18 | 19 | frappe: 20 | image: frappe/bench:latest 21 | command: bash /workspace/init.sh 22 | environment: 23 | - SHELL=/bin/bash 24 | working_dir: /home/frappe 25 | volumes: 26 | - .:/workspace 27 | ports: 28 | - 8000:8000 29 | - 9000:9000 30 | 31 | volumes: 32 | mariadb-data: -------------------------------------------------------------------------------- /docker/init.sh: -------------------------------------------------------------------------------- 1 | #!bin/bash 2 | 3 | if [ -d "/home/frappe/frappe-bench/apps/frappe" ]; then 4 | echo "Bench already exists, skipping init" 5 | cd frappe-bench 6 | bench start 7 | else 8 | echo "Creating new bench..." 9 | fi 10 | 11 | bench init --skip-redis-config-generation frappe-bench 12 | 13 | cd frappe-bench 14 | 15 | # Use containers instead of localhost 16 | bench set-mariadb-host mariadb 17 | bench set-redis-cache-host redis:6379 18 | bench set-redis-queue-host redis:6379 19 | bench set-redis-socketio-host redis:6379 20 | 21 | # Remove redis, watch from Procfile 22 | sed -i '/redis/d' ./Procfile 23 | sed -i '/watch/d' ./Procfile 24 | 25 | bench get-app gameplan 26 | 27 | bench new-site gameplan.localhost \ 28 | --force \ 29 | --mariadb-root-password 123 \ 30 | --admin-password admin \ 31 | --no-mariadb-socket 32 | 33 | bench --site gameplan.localhost install-app gameplan 34 | bench --site gameplan.localhost set-config developer_mode 1 35 | bench --site gameplan.localhost clear-cache 36 | bench --site gameplan.localhost set-config mute_emails 1 37 | bench --site gameplan.localhost add-user alex@example.com --first-name Alex --last-name Scott --password 123 --user-type 'System User' --add-role 'Gameplan Admin' 38 | bench use gameplan.localhost 39 | 40 | bench start 41 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local -------------------------------------------------------------------------------- /frontend/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "printWidth": 100 5 | } 6 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Gameplan 2 | 3 | Team discussion and collaboration tool 4 | -------------------------------------------------------------------------------- /frontend/cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | projectId: 'y2q697', 5 | e2e: { 6 | baseUrl: 'http://gameplan-demo.test:8000', 7 | adminPassword: 'admin', 8 | }, 9 | retries: { 10 | runMode: 2, 11 | openMode: 0, 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /frontend/cypress/e2e/onboarding.cy.js: -------------------------------------------------------------------------------- 1 | describe('Onboarding', () => { 2 | it('onboarding works', () => { 3 | cy.login() 4 | cy.request({ 5 | method: 'POST', 6 | url: '/api/method/gameplan.test_api.clear_data', 7 | }) 8 | cy.visit('/g') 9 | 10 | cy.get('input[placeholder=Marketing]').type('Marketing') 11 | cy.get('button').contains('Next').click() 12 | cy.get('input[placeholder="Product Launch"]').type('Product Launch') 13 | cy.get('button').contains('Next').click() 14 | cy.get('input[placeholder="jane@example.com"]:first').type('test@example.com') 15 | cy.get('button').contains('Complete Setup').click() 16 | 17 | cy.url().should('include', '/g/marketing') 18 | 19 | cy.get('a:contains("Product Launch")').should('exist') 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /frontend/cypress/e2e/project.cy.js: -------------------------------------------------------------------------------- 1 | describe('Project', () => { 2 | it('project creation, move to team and archive', () => { 3 | cy.login() 4 | cy.request({ 5 | method: 'POST', 6 | url: '/api/method/gameplan.test_api.clear_data?onboard=1', 7 | }) 8 | cy.request('POST', '/api/method/frappe.client.insert_many', { 9 | docs: [ 10 | { 11 | doctype: 'GP Team', 12 | title: 'Engineering', 13 | }, 14 | { 15 | doctype: 'GP Team', 16 | title: 'DevOps', 17 | }, 18 | ], 19 | }) 20 | cy.visit('/g/engineering/') 21 | 22 | cy.intercept('POST', '/api/method/frappe.client.insert').as('project') 23 | cy.button('Add Project').click() 24 | cy.contains('label', 'Title').parent().find('input').type('Project 1') 25 | cy.get('button').contains('Create').click() 26 | cy.get('h3:contains("Project 1")').should('exist') 27 | cy.wait('@project') 28 | .its('response.body.message') 29 | .then((project) => { 30 | cy.url().should('include', `/g/engineering/projects/${project.name}`) 31 | }) 32 | 33 | // move to team 34 | cy.get('button[aria-label="Options"]').click() 35 | cy.get('button:contains("Move to another team")').click() 36 | cy.get('button:contains("Select a team")').click() 37 | cy.get('li:contains("DevOps")').click() 38 | cy.get('button:contains("Move to DevOps")').click() 39 | cy.get('@project') 40 | .its('response.body.message') 41 | .then((project) => { 42 | cy.url().should('include', `/g/devops/projects/${project.name}`) 43 | }) 44 | 45 | // archive 46 | cy.get('button[aria-label="Options"]').click() 47 | cy.get('button:contains("Archive")').click() 48 | cy.button('Archive').click() 49 | cy.contains('div', 'Archived').should('exist') 50 | cy.visit('/g/devops/') 51 | cy.contains('Project 1').should('not.exist') 52 | cy.get('button:contains("Archived")').click() 53 | cy.contains('Project 1').should('exist') 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /frontend/cypress/e2e/team.cy.js: -------------------------------------------------------------------------------- 1 | describe('Team', () => { 2 | it('team creation, readme edit and archive', () => { 3 | cy.login() 4 | cy.request({ 5 | method: 'POST', 6 | url: '/api/method/gameplan.test_api.clear_data?onboard=1', 7 | }) 8 | cy.visit('/g') 9 | cy.get('button[aria-label="Create Team"]').click() 10 | cy.get('input[placeholder="Team Name"]').type('Engineering') 11 | cy.get('button').contains('Create Team').click() 12 | cy.url().should('include', '/g/engineering') 13 | 14 | cy.intercept('POST', '/api/method/frappe.client.set_value').as('set_value') 15 | cy.get('button[aria-label="Edit"]').click() 16 | cy.focused().type('{selectAll}{backspace}## Engineering 2{enter}some description') 17 | cy.get('button').contains('Save').click() 18 | cy.get('button').contains('Save').should('not.exist') 19 | cy.get('h2').contains('Engineering 2').should('exist') 20 | cy.get('@set_value').its('response.statusCode').should('eq', 200) 21 | 22 | // archive teams 23 | cy.get('button[aria-label="Options"]').click() 24 | cy.contains('button[role="menuitem"]', 'Archive').click() 25 | cy.dialog('button').contains('Archive').click() 26 | 27 | cy.reload() 28 | 29 | cy.get('button').contains('Gameplan').click() 30 | cy.get('button').contains('Settings').click() 31 | cy.get('button').contains('Archive').click() 32 | cy.contains('Engineering').should('exist') 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /frontend/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 26 | 27 | Cypress.Commands.add('login', (email, password) => { 28 | if (!email) { 29 | email = Cypress.config('testUser') || 'Administrator' 30 | } 31 | if (!password) { 32 | password = Cypress.config('adminPassword') 33 | } 34 | cy.request({ 35 | url: '/api/method/login', 36 | method: 'POST', 37 | body: { usr: email, pwd: password }, 38 | }) 39 | }) 40 | 41 | Cypress.Commands.add('button', (text) => { 42 | return cy.get(`button:contains("${text}"):visible`) 43 | }) 44 | 45 | Cypress.Commands.add('iconButton', (text) => { 46 | return cy.get(`button[aria-label="${text}"]:visible`) 47 | }) 48 | 49 | Cypress.Commands.add('dialog', (selector) => { 50 | return cy.get(`[role=dialog] ${selector}`) 51 | }) 52 | -------------------------------------------------------------------------------- /frontend/cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /frontend/lucideIcons.js: -------------------------------------------------------------------------------- 1 | import * as LucideIcons from 'lucide-static' 2 | 3 | let icons = {} 4 | for (const icon in LucideIcons) { 5 | if (icon == 'default') { 6 | continue 7 | } 8 | let iconSvg = LucideIcons[icon] 9 | 10 | // set stroke-width to 1.5 11 | if (iconSvg && iconSvg.includes('stroke-width')) { 12 | iconSvg = iconSvg.replace(/stroke-width="2"/g, 'stroke-width="1.5"') 13 | } 14 | icons[icon] = iconSvg 15 | 16 | let dashKeys = camelToDash(icon) 17 | for (let dashKey of dashKeys) { 18 | if (dashKey !== icon) { 19 | icons[dashKey] = iconSvg 20 | } 21 | } 22 | } 23 | 24 | export default icons 25 | 26 | function camelToDash(key) { 27 | // barChart2 -> bar-chart-2 28 | let withNumber = key.replace(/[A-Z0-9]/g, (m) => '-' + m.toLowerCase()) 29 | if (withNumber.startsWith('-')) { 30 | withNumber = withNumber.substring(1) 31 | } 32 | // barChart2 -> bar-chart2 33 | let withoutNumber = key.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()) 34 | if (withoutNumber.startsWith('-')) { 35 | withoutNumber = withoutNumber.substring(1) 36 | } 37 | 38 | if (withNumber !== withoutNumber) { 39 | // both are required because unplugin icon resolver doesn't put a dash before numbers 40 | return [withNumber, withoutNumber] 41 | } 42 | return [withNumber] 43 | } 44 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gameplan-ui", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "serve": "vite preview", 9 | "test-local": "cypress open --e2e --browser chrome", 10 | "test": "cypress run --record --key 9c23c39a-b56b-46d5-b673-44faf1f1da83" 11 | }, 12 | "dependencies": { 13 | "@headlessui/vue": "^1.7.14", 14 | "@sentry/vue": "^8.37.1", 15 | "@tiptap/core": "^2.12.0", 16 | "@tiptap/pm": "^2.12.0", 17 | "@tiptap/vue-3": "^2.11.7", 18 | "@vueuse/core": "^10.1.2", 19 | "dayjs": "^1.10.7", 20 | "feather-icons": "^4.28.0", 21 | "frappe-ui": "^0.1.149", 22 | "fuzzysort": "^2.0.4", 23 | "gemoji": "^7.1.0", 24 | "htmldiff-js": "^1.0.5", 25 | "idb-keyval": "^6.2.1", 26 | "reka-ui": "^2.0.2", 27 | "socket.io-client": "^4.7.2", 28 | "tippy.js": "^6.3.7", 29 | "vue": "^3.5.13", 30 | "vue-router": "^4.5.0" 31 | }, 32 | "devDependencies": { 33 | "@tailwindcss/container-queries": "^0.1.1", 34 | "@vitejs/plugin-vue": "^4.2.3", 35 | "@vitejs/plugin-vue-jsx": "^3.0.1", 36 | "autoprefixer": "^10.4.2", 37 | "cypress": "10.11.0", 38 | "lucide-static": "^0.469.0", 39 | "postcss": "^8.4.5", 40 | "prettier": "^3.3.3", 41 | "prettier-plugin-tailwindcss": "^0.6.8", 42 | "rollup-plugin-visualizer": "^5.8.2", 43 | "tailwindcss": "^3.4.15", 44 | "vite": "^4.4.9", 45 | "vue-template-compiler": "^2.7.14" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/frontend/public/favicon.png -------------------------------------------------------------------------------- /frontend/public/gameplan-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 36 | -------------------------------------------------------------------------------- /frontend/src/components/ActionSheet.vue: -------------------------------------------------------------------------------- 1 | 29 | 39 | 53 | -------------------------------------------------------------------------------- /frontend/src/components/AddTeamDialog.vue: -------------------------------------------------------------------------------- 1 | 37 | 73 | -------------------------------------------------------------------------------- /frontend/src/components/AppLink.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 53 | -------------------------------------------------------------------------------- /frontend/src/components/AppSidebarLink.vue: -------------------------------------------------------------------------------- 1 | 20 | 28 | -------------------------------------------------------------------------------- /frontend/src/components/CommandPalette/Item.vue: -------------------------------------------------------------------------------- 1 | 12 | 20 | -------------------------------------------------------------------------------- /frontend/src/components/CommandPalette/ItemProject.vue: -------------------------------------------------------------------------------- 1 | 8 | 21 | -------------------------------------------------------------------------------- /frontend/src/components/CommandPalette/ItemTeam.vue: -------------------------------------------------------------------------------- 1 | 10 | 18 | -------------------------------------------------------------------------------- /frontend/src/components/CommandPalette/commandPalette.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | export const show = ref(false) 4 | 5 | export function showCommandPalette() { 6 | show.value = true 7 | } 8 | 9 | export function hideCommandPalette() { 10 | show.value = false 11 | } 12 | 13 | export function toggleCommandPalette() { 14 | show.value = !show.value 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/components/DesktopLayout.vue: -------------------------------------------------------------------------------- 1 | 26 | 34 | -------------------------------------------------------------------------------- /frontend/src/components/DiscussionBreadcrumbs.vue: -------------------------------------------------------------------------------- 1 | 29 | 43 | -------------------------------------------------------------------------------- /frontend/src/components/DiscussionMeta.vue: -------------------------------------------------------------------------------- 1 | 20 | 30 | -------------------------------------------------------------------------------- /frontend/src/components/DragHandleIcon.vue: -------------------------------------------------------------------------------- 1 | 19 | 24 | -------------------------------------------------------------------------------- /frontend/src/components/DropdownMoreOptions.vue: -------------------------------------------------------------------------------- 1 | 12 | 16 | -------------------------------------------------------------------------------- /frontend/src/components/EmptyStateBox.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /frontend/src/components/GameplanLogo.vue: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /frontend/src/components/ImagePreview.vue: -------------------------------------------------------------------------------- 1 | 19 | 47 | -------------------------------------------------------------------------------- /frontend/src/components/KeyboardShortcut.vue: -------------------------------------------------------------------------------- 1 | 12 | 25 | -------------------------------------------------------------------------------- /frontend/src/components/LastPostReminder.vue: -------------------------------------------------------------------------------- 1 | 26 | 58 | -------------------------------------------------------------------------------- /frontend/src/components/Link.vue: -------------------------------------------------------------------------------- 1 | 20 | 42 | -------------------------------------------------------------------------------- /frontend/src/components/Links.vue: -------------------------------------------------------------------------------- 1 | 13 | 24 | -------------------------------------------------------------------------------- /frontend/src/components/NewTaskDialog/index.ts: -------------------------------------------------------------------------------- 1 | export { default as NewTaskDialog } from './NewTaskDialog.vue' 2 | export { showNewTaskDialog } from './state' 3 | -------------------------------------------------------------------------------- /frontend/src/components/NewTaskDialog/state.ts: -------------------------------------------------------------------------------- 1 | import { GPTask } from '@/types/doctypes' 2 | import { useNewDoc } from 'frappe-ui/src/data-fetching' 3 | import { ref } from 'vue' 4 | 5 | export const showDialog = ref(false) 6 | export const newTask = ref | null>(null) 7 | export const _onSuccess = ref<(doc: GPTask) => void>(() => {}) 8 | 9 | export function showNewTaskDialog({ defaults = {}, onSuccess = (doc: GPTask) => {} } = {}) { 10 | newTask.value = newDraftTask() 11 | Object.assign(newTask.value.doc, defaults || {}) 12 | showDialog.value = true 13 | _onSuccess.value = onSuccess 14 | } 15 | 16 | function newDraftTask() { 17 | return useNewDoc('GP Task', { 18 | title: '', 19 | description: '', 20 | status: 'Backlog', 21 | assigned_to: '', 22 | project: '', 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/components/PageHeader.vue: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /frontend/src/components/PageList.vue: -------------------------------------------------------------------------------- 1 | 41 | 63 | -------------------------------------------------------------------------------- /frontend/src/components/Pie.vue: -------------------------------------------------------------------------------- 1 | 18 | 24 | 37 | -------------------------------------------------------------------------------- /frontend/src/components/PollEditor.vue: -------------------------------------------------------------------------------- 1 | 32 | 55 | -------------------------------------------------------------------------------- /frontend/src/components/ReactionFaceIcon.vue: -------------------------------------------------------------------------------- 1 | 16 | 21 | -------------------------------------------------------------------------------- /frontend/src/components/RichQuoteExtension/RichQuoteNodeView.vue: -------------------------------------------------------------------------------- 1 | 17 | 51 | -------------------------------------------------------------------------------- /frontend/src/components/ScrollBar.vue: -------------------------------------------------------------------------------- 1 | 11 | 14 | -------------------------------------------------------------------------------- /frontend/src/components/ScrollContainer.vue: -------------------------------------------------------------------------------- 1 | 7 | 11 | -------------------------------------------------------------------------------- /frontend/src/components/Settings/SettingsTab.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /frontend/src/components/SpaceBreadcrumbs.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 49 | 50 | 55 | -------------------------------------------------------------------------------- /frontend/src/components/SpaceTabs.vue: -------------------------------------------------------------------------------- 1 | 5 | 45 | -------------------------------------------------------------------------------- /frontend/src/components/Tabs.vue: -------------------------------------------------------------------------------- 1 | 15 | 35 | -------------------------------------------------------------------------------- /frontend/src/components/TeamMembers.vue: -------------------------------------------------------------------------------- 1 | 23 | 34 | -------------------------------------------------------------------------------- /frontend/src/components/TextEditorTaskExtension/Component.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 43 | -------------------------------------------------------------------------------- /frontend/src/components/TextEditorTaskExtension/index.js: -------------------------------------------------------------------------------- 1 | import { Node, mergeAttributes } from '@tiptap/core' 2 | import { VueNodeViewRenderer } from '@tiptap/vue-3' 3 | import Component from './Component.vue' 4 | 5 | export default Node.create({ 6 | name: 'team-task', 7 | group: 'block', 8 | atom: true, 9 | addAttributes() { 10 | return { 11 | taskId: { 12 | default: 0, 13 | parseHTML: (element) => element.getAttribute('task-id'), 14 | }, 15 | } 16 | }, 17 | parseHTML() { 18 | return [ 19 | { 20 | tag: 'team-task', 21 | }, 22 | ] 23 | }, 24 | renderHTML({ HTMLAttributes }) { 25 | return ['team-task', mergeAttributes(HTMLAttributes)] 26 | }, 27 | addNodeView() { 28 | return VueNodeViewRenderer(Component) 29 | }, 30 | addCommands() { 31 | return { 32 | insertTask: 33 | (options) => 34 | ({ chain, editor }) => { 35 | const { selection } = editor.state 36 | const pos = selection.$head 37 | 38 | return chain() 39 | .insertContentAt(pos.before(), [ 40 | { 41 | type: this.name, 42 | attrs: { taskId: 'new' }, 43 | }, 44 | ]) 45 | .run() 46 | }, 47 | } 48 | }, 49 | }) 50 | -------------------------------------------------------------------------------- /frontend/src/components/UserAvatar.vue: -------------------------------------------------------------------------------- 1 | 14 | 28 | -------------------------------------------------------------------------------- /frontend/src/components/UserAvatarWithHover.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 58 | -------------------------------------------------------------------------------- /frontend/src/components/UserImage.vue: -------------------------------------------------------------------------------- 1 | 11 | 22 | -------------------------------------------------------------------------------- /frontend/src/components/UserInfo.vue: -------------------------------------------------------------------------------- 1 | 4 | 17 | -------------------------------------------------------------------------------- /frontend/src/components/UserProfileLink.vue: -------------------------------------------------------------------------------- 1 | 13 | 29 | -------------------------------------------------------------------------------- /frontend/src/components/icons/ChevronTriangle.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Pin.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/icons/TaskPriorityIcon.vue: -------------------------------------------------------------------------------- 1 | 13 | 24 | -------------------------------------------------------------------------------- /frontend/src/data/newDoc.js: -------------------------------------------------------------------------------- 1 | import { reactive, unref } from 'vue' 2 | import { createResource } from 'frappe-ui' 3 | 4 | export function useNewDoc(doctype, doc = {}, resourceOptions = {}) { 5 | doc = reactive(doc) 6 | const resource = createResource({ 7 | url: 'frappe.client.insert', 8 | makeParams(_values) { 9 | let payload = { doctype } 10 | for (let key in doc) { 11 | payload[key] = unref(doc[key]) 12 | } 13 | return { 14 | doc: payload, 15 | } 16 | }, 17 | ...resourceOptions, 18 | }) 19 | 20 | resource.doc = doc 21 | return resource 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/data/notifications.ts: -------------------------------------------------------------------------------- 1 | import { useCall } from 'frappe-ui/src/data-fetching' 2 | 3 | export let unreadNotifications = useCall({ 4 | cacheKey: 'Unread Notifications Count', 5 | url: '/api/v2/method/gameplan.api.unread_notifications', 6 | initialData: 0, 7 | }) 8 | -------------------------------------------------------------------------------- /frontend/src/data/session.ts: -------------------------------------------------------------------------------- 1 | import { computed, MaybeRef, reactive, ref } from 'vue' 2 | import { useCall } from 'frappe-ui/src/data-fetching' 3 | import { users } from './users' 4 | import router from '@/router' 5 | 6 | interface LoginResponse { 7 | user: string 8 | default_route?: string 9 | } 10 | 11 | interface LoginParams { 12 | usr: MaybeRef 13 | pwd: MaybeRef 14 | } 15 | 16 | type LogoutResponse = void 17 | 18 | export let sessionUser = ref(getSessionUserFromCookie()) 19 | 20 | export let session = reactive({ 21 | login: useCall({ 22 | url: '/api/v2/method/login', 23 | immediate: false, 24 | onSuccess(data) { 25 | users.reload() 26 | sessionUser.value = getSessionUserFromCookie() 27 | session.login.reset() 28 | router.replace(data.default_route || '/') 29 | }, 30 | }), 31 | logout: useCall({ 32 | url: '/api/v2/method/logout', 33 | immediate: false, 34 | onSuccess() { 35 | sessionUser.value = getSessionUserFromCookie() 36 | window.location.href = '/login' 37 | }, 38 | }), 39 | user: sessionUser, 40 | isLoggedIn: computed(() => sessionUser.value != null), 41 | }) 42 | 43 | export function isSessionUser(user: string) { 44 | return session.user === user 45 | } 46 | 47 | function getSessionUserFromCookie() { 48 | let cookies = new URLSearchParams(document.cookie.split('; ').join('&')) 49 | let _sessionUser = cookies.get('user_id') 50 | if (_sessionUser === 'Guest') { 51 | _sessionUser = null 52 | } 53 | return _sessionUser 54 | } 55 | -------------------------------------------------------------------------------- /frontend/src/data/tags.ts: -------------------------------------------------------------------------------- 1 | import { GPTag } from '@/types/doctypes' 2 | import { useList } from 'frappe-ui' 3 | 4 | export const tags = useList({ 5 | doctype: 'GP Tag', 6 | fields: ['name', 'label'], 7 | limit: 9999, 8 | }) 9 | -------------------------------------------------------------------------------- /frontend/src/data/tasks.ts: -------------------------------------------------------------------------------- 1 | import { MaybeRefOrGetter, toValue } from 'vue' 2 | import { useDoc } from 'frappe-ui/src/data-fetching' 3 | import { GPTask } from '@/types/doctypes' 4 | 5 | let tasksCache: Record> = {} 6 | 7 | export function useTask(taskId: MaybeRefOrGetter) { 8 | interface Task extends GPTask {} 9 | 10 | interface TaskMethods { 11 | trackVisit: () => void 12 | } 13 | 14 | let name = toValue(taskId) 15 | if (!tasksCache[name]) { 16 | tasksCache[name] = useDoc({ 17 | doctype: 'GP Task', 18 | name: taskId, 19 | methods: { 20 | trackVisit: 'track_visit', 21 | }, 22 | }) 23 | } 24 | return tasksCache[name] as ReturnType> 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/data/teams.ts: -------------------------------------------------------------------------------- 1 | import { computed, MaybeRefOrGetter, toValue } from 'vue' 2 | import { useList } from 'frappe-ui/src/data-fetching' 3 | import { GPTeam } from '@/types/doctypes' 4 | 5 | export interface Team 6 | extends Pick< 7 | GPTeam, 8 | 'name' | 'title' | 'icon' | 'modified' | 'creation' | 'archived_at' | 'is_private' 9 | > {} 10 | 11 | export let teams = useList({ 12 | doctype: 'GP Team', 13 | fields: ['name', 'title', 'icon', 'modified', 'creation', 'archived_at', 'is_private'], 14 | orderBy: 'title asc', 15 | initialData: [], 16 | cacheKey: 'Teams', 17 | limit: 999, 18 | immediate: true, 19 | }) 20 | 21 | export let activeTeams = computed(() => { 22 | return (teams.data || []).filter((team) => !team.archived_at) 23 | }) 24 | 25 | export let useTeam = (teamId?: MaybeRefOrGetter) => { 26 | return computed(() => { 27 | let _teamId = toValue(teamId) 28 | if (!_teamId) { 29 | return null 30 | } 31 | return getTeam(_teamId) 32 | }) 33 | } 34 | 35 | export let getTeam = (teamId: string) => { 36 | return (teams.data || []).find((team) => team.name.toString() === teamId.toString()) 37 | } 38 | -------------------------------------------------------------------------------- /frontend/src/directives/focus.ts: -------------------------------------------------------------------------------- 1 | import type { ObjectDirective, DirectiveBinding } from 'vue' 2 | 3 | interface FocusDirective extends ObjectDirective { 4 | mounted(el: HTMLElement, binding: DirectiveBinding): void 5 | } 6 | 7 | const focusDirective: FocusDirective = { 8 | mounted(el, binding) { 9 | if (binding.value === false) { 10 | return 11 | } 12 | let firstFocusableElement = getFirstFocusableElement(el) 13 | if (firstFocusableElement) { 14 | firstFocusableElement.focus() 15 | if ( 16 | binding.arg === 'autoselect' && 17 | (firstFocusableElement instanceof HTMLInputElement || 18 | firstFocusableElement instanceof HTMLTextAreaElement) 19 | ) { 20 | firstFocusableElement.select() 21 | } 22 | } 23 | }, 24 | } 25 | 26 | function getFirstFocusableElement(parent: HTMLElement): HTMLElement | null { 27 | if (!parent) { 28 | return null 29 | } 30 | const focusableSelector = 31 | 'a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])' 32 | 33 | if (parent.matches(focusableSelector)) { 34 | return parent 35 | } 36 | 37 | const focusableElements = parent.querySelectorAll(focusableSelector) 38 | return focusableElements.length > 0 ? focusableElements[0] : null 39 | } 40 | 41 | export default focusDirective 42 | -------------------------------------------------------------------------------- /frontend/src/directives/index.ts: -------------------------------------------------------------------------------- 1 | export { default as vFocus } from './focus' 2 | -------------------------------------------------------------------------------- /frontend/src/globals.d.ts: -------------------------------------------------------------------------------- 1 | // defining global components and properties here for autocompletion 2 | // https://github.com/johnsoncodehk/volar/tree/master/extensions/vscode-vue-language-features 3 | 4 | import { isSessionUser } from './data/session' 5 | import { useUser } from './data/users' 6 | import { dayjs } from './utils' 7 | import { getPlatform } from './utils' 8 | 9 | declare module '@vue/runtime-core' { 10 | export interface GlobalComponents { 11 | RouterLink: (typeof import('vue-router'))['RouterLink'] 12 | RouterView: (typeof import('vue-router'))['RouterView'] 13 | Button: (typeof import('frappe-ui'))['Button'] 14 | Input: (typeof import('frappe-ui'))['Input'] 15 | TextInput: (typeof import('frappe-ui'))['TextInput'] 16 | ErrorMessage: (typeof import('frappe-ui'))['ErrorMessage'] 17 | Dialog: (typeof import('frappe-ui'))['Dialog'] 18 | FeatherIcon: (typeof import('frappe-ui'))['FeatherIcon'] 19 | Alert: (typeof import('frappe-ui'))['Alert'] 20 | Badge: (typeof import('frappe-ui'))['Badge'] 21 | UserInfo: (typeof import('frappe-ui'))['UserInfo'] 22 | UserAvatar: typeof import('./components/UserAvatar.vue') 23 | } 24 | } 25 | 26 | declare module 'vue' { 27 | interface ComponentCustomProperties { 28 | $platform: ReturnType 29 | $user: typeof useUser 30 | $dayjs: typeof dayjs 31 | $isSessionUser: typeof isSessionUser 32 | } 33 | } 34 | 35 | export {} 36 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @import 'frappe-ui/src/fonts/Inter/inter.css'; 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | /* disable webkit tap highlight color */ 8 | body { 9 | -webkit-tap-highlight-color: transparent; 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/pages/ComingSoon.vue: -------------------------------------------------------------------------------- 1 | 4 | 9 | -------------------------------------------------------------------------------- /frontend/src/pages/Home.vue: -------------------------------------------------------------------------------- 1 | 16 | 52 | -------------------------------------------------------------------------------- /frontend/src/pages/HomeOverview.vue: -------------------------------------------------------------------------------- 1 | 34 | 56 | -------------------------------------------------------------------------------- /frontend/src/pages/PersonProfileAboutMe.vue: -------------------------------------------------------------------------------- 1 | 17 | 25 | -------------------------------------------------------------------------------- /frontend/src/pages/PersonProfileBookmarks.vue: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /frontend/src/pages/PersonProfilePosts.vue: -------------------------------------------------------------------------------- 1 | 6 | 14 | -------------------------------------------------------------------------------- /frontend/src/pages/PersonProfileReplies.vue: -------------------------------------------------------------------------------- 1 | 6 | 14 | -------------------------------------------------------------------------------- /frontend/src/pages/ProjectDiscussion.vue: -------------------------------------------------------------------------------- 1 | 10 | 48 | -------------------------------------------------------------------------------- /frontend/src/pages/ProjectDiscussions.vue: -------------------------------------------------------------------------------- 1 | 25 | 50 | -------------------------------------------------------------------------------- /frontend/src/pages/ProjectLayout.vue: -------------------------------------------------------------------------------- 1 | 9 | 46 | -------------------------------------------------------------------------------- /frontend/src/pages/ProjectOverviewReadme.vue: -------------------------------------------------------------------------------- 1 | 10 | 18 | -------------------------------------------------------------------------------- /frontend/src/pages/ProjectTaskDetail.vue: -------------------------------------------------------------------------------- 1 | 4 | 22 | -------------------------------------------------------------------------------- /frontend/src/pages/ProjectTasks.vue: -------------------------------------------------------------------------------- 1 | 18 | 53 | -------------------------------------------------------------------------------- /frontend/src/pages/Space.vue: -------------------------------------------------------------------------------- 1 | 17 | 42 | -------------------------------------------------------------------------------- /frontend/src/pages/SpaceDiscussion.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 35 | -------------------------------------------------------------------------------- /frontend/src/pages/SpaceDiscussions.vue: -------------------------------------------------------------------------------- 1 | 16 | 24 | -------------------------------------------------------------------------------- /frontend/src/pages/SpaceTasks.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 49 | -------------------------------------------------------------------------------- /frontend/src/pages/Task.vue: -------------------------------------------------------------------------------- 1 | 40 | 59 | -------------------------------------------------------------------------------- /frontend/src/pages/TeamDiscussions.vue: -------------------------------------------------------------------------------- 1 | 15 | 19 | -------------------------------------------------------------------------------- /frontend/src/pages/TeamLayout.vue: -------------------------------------------------------------------------------- 1 | 18 | 46 | -------------------------------------------------------------------------------- /frontend/src/pages/Teams.vue: -------------------------------------------------------------------------------- 1 | 24 | 28 | -------------------------------------------------------------------------------- /frontend/src/socket.js: -------------------------------------------------------------------------------- 1 | import { io } from 'socket.io-client' 2 | import { socketio_port } from '../../../../sites/common_site_config.json' 3 | import { getCachedListResource } from 'frappe-ui/src/resources/listResource' 4 | import { getCachedResource } from 'frappe-ui/src/resources/resources' 5 | 6 | let socket = null 7 | export function initSocket() { 8 | let host = window.location.hostname 9 | let siteName = window.site_name 10 | let port = window.location.port ? `:${socketio_port}` : '' 11 | let protocol = port ? 'http' : 'https' 12 | let url = `${protocol}://${host}${port}/${siteName}` 13 | 14 | socket = io(url, { 15 | withCredentials: true, 16 | reconnectionAttempts: 5, 17 | }) 18 | socket.on('refetch_resource', (data) => { 19 | if (data.cache_key) { 20 | let resource = getCachedResource(data.cache_key) || getCachedListResource(data.cache_key) 21 | if (resource) { 22 | resource.reload() 23 | } 24 | } 25 | }) 26 | return socket 27 | } 28 | 29 | export function useSocket() { 30 | return socket 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/utils/composables.ts: -------------------------------------------------------------------------------- 1 | import { computed, onMounted, onUnmounted, reactive } from 'vue' 2 | 3 | export function useScreenSize() { 4 | const size = reactive({ 5 | width: window.innerWidth, 6 | height: window.innerHeight, 7 | }) 8 | 9 | const onResize = () => { 10 | size.width = window.innerWidth 11 | size.height = window.innerHeight 12 | } 13 | 14 | onMounted(() => { 15 | window.addEventListener('resize', onResize) 16 | }) 17 | 18 | onUnmounted(() => { 19 | window.removeEventListener('resize', onResize) 20 | }) 21 | 22 | return size 23 | } 24 | 25 | export function isMobile() { 26 | let size = useScreenSize() 27 | return computed(() => size.width < 640) 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/utils/dayjs.ts: -------------------------------------------------------------------------------- 1 | import _dayjs from 'dayjs/esm' 2 | import relativeTime from 'dayjs/esm/plugin/relativeTime' 3 | import localizedFormat from 'dayjs/esm/plugin/localizedFormat' 4 | import updateLocale from 'dayjs/esm/plugin/updateLocale' 5 | import isToday from 'dayjs/esm/plugin/isToday' 6 | 7 | _dayjs.extend(updateLocale) 8 | _dayjs.extend(relativeTime) 9 | _dayjs.extend(localizedFormat) 10 | _dayjs.extend(isToday) 11 | 12 | export const dayjs = _dayjs 13 | -------------------------------------------------------------------------------- /frontend/src/utils/formatters.js: -------------------------------------------------------------------------------- 1 | import { projects } from '@/data/projects' 2 | import { computed } from 'vue' 3 | 4 | let projectFormatters = {} 5 | export function projectTitle(project) { 6 | if (project == null) { 7 | return '' 8 | } 9 | 10 | let projectId = project.toString() 11 | if (!projectFormatters[projectId]) { 12 | projectFormatters[projectId] = computed(() => { 13 | if (projects.data.length > 0) { 14 | const project = projects.data.find((p) => p.name.toString() === projectId) 15 | if (project) { 16 | return project.title 17 | } 18 | } 19 | return projectId 20 | }) 21 | } 22 | return projectFormatters[projectId] 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { dayjs as _dayjs } from './dayjs' 2 | 3 | export const dayjs = _dayjs 4 | 5 | export function getImgDimensions( 6 | imgSrc: string, 7 | ): Promise<{ width: number; height: number; ratio: number }> { 8 | return new Promise((resolve) => { 9 | let img = new Image() 10 | img.onload = function () { 11 | let { width, height } = img 12 | resolve({ width, height, ratio: width / height }) 13 | } 14 | img.src = imgSrc 15 | }) 16 | } 17 | 18 | export function htmlToText(html: string): string { 19 | let tmp = document.createElement('div') 20 | tmp.innerHTML = html 21 | return tmp.textContent || tmp.innerText || '' 22 | } 23 | 24 | export function copyToClipboard(text: string): void { 25 | let textField = document.createElement('textarea') 26 | textField.value = text 27 | document.body.appendChild(textField) 28 | textField.select() 29 | document.execCommand('copy') 30 | textField.remove() 31 | } 32 | 33 | export function getScrollParent(node: HTMLElement | null): HTMLElement | null { 34 | if (node == null) { 35 | return null 36 | } 37 | 38 | if (node.scrollHeight > node.clientHeight) { 39 | return node 40 | } else { 41 | return getScrollParent(node.parentNode as HTMLElement) 42 | } 43 | } 44 | 45 | export function getRandomNumber(min: number, max: number): number { 46 | return Math.floor(Math.random() * (max - min + 1)) + min 47 | } 48 | 49 | export function getPlatform(): 'win' | 'mac' | 'linux' | undefined { 50 | let ua = navigator.userAgent.toLowerCase() 51 | if (ua.indexOf('win') > -1) { 52 | return 'win' 53 | } else if (ua.indexOf('mac') > -1) { 54 | return 'mac' 55 | } else if (ua.indexOf('x11') > -1 || ua.indexOf('linux') > -1) { 56 | return 'linux' 57 | } 58 | } 59 | 60 | export function relativeTimestamp(timestamp: string): string { 61 | if (dayjs().diff(timestamp, 'day') < 3) { 62 | return dayjs(timestamp).fromNow() 63 | } 64 | if (dayjs().diff(timestamp, 'year') < 1) { 65 | return dayjs(timestamp).format('D MMM') 66 | } 67 | return dayjs(timestamp).format('D MMM YYYY') 68 | } 69 | -------------------------------------------------------------------------------- /frontend/src/utils/resetDataMixin.js: -------------------------------------------------------------------------------- 1 | export default { 2 | methods: { 3 | $resetData(resetKeys) { 4 | let data = this.$options.data.call(this) 5 | if (!resetKeys) { 6 | resetKeys = Object.keys(data) 7 | } 8 | for (let key of resetKeys) { 9 | this[key] = data[key] 10 | } 11 | }, 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/utils/scrollContainer.ts: -------------------------------------------------------------------------------- 1 | import { ref, onMounted, onBeforeUnmount } from 'vue' 2 | 3 | export function scrollTo(...options) { 4 | if (!options || options.length === 0) return 5 | const container = getScrollContainer() 6 | if (!container) return 7 | container.scrollTo(...options) 8 | } 9 | 10 | export function getScrollContainer() { 11 | // window.scrollContainer is reference to the scroll container in DesktopLayout.vue and MobileLayout.vue 12 | return window.scrollContainer as HTMLElement 13 | } 14 | 15 | export function useScrollPosition(options = { threshold: 200 }) { 16 | const isScrolled = ref(false) 17 | 18 | function updateScrollPosition() { 19 | const scrollContainer = getScrollContainer() 20 | isScrolled.value = scrollContainer.scrollTop > options.threshold 21 | } 22 | 23 | onMounted(() => { 24 | const scrollContainer = getScrollContainer() 25 | scrollContainer.addEventListener('scroll', updateScrollPosition) 26 | }) 27 | 28 | onBeforeUnmount(() => { 29 | const scrollContainer = getScrollContainer() 30 | scrollContainer.removeEventListener('scroll', updateScrollPosition) 31 | }) 32 | 33 | return { 34 | isScrolled, 35 | scrollToTop, 36 | } 37 | } 38 | 39 | export function scrollToTop() { 40 | const scrollContainer = getScrollContainer() 41 | scrollContainer.scrollTo({ top: 0, behavior: 'smooth' }) 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/utils/sidebarResize.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | export function useSidebarResize() { 4 | let storedWidth = localStorage.getItem('sidebarWidth') 5 | const sidebarWidth = ref(storedWidth ? parseInt(storedWidth) : 256) 6 | const sidebarResizing = ref(false) 7 | 8 | function startResize() { 9 | document.addEventListener('mousemove', resize) 10 | document.addEventListener('mouseup', () => { 11 | document.body.classList.remove('select-none') 12 | document.body.classList.remove('cursor-col-resize') 13 | localStorage.setItem('sidebarWidth', sidebarWidth.value.toString()) 14 | sidebarResizing.value = false 15 | document.removeEventListener('mousemove', resize) 16 | }) 17 | } 18 | 19 | function resize(e: MouseEvent) { 20 | sidebarResizing.value = true 21 | document.body.classList.add('select-none') 22 | document.body.classList.add('cursor-col-resize') 23 | sidebarWidth.value = e.clientX 24 | 25 | // snap to 256 26 | let range = [256 - 10, 256 + 10] 27 | if (sidebarWidth.value > range[0] && sidebarWidth.value < range[1]) { 28 | sidebarWidth.value = 256 29 | } 30 | 31 | if (sidebarWidth.value < 12 * 16) { 32 | sidebarWidth.value = 12 * 16 33 | } 34 | if (sidebarWidth.value > 24 * 16) { 35 | sidebarWidth.value = 24 * 16 36 | } 37 | } 38 | 39 | return { 40 | sidebarWidth, 41 | sidebarResizing, 42 | startResize, 43 | resize, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /frontend/src/utils/theme.js: -------------------------------------------------------------------------------- 1 | import resolveConfig from 'tailwindcss/resolveConfig' 2 | import tailwindConfig from 'tailwind.config.js' 3 | 4 | export const config = resolveConfig(tailwindConfig) 5 | export const theme = config.theme 6 | -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | import frappeUIPreset from 'frappe-ui/src/tailwind/preset' 2 | import containerQueries from '@tailwindcss/container-queries' 3 | 4 | export default { 5 | presets: [frappeUIPreset], 6 | content: [ 7 | './index.html', 8 | './src/**/*.{vue,js,ts,jsx,tsx}', 9 | './node_modules/frappe-ui/src/components/**/*.{vue,js,ts,jsx,tsx}', 10 | '../node_modules/frappe-ui/src/components/**/*.{vue,js,ts,jsx,tsx}', 11 | ], 12 | theme: { 13 | container: { 14 | center: true, 15 | padding: { 16 | DEFAULT: '1rem', 17 | sm: '2rem', 18 | lg: '2rem', 19 | xl: '4rem', 20 | '2xl': '4rem', 21 | }, 22 | }, 23 | extend: { 24 | maxWidth: { 25 | 'main-content': '768px', 26 | }, 27 | screens: { 28 | standalone: { 29 | raw: '(display-mode: standalone)', 30 | }, 31 | }, 32 | }, 33 | }, 34 | plugins: [containerQueries], 35 | } 36 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "lib": ["ESNext", "DOM"], 13 | "skipLibCheck": true, 14 | "noEmit": true, 15 | "paths": { 16 | "@/*": ["./src/*"] 17 | }, 18 | "types": ["unplugin-icons/types/vue"] 19 | }, 20 | "include": ["./src/**/*", "./intellisense.d.ts"], 21 | "exclude": ["node_modules", "**/*.spec.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /frontend/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import vueJsx from '@vitejs/plugin-vue-jsx' 4 | import path from 'path' 5 | import { visualizer } from 'rollup-plugin-visualizer' 6 | import frappeui from 'frappe-ui/vite' 7 | 8 | export default defineConfig({ 9 | define: { 10 | __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false', 11 | }, 12 | plugins: [ 13 | frappeui({ 14 | frappeProxy: true, 15 | lucideIcons: true, 16 | jinjaBootData: true, 17 | frappeTypes: { 18 | input: { 19 | gameplan: [ 20 | 'gp_project', 21 | 'gp_member', 22 | 'gp_team', 23 | 'gp_comment', 24 | 'gp_discussion', 25 | 'gp_page', 26 | 'gp_task', 27 | 'gp_poll', 28 | 'gp_guest_access', 29 | 'gp_invitation', 30 | 'gp_user_profile', 31 | 'gp_notification', 32 | 'gp_activity', 33 | 'gp_search_feedback', 34 | 'gp_draft', 35 | 'gp_tag', 36 | ], 37 | }, 38 | }, 39 | buildConfig: { 40 | indexHtmlPath: '../gameplan/www/g.html', 41 | }, 42 | }), 43 | vue(), 44 | vueJsx(), 45 | visualizer({ emitFile: true }), 46 | ], 47 | server: { 48 | allowedHosts: true, 49 | }, 50 | resolve: { 51 | alias: { 52 | '@': path.resolve(__dirname, 'src'), 53 | 'tailwind.config.js': path.resolve(__dirname, 'tailwind.config.js'), 54 | }, 55 | }, 56 | optimizeDeps: { 57 | include: ['feather-icons', 'showdown', 'tailwind.config.js'], 58 | }, 59 | }) 60 | -------------------------------------------------------------------------------- /gameplan/__init__.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | 3 | __version__ = "0.0.1" 4 | 5 | 6 | def is_guest(): 7 | if frappe.session.user == "Administrator": 8 | return False 9 | roles = frappe.get_roles() 10 | if "Gameplan Member" in roles or "Gameplan Admin" in roles: 11 | return False 12 | return "Gameplan Guest" in roles 13 | 14 | 15 | def refetch_resource(cache_key: str | list, user=None): 16 | frappe.publish_realtime( 17 | "refetch_resource", {"cache_key": cache_key}, user=user or frappe.session.user, after_commit=True 18 | ) 19 | -------------------------------------------------------------------------------- /gameplan/command_palette.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | import frappe 6 | 7 | from gameplan.search import GameplanSearch 8 | 9 | 10 | @frappe.whitelist() 11 | def search(query): 12 | search = GameplanSearch() 13 | query = search.clean_query(query) 14 | 15 | query_parts = query.split(" ") 16 | if len(query_parts) == 1 and not query_parts[0].endswith("*"): 17 | query = f"{query_parts[0]}*" 18 | if len(query_parts) > 1: 19 | query = " ".join([f"%{q}%" for q in query_parts]) 20 | 21 | query = f"@title:({query})" 22 | result = search.search(query, start=0, sort_by="modified desc", with_payloads=True) 23 | 24 | groups = {} 25 | for r in result.docs: 26 | doctype, name = r.id.split(":") 27 | r.doctype = doctype 28 | r.name = name 29 | 30 | if doctype == "GP Discussion": 31 | groups.setdefault("Discussions", []).append(r) 32 | elif doctype == "GP Task": 33 | groups.setdefault("Tasks", []).append(r) 34 | elif doctype == "GP Page": 35 | groups.setdefault("Pages", []).append(r) 36 | 37 | out = [] 38 | for key in groups: 39 | out.append({"title": key, "items": groups[key]}) 40 | return out 41 | 42 | 43 | @frappe.whitelist() 44 | def search2(query): 45 | from gameplan.search2 import GameplanSearch 46 | 47 | search = GameplanSearch() 48 | result = search.search(query, title_only=True) 49 | 50 | groups = {} 51 | for r in result["results"]: 52 | doctype = r["doctype"] 53 | 54 | if doctype == "GP Discussion": 55 | groups.setdefault("Discussions", []).append(r) 56 | elif doctype == "GP Task": 57 | groups.setdefault("Tasks", []).append(r) 58 | elif doctype == "GP Page": 59 | groups.setdefault("Pages", []).append(r) 60 | 61 | out = [] 62 | for key in groups: 63 | out.append({"title": key, "items": groups[key]}) 64 | 65 | return out 66 | -------------------------------------------------------------------------------- /gameplan/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/config/__init__.py -------------------------------------------------------------------------------- /gameplan/config/desktop.py: -------------------------------------------------------------------------------- 1 | from frappe import _ 2 | 3 | 4 | def get_data(): 5 | return [{"module_name": "Gameplan", "type": "module", "label": _("Gameplan")}] 6 | -------------------------------------------------------------------------------- /gameplan/config/docs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration for docs 3 | """ 4 | 5 | # source_link = "https://github.com/[org_name]/gameplan" 6 | # headline = "App that does everything" 7 | # sub_heading = "Yes, you got that right the first time, everything" 8 | 9 | 10 | def get_context(context): 11 | context.brand_html = "Gameplan" 12 | -------------------------------------------------------------------------------- /gameplan/demo/__init__.py: -------------------------------------------------------------------------------- 1 | # Demo module for Gameplan 2 | -------------------------------------------------------------------------------- /gameplan/extends/client.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | import frappe 6 | from frappe.model.base_document import get_controller 7 | 8 | 9 | @frappe.whitelist() 10 | def get_list( 11 | doctype=None, 12 | fields=None, 13 | filters=None, 14 | order_by=None, 15 | start=0, 16 | limit=20, 17 | group_by=None, 18 | parent=None, 19 | debug=False, 20 | ): 21 | check_permissions(doctype, parent) 22 | query = frappe.qb.get_query( 23 | table=doctype, 24 | fields=fields, 25 | filters=filters, 26 | order_by=order_by, 27 | offset=start, 28 | limit=limit, 29 | group_by=group_by, 30 | ) 31 | query = apply_custom_filters(doctype, query) 32 | return query.run(as_dict=True, debug=debug) 33 | 34 | 35 | def check_permissions(doctype, parent): 36 | user = frappe.session.user 37 | if not frappe.has_permission( 38 | doctype, "select", user=user, parent_doctype=parent 39 | ) and not frappe.has_permission(doctype, "read", user=user, parent_doctype=parent): 40 | frappe.throw(f"Insufficient Permission for {doctype}", frappe.PermissionError) 41 | 42 | 43 | def apply_custom_filters(doctype, query): 44 | """Apply custom filters to query""" 45 | controller = get_controller(doctype) 46 | if hasattr(controller, "get_list_query"): 47 | return_value = controller.get_list_query(query) 48 | if return_value is not None: 49 | query = return_value 50 | 51 | return query 52 | 53 | 54 | @frappe.whitelist() 55 | def batch(requests): 56 | from frappe.app import handle_exception 57 | from frappe.handler import execute_cmd 58 | 59 | requests = frappe.parse_json(requests) 60 | responses = [] 61 | 62 | for i, request_params in enumerate(requests): 63 | savepoint = f"batch_request_{i}" 64 | try: 65 | frappe.db.savepoint(savepoint) 66 | cmd = request_params.pop("cmd") 67 | frappe.form_dict.update(request_params) 68 | response = execute_cmd(cmd) 69 | frappe.db.release_savepoint(savepoint) 70 | except Exception as e: 71 | frappe.db.rollback(save_point=savepoint) 72 | response = handle_exception(e) 73 | 74 | responses.append(response) 75 | 76 | return responses 77 | -------------------------------------------------------------------------------- /gameplan/fixtures/role.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "bulk_actions": 0, 4 | "dashboard": 0, 5 | "desk_access": 0, 6 | "disabled": 0, 7 | "docstatus": 0, 8 | "doctype": "Role", 9 | "form_sidebar": 0, 10 | "home_page": null, 11 | "is_custom": 0, 12 | "list_sidebar": 0, 13 | "modified": "2024-05-13 20:28:46.909230", 14 | "name": "Gameplan Guest", 15 | "notifications": 0, 16 | "restrict_to_domain": null, 17 | "role_name": "Gameplan Guest", 18 | "search_bar": 0, 19 | "timeline": 0, 20 | "two_factor_auth": 0, 21 | "view_switcher": 0 22 | }, 23 | { 24 | "bulk_actions": 0, 25 | "dashboard": 0, 26 | "desk_access": 0, 27 | "disabled": 0, 28 | "docstatus": 0, 29 | "doctype": "Role", 30 | "form_sidebar": 0, 31 | "home_page": null, 32 | "is_custom": 0, 33 | "list_sidebar": 0, 34 | "modified": "2024-05-13 20:28:46.893672", 35 | "name": "Gameplan Admin", 36 | "notifications": 0, 37 | "restrict_to_domain": null, 38 | "role_name": "Gameplan Admin", 39 | "search_bar": 0, 40 | "timeline": 0, 41 | "two_factor_auth": 0, 42 | "view_switcher": 0 43 | }, 44 | { 45 | "bulk_actions": 0, 46 | "dashboard": 0, 47 | "desk_access": 0, 48 | "disabled": 0, 49 | "docstatus": 0, 50 | "doctype": "Role", 51 | "form_sidebar": 0, 52 | "home_page": null, 53 | "is_custom": 0, 54 | "list_sidebar": 0, 55 | "modified": "2024-05-13 20:28:46.862006", 56 | "name": "Gameplan Member", 57 | "notifications": 0, 58 | "restrict_to_domain": null, 59 | "role_name": "Gameplan Member", 60 | "search_bar": 0, 61 | "timeline": 0, 62 | "two_factor_auth": 0, 63 | "view_switcher": 0 64 | } 65 | ] -------------------------------------------------------------------------------- /gameplan/gameplan/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/doctype/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/discourse_id_map/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/doctype/discourse_id_map/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/discourse_id_map/discourse_id_map.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Frappe Technologies Pvt Ltd and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("Discourse ID Map", { 5 | // refresh: function(frm) { 6 | // } 7 | }); 8 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/discourse_id_map/discourse_id_map.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "creation": "2022-07-19 00:24:35.340881", 5 | "doctype": "DocType", 6 | "editable_grid": 1, 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "reference_doctype", 10 | "reference_name", 11 | "discourse_table", 12 | "discourse_id" 13 | ], 14 | "fields": [ 15 | { 16 | "fieldname": "reference_doctype", 17 | "fieldtype": "Data", 18 | "in_list_view": 1, 19 | "in_standard_filter": 1, 20 | "label": "Reference DocType", 21 | "reqd": 1 22 | }, 23 | { 24 | "fieldname": "reference_name", 25 | "fieldtype": "Data", 26 | "in_list_view": 1, 27 | "in_standard_filter": 1, 28 | "label": "Reference Name", 29 | "reqd": 1 30 | }, 31 | { 32 | "fieldname": "discourse_table", 33 | "fieldtype": "Data", 34 | "in_list_view": 1, 35 | "in_standard_filter": 1, 36 | "label": "Discourse Table", 37 | "reqd": 1 38 | }, 39 | { 40 | "fieldname": "discourse_id", 41 | "fieldtype": "Data", 42 | "in_list_view": 1, 43 | "in_standard_filter": 1, 44 | "label": "Discourse ID", 45 | "reqd": 1 46 | } 47 | ], 48 | "index_web_pages_for_search": 1, 49 | "links": [], 50 | "modified": "2022-07-19 00:25:09.920635", 51 | "modified_by": "Administrator", 52 | "module": "Gameplan", 53 | "name": "Discourse ID Map", 54 | "owner": "Administrator", 55 | "permissions": [ 56 | { 57 | "create": 1, 58 | "delete": 1, 59 | "email": 1, 60 | "export": 1, 61 | "print": 1, 62 | "read": 1, 63 | "report": 1, 64 | "role": "System Manager", 65 | "share": 1, 66 | "write": 1 67 | } 68 | ], 69 | "sort_field": "modified", 70 | "sort_order": "DESC", 71 | "states": [] 72 | } -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/discourse_id_map/discourse_id_map.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt Ltd and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | 8 | class DiscourseIDMap(Document): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/discourse_id_map/test_discourse_id_map.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt Ltd and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestDiscourseIDMap(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_activity/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/doctype/gp_activity/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_activity/gp_activity.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Frappe Technologies Pvt Ltd and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("GP Activity", { 5 | // refresh: function(frm) { 6 | // } 7 | }); 8 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_activity/gp_activity.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt Ltd and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | 8 | class GPActivity(Document): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_activity/test_gp_activity.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt Ltd and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestGPActivity(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_bookmark/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/doctype/gp_bookmark/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_bookmark/gp_bookmark.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024, Frappe Technologies Pvt Ltd and contributors 2 | // For license information, please see license.txt 3 | 4 | // frappe.ui.form.on("GP Bookmark", { 5 | // refresh(frm) { 6 | 7 | // }, 8 | // }); 9 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_bookmark/gp_bookmark.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "creation": "2024-10-22 15:11:26.303072", 5 | "doctype": "DocType", 6 | "engine": "InnoDB", 7 | "field_order": [ 8 | "user", 9 | "discussion" 10 | ], 11 | "fields": [ 12 | { 13 | "fieldname": "user", 14 | "fieldtype": "Link", 15 | "in_list_view": 1, 16 | "label": "User", 17 | "options": "User", 18 | "reqd": 1 19 | }, 20 | { 21 | "fieldname": "discussion", 22 | "fieldtype": "Link", 23 | "label": "Discussion", 24 | "options": "GP Discussion" 25 | } 26 | ], 27 | "index_web_pages_for_search": 1, 28 | "links": [], 29 | "modified": "2024-12-03 16:50:02.939711", 30 | "modified_by": "Administrator", 31 | "module": "Gameplan", 32 | "name": "GP Bookmark", 33 | "owner": "Administrator", 34 | "permissions": [ 35 | { 36 | "create": 1, 37 | "delete": 1, 38 | "email": 1, 39 | "export": 1, 40 | "print": 1, 41 | "read": 1, 42 | "report": 1, 43 | "role": "System Manager", 44 | "share": 1, 45 | "write": 1 46 | }, 47 | { 48 | "create": 1, 49 | "delete": 1, 50 | "email": 1, 51 | "export": 1, 52 | "print": 1, 53 | "read": 1, 54 | "report": 1, 55 | "role": "Gameplan Admin", 56 | "share": 1, 57 | "write": 1 58 | }, 59 | { 60 | "create": 1, 61 | "delete": 1, 62 | "email": 1, 63 | "export": 1, 64 | "print": 1, 65 | "read": 1, 66 | "report": 1, 67 | "role": "Gameplan Member", 68 | "share": 1, 69 | "write": 1 70 | }, 71 | { 72 | "create": 1, 73 | "delete": 1, 74 | "email": 1, 75 | "export": 1, 76 | "print": 1, 77 | "read": 1, 78 | "report": 1, 79 | "role": "Gameplan Guest", 80 | "share": 1, 81 | "write": 1 82 | } 83 | ], 84 | "sort_field": "creation", 85 | "sort_order": "DESC", 86 | "states": [] 87 | } -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_bookmark/gp_bookmark.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Frappe Technologies Pvt Ltd and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | 8 | class GPBookmark(Document): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_bookmark/test_gp_bookmark.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Frappe Technologies Pvt Ltd and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestGPBookmark(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_comment/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/doctype/gp_comment/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_comment/gp_comment.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Frappe Technologies Pvt Ltd and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("GP Comment", { 5 | // refresh: function(frm) { 6 | // } 7 | }); 8 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_comment/test_gp_comment.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt Ltd and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestGPComment(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_discussion/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/doctype/gp_discussion/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_discussion/gp_discussion.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Frappe Technologies Pvt Ltd and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("GP Discussion", { 5 | refresh: function (frm) { 6 | frm.add_custom_button(__("Show in Gameplan"), function () { 7 | window.open(`/g/space/${frm.doc.project}/discussion/${frm.doc.name}`); 8 | }); 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_discussion/patches/add_full_text_search_index.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | from ..gp_discussion import make_full_text_search_index 6 | 7 | 8 | def execute(): 9 | make_full_text_search_index() 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_discussion/patches/migrate_gp_bookmark_child.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors 2 | 3 | 4 | import frappe 5 | 6 | 7 | def execute(): 8 | if not frappe.db.get_value("DocType", "GP Bookmark"): 9 | return 10 | frappe.reload_doctype("GP Bookmark") 11 | for d in frappe.db.get_all("GP Bookmark"): 12 | try: 13 | doc = frappe.get_doc("GP Bookmark", d.name) 14 | for row in doc.bookmarks: 15 | bdoc = frappe.new_doc("GP Bookmark", discussion=row.discussion, user=doc.user).insert() 16 | bdoc.db_set("creation", row.date_added) 17 | doc.delete() 18 | except Exception as e: 19 | print(f"Could not migrate bookmark {d.name}: {e}") 20 | frappe.db.rollback() 21 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_discussion/patches/rename_team_project_discussion_to_team_discussion.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | import frappe 6 | from frappe.model.rename_doc import rename_doc 7 | 8 | 9 | def execute(): 10 | if frappe.db.exists("DocType", "Team Project Discussion") and not frappe.db.exists( 11 | "DocType", "Team Discussion" 12 | ): 13 | rename_doc("DocType", "Team Project Discussion", "Team Discussion") 14 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_discussion/patches/rename_team_project_status_update_doctype.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | import frappe 6 | from frappe.model.rename_doc import rename_doc 7 | 8 | 9 | def execute(): 10 | if frappe.db.exists("DocType", "Team Project Status Update") and not frappe.db.exists( 11 | "DocType", "Team Discussion" 12 | ): 13 | rename_doc("DocType", "Team Project Status Update", "Team Discussion") 14 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_discussion/patches/set_last_post.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors 2 | import frappe 3 | 4 | 5 | def execute(): 6 | frappe.db.sql(""" 7 | UPDATE `tabGP Discussion` d 8 | INNER JOIN ( 9 | SELECT discussion, creation, owner, name, type, 10 | ROW_NUMBER() OVER (PARTITION BY discussion ORDER BY creation DESC) as rn 11 | FROM ( 12 | SELECT reference_name as discussion, creation, owner, name, 'GP Comment' as type 13 | FROM `tabGP Comment` 14 | WHERE reference_doctype = 'GP Discussion' 15 | UNION ALL 16 | SELECT discussion, creation, owner, name, 'GP Poll' as type 17 | FROM `tabGP Poll` 18 | ) combined_posts 19 | ) p ON d.name = p.discussion AND p.rn = 1 20 | SET 21 | d.last_post_type = p.type, 22 | d.last_post = p.name, 23 | d.last_post_at = p.creation, 24 | d.last_post_by = p.owner 25 | """) 26 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_discussion/patches/set_title_slug.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | import frappe 6 | 7 | 8 | def execute(): 9 | for d in frappe.db.get_all("GP Discussion", pluck="name"): 10 | doc = frappe.get_doc("GP Discussion", d) 11 | doc.update_slug() 12 | doc.db_set("slug", doc.slug) 13 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_discussion/patches/update_participants_count.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | import frappe 6 | from frappe.utils import update_progress_bar 7 | 8 | 9 | def execute(): 10 | discussions = frappe.get_all("GP Discussion", pluck="name") 11 | failed = [] 12 | for i, discussion in enumerate(discussions): 13 | update_progress_bar("Updating participants count", i, len(discussions), absolute=True) 14 | doc = frappe.get_doc("GP Discussion", discussion) 15 | try: 16 | doc.update_participants_count() 17 | doc.db_set("participants_count", doc.participants_count, update_modified=False) 18 | except Exception: 19 | failed.append(discussion) 20 | 21 | if failed: 22 | print("Failed to update participants count for", failed) 23 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_discussion/test_gp_discussion.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Frappe Technologies Pvt Ltd and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestGPDiscussion(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_discussion_visit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/doctype/gp_discussion_visit/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_discussion_visit/gp_discussion_visit.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Frappe Technologies Pvt Ltd and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("GP Discussion Visit", { 5 | // refresh: function(frm) { 6 | // } 7 | }); 8 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_discussion_visit/gp_discussion_visit.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "autoname": "autoincrement", 4 | "creation": "2022-08-10 13:45:31.991076", 5 | "doctype": "DocType", 6 | "editable_grid": 1, 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "user", 10 | "discussion", 11 | "last_visit" 12 | ], 13 | "fields": [ 14 | { 15 | "fieldname": "user", 16 | "fieldtype": "Link", 17 | "in_list_view": 1, 18 | "label": "User", 19 | "options": "User", 20 | "reqd": 1, 21 | "search_index": 1 22 | }, 23 | { 24 | "fieldname": "discussion", 25 | "fieldtype": "Link", 26 | "in_list_view": 1, 27 | "label": "Discussion", 28 | "options": "GP Discussion", 29 | "reqd": 1 30 | }, 31 | { 32 | "fieldname": "last_visit", 33 | "fieldtype": "Datetime", 34 | "label": "Last Visit" 35 | } 36 | ], 37 | "index_web_pages_for_search": 1, 38 | "links": [], 39 | "modified": "2025-02-21 12:22:43.050952", 40 | "modified_by": "Administrator", 41 | "module": "Gameplan", 42 | "name": "GP Discussion Visit", 43 | "naming_rule": "Autoincrement", 44 | "owner": "Administrator", 45 | "permissions": [ 46 | { 47 | "create": 1, 48 | "delete": 1, 49 | "email": 1, 50 | "export": 1, 51 | "print": 1, 52 | "read": 1, 53 | "report": 1, 54 | "role": "System Manager", 55 | "share": 1, 56 | "write": 1 57 | }, 58 | { 59 | "create": 1, 60 | "delete": 1, 61 | "email": 1, 62 | "export": 1, 63 | "print": 1, 64 | "read": 1, 65 | "report": 1, 66 | "role": "Gameplan Admin", 67 | "share": 1, 68 | "write": 1 69 | }, 70 | { 71 | "create": 1, 72 | "delete": 1, 73 | "email": 1, 74 | "export": 1, 75 | "print": 1, 76 | "read": 1, 77 | "report": 1, 78 | "role": "Gameplan Member", 79 | "share": 1, 80 | "write": 1 81 | }, 82 | { 83 | "create": 1, 84 | "delete": 1, 85 | "email": 1, 86 | "export": 1, 87 | "print": 1, 88 | "read": 1, 89 | "report": 1, 90 | "role": "Gameplan Guest", 91 | "share": 1, 92 | "write": 1 93 | } 94 | ], 95 | "sort_field": "modified", 96 | "sort_order": "DESC", 97 | "states": [] 98 | } -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_discussion_visit/gp_discussion_visit.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt Ltd and contributors 2 | # For license information, please see license.txt 3 | 4 | import frappe 5 | from frappe.model.document import Document 6 | 7 | import gameplan 8 | 9 | 10 | class GPDiscussionVisit(Document): 11 | def after_insert(self): 12 | gameplan.refetch_resource("UnreadItems", user=self.user) 13 | 14 | def on_change(self): 15 | if self.has_value_changed("last_visit"): 16 | gameplan.refetch_resource("UnreadItems", user=self.user) 17 | 18 | 19 | def on_doctype_update(): 20 | frappe.db.add_index("GP Discussion Visit", ["user", "discussion", "last_visit"]) 21 | 22 | 23 | def after_doctype_insert(): 24 | frappe.db.add_unique("GP Discussion Visit", ["discussion", "user"]) 25 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_discussion_visit/patches/add_unique_constraint.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | import frappe 6 | 7 | from gameplan.gameplan.doctype.gp_discussion_visit.gp_discussion_visit import after_doctype_insert 8 | 9 | 10 | def execute(): 11 | delete_duplicates() 12 | after_doctype_insert() 13 | 14 | 15 | def delete_duplicates(): 16 | from frappe.query_builder.functions import Count 17 | 18 | DiscussionVisit = frappe.qb.DocType("GP Discussion Visit") 19 | duplicates = ( 20 | frappe.qb.from_(DiscussionVisit) 21 | .select(DiscussionVisit.name) 22 | .groupby(DiscussionVisit.discussion, DiscussionVisit.user) 23 | .having(Count(DiscussionVisit.name) > 1) 24 | ).run(as_dict=1, pluck="name") 25 | 26 | if duplicates: 27 | for name in duplicates: 28 | frappe.delete_doc_if_exists("GP Discussion Visit", name) 29 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_discussion_visit/test_gp_discussion_visit.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt Ltd and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestGPDiscussionVisit(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_draft/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/doctype/gp_draft/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_draft/gp_draft.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025, Frappe Technologies Pvt Ltd and contributors 2 | // For license information, please see license.txt 3 | 4 | // frappe.ui.form.on("GP Draft", { 5 | // refresh(frm) { 6 | 7 | // }, 8 | // }); 9 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_draft/gp_draft.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025, Frappe Technologies Pvt Ltd and contributors 2 | # For license information, please see license.txt 3 | 4 | import re 5 | 6 | import frappe 7 | from frappe.model.document import Document 8 | 9 | 10 | class GPDraft(Document): 11 | @staticmethod 12 | def get_list(query): 13 | GPDraft = frappe.qb.DocType("GP Draft") 14 | query = query.where(GPDraft.owner == frappe.session.user) 15 | return query 16 | 17 | @frappe.whitelist() 18 | def publish(self): 19 | if self.owner != frappe.session.user: 20 | frappe.throw("You are not allowed to publish this draft") 21 | 22 | if self.type == "Discussion": 23 | content = remove_query_params_from_images(self.content) 24 | discussion = frappe.new_doc( 25 | "GP Discussion", title=self.title, content=content, project=self.project 26 | ).insert() 27 | attachments = frappe.db.get_all( 28 | "File", 29 | filters={"attached_to_doctype": "GP Draft", "attached_to_name": self.name}, 30 | fields=["file_name", "file_url", "is_private", "name"], 31 | ) 32 | for attachment in attachments: 33 | file = frappe.new_doc( 34 | "File", 35 | file_name=attachment.file_name, 36 | file_url=attachment.file_url, 37 | is_private=attachment.is_private, 38 | attached_to_doctype=discussion.doctype, 39 | attached_to_name=discussion.name, 40 | ) 41 | file.insert() 42 | 43 | self.delete() 44 | return discussion.name 45 | 46 | 47 | def remove_query_params_from_images(content): 48 | # replace strings like src="/path/to/image.jpg?fid=param" with src="/path/to/image.jpg" 49 | # because when we publish draft, images linked to the draft are deleted 50 | # presence of fid= in the image url prevents the image from being displayed 51 | pattern = r'(src="[^"]+)\?[^"]*(")' 52 | return re.sub(pattern, r"\1\2", content) 53 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_draft/test_gp_draft.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025, Frappe Technologies Pvt Ltd and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests import IntegrationTestCase, UnitTestCase 6 | 7 | # On IntegrationTestCase, the doctype test records and all 8 | # link-field test record dependencies are recursively loaded 9 | # Use these module variables to add/remove to/from that list 10 | EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] 11 | IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] 12 | 13 | 14 | class UnitTestGPDraft(UnitTestCase): 15 | """ 16 | Unit tests for GPDraft. 17 | Use this class for testing individual functions and methods. 18 | """ 19 | 20 | pass 21 | 22 | 23 | class IntegrationTestGPDraft(IntegrationTestCase): 24 | """ 25 | Integration tests for GPDraft. 26 | Use this class for testing interactions between multiple components. 27 | """ 28 | 29 | pass 30 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_followed_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/doctype/gp_followed_project/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_followed_project/gp_followed_project.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023, Frappe Technologies Pvt Ltd and contributors 2 | // For license information, please see license.txt 3 | 4 | // frappe.ui.form.on("GP Followed Project", { 5 | // refresh(frm) { 6 | 7 | // }, 8 | // }); 9 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_followed_project/gp_followed_project.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Frappe Technologies Pvt Ltd and contributors 2 | # For license information, please see license.txt 3 | 4 | import frappe 5 | from frappe.model.document import Document 6 | 7 | 8 | class GPFollowedProject(Document): 9 | def before_insert(self): 10 | if not self.user: 11 | self.user = frappe.session.user 12 | 13 | @staticmethod 14 | def get_list_query(query): 15 | FollowedProject = frappe.qb.DocType("GP Followed Project") 16 | query = query.where(FollowedProject.user == frappe.session.user) 17 | return query 18 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_followed_project/test_gp_followed_project.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Frappe Technologies Pvt Ltd and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestGPFollowedProject(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_guest_access/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/doctype/gp_guest_access/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_guest_access/gp_guest_access.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Frappe Technologies Pvt Ltd and contributors 2 | // For license information, please see license.txt 3 | 4 | // frappe.ui.form.on("GP Guest Access", { 5 | // refresh(frm) { 6 | 7 | // }, 8 | // }); 9 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_guest_access/gp_guest_access.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "autoname": "autoincrement", 4 | "creation": "2022-12-02 17:24:50.553092", 5 | "default_view": "List", 6 | "doctype": "DocType", 7 | "editable_grid": 1, 8 | "engine": "InnoDB", 9 | "field_order": [ 10 | "user", 11 | "project", 12 | "team" 13 | ], 14 | "fields": [ 15 | { 16 | "fieldname": "user", 17 | "fieldtype": "Link", 18 | "in_list_view": 1, 19 | "label": "User", 20 | "options": "User", 21 | "reqd": 1 22 | }, 23 | { 24 | "fieldname": "project", 25 | "fieldtype": "Link", 26 | "in_list_view": 1, 27 | "label": "Project", 28 | "options": "GP Project", 29 | "reqd": 1 30 | }, 31 | { 32 | "fetch_from": "project.team", 33 | "fieldname": "team", 34 | "fieldtype": "Link", 35 | "label": "Team", 36 | "options": "GP Team" 37 | } 38 | ], 39 | "index_web_pages_for_search": 1, 40 | "links": [], 41 | "modified": "2023-05-08 16:57:35.133580", 42 | "modified_by": "Administrator", 43 | "module": "Gameplan", 44 | "name": "GP Guest Access", 45 | "naming_rule": "Autoincrement", 46 | "owner": "Administrator", 47 | "permissions": [ 48 | { 49 | "create": 1, 50 | "delete": 1, 51 | "email": 1, 52 | "export": 1, 53 | "print": 1, 54 | "read": 1, 55 | "report": 1, 56 | "role": "System Manager", 57 | "share": 1, 58 | "write": 1 59 | }, 60 | { 61 | "create": 1, 62 | "delete": 1, 63 | "email": 1, 64 | "export": 1, 65 | "print": 1, 66 | "read": 1, 67 | "report": 1, 68 | "role": "Gameplan Admin", 69 | "share": 1, 70 | "write": 1 71 | }, 72 | { 73 | "create": 1, 74 | "delete": 1, 75 | "email": 1, 76 | "export": 1, 77 | "print": 1, 78 | "read": 1, 79 | "report": 1, 80 | "role": "Gameplan Member", 81 | "share": 1, 82 | "write": 1 83 | }, 84 | { 85 | "email": 1, 86 | "export": 1, 87 | "print": 1, 88 | "read": 1, 89 | "report": 1, 90 | "role": "Gameplan Guest", 91 | "share": 1 92 | } 93 | ], 94 | "sort_field": "modified", 95 | "sort_order": "DESC", 96 | "states": [], 97 | "title_field": "user" 98 | } -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_guest_access/gp_guest_access.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt Ltd and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | from gameplan.mixins.on_delete import delete_linked_records 8 | 9 | 10 | class GPGuestAccess(Document): 11 | pass 12 | 13 | 14 | def on_user_delete(doc, method): 15 | delete_linked_records("User", doc.name, ["GP Guest Access"]) 16 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_guest_access/test_gp_guest_access.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt Ltd and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestGPGuestAccess(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_invitation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/doctype/gp_invitation/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_invitation/gp_invitation.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Frappe Technologies Pvt Ltd and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("GP Invitation", { 5 | refresh(frm) { 6 | if (frm.doc.status != "Accepted") { 7 | frm.add_custom_button(__("Accept Invitation"), () => { 8 | return frm.call("accept_invitation"); 9 | }); 10 | } 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_invitation/test_gp_invitation.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt Ltd and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestGPInvitation(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_member/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/doctype/gp_member/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_member/gp_member.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "autoname": "autoincrement", 4 | "creation": "2022-01-30 19:01:32.894901", 5 | "doctype": "DocType", 6 | "editable_grid": 1, 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "user" 10 | ], 11 | "fields": [ 12 | { 13 | "fieldname": "user", 14 | "fieldtype": "Link", 15 | "in_list_view": 1, 16 | "label": "User", 17 | "options": "User" 18 | } 19 | ], 20 | "index_web_pages_for_search": 1, 21 | "istable": 1, 22 | "links": [], 23 | "modified": "2022-12-09 12:53:23.011368", 24 | "modified_by": "Administrator", 25 | "module": "Gameplan", 26 | "name": "GP Member", 27 | "naming_rule": "Autoincrement", 28 | "owner": "Administrator", 29 | "permissions": [], 30 | "sort_field": "modified", 31 | "sort_order": "DESC", 32 | "states": [] 33 | } -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_member/gp_member.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt Ltd and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | 8 | class GPMember(Document): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_notification/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/doctype/gp_notification/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_notification/gp_notification.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Frappe Technologies Pvt Ltd and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("GP Notification", { 5 | // refresh: function(frm) { 6 | // } 7 | }); 8 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_notification/gp_notification.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt Ltd and contributors 2 | # For license information, please see license.txt 3 | 4 | import frappe 5 | from frappe.model.document import Document 6 | 7 | import gameplan 8 | 9 | 10 | class GPNotification(Document): 11 | def after_insert(self): 12 | gameplan.refetch_resource("Unread Notifications Count", user=self.to_user) 13 | 14 | @staticmethod 15 | def clear_notifications(discussion=None, comment=None, task=None, user=None): 16 | if not user: 17 | user = frappe.session.user 18 | filters = {"to_user": user} 19 | if discussion: 20 | filters["discussion"] = discussion 21 | if comment: 22 | filters["comment"] = comment 23 | if task: 24 | filters["task"] = task 25 | 26 | for notification in frappe.get_all("GP Notification", filters=filters): 27 | doc = frappe.get_doc("GP Notification", notification.name) 28 | doc.read = 1 29 | doc.save() 30 | 31 | gameplan.refetch_resource("Unread Notifications Count", user=user) 32 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_notification/test_gp_notification.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt Ltd and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestGPNotification(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_page/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/doctype/gp_page/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_page/gp_page.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023, Frappe Technologies Pvt Ltd and contributors 2 | // For license information, please see license.txt 3 | 4 | // frappe.ui.form.on("GP Page", { 5 | // refresh(frm) { 6 | 7 | // }, 8 | // }); 9 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_page/gp_page.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Frappe Technologies Pvt Ltd and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | from gameplan.search2 import GameplanSearch, GameplanSearchIndexMissingError 8 | from gameplan.utils import url_safe_slug 9 | 10 | 11 | class GPPage(Document): 12 | def before_save(self): 13 | self.slug = url_safe_slug(self.title) 14 | 15 | def on_update(self): 16 | self.update_search_index() 17 | 18 | def update_search_index(self): 19 | if self.has_value_changed("title") or self.has_value_changed("content"): 20 | try: 21 | search = GameplanSearch() 22 | search.index_doc(self) 23 | except GameplanSearchIndexMissingError: 24 | pass 25 | 26 | def on_trash(self): 27 | try: 28 | search = GameplanSearch() 29 | search.remove_doc(self) 30 | except GameplanSearchIndexMissingError: 31 | pass 32 | 33 | 34 | def has_permission(doc, user, ptype): 35 | if doc.project: 36 | # pages in projects accessible by everyone 37 | return True 38 | if doc.owner == user: 39 | # private pages 40 | return True 41 | return False 42 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_page/test_gp_page.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Frappe Technologies Pvt Ltd and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestGPPage(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_pinned_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/doctype/gp_pinned_project/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_pinned_project/gp_pinned_project.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023, Frappe Technologies Pvt Ltd and contributors 2 | // For license information, please see license.txt 3 | 4 | // frappe.ui.form.on("GP Pinned Project", { 5 | // refresh(frm) { 6 | 7 | // }, 8 | // }); 9 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_pinned_project/gp_pinned_project.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Frappe Technologies Pvt Ltd and contributors 2 | # For license information, please see license.txt 3 | 4 | import frappe 5 | from frappe.model.document import Document 6 | 7 | 8 | class GPPinnedProject(Document): 9 | def before_insert(self): 10 | self.user = frappe.session.user 11 | self.order = frappe.db.count("GP Pinned Project", {"user": self.user}) + 1 12 | 13 | if frappe.db.exists("GP Pinned Project", {"user": self.user, "project": self.project}): 14 | frappe.throw("This project is already pinned") 15 | 16 | @staticmethod 17 | def get_list_query(query): 18 | Pin = frappe.qb.DocType("GP Pinned Project") 19 | query = query.where(Pin.user == frappe.session.user) 20 | return query 21 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_pinned_project/test_gp_pinned_project.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Frappe Technologies Pvt Ltd and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestGPPinnedProject(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_poll/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/doctype/gp_poll/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_poll/gp_poll.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023, Frappe Technologies Pvt Ltd and contributors 2 | // For license information, please see license.txt 3 | 4 | // frappe.ui.form.on("GP Poll", { 5 | // refresh(frm) { 6 | 7 | // }, 8 | // }); 9 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_poll/gp_poll_attributes.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | import datetime 6 | 7 | 8 | class GPPollAttributes: 9 | title: str 10 | options: list 11 | multiple_answers: bool 12 | discussion: str 13 | votes: list 14 | total_votes: int 15 | stopped_at: datetime.datetime 16 | anonymous: bool 17 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_poll/test_gp_poll.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Frappe Technologies Pvt Ltd and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestGPPoll(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_poll_option/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/doctype/gp_poll_option/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_poll_option/gp_poll_option.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "creation": "2023-05-04 19:01:04.163589", 5 | "default_view": "List", 6 | "doctype": "DocType", 7 | "editable_grid": 1, 8 | "engine": "InnoDB", 9 | "field_order": [ 10 | "title", 11 | "percentage", 12 | "votes" 13 | ], 14 | "fields": [ 15 | { 16 | "fieldname": "title", 17 | "fieldtype": "Data", 18 | "in_list_view": 1, 19 | "label": "Title", 20 | "reqd": 1 21 | }, 22 | { 23 | "fieldname": "percentage", 24 | "fieldtype": "Percent", 25 | "label": "Percentage", 26 | "read_only": 1 27 | }, 28 | { 29 | "fieldname": "votes", 30 | "fieldtype": "Int", 31 | "label": "Votes", 32 | "read_only": 1 33 | } 34 | ], 35 | "index_web_pages_for_search": 1, 36 | "istable": 1, 37 | "links": [], 38 | "modified": "2023-05-05 17:19:19.900086", 39 | "modified_by": "Administrator", 40 | "module": "Gameplan", 41 | "name": "GP Poll Option", 42 | "owner": "Administrator", 43 | "permissions": [], 44 | "sort_field": "modified", 45 | "sort_order": "DESC", 46 | "states": [] 47 | } -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_poll_option/gp_poll_option.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Frappe Technologies Pvt Ltd and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | 8 | class GPPollOption(Document): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_poll_vote/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/doctype/gp_poll_vote/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_poll_vote/gp_poll_vote.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "creation": "2023-05-04 21:22:37.139619", 5 | "default_view": "List", 6 | "doctype": "DocType", 7 | "editable_grid": 1, 8 | "engine": "InnoDB", 9 | "field_order": [ 10 | "user", 11 | "option" 12 | ], 13 | "fields": [ 14 | { 15 | "fieldname": "user", 16 | "fieldtype": "Link", 17 | "in_list_view": 1, 18 | "label": "User", 19 | "options": "User", 20 | "reqd": 1 21 | }, 22 | { 23 | "fieldname": "option", 24 | "fieldtype": "Data", 25 | "in_list_view": 1, 26 | "label": "Option" 27 | } 28 | ], 29 | "index_web_pages_for_search": 1, 30 | "istable": 1, 31 | "links": [], 32 | "modified": "2023-05-10 16:53:16.263855", 33 | "modified_by": "Administrator", 34 | "module": "Gameplan", 35 | "name": "GP Poll Vote", 36 | "owner": "Administrator", 37 | "permissions": [], 38 | "sort_field": "modified", 39 | "sort_order": "DESC", 40 | "states": [] 41 | } -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_poll_vote/gp_poll_vote.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Frappe Technologies Pvt Ltd and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | 8 | class GPPollVote(Document): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/doctype/gp_project/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_project/gp_project.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Frappe Technologies Pvt Ltd and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("GP Project", { 5 | // refresh: function(frm) { 6 | // } 7 | }); 8 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_project/patches/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/doctype/gp_project/patches/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_project/patches/migrate_members_from_team.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors 2 | 3 | import frappe 4 | 5 | 6 | def execute(): 7 | private_projects = frappe.db.get_all("GP Project", filters={"is_private": 1}, fields=["name", "team"]) 8 | 9 | for p in private_projects: 10 | if not p.team: 11 | continue 12 | 13 | team = frappe.get_cached_doc("GP Team", p.team) 14 | project = frappe.get_doc("GP Project", p.name) 15 | 16 | for member in team.members: 17 | project.add_member(member.user) 18 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_project/test_gp_project.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt Ltd and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | import unittest 6 | 7 | 8 | class TestGPProject(unittest.TestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_project_visit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/doctype/gp_project_visit/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_project_visit/gp_project_visit.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Frappe Technologies Pvt Ltd and contributors 2 | // For license information, please see license.txt 3 | 4 | // frappe.ui.form.on("GP Project Visit", { 5 | // refresh(frm) { 6 | 7 | // }, 8 | // }); 9 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_project_visit/gp_project_visit.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt Ltd and contributors 2 | # For license information, please see license.txt 3 | 4 | import frappe 5 | from frappe.model.document import Document 6 | 7 | 8 | class GPProjectVisit(Document): 9 | @staticmethod 10 | def get_list_query(query): 11 | ProjectVisit = frappe.qb.DocType("GP Project Visit") 12 | query = query.where(ProjectVisit.user == frappe.session.user) 13 | return query 14 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_project_visit/test_gp_project_visit.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt Ltd and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestGPProjectVisit(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_reaction/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/doctype/gp_reaction/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_reaction/gp_reaction.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Frappe Technologies Pvt Ltd and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("GP Reaction", { 5 | // refresh: function(frm) { 6 | // } 7 | }); 8 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_reaction/gp_reaction.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "autoname": "autoincrement", 4 | "creation": "2022-05-25 22:48:06.287407", 5 | "doctype": "DocType", 6 | "editable_grid": 1, 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "emoji", 10 | "user" 11 | ], 12 | "fields": [ 13 | { 14 | "fieldname": "emoji", 15 | "fieldtype": "Data", 16 | "in_list_view": 1, 17 | "label": "Emoji", 18 | "reqd": 1 19 | }, 20 | { 21 | "fieldname": "user", 22 | "fieldtype": "Data", 23 | "in_list_view": 1, 24 | "label": "User", 25 | "read_only": 1 26 | } 27 | ], 28 | "index_web_pages_for_search": 1, 29 | "istable": 1, 30 | "links": [], 31 | "modified": "2022-08-11 18:36:55.799372", 32 | "modified_by": "Administrator", 33 | "module": "Gameplan", 34 | "name": "GP Reaction", 35 | "naming_rule": "Autoincrement", 36 | "owner": "Administrator", 37 | "permissions": [], 38 | "sort_field": "modified", 39 | "sort_order": "DESC", 40 | "states": [] 41 | } -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_reaction/gp_reaction.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt Ltd and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | 8 | class GPReaction(Document): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_reaction/test_gp_reaction.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt Ltd and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestGPReaction(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_search_feedback/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/doctype/gp_search_feedback/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_search_feedback/gp_search_feedback.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025, Frappe Technologies Pvt Ltd and contributors 2 | // For license information, please see license.txt 3 | 4 | // frappe.ui.form.on("GP Search Feedback", { 5 | // refresh(frm) { 6 | 7 | // }, 8 | // }); 9 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_search_feedback/gp_search_feedback.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "creation": "2025-02-17 15:49:09.650548", 5 | "doctype": "DocType", 6 | "engine": "InnoDB", 7 | "field_order": [ 8 | "user", 9 | "query", 10 | "helpful" 11 | ], 12 | "fields": [ 13 | { 14 | "fieldname": "user", 15 | "fieldtype": "Link", 16 | "in_list_view": 1, 17 | "label": "User", 18 | "options": "User", 19 | "reqd": 1 20 | }, 21 | { 22 | "fieldname": "helpful", 23 | "fieldtype": "Select", 24 | "in_list_view": 1, 25 | "label": "Helpful", 26 | "options": "\nYes\nNo", 27 | "reqd": 1 28 | }, 29 | { 30 | "fieldname": "query", 31 | "fieldtype": "Data", 32 | "label": "Query" 33 | } 34 | ], 35 | "index_web_pages_for_search": 1, 36 | "links": [], 37 | "modified": "2025-02-17 15:58:11.863114", 38 | "modified_by": "Administrator", 39 | "module": "Gameplan", 40 | "name": "GP Search Feedback", 41 | "owner": "Administrator", 42 | "permissions": [ 43 | { 44 | "create": 1, 45 | "delete": 1, 46 | "email": 1, 47 | "export": 1, 48 | "print": 1, 49 | "read": 1, 50 | "report": 1, 51 | "role": "System Manager", 52 | "share": 1, 53 | "write": 1 54 | }, 55 | { 56 | "create": 1, 57 | "delete": 1, 58 | "email": 1, 59 | "export": 1, 60 | "print": 1, 61 | "read": 1, 62 | "report": 1, 63 | "role": "Gameplan Admin", 64 | "share": 1, 65 | "write": 1 66 | }, 67 | { 68 | "create": 1, 69 | "delete": 1, 70 | "email": 1, 71 | "export": 1, 72 | "print": 1, 73 | "read": 1, 74 | "report": 1, 75 | "role": "Gameplan Member", 76 | "share": 1, 77 | "write": 1 78 | }, 79 | { 80 | "create": 1, 81 | "email": 1, 82 | "export": 1, 83 | "print": 1, 84 | "read": 1, 85 | "report": 1, 86 | "role": "Gameplan Guest", 87 | "share": 1, 88 | "write": 1 89 | } 90 | ], 91 | "sort_field": "creation", 92 | "sort_order": "DESC", 93 | "states": [] 94 | } -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_search_feedback/gp_search_feedback.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025, Frappe Technologies Pvt Ltd and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | 8 | class GPSearchFeedback(Document): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_search_feedback/test_gp_search_feedback.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025, Frappe Technologies Pvt Ltd and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests import IntegrationTestCase, UnitTestCase 6 | 7 | # On IntegrationTestCase, the doctype test records and all 8 | # link-field test record dependencies are recursively loaded 9 | # Use these module variables to add/remove to/from that list 10 | EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] 11 | IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] 12 | 13 | 14 | class UnitTestGPSearchFeedback(UnitTestCase): 15 | """ 16 | Unit tests for GPSearchFeedback. 17 | Use this class for testing individual functions and methods. 18 | """ 19 | 20 | pass 21 | 22 | 23 | class IntegrationTestGPSearchFeedback(IntegrationTestCase): 24 | """ 25 | Integration tests for GPSearchFeedback. 26 | Use this class for testing interactions between multiple components. 27 | """ 28 | 29 | pass 30 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_tag/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/doctype/gp_tag/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_tag/gp_tag.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025, Frappe Technologies Pvt Ltd and contributors 2 | // For license information, please see license.txt 3 | 4 | // frappe.ui.form.on("GP Tag", { 5 | // refresh(frm) { 6 | 7 | // }, 8 | // }); 9 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_tag/gp_tag.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "autoname": "autoincrement", 5 | "creation": "2025-05-22 12:12:57.477422", 6 | "doctype": "DocType", 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "label" 10 | ], 11 | "fields": [ 12 | { 13 | "fieldname": "label", 14 | "fieldtype": "Data", 15 | "label": "Label" 16 | } 17 | ], 18 | "grid_page_length": 50, 19 | "index_web_pages_for_search": 1, 20 | "links": [], 21 | "modified": "2025-05-22 12:26:40.729595", 22 | "modified_by": "Administrator", 23 | "module": "Gameplan", 24 | "name": "GP Tag", 25 | "naming_rule": "Autoincrement", 26 | "owner": "Administrator", 27 | "permissions": [ 28 | { 29 | "create": 1, 30 | "delete": 1, 31 | "email": 1, 32 | "export": 1, 33 | "print": 1, 34 | "read": 1, 35 | "report": 1, 36 | "role": "System Manager", 37 | "share": 1, 38 | "write": 1 39 | }, 40 | { 41 | "create": 1, 42 | "delete": 1, 43 | "email": 1, 44 | "export": 1, 45 | "print": 1, 46 | "read": 1, 47 | "report": 1, 48 | "role": "Gameplan Admin", 49 | "share": 1, 50 | "write": 1 51 | }, 52 | { 53 | "create": 1, 54 | "delete": 1, 55 | "email": 1, 56 | "export": 1, 57 | "print": 1, 58 | "read": 1, 59 | "report": 1, 60 | "role": "Gameplan Member", 61 | "share": 1, 62 | "write": 1 63 | }, 64 | { 65 | "create": 1, 66 | "email": 1, 67 | "export": 1, 68 | "print": 1, 69 | "read": 1, 70 | "report": 1, 71 | "role": "Gameplan Guest", 72 | "share": 1, 73 | "write": 1 74 | } 75 | ], 76 | "row_format": "Dynamic", 77 | "show_title_field_in_link": 1, 78 | "sort_field": "creation", 79 | "sort_order": "DESC", 80 | "states": [], 81 | "title_field": "label" 82 | } 83 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_tag/gp_tag.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025, Frappe Technologies Pvt Ltd and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | 8 | class GPTag(Document): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_tag/test_gp_tag.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025, Frappe Technologies Pvt Ltd and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests import IntegrationTestCase, UnitTestCase 6 | 7 | # On IntegrationTestCase, the doctype test records and all 8 | # link-field test record dependencies are recursively loaded 9 | # Use these module variables to add/remove to/from that list 10 | EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] 11 | IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] 12 | 13 | 14 | class UnitTestGPTag(UnitTestCase): 15 | """ 16 | Unit tests for GPTag. 17 | Use this class for testing individual functions and methods. 18 | """ 19 | 20 | pass 21 | 22 | 23 | class IntegrationTestGPTag(IntegrationTestCase): 24 | """ 25 | Integration tests for GPTag. 26 | Use this class for testing interactions between multiple components. 27 | """ 28 | 29 | pass 30 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_tag_link/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/doctype/gp_tag_link/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_tag_link/gp_tag_link.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "creation": "2025-05-22 12:22:12.241569", 5 | "doctype": "DocType", 6 | "editable_grid": 1, 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "tag", 10 | "label" 11 | ], 12 | "fields": [ 13 | { 14 | "fieldname": "tag", 15 | "fieldtype": "Link", 16 | "in_list_view": 1, 17 | "label": "Tag", 18 | "options": "GP Tag" 19 | }, 20 | { 21 | "fetch_from": "tag.label", 22 | "fieldname": "label", 23 | "fieldtype": "Data", 24 | "in_list_view": 1, 25 | "label": "Label", 26 | "read_only": 1 27 | } 28 | ], 29 | "grid_page_length": 50, 30 | "index_web_pages_for_search": 1, 31 | "istable": 1, 32 | "links": [], 33 | "modified": "2025-05-22 13:12:57.407226", 34 | "modified_by": "Administrator", 35 | "module": "Gameplan", 36 | "name": "GP Tag Link", 37 | "owner": "Administrator", 38 | "permissions": [], 39 | "row_format": "Dynamic", 40 | "sort_field": "creation", 41 | "sort_order": "DESC", 42 | "states": [] 43 | } 44 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_tag_link/gp_tag_link.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025, Frappe Technologies Pvt Ltd and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | 8 | class GPTagLink(Document): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_task/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/doctype/gp_task/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_task/gp_task.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Frappe Technologies Pvt Ltd and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("GP Task", { 5 | // refresh: function(frm) { 6 | // } 7 | }); 8 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_task/patches/set_status.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | import frappe 6 | 7 | 8 | def execute(): 9 | Task = frappe.qb.DocType("GP Task") 10 | frappe.qb.update(Task).where(Task.status.isnull()).set(Task.status, "Backlog").run() 11 | frappe.qb.update(Task).where(Task.is_completed == 1).set(Task.status, "Done").run() 12 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_task/test_gp_task.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt Ltd and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | import unittest 6 | 7 | 8 | class TestGPTask(unittest.TestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_team/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/doctype/gp_team/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_team/gp_team.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Frappe Technologies Pvt Ltd and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("GP Team", { 5 | // refresh: function(frm) { 6 | // } 7 | }); 8 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_team/patches/remove_invited_members.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | import frappe 6 | 7 | 8 | def execute(): 9 | frappe.db.delete("GP Member", {"user": ["is", "not set"]}) 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_team/test_gp_team.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt Ltd and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | import unittest 6 | 7 | 8 | class TestGPTeam(unittest.TestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_user_profile/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/gameplan/doctype/gp_user_profile/__init__.py -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_user_profile/gp_user_profile.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Frappe Technologies Pvt Ltd and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("GP User Profile", { 5 | // refresh: function(frm) { 6 | // } 7 | }); 8 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_user_profile/patches/create_user_profile.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | import frappe 6 | 7 | 8 | def execute(): 9 | for user in frappe.get_all("User"): 10 | if user.name in ["Administrator", "Guest"]: 11 | continue 12 | frappe.get_doc(doctype="GP User Profile", user=user.name).insert(ignore_if_duplicate=True) 13 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_user_profile/patches/set_image.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | import frappe 6 | 7 | 8 | def execute(): 9 | UserProfile = frappe.qb.DocType("GP User Profile") 10 | User = frappe.qb.DocType("User") 11 | query = ( 12 | frappe.qb.update(UserProfile) 13 | .set(UserProfile.image, User.user_image) 14 | .left_join(User) 15 | .on(UserProfile.user == User.name) 16 | .where(User.user_image.isnotnull()) 17 | ) 18 | query.run() 19 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_user_profile/patches/set_name.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | import frappe 6 | 7 | 8 | def execute(): 9 | for user in frappe.get_all("GP User Profile"): 10 | doc = frappe.get_doc("GP User Profile", user.name) 11 | doc.rename(doc.generate_name()) 12 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_user_profile/patches/setup_rembg.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | from gameplan.install import download_rembg_model 6 | 7 | 8 | def execute(): 9 | download_rembg_model() 10 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_user_profile/profile_photo.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | import io 6 | 7 | from frappe.core.doctype.file.file import File 8 | 9 | 10 | def remove_background(file: File): 11 | from PIL import Image 12 | from rembg import remove 13 | 14 | input_image = Image.open(file.get_full_path()) 15 | output_image = remove(input_image) 16 | output = io.BytesIO() 17 | output_image.save(output, "png") 18 | return output.getvalue() 19 | -------------------------------------------------------------------------------- /gameplan/gameplan/doctype/gp_user_profile/test_gp_user_profile.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt Ltd and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestGPUserProfile(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /gameplan/install.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | def before_install(): 6 | check_frappe_version() 7 | 8 | 9 | def after_install(): 10 | download_rembg_model() 11 | 12 | 13 | def check_frappe_version(): 14 | from frappe import __version__ 15 | from semantic_version import Version 16 | 17 | frappe_version = Version(__version__) 18 | if (frappe_version.major or 0) < 15: 19 | raise SystemExit("Gameplan requires Frappe Framework version 15 or above") 20 | 21 | 22 | def download_rembg_model(): 23 | from rembg import new_session 24 | 25 | new_session() 26 | -------------------------------------------------------------------------------- /gameplan/mixins/activity.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | import frappe 6 | 7 | 8 | class HasActivity: 9 | """ 10 | Mixin to add utility methods to log activity under "GP Activity" doctype. 11 | """ 12 | 13 | def log_activity(self, action, user=None, data=None): 14 | activities = getattr(self, "activities", []) 15 | if not activities: 16 | raise Exception("No activities defined for this document") 17 | 18 | if action not in activities: 19 | raise Exception("Invalid action to log activity for this document") 20 | 21 | if not user: 22 | user = frappe.session.user 23 | 24 | if data and isinstance(data, dict): 25 | data = frappe.as_json(data, indent=None) 26 | 27 | activity = frappe.get_doc( 28 | doctype="GP Activity", 29 | reference_doctype=self.doctype, 30 | reference_name=self.name, 31 | action=action, 32 | user=user, 33 | data=data, 34 | ).insert(ignore_permissions=True) 35 | 36 | frappe.publish_realtime( 37 | "new_activity", 38 | {"reference_doctype": self.doctype, "reference_name": str(self.name)}, 39 | after_commit=True, 40 | ) 41 | 42 | return activity 43 | -------------------------------------------------------------------------------- /gameplan/mixins/archivable.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | import frappe 6 | 7 | 8 | class Archivable: 9 | """ 10 | Mixin to add archive and unarchive methods to a DocType. `archived_at` (Datetime) and 11 | `archived_by` (Link to User) fields are required for this mixin to work. 12 | """ 13 | 14 | @frappe.whitelist() 15 | def archive(self): 16 | self.archived_at = frappe.utils.now() 17 | self.archived_by = frappe.session.user 18 | self.save() 19 | 20 | @frappe.whitelist() 21 | def unarchive(self): 22 | self.archived_at = None 23 | self.archived_by = None 24 | self.save() 25 | -------------------------------------------------------------------------------- /gameplan/mixins/on_delete.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | import frappe 6 | 7 | 8 | def on_trash(doc, method): 9 | to_delete = getattr(doc, "on_delete_cascade", []) 10 | for doctype in to_delete: 11 | for record in get_linked_records(doc.doctype, doc.name, doctype): 12 | frappe.delete_doc(doctype, record.name) 13 | 14 | to_set_null = getattr(doc, "on_delete_set_null", []) 15 | for doctype in to_set_null: 16 | linked_records = get_linked_records(doc.doctype, doc.name, doctype) 17 | for record in linked_records: 18 | if record.fieldtype == "Link": 19 | frappe.db.set_value(doctype, record.name, record.fieldname, None) 20 | elif record.fieldtype == "Dynamic Link": 21 | frappe.db.set_value( 22 | doctype, record.name, {record.fieldname: None, record.doctype_fieldname: None} 23 | ) 24 | 25 | 26 | def delete_linked_records(doctype, name, linked_doctypes): 27 | for linked_doctype in linked_doctypes: 28 | for record in get_linked_records(doctype, name, linked_doctype): 29 | frappe.delete_doc(linked_doctype, record.name) 30 | 31 | 32 | def get_linked_records(link_doctype, link_name, doctype): 33 | records = [] 34 | meta = frappe.get_meta(doctype) 35 | link_fields = meta.get("fields", {"fieldtype": "Link", "options": link_doctype}) 36 | for field in link_fields: 37 | result = frappe.db.get_all(doctype, {field.fieldname: link_name}) 38 | for r in result: 39 | r.fieldname = field.fieldname 40 | r.fieldtype = "Link" 41 | records += result 42 | 43 | dynamic_link_fields = meta.get("fields", {"fieldtype": "Dynamic Link"}) 44 | for field in dynamic_link_fields: 45 | result = frappe.db.get_all(doctype, {field.options: link_doctype, field.fieldname: link_name}) 46 | for r in result: 47 | r.fieldname = field.fieldname 48 | r.doctype_fieldname = field.options 49 | r.fieldtype = "Dynamic Link" 50 | records += result 51 | 52 | return records 53 | -------------------------------------------------------------------------------- /gameplan/mixins/reactions.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | import frappe 6 | 7 | 8 | class HasReactions: 9 | def notify_reactions(self): 10 | previous = self.get_doc_before_save() 11 | if previous and len(previous.get("reactions")) == len(self.get("reactions")): 12 | return 13 | if len(self.get("reactions")) == 0: 14 | return 15 | 16 | people = list(set([r.user for r in self.get("reactions")])) 17 | match len(people): 18 | case 0: 19 | message = "" 20 | case 1: 21 | message = "1 person reacted to your post" 22 | case _: 23 | message = f"{len(people)} people reacted to your post" 24 | values = frappe._dict( 25 | to_user=self.owner, 26 | type="Reaction", 27 | ) 28 | if self.doctype == "GP Discussion": 29 | values.discussion = self.name 30 | elif self.doctype == "GP Comment": 31 | values.comment = self.name 32 | 33 | if frappe.db.exists("GP Notification", values): 34 | doc = frappe.get_doc("GP Notification", values) 35 | else: 36 | doc = frappe.get_doc(doctype="GP Notification") 37 | doc.update(values) 38 | if self.doctype == "GP Comment": 39 | doc.discussion = self.reference_name if self.reference_doctype == "GP Discussion" else None 40 | doc.task = self.reference_name if self.reference_doctype == "GP Task" else None 41 | doc.message = message 42 | doc.read = 0 43 | doc.flags.ignore_permissions = True 44 | doc.save() 45 | 46 | def de_duplicate_reactions(self): 47 | seen = [] 48 | reactions = [] 49 | for reaction in self.reactions: 50 | row = (reaction.user, reaction.emoji) 51 | if row not in seen: 52 | reactions.append(reaction) 53 | seen.append(row) 54 | self.reactions = reactions 55 | -------------------------------------------------------------------------------- /gameplan/modules.txt: -------------------------------------------------------------------------------- 1 | Gameplan -------------------------------------------------------------------------------- /gameplan/patches.txt: -------------------------------------------------------------------------------- 1 | [pre_model_sync] 2 | gameplan.patches.rename_doctypes_with_gp_prefix 3 | gameplan.gameplan.doctype.gp_discussion.patches.migrate_gp_bookmark_child 4 | 5 | [post_model_sync] 6 | gameplan.gameplan.doctype.team_user_profile.patches.create_user_profile 7 | gameplan.gameplan.doctype.team_user_profile.patches.set_name 8 | gameplan.gameplan.doctype.team_project_discussion.patches.rename_team_project_status_update_doctype 9 | gameplan.gameplan.doctype.team_project_discussion.patches.add_full_text_search_index 10 | gameplan.gameplan.doctype.team_discussion.patches.rename_team_project_discussion_to_team_discussion 11 | execute:frappe.delete_doc('DocType', 'Team Project Section', force=1) 12 | execute:frappe.delete_doc('DocType', 'Task Status', force=1) 13 | execute:frappe.delete_doc('DocType', 'Team Document', force=1) 14 | execute:frappe.delete_doc('DocType', 'Team Attachment', force=1) 15 | execute:frappe.delete_doc('DocType', 'Team Note', force=1) 16 | execute:frappe.delete_doc('DocType', 'Team Link', force=1) 17 | gameplan.gameplan.doctype.team_discussion.patches.set_title_slug 18 | gameplan.gameplan.doctype.team_discussion.patches.update_participants_count 19 | gameplan.patches.update_gameplan_roles 20 | gameplan.gameplan.doctype.team.patches.remove_invited_members 21 | execute:frappe.delete_doc_if_exists('DocType', 'Team Project Discussion', force=1) 22 | gameplan.gameplan.doctype.team_user_profile.patches.setup_rembg 23 | gameplan.gameplan.doctype.team_user_profile.patches.set_image 24 | gameplan.gameplan.doctype.gp_task.patches.set_status 25 | gameplan.gameplan.doctype.gp_discussion_visit.patches.add_unique_constraint 26 | gameplan.gameplan.doctype.gp_project.patches.migrate_members_from_team 27 | gameplan.gameplan.doctype.gp_discussion.patches.set_last_post -------------------------------------------------------------------------------- /gameplan/patches/update_gameplan_roles.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | 5 | import frappe 6 | 7 | 8 | def execute(): 9 | HasRole = frappe.qb.DocType("Has Role") 10 | query = frappe.qb.update(HasRole).set(HasRole.role, "Gameplan Member").where(HasRole.role == "Teams User") 11 | query.run() 12 | 13 | frappe.delete_doc_if_exists("Role", "Teams User") 14 | -------------------------------------------------------------------------------- /gameplan/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/.gitkeep -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-icon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-icon-180.png -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-1125-2436.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-1125-2436.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-1136-640.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-1136-640.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-1170-2532.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-1170-2532.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-1179-2556.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-1179-2556.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-1242-2208.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-1242-2208.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-1242-2688.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-1242-2688.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-1284-2778.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-1284-2778.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-1290-2796.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-1290-2796.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-1334-750.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-1334-750.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-1536-2048.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-1536-2048.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-1620-2160.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-1620-2160.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-1668-2224.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-1668-2224.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-1668-2388.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-1668-2388.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-1792-828.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-1792-828.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-2048-1536.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-2048-1536.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-2048-2732.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-2048-2732.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-2160-1620.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-2160-1620.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-2208-1242.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-2208-1242.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-2224-1668.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-2224-1668.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-2388-1668.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-2388-1668.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-2436-1125.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-2436-1125.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-2532-1170.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-2532-1170.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-2556-1179.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-2556-1179.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-2688-1242.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-2688-1242.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-2732-2048.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-2732-2048.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-2778-1284.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-2778-1284.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-2796-1290.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-2796-1290.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-640-1136.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-640-1136.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-750-1334.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-750-1334.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/apple-splash-828-1792.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/apple-splash-828-1792.jpg -------------------------------------------------------------------------------- /gameplan/public/manifest/favicon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/favicon-180.png -------------------------------------------------------------------------------- /gameplan/public/manifest/favicon-196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/favicon-196.png -------------------------------------------------------------------------------- /gameplan/public/manifest/manifest-icon-192.maskable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/manifest-icon-192.maskable.png -------------------------------------------------------------------------------- /gameplan/public/manifest/manifest-icon-512.maskable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/public/manifest/manifest-icon-512.maskable.png -------------------------------------------------------------------------------- /gameplan/public/manifest/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "display": "standalone", 3 | "short_name": "Gameplan", 4 | "icons": [ 5 | { 6 | "src": "/assets/gameplan/manifest/manifest-icon-192.maskable.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "any" 10 | }, 11 | { 12 | "src": "/assets/gameplan/manifest/manifest-icon-192.maskable.png", 13 | "sizes": "192x192", 14 | "type": "image/png", 15 | "purpose": "maskable" 16 | }, 17 | { 18 | "src": "/assets/gameplan/manifest/manifest-icon-512.maskable.png", 19 | "sizes": "512x512", 20 | "type": "image/png", 21 | "purpose": "any" 22 | }, 23 | { 24 | "src": "/assets/gameplan/manifest/manifest-icon-512.maskable.png", 25 | "sizes": "512x512", 26 | "type": "image/png", 27 | "purpose": "maskable" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /gameplan/templates/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/templates/__init__.py -------------------------------------------------------------------------------- /gameplan/templates/emails/gameplan_invitation.html: -------------------------------------------------------------------------------- 1 |

You have been invited to join Gameplan

2 |

3 | Accept Invitation 4 |

5 | -------------------------------------------------------------------------------- /gameplan/templates/pages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/templates/pages/__init__.py -------------------------------------------------------------------------------- /gameplan/test_api.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | import frappe 5 | 6 | 7 | def whitelist(fn): 8 | if not frappe.conf.enable_ui_tests: 9 | frappe.throw("Cannot run UI tests. Set 'enable_ui_tests' in site_config.json to continue.") 10 | 11 | whitelisted = frappe.whitelist()(fn) 12 | return whitelisted 13 | 14 | 15 | @whitelist 16 | def clear_data(onboard=None): 17 | doctypes = frappe.get_all("DocType", filters={"module": "Gameplan"}, pluck="name") 18 | for doctype in doctypes: 19 | frappe.db.delete(doctype) 20 | 21 | admin = frappe.get_doc("User", "Administrator") 22 | admin.add_roles("Gameplan Admin") 23 | 24 | if not frappe.db.exists("User", "john@example.com"): 25 | frappe.get_doc( 26 | doctype="User", 27 | email="john@example.com", 28 | first_name="John", 29 | last_name="Doe", 30 | send_welcome_email=0, 31 | roles=[{"role": "Gameplan Member"}], 32 | ).insert() 33 | 34 | if not frappe.db.exists("User", "system@example.com"): 35 | frappe.get_doc( 36 | doctype="User", 37 | email="system@example.com", 38 | first_name="System", 39 | last_name="User", 40 | send_welcome_email=0, 41 | roles=[{"role": "Gameplan Admin"}, {"role": "System Manager"}], 42 | ).insert() 43 | 44 | keep_users = ["Administrator", "Guest", "john@example.com", "system@example.com"] 45 | for user in frappe.get_all("User", filters={"name": ["not in", keep_users]}): 46 | frappe.delete_doc("User", user.name) 47 | 48 | if onboard: 49 | frappe.get_doc(doctype="GP Team", title="Test Team").insert() 50 | -------------------------------------------------------------------------------- /gameplan/unsplash.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors 2 | # See license.txt 3 | 4 | 5 | import frappe 6 | 7 | base_url = "https://api.unsplash.com" 8 | 9 | 10 | def get_by_keyword(keyword): 11 | data = make_unsplash_request(f"/search/photos?query={keyword}") 12 | return data.get("results") 13 | 14 | 15 | def get_list(): 16 | return make_unsplash_request("/photos") 17 | 18 | 19 | def get_random(params=None): 20 | query_string = "" 21 | for key, value in params.items(): 22 | query_string += f"{key}={value}&" 23 | return make_unsplash_request(f"/photos/random?{query_string}") 24 | 25 | 26 | def make_unsplash_request(path): 27 | if "unsplash_access_key" not in frappe.conf: 28 | frappe.throw("Please set unsplash_access_key in site_config.json") 29 | 30 | import requests 31 | 32 | url = f"{base_url}{path}" 33 | print(url) 34 | res = requests.get( 35 | url, 36 | headers={ 37 | "Accept-Version": "v1", 38 | "Authorization": f"Client-ID {frappe.conf.unsplash_access_key}", 39 | }, 40 | ) 41 | res.raise_for_status() 42 | data = res.json() 43 | return data 44 | -------------------------------------------------------------------------------- /gameplan/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .utils import * 2 | -------------------------------------------------------------------------------- /gameplan/www/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/gameplan/69025284cda530e11b2cbe9101c3243f99b44b06/gameplan/www/__init__.py -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "check-pnpm": "chmod +x ./scripts/install-pnpm.sh && ./scripts/install-pnpm.sh", 6 | "frontend:pnpm_install": "npm run check-pnpm && cd frontend && pnpm install", 7 | "postinstall": "npm run frontend:pnpm_install", 8 | "dev": "cd frontend && pnpm dev", 9 | "build": "pnpm check-pnpm && cd frontend && pnpm build", 10 | "disable-workspaces": "chmod +x ./scripts/manage_workspaces.sh && ./scripts/manage_workspaces.sh disable", 11 | "enable-workspaces": "chmod +x ./scripts/manage_workspaces.sh && ./scripts/manage_workspaces.sh enable", 12 | "upgrade-frappeui": "cd frontend && pnpm add frappe-ui@latest && cd .." 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml.disabled: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'frontend' 3 | - 'frappe-ui' -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "gameplan" 3 | authors = [ 4 | { name = "Frappe Technologies Pvt Ltd", email = "developers@frappe.io" }, 5 | ] 6 | description = "Team discussion and collaboration tool" 7 | requires-python = ">=3.10" 8 | readme = "README.md" 9 | dynamic = ["version"] 10 | dependencies = [ 11 | "rembg>=2.0.49,<2.1", 12 | "numpy==1.26.1", 13 | "onnxruntime==1.16.2", 14 | "faker==37.3.0", 15 | ] 16 | 17 | [build-system] 18 | requires = ["flit_core >=3.4,<4"] 19 | build-backend = "flit_core.buildapi" 20 | 21 | [tool.ruff] 22 | line-length = 110 23 | target-version = "py310" 24 | 25 | [tool.ruff.lint] 26 | select = ["F", "E", "W", "I", "UP", "B"] 27 | ignore = [ 28 | "F403", # can't detect undefined names from * import 29 | "W191", # indentation contains tabs 30 | ] 31 | 32 | [tool.ruff.lint.per-file-ignores] 33 | "**/demo/**" = ["E501"] # Disable line too long for demo files 34 | 35 | [tool.ruff.format] 36 | quote-style = "double" 37 | indent-style = "tab" 38 | docstring-code-format = true 39 | -------------------------------------------------------------------------------- /scripts/install-pnpm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if ! command -v pnpm &> /dev/null 4 | then 5 | echo "pnpm could not be found, installing..." 6 | npm install -g pnpm@latest-10 7 | else 8 | echo "pnpm is already installed." 9 | fi 10 | 11 | echo "pnpm version: $(pnpm --version)" 12 | 13 | exit 0 14 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | --------------------------------------------------------------------------------