diff --git a/python/intellij.python.community.impl.iml b/python/intellij.python.community.impl.iml index 605dd0ff08ec..47086ef55bb4 100644 --- a/python/intellij.python.community.impl.iml +++ b/python/intellij.python.community.impl.iml @@ -153,6 +153,8 @@ + + diff --git a/python/pluginCore/resources/META-INF/plugin.xml b/python/pluginCore/resources/META-INF/plugin.xml index 58e36234692b..7f7363603493 100644 --- a/python/pluginCore/resources/META-INF/plugin.xml +++ b/python/pluginCore/resources/META-INF/plugin.xml @@ -638,6 +638,10 @@ The Python plug-in provides smart editing for Python scripts. The feature set of key="quickfix.ranking.ml" defaultValue="[IN_EXPERIMENT*|ENABLED|DISABLED]" description="Enable ML ranking in quick fix for missing imports"/> + + + + diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties index 4a464135643a..55e62e605e3b 100644 --- a/python/pluginResources/messages/PyBundle.properties +++ b/python/pluginResources/messages/PyBundle.properties @@ -1551,4 +1551,19 @@ package.install.with.options.dialog.message=Options: package.install.with.options.dialog.title=Package Install with Options python.toolwindow.packages.collapse.all.action=Collapse All django.template.language=Template Language -python.error=Error \ No newline at end of file +python.error=Error + + +python.survey.user.job.notification.group=PyCharm Job Survey +python.survey.user.job.notification.title=Feedback In IDE +python.survey.user.job.notification.content=Tell us about your Python development experience. Just one question. + +python.survey.user.job.dialog.title=Feedback +python.survey.user.job.dialog.blocks.top=Python development +python.survey.user.job.dialog.blocks.checkbox.group=What do you use Python for? +python.survey.user.job.dialog.blocks.checkbox.data=Data analysis +python.survey.user.job.dialog.blocks.checkbox.ml=Machine learning +python.survey.user.job.dialog.blocks.checkbox.web=Web development +python.survey.user.job.dialog.blocks.checkbox.scripts=Writing automation scripts / parsers / tests / system administration +python.survey.user.job.dialog.blocks.checkbox.other=Other + diff --git a/python/src/com/jetbrains/python/statistics/feedback/PyCharmJobSurveyUtils.kt b/python/src/com/jetbrains/python/statistics/feedback/PyCharmJobSurveyUtils.kt new file mode 100644 index 000000000000..74b9b9efec5c --- /dev/null +++ b/python/src/com/jetbrains/python/statistics/feedback/PyCharmJobSurveyUtils.kt @@ -0,0 +1,35 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.statistics.feedback + +import com.intellij.openapi.components.BaseState +import com.intellij.openapi.project.Project +import com.intellij.openapi.startup.ProjectActivity +import kotlinx.datetime.Clock +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toJavaLocalDateTime +import kotlinx.datetime.toLocalDateTime +import java.time.Duration + + +class PythonJobSurveyState : BaseState() { + var firstLaunch by string() +} + +class PythonFirstLaunchChecker : ProjectActivity { + override suspend fun execute(project: Project) { + val state = PythonJobSurveyService.getInstance().state ?: return + if (state.firstLaunch == null) { + val dateTime = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) + state.firstLaunch = LocalDateTime.Formats.ISO.format(dateTime) + } + } +} + + +fun shouldShowSurvey(): Boolean { + val firstLaunch = PythonJobSurveyService.getInstance().state?.firstLaunch ?: return false + val dateTime = LocalDateTime.Formats.ISO.parse(firstLaunch) + // Start checking on the third day of usage + return Duration.between(dateTime.toJavaLocalDateTime(), java.time.LocalDateTime.now()).toDays() >= 3 +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/statistics/feedback/PythonJobStatisticsCollector.kt b/python/src/com/jetbrains/python/statistics/feedback/PythonJobStatisticsCollector.kt new file mode 100644 index 000000000000..336bc6e1b8a9 --- /dev/null +++ b/python/src/com/jetbrains/python/statistics/feedback/PythonJobStatisticsCollector.kt @@ -0,0 +1,38 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.statistics.feedback + + +import com.intellij.internal.statistic.eventLog.EventLogGroup +import com.intellij.internal.statistic.eventLog.events.EventFields +import com.intellij.internal.statistic.service.fus.collectors.CounterUsagesCollector +import com.intellij.internal.statistic.eventLog.events.EventId2 +import com.intellij.internal.statistic.eventLog.validator.ValidationResultType +import com.intellij.internal.statistic.eventLog.validator.rules.EventContext +import com.intellij.internal.statistic.eventLog.validator.rules.impl.CustomValidationRule + +object PythonJobStatisticsCollector : CounterUsagesCollector() { + private val GROUP = EventLogGroup("python.job.statistics", 1) + private val USE_FOR = EventFields.StringList("use_for", listOf("data_analysis", "ml", "web_dev", "scripts")) + private val OTHER = EventFields.StringValidatedByCustomRule("other") + private val JOB_EVENT: EventId2, String?> = GROUP.registerEvent("job.event", USE_FOR, OTHER) + + @JvmStatic + fun logJobEvent(useFor: List, other: String?) { + JOB_EVENT.log(useFor, other) + } + + override fun getGroup(): EventLogGroup { + return GROUP + } +} + + +class TrueValidationRule() : CustomValidationRule() { + override fun getRuleId(): String { + return "python-user-job-other" + } + + override fun doValidate(data: String, context: EventContext): ValidationResultType { + return ValidationResultType.ACCEPTED + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/statistics/feedback/PythonJobSurvey.kt b/python/src/com/jetbrains/python/statistics/feedback/PythonJobSurvey.kt new file mode 100644 index 000000000000..e33fecadca01 --- /dev/null +++ b/python/src/com/jetbrains/python/statistics/feedback/PythonJobSurvey.kt @@ -0,0 +1,11 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.statistics.feedback + +import com.intellij.platform.feedback.FeedbackSurvey +import com.intellij.platform.feedback.InIdeFeedbackSurveyConfig +import com.intellij.platform.feedback.InIdeFeedbackSurveyType + +class PythonJobSurvey : FeedbackSurvey() { + override val feedbackSurveyType: InIdeFeedbackSurveyType = + InIdeFeedbackSurveyType(PythonJobSurveyConfig()) +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/statistics/feedback/PythonJobSurveyConfig.kt b/python/src/com/jetbrains/python/statistics/feedback/PythonJobSurveyConfig.kt new file mode 100644 index 000000000000..d057515c22a3 --- /dev/null +++ b/python/src/com/jetbrains/python/statistics/feedback/PythonJobSurveyConfig.kt @@ -0,0 +1,42 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.statistics.feedback + +import com.intellij.openapi.project.Project +import com.intellij.platform.feedback.InIdeFeedbackSurveyConfig +import com.intellij.platform.feedback.dialog.BlockBasedFeedbackDialog +import com.intellij.platform.feedback.dialog.SystemDataJsonSerializable +import com.intellij.platform.feedback.impl.notification.RequestFeedbackNotification +import com.intellij.util.PlatformUtils +import com.jetbrains.python.PyBundle.message +import kotlinx.datetime.LocalDate + +class PythonJobSurveyConfig : InIdeFeedbackSurveyConfig { + + override fun createFeedbackDialog(project: Project, forTest: Boolean): BlockBasedFeedbackDialog { + return PythonUserJobFeedbackDialog(project, forTest) + } + + override val surveyId: String = "python_user_job_survey" + override val requireIdeEAP: Boolean = true + override val lastDayOfFeedbackCollection: LocalDate + get() = LocalDate(2024, 12, 31) + + override fun checkIdeIsSuitable(): Boolean = PlatformUtils.isPyCharmCommunity() + override fun checkExtraConditionSatisfied(project: Project): Boolean = shouldShowSurvey() + + override fun createNotification(project: Project, forTest: Boolean): RequestFeedbackNotification { + return RequestFeedbackNotification( + message("python.survey.user.job.notification.group"), + message("python.survey.user.job.notification.title"), + message("python.survey.user.job.notification.content"), + ) + } + + override fun updateStateAfterNotificationShowed(project: Project) { + + } + + override fun updateStateAfterDialogClosedOk(project: Project) { + + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/statistics/feedback/PythonJobSurveyService.kt b/python/src/com/jetbrains/python/statistics/feedback/PythonJobSurveyService.kt new file mode 100644 index 000000000000..5f7a941ebab8 --- /dev/null +++ b/python/src/com/jetbrains/python/statistics/feedback/PythonJobSurveyService.kt @@ -0,0 +1,25 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.statistics.feedback + +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.openapi.components.service + +@Service(Service.Level.APP) +@State(name="PythonJobSurveyService", storages = [Storage("pycharm-job-survey-service.xml")]) +class PythonJobSurveyService : PersistentStateComponent { + private var state = PythonJobSurveyState() + + override fun getState(): PythonJobSurveyState? = state + + override fun loadState(state: PythonJobSurveyState) { + this.state = state + } + + companion object { + @JvmStatic + fun getInstance(): PythonJobSurveyService = service() + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/statistics/feedback/PythonUserJobFeedbackDialog.kt b/python/src/com/jetbrains/python/statistics/feedback/PythonUserJobFeedbackDialog.kt new file mode 100644 index 000000000000..c4a34fc06324 --- /dev/null +++ b/python/src/com/jetbrains/python/statistics/feedback/PythonUserJobFeedbackDialog.kt @@ -0,0 +1,62 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.statistics.feedback + +import com.intellij.openapi.project.Project +import com.intellij.platform.feedback.dialog.BlockBasedFeedbackDialog +import com.intellij.platform.feedback.dialog.CommonFeedbackSystemData +import com.intellij.platform.feedback.dialog.showFeedbackSystemInfoDialog +import com.intellij.platform.feedback.dialog.uiBlocks.CheckBoxGroupBlock +import com.intellij.platform.feedback.dialog.uiBlocks.CheckBoxItemData +import com.intellij.platform.feedback.dialog.uiBlocks.FeedbackBlock +import com.intellij.platform.feedback.dialog.uiBlocks.TopLabelBlock +import com.jetbrains.python.PyBundle.message + +class PythonUserJobFeedbackDialog( + project: Project?, + forTest: Boolean, +) : BlockBasedFeedbackDialog(project, forTest) { + + override val myFeedbackReportId: String + get() = "python_user_job_survey" + + + override val myTitle: String + get() = message("python.survey.user.job.dialog.title") + + + val items = listOf( + CheckBoxItemData(message("python.survey.user.job.dialog.blocks.checkbox.data"), "data_analysis"), + CheckBoxItemData(message("python.survey.user.job.dialog.blocks.checkbox.ml"), "ml"), + CheckBoxItemData(message("python.survey.user.job.dialog.blocks.checkbox.web"), "web_dev"), + CheckBoxItemData(message("python.survey.user.job.dialog.blocks.checkbox.scripts"), "scripts"), + ) + + override val myBlocks: List = listOf( + TopLabelBlock(message("python.survey.user.job.dialog.blocks.top")), + CheckBoxGroupBlock(message("python.survey.user.job.dialog.blocks.checkbox.group"), items, "python_job_checkbox") + .addOtherTextField(message("python.survey.user.job.dialog.blocks.checkbox.other")) + .requireAnswer(), + ) + + override val mySystemInfoData: CommonFeedbackSystemData + get() = CommonFeedbackSystemData.getCurrentData() + + override val myShowFeedbackSystemInfoDialog: () -> Unit = { + showFeedbackSystemInfoDialog(myProject, mySystemInfoData) + } + + override fun sendFeedbackData() { + super.sendFeedbackData() + + val builder = StringBuilder() + (myBlocks[1] as CheckBoxGroupBlock).collectBlockTextDescription(builder) + val selectedItems = items.filter { it.property }.map { it.jsonElementName } + val other = builder.toString().lines()[5].substringAfter(message("python.survey.user.job.dialog.blocks.checkbox.other") + ": ") + + PythonJobStatisticsCollector.logJobEvent(selectedItems, other) + } + + init { + init() + } +} \ No newline at end of file