diff --git a/api4/cloud.go b/api4/cloud.go index 2499164fac..d517291d48 100644 --- a/api4/cloud.go +++ b/api4/cloud.go @@ -40,6 +40,7 @@ func (api *API) InitCloud() { api.BaseRoutes.Cloud.Handle("/subscription", api.APISessionRequired(getSubscription)).Methods("GET") api.BaseRoutes.Cloud.Handle("/subscription/invoices", api.APISessionRequired(getInvoicesForSubscription)).Methods("GET") api.BaseRoutes.Cloud.Handle("/subscription/invoices/{invoice_id:[A-Za-z0-9]+}/pdf", api.APISessionRequired(getSubscriptionInvoicePDF)).Methods("GET") + api.BaseRoutes.Cloud.Handle("/subscription/expand", api.APISessionRequired(GetLicenseExpandStatus)).Methods("GET") api.BaseRoutes.Cloud.Handle("/subscription", api.APISessionRequired(changeSubscription)).Methods("PUT") // GET /api/v4/cloud/request-trial @@ -413,6 +414,34 @@ func getCloudCustomer(c *Context, w http.ResponseWriter, r *http.Request) { w.Write(json) } +func GetLicenseExpandStatus(c *Context, w http.ResponseWriter, r *http.Request) { + if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageLicenseInformation) { + c.SetPermissionError(model.PermissionManageLicenseInformation) + return + } + + _, token, err := c.App.Srv().GenerateLicenseRenewalLink() + + if err != nil { + c.Err = err + return + } + + res, cloudErr := c.App.Cloud().GetLicenseExpandStatus(c.AppContext.Session().UserId, token) + if cloudErr != nil { + c.Err = model.NewAppError("Api4.GetLicenseExpandStatusForSubscription", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(cloudErr) + return + } + + json, jsonErr := json.Marshal(res) + if jsonErr != nil { + c.Err = model.NewAppError("Api4.GetLicenseExpandStatusForSubscription", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr) + return + } + + w.Write(json) +} + func updateCloudCustomer(c *Context, w http.ResponseWriter, r *http.Request) { if !c.App.Channels().License().IsCloud() { c.Err = model.NewAppError("Api4.updateCloudCustomer", "api.cloud.license_error", nil, "", http.StatusForbidden) diff --git a/api4/cloud_test.go b/api4/cloud_test.go index 75bf89f631..399ee67f45 100644 --- a/api4/cloud_test.go +++ b/api4/cloud_test.go @@ -649,6 +649,58 @@ func TestGetCloudProducts(t *testing.T) { }) } +func Test_GetExpandStatsForSubscription(t *testing.T) { + isExpandable := &model.SubscriptionExpandStatus{ + IsExpandable: true, + } + + licenseId := "licenseID" + + t.Run("NON Admin users are UNABLE to request expand stats for the subscription", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + th.Client.Login(th.BasicUser.Email, th.BasicUser.Password) + + cloud := mocks.CloudInterface{} + + cloud.Mock.On("GetLicenseExpandStatus", mock.Anything).Return(isExpandable, nil) + + cloudImpl := th.App.Srv().Cloud + defer func() { + th.App.Srv().Cloud = cloudImpl + }() + th.App.Srv().Cloud = &cloud + + subscriptionExpandable, r, err := th.Client.GetExpandStats(licenseId) + require.Error(t, err) + require.Nil(t, subscriptionExpandable) + require.Equal(t, http.StatusForbidden, r.StatusCode, "403 Forbidden") + }) + + t.Run("Admin users are UNABLE to request licenses is expendable due missing the id", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + th.Client.Login(th.SystemAdminUser.Email, th.SystemAdminUser.Password) + + cloud := mocks.CloudInterface{} + + cloud.Mock.On("GetLicenseExpandStatus", mock.Anything).Return(isExpandable, nil) + + cloudImpl := th.App.Srv().Cloud + defer func() { + th.App.Srv().Cloud = cloudImpl + }() + th.App.Srv().Cloud = &cloud + + subscriptionExpandable, r, err := th.Client.GetExpandStats("") + require.Error(t, err) + require.Nil(t, subscriptionExpandable) + require.Equal(t, http.StatusBadRequest, r.StatusCode, "400 Bad Request") + }) +} + func TestGetSelfHostedProducts(t *testing.T) { products := []*model.Product{ { diff --git a/app/export.go b/app/export.go index 6afd544534..8e52fa74e7 100644 --- a/app/export.go +++ b/app/export.go @@ -398,8 +398,15 @@ func (a *App) buildUserNotifyProps(notifyProps model.StringMap) *imports.UserNot func (a *App) exportAllPosts(ctx request.CTX, writer io.Writer, withAttachments bool) ([]imports.AttachmentImportData, *model.AppError) { var attachments []imports.AttachmentImportData afterId := strings.Repeat("0", 26) + var postProcessCount uint64 + logCheckpoint := time.Now() for { + if time.Since(logCheckpoint) > 5*time.Minute { + ctx.Logger().Debug(fmt.Sprintf("Bulk Export: processed %d posts", postProcessCount)) + logCheckpoint = time.Now() + } + posts, nErr := a.Srv().Store().Post().GetParentsForExportAfter(1000, afterId) if nErr != nil { return nil, model.NewAppError("exportAllPosts", "app.post.get_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) @@ -411,6 +418,7 @@ func (a *App) exportAllPosts(ctx request.CTX, writer io.Writer, withAttachments for _, post := range posts { afterId = post.Id + postProcessCount++ // Skip deleted. if post.DeleteAt != 0 { @@ -677,7 +685,15 @@ func (a *App) buildFavoritedByList(channelID string) ([]string, *model.AppError) func (a *App) exportAllDirectPosts(ctx request.CTX, writer io.Writer, withAttachments bool) ([]imports.AttachmentImportData, *model.AppError) { var attachments []imports.AttachmentImportData afterId := strings.Repeat("0", 26) + var postProcessCount uint64 + logCheckpoint := time.Now() + for { + if time.Since(logCheckpoint) > 5*time.Minute { + ctx.Logger().Debug(fmt.Sprintf("Bulk Export: processed %d direct posts", postProcessCount)) + logCheckpoint = time.Now() + } + posts, err := a.Srv().Store().Post().GetDirectPostParentsForExportAfter(1000, afterId) if err != nil { return nil, model.NewAppError("exportAllDirectPosts", "app.post.get_direct_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(err) @@ -689,6 +705,7 @@ func (a *App) exportAllDirectPosts(ctx request.CTX, writer io.Writer, withAttach for _, post := range posts { afterId = post.Id + postProcessCount++ // Skip deleted. if post.DeleteAt != 0 { diff --git a/einterfaces/cloud.go b/einterfaces/cloud.go index d58e8c92dd..0c70c02cf3 100644 --- a/einterfaces/cloud.go +++ b/einterfaces/cloud.go @@ -17,6 +17,7 @@ type CloudInterface interface { ConfirmCustomerPayment(userID string, confirmRequest *model.ConfirmPaymentMethodRequest) error GetCloudCustomer(userID string) (*model.CloudCustomer, error) + GetLicenseExpandStatus(userID string, token string) (*model.SubscriptionExpandStatus, error) UpdateCloudCustomer(userID string, customerInfo *model.CloudCustomerInfo) (*model.CloudCustomer, error) UpdateCloudCustomerAddress(userID string, address *model.Address) (*model.CloudCustomer, error) diff --git a/einterfaces/mocks/CloudInterface.go b/einterfaces/mocks/CloudInterface.go index 4b89135dc0..a447399cd8 100644 --- a/einterfaces/mocks/CloudInterface.go +++ b/einterfaces/mocks/CloudInterface.go @@ -325,6 +325,29 @@ func (_m *CloudInterface) GetInvoicesForSubscription(userID string) ([]*model.In return r0, r1 } +// GetLicenseExpandStatus provides a mock function with given fields: userID, token +func (_m *CloudInterface) GetLicenseExpandStatus(userID string, token string) (*model.SubscriptionExpandStatus, error) { + ret := _m.Called(userID, token) + + var r0 *model.SubscriptionExpandStatus + if rf, ok := ret.Get(0).(func(string, string) *model.SubscriptionExpandStatus); ok { + r0 = rf(userID, token) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.SubscriptionExpandStatus) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, string) error); ok { + r1 = rf(userID, token) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetLicenseRenewalStatus provides a mock function with given fields: userID, token func (_m *CloudInterface) GetLicenseRenewalStatus(userID string, token string) error { ret := _m.Called(userID, token) diff --git a/i18n/bg.json b/i18n/bg.json index 1e2ac3b534..f9fa12c015 100644 --- a/i18n/bg.json +++ b/i18n/bg.json @@ -1887,14 +1887,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "Изтече времето на AD/LDAP задачата за синхронизация." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Не може да се анализира заданието за износ на съобщения ExportFromTimestamp." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Не може да се анализира заданието за износ на съобщения BatchSize." - }, { "id": "ent.id_loaded.license_disable.app_error", "translation": "Лицензът ви не поддържа ID заредени изскачащи известия." diff --git a/i18n/de.json b/i18n/de.json index 7daafc82da..82a8cccb82 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -6355,14 +6355,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "AD/LDAP-Synchronisierungs-Job-Timeout erreicht." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Konnte ExportFromTimestamp des Nachrichten-Export-Jobs nicht verarbeiten." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Konnte BatchSize des Nachrichten-Export-Jobs nicht verarbeiten." - }, { "id": "ent.cluster.404.app_error", "translation": "Cluster-API-Endpunkt nicht gefunden." diff --git a/i18n/en.json b/i18n/en.json index 8fb648f2ef..94b78b2f23 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -7595,14 +7595,6 @@ "id": "ent.id_loaded.license_disable.app_error", "translation": "Your license does not support ID Loaded Push Notifications." }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Could not parse message export job BatchSize." - }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Could not parse message export job ExportFromTimestamp." - }, { "id": "ent.jobs.start_synchronize_job.timeout", "translation": "Reached AD/LDAP synchronization job timeout." diff --git a/i18n/en_AU.json b/i18n/en_AU.json index 3283b75397..e03b05824f 100644 --- a/i18n/en_AU.json +++ b/i18n/en_AU.json @@ -3251,14 +3251,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "Reached AD/LDAP synchronisation job timeout." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Could not parse message export job ExportFromTimestamp." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Could not parse message export job BatchSize." - }, { "id": "ent.id_loaded.license_disable.app_error", "translation": "Your licence does not support ID Loaded Push Notifications." @@ -9693,5 +9685,9 @@ { "id": "api.user.get_users.validation.app_error", "translation": "Error fetching roles during validation." + }, + { + "id": "api.server.hosted_signup_unavailable.error", + "translation": "Portal unavailable for self-hosted signup." } ] diff --git a/i18n/es.json b/i18n/es.json index 68dd0230be..4de9de9d37 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -6571,14 +6571,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "El tiempo de espera del trabajo de sincronización AD/LDAP fue alcanzado." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "No se pudo interpretar la fecha y hora de inicio de la exportación de mensajes." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "No se pudo interpretar el tamaño de bloque de datos de la exportación de mensajes." - }, { "id": "ent.cluster.404.app_error", "translation": "No se encontró el endpoint del API para el agrupamiento de servidores." diff --git a/i18n/fa.json b/i18n/fa.json index cdee577b6c..f348528ffb 100644 --- a/i18n/fa.json +++ b/i18n/fa.json @@ -1919,14 +1919,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "مهلت زمانی هماهنگ سازی AD/LDAP رسیده است." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "تجزیه و تحلیل کار صادرات پیام ExportFromTimestamp امکان پذیر نیست." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "تجزیه و تحلیل کار صادرات پیام صادرات از مهر زمان انجام نمی شود.." - }, { "id": "ent.id_loaded.license_disable.app_error", "translation": "مجوز شما از ID Loaded Push Notifications پشتیبانی نمی کند." diff --git a/i18n/fr.json b/i18n/fr.json index 107121d484..356a12a5e3 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -6343,14 +6343,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "Temps d'attente pour la tâche de synchronisation AD/LDAP atteint." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Impossible d'interpréter le paramètre ExportFromTimestamp de la tâche d'exportation de messages." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Impossible d'interpréter le paramètre BatchSize de la tâche d'exportation de messages." - }, { "id": "ent.cluster.404.app_error", "translation": "Le nœud d'API cluster est introuvable." diff --git a/i18n/hu.json b/i18n/hu.json index 3ddcd9f7b7..c010bb1704 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -3291,14 +3291,6 @@ "id": "model.upload_session.is_valid.id.app_error", "translation": "Érvénytelen érték a Id -nak" }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Nem sikerült feldolgozni az üzenet export munka ExportFromTimestamp értékét." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Nem sikerült feldolgozni az üzenet export BatchSize értékét." - }, { "id": "ent.id_loaded.license_disable.app_error", "translation": "Az Ön licensze nem támogatja az azonosítóval betöltött push értesítéseket." diff --git a/i18n/it.json b/i18n/it.json index 762c7d9bcc..14f201c891 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -6587,14 +6587,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "Raggiunto timeout nel lavoro di sincronizzazione AD/LDAP." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Impossibile analizzare il messaggio del lavoro di esportazione ExportFromTimestamp." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Impossibile analizzare il messaggio del lavoro di esportazione BatchSize." - }, { "id": "ent.cluster.404.app_error", "translation": "Cluster API non trovate." diff --git a/i18n/ja.json b/i18n/ja.json index 8e854aa2e9..7625ed5305 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -6563,14 +6563,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "AD/LDAP同期処理がタイムアウトしました。" }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "メッセージエクスポートジョブのExportFromTimestampを解析できませんでした。" - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "メッセージエクスポートジョブのBatchSizeを解析できませんでした。" - }, { "id": "ent.cluster.404.app_error", "translation": "クラスターAPIエンドポイントが見つかりませんでした。" diff --git a/i18n/ko.json b/i18n/ko.json index 82c9a20ffd..ad4733a1aa 100644 --- a/i18n/ko.json +++ b/i18n/ko.json @@ -6519,14 +6519,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "Reached AD/LDAP synchronization job timeout." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Could not parse message export job ExportFromTimestamp." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Could not parse message export job BatchSize." - }, { "id": "ent.cluster.404.app_error", "translation": "Cluster API endpoint not found." @@ -7929,7 +7921,7 @@ }, { "id": "api.cloud.delinquency_email.missing_email_to_trigger", - "translation": "연체 이메일을 보내기 위한 필수 항목이 누락되었습니다." + "translation": "연체 알림 전자우편을 보내기 위한 필수 항목이 누락되었습니다." }, { "id": "api.error_set_first_admin_complete_setup", @@ -7958,5 +7950,89 @@ { "id": "sharedchannel.cannot_deliver_post", "translation": "{{.Remote}} 원격 사이트가 오프라인이기 때문에 하나 또는 그 이상의 포스트가 전송되지 않았습니다. 포스트는 해당 사이트가 온라인일 때 전송될 것입니다." + }, + { + "id": "app.notification.body.thread.title", + "translation": "{{.SenderName}}님이 글타래에 답장을 남겼습니다" + }, + { + "id": "app.notification.body.thread_channel.subTitle", + "translation": "자리를 비운 동안, {{.SenderName}}님이 지켜보는 중인 글타래에 답장을 남겼습니다." + }, + { + "id": "app.notification.body.thread_channel_full.subTitle", + "translation": "자리를 비운 동안, {{.SenderName}}님이 {{.ChannelName}} 채널에 있는 지켜보는 중인 글타래에 답장을 남겼습니다." + }, + { + "id": "app.notification.body.thread_gm.subTitle", + "translation": "자리를 비운 동안, {{.SenderName}}님이 그룹 글타래에 답장을 남겼습니다." + }, + { + "id": "app.notification.body.thread_dm.subTitle", + "translation": "자리를 비운 동안, {{.SenderName}}님이 당신이 직접 보낸 메시지에 답장을 남겼습니다." + }, + { + "id": "app.channel.autofollow.app_error", + "translation": "언급된 사용자의 글타래 권한을 갱신하지 못했습니다" + }, + { + "id": "api.getThreadsForUser.bad_params", + "translation": "getThreadsForUser의 Before와 After 매개변수는 상호 배타적입니다" + }, + { + "id": "api.getThreadsForUser.bad_only_params", + "translation": "getThreadsForUser의 OnlyThreads와 OnlyTotals 매개변수는 상호 배타적입니다" + }, + { + "id": "app.post.get_top_threads_for_team_since.app_error", + "translation": "팀의 상위 글타래를 가져올 수 없습니다." + }, + { + "id": "app.post.get_top_threads_for_user_since.app_error", + "translation": "사용자의 상위 글타래를 가져올 수 없습니다." + }, + { + "id": "app.user.get_thread_count_for_user.app_error", + "translation": "사용자의 글타래 개수를 가져올 수 없습니다." + }, + { + "id": "app.user.get_thread_membership_for_user.app_error", + "translation": "사용자 글타래 권한을 가져올 수 없습니다" + }, + { + "id": "app.user.get_thread_membership_for_user.not_found", + "translation": "사용자 글타래 권한이 없습니다" + }, + { + "id": "app.user.get_threads_for_user.app_error", + "translation": "사용자 글타래들을 가져올 수 없습니다" + }, + { + "id": "app.user.get_threads_for_user.not_found", + "translation": "사용자 글타래가 존재하지 않거나 지켜보고 있지 않습니다" + }, + { + "id": "app.user.update_thread_follow_for_user.app_error", + "translation": "글타래의 지켜보기 상태를 갱신할 수 없습니다" + }, + { + "id": "app.user.update_thread_read_for_user.app_error", + "translation": "글타래의 읽음 상태를 갱신할 수 없습니다" + }, + { + "id": "app.user.update_thread_read_for_user_by_post.app_error", + "translation": "유효하지 않은 post_id" + }, + { + "id": "model.config.is_valid.collapsed_threads.app_error", + "translation": "CollapsedThreads 설정은 disabled, default_on 혹은 default_off 중 하나여야만 합니다" + }, + { + "id": "model.config.is_valid.collapsed_threads.autofollow.app_error", + "translation": "CollapsedThreads 기능을 활성화하려면 ThreadAutoFollow 기능이 활성화되어야 합니다" + }, + { + "id": "app.user.update_threads_read_for_user.app_error", + "translation": "모든 사용자 글타래들을 읽음 상태로 변경할 수 없습니다" } ] diff --git a/i18n/nl.json b/i18n/nl.json index 683aa0f875..45c0f6813e 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -4769,7 +4769,7 @@ }, { "id": "web.error.unsupported_browser.min_os_version.mac", - "translation": "macOS 10.14+" + "translation": "macOS 11+" }, { "id": "web.error.unsupported_browser.min_browser_version.safari", @@ -6603,14 +6603,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "Time-out voor synchronisatie van AD/LDAP werd bereikt." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Fout bij het verwerken van bericht export taak ExportFromTimeStamp." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Fout bij ontleden van bericht exporttaak BatchSize." - }, { "id": "ent.cluster.404.app_error", "translation": "Cluster API endpoint niet gevonden." @@ -9264,7 +9256,7 @@ }, { "id": "api.templates.delinquency_14.subject", - "translation": "Betaling is overtijd voor jouw Mattermost {{.Plan}}." + "translation": "Betaling is laattijdig voor jouw Mattermost {{.Plan}}" }, { "id": "api.templates.delinquency_14.button", @@ -9300,7 +9292,7 @@ }, { "id": "api.templates.delinquency_7.subtitle1", - "translation": "We konden jouw laatste betaling niet verwerken" + "translation": "We konden jouw laatste betaling niet verwerken." }, { "id": "api.templates.delinquency_7.button", @@ -9348,7 +9340,7 @@ }, { "id": "api.templates.delinquency_45.subtitle1", - "translation": "We hebben geen betaling kunnen innen voor openstaande facturen van {{.DelinquencyDate}}. Jouw werkruimte loopt het risico om gedowngraded te worden." + "translation": "We hebben geen betaling kunnen innen voor openstaande facturen vanaf {{.DelinquencyDate}}. Jouw werkruimte loopt het risico om gedowngraded te worden." }, { "id": "api.templates.delinquency_45.subject", @@ -9364,7 +9356,7 @@ }, { "id": "api.templates.delinquency_30.subtitle2", - "translation": "als geen actie wordt ondernomen, zal jouw werkruimte worden gedowngraded en kunnen de volgende gegevens worden gearchiveerd:" + "translation": "Als er geen actie wordt ondernomen, zal jouw werkruimte worden gedowngraded en kunnen de volgende gegevens worden gearchiveerd:" }, { "id": "api.templates.delinquency_30.subtitle1", @@ -9561,5 +9553,177 @@ { "id": "api.admin.syncables_error", "translation": "kon gebruiker niet toevoegen aan groep-teams en groep-kanalen" + }, + { + "id": "api.acknowledgement.save.archived_channel.app_error", + "translation": "Je kan niet bevestigen in een gearchiveerd kanaal." + }, + { + "id": "api.acknowledgement.delete.deadline.app_error", + "translation": "Je kan een bevestiging niet wissen nadat 5min verstreken zijn." + }, + { + "id": "api.acknowledgement.delete.archived_channel.app_error", + "translation": "Je kan een bevestiging in een gearchiveerd kanaal niet verwijderen." + }, + { + "id": "worktemplate.product_teams.feature_release.description.playbook", + "translation": "Creëer transparante workflows tussen ontwikkelingsteams om ervoor te zorgen dat jouw ontwikkelingsproces naadloos verloopt." + }, + { + "id": "worktemplate.product_teams.feature_release.description.integration", + "translation": "Verhoog de productiviteit in je kanaal door een Jira bot en Github bot te integreren. Deze worden voor jou gedownload." + }, + { + "id": "worktemplate.product_teams.feature_release.description.channel", + "translation": "Chat met je team in een Feature Release-kanaal dat gemakkelijk verbinding maakt met je boards, playbooks en app bots." + }, + { + "id": "worktemplate.product_teams.feature_release.description.board", + "translation": "Gebruik ons vergaderagenda-bord voor terugkerende vergaderingen zoals stand-up en ons Projecttakenbord om de voortgang van taken onderweg te beheren." + }, + { + "id": "worktemplate.category.product_teams", + "translation": "Productteams" + }, + { + "id": "model.draft.is_valid.user_id.app_error", + "translation": "Ongeldig gebruikers-id." + }, + { + "id": "model.draft.is_valid.update_at.app_error", + "translation": "Het veld 'update at' moet een geldige tijd zijn." + }, + { + "id": "model.draft.is_valid.root_id.app_error", + "translation": "Ongeldig root id." + }, + { + "id": "model.draft.is_valid.props.app_error", + "translation": "Ongeldige eigenschappen." + }, + { + "id": "model.draft.is_valid.priority.app_error", + "translation": "Ongeldige prioriteit" + }, + { + "id": "model.draft.is_valid.msg.app_error", + "translation": "Ongeldig bericht." + }, + { + "id": "model.draft.is_valid.file_ids.app_error", + "translation": "Ongeldige bestandids." + }, + { + "id": "model.draft.is_valid.create_at.app_error", + "translation": "Create at moet een geldige tijd zijn." + }, + { + "id": "model.acknowledgement.is_valid.user_id.app_error", + "translation": "Ongeldig gebruikersid." + }, + { + "id": "model.draft.is_valid.channel_id.app_error", + "translation": "Ongeldig kanaalid." + }, + { + "id": "model.acknowledgement.is_valid.post_id.app_error", + "translation": "Ongeldig bericht-id." + }, + { + "id": "app.worktemplates.get_templates.app_error", + "translation": "Kon geen werksjablonen ophalen" + }, + { + "id": "app.worktemplates.get_categories.app_error", + "translation": "Kan geen werksjablooncategorieën ophalen" + }, + { + "id": "app.post_prority.get_for_post.app_error", + "translation": "Kon geen berichtprioriteit ophalen voor bericht" + }, + { + "id": "app.draft.update.app_error", + "translation": "Kan het concept niet bijwerken." + }, + { + "id": "app.draft.save.app_error", + "translation": "Kan het concept niet opslaan." + }, + { + "id": "app.draft.get_for_draft.app_error", + "translation": "Kan geen bestanden ophalen voor concept." + }, + { + "id": "app.draft.get_drafts.app_error", + "translation": "Kon de Concepten van de gebruiker niet ophalen." + }, + { + "id": "app.draft.get.app_error", + "translation": "Kon het concept niet ophalen." + }, + { + "id": "app.draft.feature_disabled", + "translation": "De Conceptfunctie is uitgeschakeld." + }, + { + "id": "app.draft.delete.app_error", + "translation": "Kan het concept niet verwijderen." + }, + { + "id": "app.channel.get_priority_for_posts.app_error", + "translation": "Fout bij het ophalen van de prioriteiten voor berichten" + }, + { + "id": "app.channel.count_urgent_posts_since.app_error", + "translation": "Fout bij het tellen van de dringende berichten sinds de opgegeven datum." + }, + { + "id": "app.acknowledgement.save.save.app_error", + "translation": "Fout hij het bewaren van de bevestiging voor het bericht." + }, + { + "id": "app.acknowledgement.getforpost.get.app_error", + "translation": "Fout hij het ophalen van de bevestiging voor bericht." + }, + { + "id": "app.acknowledgement.get.app_error", + "translation": "Fout hij het ophalen van de bevestiging." + }, + { + "id": "app.acknowledgement.delete.app_error", + "translation": "Kan bevestiging niet verwijderen." + }, + { + "id": "api.user.get_users.validation.app_error", + "translation": "Fout bij het ophalen van rollen tijdens de validatie." + }, + { + "id": "api.upload.create.upload_too_large.app_error", + "translation": "Kan bestand niet uploaden. Bestand is te groot." + }, + { + "id": "api.templates.cloud_welcome_email.yearly_plan_button", + "translation": "Bekijk jouw factuur" + }, + { + "id": "api.templates.cloud_upgrade_confirmation_yearly.subtitle", + "translation": "Jouw {{.WorkspaceName}} werkruimte is nu opgewaardeerd." + }, + { + "id": "api.templates.cloud_upgrade_confirmation_monthly.subtitle", + "translation": "Jouw {{.WorkspaceName}} werkruimte is nu geüpgraded. Dit zal gefactureerd worden vanaf {{.Date}}" + }, + { + "id": "api.server.hosted_signup_unavailable.error", + "translation": "Portaal niet beschikbaar voor self-hosted signup." + }, + { + "id": "api.drafts.disabled.app_error", + "translation": "De Conceptfunctie is uitgeschakeld." + }, + { + "id": "api.draft.create_draft.can_not_draft_to_deleted.error", + "translation": "Kan concept niet bewaren in een verwijderd kanaal" } ] diff --git a/i18n/pl.json b/i18n/pl.json index cc23eeb202..ae492d4db1 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -6351,14 +6351,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "Osiągnięto limit czasu zadania synchronizacji AD/LDAP." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Nie można przeanalizować zadania eksportowania komunikatu ExportFromTimestamp." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Nie można przeanalizować zadania eksportu komunikatu BatchSize." - }, { "id": "ent.cluster.404.app_error", "translation": "Nie znaleziono punktu końcowego interfejsu API klastra." diff --git a/i18n/pt-BR.json b/i18n/pt-BR.json index 134b851682..81fed091ea 100644 --- a/i18n/pt-BR.json +++ b/i18n/pt-BR.json @@ -6571,14 +6571,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "Tarefa de sincronização do AD/LDAP alcançou o limite de tempo." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Não foi possível analisar a tarefa de exportação de mensagens ExportFromTimestamp." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Não foi possível analisar a tarefa de exportação de mensagens BatchSize." - }, { "id": "ent.cluster.404.app_error", "translation": "Endpoint Cluster API não encontrado." diff --git a/i18n/ro.json b/i18n/ro.json index f2686f070c..a7ec824541 100644 --- a/i18n/ro.json +++ b/i18n/ro.json @@ -6619,14 +6619,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "Am atins intervalul de timp pentru lucrarea de sincronizare AD/LDAP." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Ar putea analiza mesaj export job ExportFromTimestamp." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Ar putea analiza mesaj export job BatchSize." - }, { "id": "ent.cluster.404.app_error", "translation": "Clustering API final nu a fost găsit." diff --git a/i18n/ru.json b/i18n/ru.json index b31289ad9c..62022682fd 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -6615,14 +6615,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "Достигнут тайм-аут задания синхронизации AD/LDAP." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Не удалось разобрать задание экспорта сообщения ExportFromTimestamp." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Не удалось проанализировать задание экспорта сообщения BatchSize." - }, { "id": "ent.cluster.404.app_error", "translation": "Не найдена конечная точка API кластера." diff --git a/i18n/sv.json b/i18n/sv.json index 3d3d19a447..f5b180b542 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -3039,14 +3039,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "Nådde timeout för AD/LDAP-synkronisering." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Kunde inte tolka värdet ExportFromTimestamp i export job." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Kunde inte tolka värdet BatchSize i export job." - }, { "id": "ent.id_loaded.license_disable.app_error", "translation": "Din licens tillåter inte pushnotifiering via meddelande-ID." @@ -9729,5 +9721,9 @@ { "id": "model.draft.is_valid.props.app_error", "translation": "Ogiltiga attribut." + }, + { + "id": "api.server.hosted_signup_unavailable.error", + "translation": "Portalen är inte tillgänglig för egen-hostad registrering." } ] diff --git a/i18n/tr.json b/i18n/tr.json index 39a1074bc0..2eadae012a 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -6603,14 +6603,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "AD/LDAP eşitleme görevi zaman aşımına uğradı." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "İleti dışa aktarma görevinde ExportFromTimestamp değeri çözümlenemedi." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "İleti dışa aktarma görevinde BatchSize değeri çözümlenemedi." - }, { "id": "ent.cluster.404.app_error", "translation": "Küme API uç noktası bulunamadı." diff --git a/i18n/uk.json b/i18n/uk.json index cbba647d9d..7ffd8d2e6d 100644 --- a/i18n/uk.json +++ b/i18n/uk.json @@ -6335,14 +6335,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "Досягнута тайм-аут завдання синхронізації AD / LDAP." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Не вдалося проаналізувати завдання експортування повідомлень ExportFromTimestamp." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Не вдалося проаналізувати BatchSize завдання експортування повідомлення." - }, { "id": "ent.cluster.404.app_error", "translation": "Не знайдена кінцева точка API кластера." diff --git a/i18n/zh-CN.json b/i18n/zh-CN.json index 2d91449bce..4fb33345ac 100644 --- a/i18n/zh-CN.json +++ b/i18n/zh-CN.json @@ -6479,14 +6479,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "AD/LDAP 同步任务超时。" }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "无法解析 ExportFromTimestamp 导出任务消息。" - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "无法解析 BatchSize 导出任务消息。" - }, { "id": "ent.cluster.404.app_error", "translation": "未找到机群 API 接口。" diff --git a/i18n/zh-TW.json b/i18n/zh-TW.json index e1a171691e..caedf2787b 100644 --- a/i18n/zh-TW.json +++ b/i18n/zh-TW.json @@ -6539,14 +6539,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "AD/LDAP 同步工作逾時。" }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "無法解析訊息匯出工作 ExportFromTimestamp。" - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "無法解析訊息匯出工作 BatchSize。" - }, { "id": "ent.cluster.404.app_error", "translation": "找不到叢集 API 端點。" diff --git a/jobs/export_process/worker.go b/jobs/export_process/worker.go index ddf9462b14..7f80ac1950 100644 --- a/jobs/export_process/worker.go +++ b/jobs/export_process/worker.go @@ -55,7 +55,8 @@ func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker { } }() - appErr := app.BulkExport(request.EmptyContext(app.Log()), wr, outPath, opts) + logger := app.Log().With(mlog.String("job_id", job.Id)) + appErr := app.BulkExport(request.EmptyContext(logger), wr, outPath, opts) wr.Close() // Close never returns an error if appErr != nil { diff --git a/model/client4.go b/model/client4.go index 48d63e959c..298334d71a 100644 --- a/model/client4.go +++ b/model/client4.go @@ -8213,6 +8213,19 @@ func (c *Client4) GetCloudCustomer() (*CloudCustomer, *Response, error) { return cloudCustomer, BuildResponse(r), nil } +func (c *Client4) GetExpandStats(licenseId string) (*SubscriptionExpandStatus, *Response, error) { + r, err := c.DoAPIGet(fmt.Sprintf("%s%s?licenseID=%s", c.cloudRoute(), "/subscription/expand", licenseId), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var subscriptionExpandable *SubscriptionExpandStatus + json.NewDecoder(r.Body).Decode(&subscriptionExpandable) + + return subscriptionExpandable, BuildResponse(r), nil +} + func (c *Client4) GetSubscription() (*Subscription, *Response, error) { r, err := c.DoAPIGet(c.cloudRoute()+"/subscription", "") if err != nil { diff --git a/model/cloud.go b/model/cloud.go index 7feae13e3d..36c639d66c 100644 --- a/model/cloud.go +++ b/model/cloud.go @@ -124,6 +124,10 @@ type ValidateBusinessEmailResponse struct { IsValid bool `json:"is_valid"` } +type SubscriptionExpandStatus struct { + IsExpandable bool `json:"is_expandable"` +} + // CloudCustomerInfo represents editable info of a customer. type CloudCustomerInfo struct { Name string `json:"name"` diff --git a/model/post.go b/model/post.go index 2a7c4538e0..e7c11edc44 100644 --- a/model/post.go +++ b/model/post.go @@ -333,13 +333,16 @@ type GetPostsOptions struct { type PostCountOptions struct { // Only include posts on a specific team. "" for any team. - TeamId string - MustHaveFile bool - MustHaveHashtag bool - ExcludeDeleted bool - UsersPostsOnly bool + TeamId string + MustHaveFile bool + MustHaveHashtag bool + ExcludeDeleted bool + ExcludeSystemPosts bool + UsersPostsOnly bool // AllowFromCache looks up cache only when ExcludeDeleted and UsersPostsOnly are true and rest are falsy. AllowFromCache bool + SincePostID string + SinceUpdateAt int64 } func (o *Post) Etag() string { diff --git a/product/api.go b/product/api.go index 587f9d788c..54d473bb61 100644 --- a/product/api.go +++ b/product/api.go @@ -209,4 +209,5 @@ type BoardsService interface { PatchCard(cardPatch *fb_model.CardPatch, cardID string, userID string) (*fb_model.Card, error) DeleteCard(cardID string, userID string) error HasPermissionToBoard(userID, boardID string, permission *model.Permission) bool + DuplicateBoard(boardID string, userID string, toTeam string, asTemplate bool) (*fb_model.BoardsAndBlocks, []*fb_model.BoardMember, error) } diff --git a/store/sqlstore/post_store.go b/store/sqlstore/post_store.go index b7f5979cd9..8bb4de38cd 100644 --- a/store/sqlstore/post_store.go +++ b/store/sqlstore/post_store.go @@ -2272,6 +2272,20 @@ func (s *SqlPostStore) AnalyticsPostCount(options *model.PostCountOptions) (int6 query = query.Where(sq.Eq{"p.DeleteAt": 0}) } + if options.ExcludeSystemPosts { + query = query.Where("p.Type NOT LIKE 'system_%'") + } + + if options.SinceUpdateAt > 0 { + query = query.Where(sq.Or{ + sq.Gt{"p.UpdateAt": options.SinceUpdateAt}, + sq.And{ + sq.Eq{"p.UpdateAt": options.SinceUpdateAt}, + sq.Gt{"p.Id": options.SincePostID}, + }, + }) + } + queryString, args, err := query.ToSql() if err != nil { return 0, errors.Wrap(err, "post_tosql") diff --git a/store/storetest/post_store.go b/store/storetest/post_store.go index f3c7d91630..c6e9ecd7ef 100644 --- a/store/storetest/post_store.go +++ b/store/storetest/post_store.go @@ -38,6 +38,7 @@ func TestPostStore(t *testing.T, ss store.Store, s SqlStore) { t.Run("GetPostBeforeAfter", func(t *testing.T) { testPostStoreGetPostBeforeAfter(t, ss) }) t.Run("UserCountsWithPostsByDay", func(t *testing.T) { testUserCountsWithPostsByDay(t, ss) }) t.Run("PostCountsByDuration", func(t *testing.T) { testPostCountsByDay(t, ss) }) + t.Run("PostCounts", func(t *testing.T) { testPostCounts(t, ss) }) t.Run("GetFlaggedPostsForTeam", func(t *testing.T) { testPostStoreGetFlaggedPostsForTeam(t, ss, s) }) t.Run("GetFlaggedPosts", func(t *testing.T) { testPostStoreGetFlaggedPosts(t, ss) }) t.Run("GetFlaggedPostsForChannel", func(t *testing.T) { testPostStoreGetFlaggedPostsForChannel(t, ss) }) @@ -2685,45 +2686,169 @@ func testPostCountsByDay(t *testing.T, ss store.Store) { r1, err = ss.Post().AnalyticsPostCountsByDay(postCountsOptions) require.NoError(t, err) assert.Equal(t, float64(1), r1[0].Value) +} + +func testPostCounts(t *testing.T, ss store.Store) { + now := time.Now() + twentyMinAgo := now.Add(-20 * time.Minute).UnixMilli() + fifteenMinAgo := now.Add(-15 * time.Minute).UnixMilli() + tenMinAgo := now.Add(-10 * time.Minute).UnixMilli() + + t1 := &model.Team{} + t1.DisplayName = "DisplayName" + t1.Name = NewTestId() + t1.Email = MakeEmail() + t1.Type = model.TeamOpen + t1, err := ss.Team().Save(t1) + require.NoError(t, err) + + c1 := &model.Channel{} + c1.TeamId = t1.Id + c1.DisplayName = "Channel2" + c1.Name = NewTestId() + c1.Type = model.ChannelTypeOpen + c1, nErr := ss.Channel().Save(c1, -1) + require.NoError(t, nErr) + + // system post + p1 := &model.Post{} + p1.Type = "system_add_to_channel" + p1.ChannelId = c1.Id + p1.UserId = model.NewId() + p1.Message = NewTestId() + p1.CreateAt = twentyMinAgo + p1.UpdateAt = twentyMinAgo + _, nErr = ss.Post().Save(p1) + require.NoError(t, nErr) + + p2 := &model.Post{} + p2.ChannelId = c1.Id + p2.UserId = model.NewId() + p2.Message = NewTestId() + p2.Hashtags = "hashtag" + p2.CreateAt = twentyMinAgo + p2.UpdateAt = twentyMinAgo + p2, nErr = ss.Post().Save(p2) + require.NoError(t, nErr) + + p3 := &model.Post{} + p3.ChannelId = c1.Id + p3.UserId = model.NewId() + p3.Message = NewTestId() + p3.FileIds = []string{"fileId1"} + p3.CreateAt = twentyMinAgo + p3.UpdateAt = twentyMinAgo + _, nErr = ss.Post().Save(p3) + require.NoError(t, nErr) + + p4 := &model.Post{} + p4.ChannelId = c1.Id + p4.UserId = model.NewId() + p4.Message = NewTestId() + p4.Filenames = []string{"filename1"} + p4.CreateAt = tenMinAgo + p4.UpdateAt = tenMinAgo + p4, nErr = ss.Post().Save(p4) + require.NoError(t, nErr) + + p5 := &model.Post{} + p5.ChannelId = c1.Id + p5.UserId = p4.UserId + p5.Message = NewTestId() + p5.Hashtags = "hashtag" + p5.FileIds = []string{"fileId2"} + p5.CreateAt = tenMinAgo + p5.UpdateAt = tenMinAgo + _, nErr = ss.Post().Save(p5) + require.NoError(t, nErr) + + bot1 := &model.Bot{ + Username: "username", + Description: "a bot", + OwnerId: model.NewId(), + UserId: model.NewId(), + } + _, nErr = ss.Bot().Save(bot1) + require.NoError(t, nErr) + + p6 := &model.Post{} + p6.Message = "bot message one" + p6.ChannelId = c1.Id + p6.UserId = bot1.UserId + p6.CreateAt = twentyMinAgo + p6.UpdateAt = twentyMinAgo + _, nErr = ss.Post().Save(p6) + require.NoError(t, nErr) + + p7 := &model.Post{} + p7.Message = "bot message two" + p7.ChannelId = c1.Id + p7.UserId = bot1.UserId + p7.CreateAt = tenMinAgo + p7.UpdateAt = tenMinAgo + _, nErr = ss.Post().Save(p7) + require.NoError(t, nErr) + + // total across all teams + c, err := ss.Post().AnalyticsPostCount(&model.PostCountOptions{}) + require.NoError(t, err) + assert.GreaterOrEqual(t, c, int64(7)) // total for single team - r2, err := ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id}) + c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id}) require.NoError(t, err) - assert.Equal(t, int64(6), r2) + assert.Equal(t, int64(7), c) - // total across teams - r2, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{}) + // with files + c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, MustHaveFile: true}) require.NoError(t, err) - assert.GreaterOrEqual(t, r2, int64(6)) + assert.Equal(t, int64(3), c) - // total across teams with files - r2, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{MustHaveFile: true}) + // with hashtags + c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, MustHaveHashtag: true}) require.NoError(t, err) - assert.GreaterOrEqual(t, r2, int64(3)) + assert.Equal(t, int64(2), c) - // total across teams with hashtags - r2, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{MustHaveHashtag: true}) + // with hashtags and files + c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, MustHaveFile: true, MustHaveHashtag: true}) require.NoError(t, err) - assert.GreaterOrEqual(t, r2, int64(2)) + assert.Equal(t, int64(1), c) - // total across teams with hashtags and files - r2, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{MustHaveFile: true, MustHaveHashtag: true}) + // excluding system posts + c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, ExcludeSystemPosts: true}) require.NoError(t, err) - assert.GreaterOrEqual(t, r2, int64(1)) + assert.Equal(t, int64(6), c) + + // before update_at time + c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, SinceUpdateAt: fifteenMinAgo}) + require.NoError(t, err) + assert.Equal(t, int64(3), c) + + // equal to update_at time + c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, SinceUpdateAt: tenMinAgo}) + require.NoError(t, err) + assert.Equal(t, int64(3), c) + + // since update_at and since post id + tenMinAgoIDs := []string{p4.Id, p5.Id, p7.Id} + sort.Strings(tenMinAgoIDs) + c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, SinceUpdateAt: tenMinAgo, SincePostID: tenMinAgoIDs[0]}) + require.NoError(t, err) + assert.Equal(t, int64(2), c) // delete 1 post - err = ss.Post().Delete(o1.Id, 1, o1.UserId) + err = ss.Post().Delete(p2.Id, 1, p2.UserId) require.NoError(t, err) // total for single team with the deleted post excluded - r2, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, ExcludeDeleted: true}) + c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, ExcludeDeleted: true}) require.NoError(t, err) - assert.Equal(t, int64(5), r2) + assert.Equal(t, int64(6), c) // total users only posts for single team with the deleted post excluded - r2, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, ExcludeDeleted: true, UsersPostsOnly: true}) + c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, ExcludeDeleted: true, UsersPostsOnly: true}) require.NoError(t, err) - assert.Equal(t, int64(3), r2) + assert.Equal(t, int64(3), c) } func testPostStoreGetFlaggedPostsForTeam(t *testing.T, ss store.Store, s SqlStore) {