mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 17:31:59 +02:00
19.0 vanilla
This commit is contained in:
parent
d1963a3c3a
commit
2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions
File diff suppressed because it is too large
Load diff
91
odoo-bringout-oca-ocb-mail/mail/static/tests/mock_server/mock_models/@types/mock_models.d.ts
vendored
Normal file
91
odoo-bringout-oca-ocb-mail/mail/static/tests/mock_server/mock_models/@types/mock_models.d.ts
vendored
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
declare module "mock_models" {
|
||||
import { Base as Base2 } from "@mail/../tests/mock_server/mock_models/base";
|
||||
import { DiscussChannel as DiscussChannel2 } from "@mail/../tests/mock_server/mock_models/discuss_channel";
|
||||
import { DiscussChannelMember as DiscussChannelMember2 } from "@mail/../tests/mock_server/mock_models/discuss_channel_member";
|
||||
import { DiscussChannelRtcSession as DiscussChannelRtcSession2 } from "@mail/../tests/mock_server/mock_models/discuss_channel_rtc_session";
|
||||
import { DiscussVoiceMetadata as DiscussVoiceMetadata2 } from "@mail/../tests/mock_server/mock_models/discuss_voice_metadata";
|
||||
import { IrAttachment as IrAttachment2 } from "@mail/../tests/mock_server/mock_models/ir_attachment";
|
||||
import { MailActivity as MailActivity2 } from "@mail/../tests/mock_server/mock_models/mail_activity";
|
||||
import { MailActivityType as MailActivityType2 } from "@mail/../tests/mock_server/mock_models/mail_activity_type";
|
||||
import { MailFollowers as MailFollowers2 } from "@mail/../tests/mock_server/mock_models/mail_followers";
|
||||
import { MailGuest as MailGuest2 } from "@mail/../tests/mock_server/mock_models/mail_guest";
|
||||
import { MailLinkPreview as MailLinkPreview2 } from "@mail/../tests/mock_server/mock_models/mail_link_preview";
|
||||
import { MailMessage as MailMessage2 } from "@mail/../tests/mock_server/mock_models/mail_message";
|
||||
import { MailMessageLinkPreview as MailMessageLinkPreview2 } from "@mail/../tests/mock_server/mock_models/mail_message_link_preview";
|
||||
import { MailMessageReaction as MailMessageReaction2 } from "@mail/../tests/mock_server/mock_models/mail_message_reaction";
|
||||
import { MailMessageSubtype as MailMessageSubtype2 } from "@mail/../tests/mock_server/mock_models/mail_message_subtype";
|
||||
import { MailNotification as MailNotification2 } from "@mail/../tests/mock_server/mock_models/mail_notification";
|
||||
import { MailScheduledMessage as MailScheduledMessage2 } from "@mail/.../tests/mock_server/mock_models/mail_scheduled_message";
|
||||
import { MailShortcode as MailShortcode2 } from "@mail/../tests/mock_server/mock_models/mail_shortcode";
|
||||
import { MailTemplate as MailTemplate2 } from "@mail/../tests/mock_server/mock_models/mail_template";
|
||||
import { MailThread as MailThread2 } from "@mail/../tests/mock_server/mock_models/mail_thread";
|
||||
import { MailTrackingValue as MailTrackingValue2 } from "@mail/../tests/mock_server/mock_models/mail_tracking_value";
|
||||
import { ResFake as ResFake2 } from "@mail/../tests/mock_server/mock_models/res_fake";
|
||||
import { ResLang as ResLang2 } from "@mail/../tests/mock_server/mock_models/res_lang";
|
||||
import { ResRole as ResRole2 } from "addons/mail/static/tests/mock_server/mock_models/res_role";
|
||||
import { ResPartner as ResPartner2 } from "@mail/../tests/mock_server/mock_models/res_partner";
|
||||
import { ResUsers as ResUsers2 } from "@mail/../tests/mock_server/mock_models/res_users";
|
||||
import { ResUsersSettings as ResUsersSettings2 } from "@mail/../tests/mock_server/mock_models/res_users_settings";
|
||||
import { ResUsersSettingsVolumes as ResUsersSettingsVolumes2 } from "@mail/../tests/mock_server/mock_models/res_users_settings_volumes";
|
||||
|
||||
export interface Base extends Base2 {}
|
||||
export interface DiscussChannel extends DiscussChannel2 {}
|
||||
export interface DiscussChannelMember extends DiscussChannelMember2 {}
|
||||
export interface DiscussChannelRtcSession extends DiscussChannelRtcSession2 {}
|
||||
export interface DiscussVoiceMetadata extends DiscussVoiceMetadata2 {}
|
||||
export interface IrAttachment extends IrAttachment2 {}
|
||||
export interface MailActivity extends MailActivity2 {}
|
||||
export interface MailActivityType extends MailActivityType2 {}
|
||||
export interface MailFollowers extends MailFollowers2 {}
|
||||
export interface MailGuest extends MailGuest2 {}
|
||||
export interface MailLinkPreview extends MailLinkPreview2 {}
|
||||
export interface MailMessage extends MailMessage2 {}
|
||||
export interface MailMessageLinkPreview extends MailMessageLinkPreview2 {}
|
||||
export interface MailMessageReaction extends MailMessageReaction2 {}
|
||||
export interface MailMessageSubtype extends MailMessageSubtype2 {}
|
||||
export interface MailNotification extends MailNotification2 {}
|
||||
export interface MailScheduledMessage extends MailScheduledMessage2 {}
|
||||
export interface MailShortcode extends MailShortcode2 {}
|
||||
export interface MailTemplate extends MailTemplate2 {}
|
||||
export interface MailThread extends MailThread2 {}
|
||||
export interface MailTrackingValue extends MailTrackingValue2 {}
|
||||
export interface ResFake extends ResFake2 {}
|
||||
export interface ResLang extends ResLang2 {}
|
||||
export interface ResPartner extends ResPartner2 {}
|
||||
export interface ResRole extends ResRole2 {}
|
||||
export interface ResUsers extends ResUsers2 {}
|
||||
export interface ResUsersSettings extends ResUsersSettings2 {}
|
||||
export interface ResUsersSettingsVolumes extends ResUsersSettingsVolumes2 {}
|
||||
|
||||
export interface Models {
|
||||
"base": Base,
|
||||
"discuss.channel": DiscussChannel,
|
||||
"discuss.channel.member": DiscussChannelMember,
|
||||
"discuss.channel.rtc.session": DiscussChannelRtcSession,
|
||||
"discuss.voice.metadata": DiscussVoiceMetadata,
|
||||
"ir.attachment": IrAttachment,
|
||||
"mail.activity": MailActivity,
|
||||
"mail.activity.type": MailActivityType,
|
||||
"mail.followers": MailFollowers,
|
||||
"mail.guest": MailGuest,
|
||||
"mail.link.preview": MailLinkPreview,
|
||||
"mail.message": MailMessage,
|
||||
"mail.message.link.preview": MailMessageLinkPreview,
|
||||
"mail.message.reaction": MailMessageReaction,
|
||||
"mail.message.subtype": MailMessageSubtype,
|
||||
"mail.notification": MailNotification,
|
||||
"mail.scheduled.message": MailScheduledMessage,
|
||||
"mail.shortcode": MailShortcode,
|
||||
"mail.template": MailTemplate,
|
||||
"mail.thread": MailThread,
|
||||
"mail.tracking.value": MailTrackingValue,
|
||||
"res.fake": ResFake,
|
||||
"res.groups": ResGroups,
|
||||
"res.lang": ResLang,
|
||||
"res.partner": ResPartner,
|
||||
"res.role": ResRole,
|
||||
"res.users": ResUsers,
|
||||
"res.users.settings": ResUsersSettings,
|
||||
"res.users.settings.volumes": ResUsersSettingsVolumes,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import { getKwArgs, models } from "@web/../tests/web_test_helpers";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(models.ServerModel.prototype, {
|
||||
/**
|
||||
* @override
|
||||
* @type {typeof models.ServerModel["prototype"]["get_views"]}
|
||||
*/
|
||||
get_views() {
|
||||
const result = super.get_views(...arguments);
|
||||
for (const modelName of Object.keys(result.models)) {
|
||||
if (this.has_activities) {
|
||||
result.models[modelName].has_activities = true;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
export class Base extends models.ServerModel {
|
||||
_name = "base";
|
||||
|
||||
/**
|
||||
* @param {Object} trackedFieldNamesToField
|
||||
* @param {Object} initialTrackedFieldValues
|
||||
* @param {Object} record
|
||||
*/
|
||||
_mail_track(trackedFieldNamesToField, initialTrackedFieldValues, record) {
|
||||
const kwargs = getKwArgs(
|
||||
arguments,
|
||||
"trackedFieldNamesToField",
|
||||
"initialTrackedFieldValues",
|
||||
"record"
|
||||
);
|
||||
trackedFieldNamesToField = kwargs.trackedFieldNamesToField;
|
||||
initialTrackedFieldValues = kwargs.initialTrackedFieldValues;
|
||||
record = kwargs.record;
|
||||
|
||||
/** @type {import("mock_models").MailTrackingValue} */
|
||||
const MailTrackingValue = this.env["mail.tracking.value"];
|
||||
|
||||
const trackingValueIds = [];
|
||||
const changedFieldNames = [];
|
||||
for (const fname in trackedFieldNamesToField) {
|
||||
const initialValue = initialTrackedFieldValues[fname];
|
||||
const newValue = record[fname];
|
||||
if (!initialValue && !newValue) {
|
||||
continue;
|
||||
}
|
||||
if (initialValue !== newValue) {
|
||||
const tracking = MailTrackingValue._create_tracking_values(
|
||||
initialValue,
|
||||
newValue,
|
||||
fname,
|
||||
trackedFieldNamesToField[fname],
|
||||
this
|
||||
);
|
||||
if (tracking) {
|
||||
trackingValueIds.push(tracking);
|
||||
}
|
||||
changedFieldNames.push(fname);
|
||||
}
|
||||
}
|
||||
return { changedFieldNames, trackingValueIds };
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,351 @@
|
|||
import { mailDataHelpers } from "@mail/../tests/mock_server/mail_mock_server";
|
||||
|
||||
import { fields, getKwArgs, makeKwArgs, models } from "@web/../tests/web_test_helpers";
|
||||
import { serializeDateTime, today } from "@web/core/l10n/dates";
|
||||
import { ensureArray } from "@web/core/utils/arrays";
|
||||
|
||||
const { DateTime } = luxon;
|
||||
|
||||
export class DiscussChannelMember extends models.ServerModel {
|
||||
_name = "discuss.channel.member";
|
||||
|
||||
is_pinned = fields.Generic({ compute: "_compute_is_pinned" });
|
||||
is_self = fields.Boolean({ compute: "_compute_is_self" });
|
||||
unpin_dt = fields.Datetime({ string: "Unpin date" });
|
||||
message_unread_counter = fields.Generic({ default: 0 });
|
||||
last_interest_dt = fields.Datetime({
|
||||
default: () => serializeDateTime(today().minus({ seconds: 1 })),
|
||||
});
|
||||
|
||||
create(values) {
|
||||
const idOrIds = super.create(values);
|
||||
this.env["discuss.channel"]._compute_channel_name_member_ids();
|
||||
const channels_needing_name_update = this.env["discuss.channel"]
|
||||
._filter([
|
||||
["channel_name_member_ids", "in", ensureArray(idOrIds)],
|
||||
["name", "=", false],
|
||||
[
|
||||
"channel_type",
|
||||
"in",
|
||||
this.env["discuss.channel"]._member_based_naming_channel_types(),
|
||||
],
|
||||
])
|
||||
.filter((channel) => channel.channel_name_member_ids.length <= 3);
|
||||
for (const channel of channels_needing_name_update) {
|
||||
const store = new mailDataHelpers.Store().add(
|
||||
this.env["discuss.channel"].browse(channel.id),
|
||||
makeKwArgs({ fields: [mailDataHelpers.Store.many("channel_name_member_ids")] })
|
||||
);
|
||||
this.env["bus.bus"]._sendone(channel, "mail.record/insert", store.get_result());
|
||||
}
|
||||
return idOrIds;
|
||||
}
|
||||
|
||||
write(ids, vals) {
|
||||
const membersToUpdate = this.browse(ids);
|
||||
const syncFields = this._sync_field_names();
|
||||
const oldValsByMember = new Map();
|
||||
for (const member of membersToUpdate) {
|
||||
const oldVals = {};
|
||||
for (const fieldName of syncFields) {
|
||||
oldVals[fieldName] = member[fieldName];
|
||||
}
|
||||
oldValsByMember.set(member.id, oldVals);
|
||||
}
|
||||
const result = super.write(ids, vals);
|
||||
for (const member of membersToUpdate) {
|
||||
const oldVals = oldValsByMember.get(member.id);
|
||||
const diff = [];
|
||||
for (const fieldName of syncFields) {
|
||||
if (member[fieldName] !== oldVals[fieldName]) {
|
||||
diff.push(fieldName);
|
||||
}
|
||||
}
|
||||
if (diff.length > 0) {
|
||||
const store = new mailDataHelpers.Store();
|
||||
diff.push("channel", "persona");
|
||||
this.browse(member.id)._to_store(store, diff);
|
||||
const [partner, guest] = this.env["res.partner"]._get_current_persona();
|
||||
const busChannel = guest ?? partner;
|
||||
this.env["bus.bus"]._sendone(busChannel, "mail.record/insert", store.get_result());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
_sync_field_names() {
|
||||
return ["last_interest_dt", "message_unread_counter", "new_message_separator", "unpin_dt"];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} ids
|
||||
* @param {boolean} is_typing
|
||||
*/
|
||||
notify_typing(ids, is_typing) {
|
||||
const kwargs = getKwArgs(arguments, "ids", "is_typing");
|
||||
ids = kwargs.ids;
|
||||
delete kwargs.ids;
|
||||
is_typing = kwargs.is_typing;
|
||||
|
||||
/** @type {import("mock_models").BusBus} */
|
||||
const BusBus = this.env["bus.bus"];
|
||||
/** @type {import("mock_models").DiscussChannel} */
|
||||
const DiscussChannel = this.env["discuss.channel"];
|
||||
/** @type {import("mock_models").DiscussChannelMember} */
|
||||
const DiscussChannelMember = this.env["discuss.channel.member"];
|
||||
|
||||
const members = this.browse(ids);
|
||||
const notifications = [];
|
||||
for (const member of members) {
|
||||
const [channel] = DiscussChannel.browse(member.channel_id);
|
||||
notifications.push([
|
||||
channel,
|
||||
"mail.record/insert",
|
||||
new mailDataHelpers.Store(DiscussChannelMember.browse(member.id))
|
||||
.add("discuss.channel.member", {
|
||||
id: member.id,
|
||||
isTyping: is_typing,
|
||||
is_typing_dt: serializeDateTime(DateTime.now()),
|
||||
})
|
||||
.get_result(),
|
||||
]);
|
||||
}
|
||||
BusBus._sendmany(notifications);
|
||||
}
|
||||
|
||||
_compute_is_pinned() {
|
||||
for (const member of this) {
|
||||
const [channel] = this.env["discuss.channel"].browse(member.channel_id);
|
||||
member.is_pinned =
|
||||
!member.unpin_dt ||
|
||||
member?.last_interest_dt >= member.unpin_dt ||
|
||||
channel?.last_interest_dt >= member.unpin_dt;
|
||||
}
|
||||
}
|
||||
|
||||
_compute_is_self() {
|
||||
const [partner, guest] = this.env["res.partner"]._get_current_persona();
|
||||
for (const member of this) {
|
||||
member.is_self = member.partner_id
|
||||
? member.partner_id === partner?.id
|
||||
: member.guest_id === guest?.id;
|
||||
}
|
||||
}
|
||||
|
||||
_compute_message_unread_counter([memberId]) {
|
||||
const [member] = this.browse(memberId);
|
||||
return this.env["mail.message"].search_count([
|
||||
["res_id", "=", member.channel_id],
|
||||
["model", "=", "discuss.channel"],
|
||||
["id", ">=", member.new_message_separator],
|
||||
]);
|
||||
}
|
||||
|
||||
/** @param {number[]} ids */
|
||||
_to_store(store, fields) {
|
||||
const kwargs = getKwArgs(arguments, "store", "fields");
|
||||
fields = kwargs.fields;
|
||||
store._add_record_fields(
|
||||
this,
|
||||
fields.filter(
|
||||
(field) => !["message_unread_counter", "persona", "channel"].includes(field)
|
||||
)
|
||||
);
|
||||
for (const member of this) {
|
||||
const data = {};
|
||||
if (fields.includes("message_unread_counter")) {
|
||||
data.message_unread_counter = this._compute_message_unread_counter([member.id]);
|
||||
data.message_unread_counter_bus_id = this.env["bus.bus"].lastBusNotificationId;
|
||||
}
|
||||
if (fields.includes("channel")) {
|
||||
data.channel_id = mailDataHelpers.Store.one(
|
||||
this.env["discuss.channel"].browse(member.channel_id),
|
||||
makeKwArgs({ as_thread: true, only_id: true })
|
||||
);
|
||||
}
|
||||
if (fields.includes("persona")) {
|
||||
store._add_record_fields(this.browse(member.id), this._to_store_persona());
|
||||
}
|
||||
|
||||
if (Object.keys(data).length) {
|
||||
store._add_record_fields(this.browse(member.id), data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_to_store_persona(fields) {
|
||||
return [
|
||||
mailDataHelpers.Store.attr(
|
||||
"partner_id",
|
||||
(m) =>
|
||||
mailDataHelpers.Store.one(
|
||||
this.env["res.partner"].browse(m.partner_id),
|
||||
makeKwArgs({
|
||||
fields: this._get_store_partner_fields(fields),
|
||||
})
|
||||
),
|
||||
makeKwArgs({
|
||||
predicate: (m) =>
|
||||
m.partner_id !== null && m.partner_id !== undefined && m.partner_id,
|
||||
})
|
||||
),
|
||||
mailDataHelpers.Store.attr(
|
||||
"guest_id",
|
||||
(m) =>
|
||||
mailDataHelpers.Store.one(
|
||||
this.env["mail.guest"].browse(m.guest_id),
|
||||
makeKwArgs({ fields })
|
||||
),
|
||||
makeKwArgs({
|
||||
predicate: (m) => m.guest_id !== null && m.guest_id !== undefined && m.guest_id,
|
||||
})
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
get _to_store_defaults() {
|
||||
return [
|
||||
mailDataHelpers.Store.one("channel_id", makeKwArgs({ as_thread: true, only_id: true })),
|
||||
"create_date",
|
||||
"fetched_message_id",
|
||||
"seen_message_id",
|
||||
"last_interest_dt",
|
||||
"last_seen_dt",
|
||||
"new_message_separator",
|
||||
].concat(this._to_store_persona());
|
||||
}
|
||||
|
||||
_get_store_partner_fields(fields) {
|
||||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} ids
|
||||
* @param {number} last_message_id
|
||||
*/
|
||||
_mark_as_read(ids, last_message_id) {
|
||||
const kwargs = getKwArgs(arguments, "ids", "last_message_id", "sync");
|
||||
ids = kwargs.ids;
|
||||
delete kwargs.ids;
|
||||
last_message_id = kwargs.last_message_id;
|
||||
const [member] = this.browse(ids);
|
||||
if (!member) {
|
||||
return;
|
||||
}
|
||||
const messages = this.env["mail.message"]._filter([
|
||||
["model", "=", "discuss.channel"],
|
||||
["res_id", "=", member.channel_id],
|
||||
]);
|
||||
if (!messages || messages.length === 0) {
|
||||
return;
|
||||
}
|
||||
this._set_last_seen_message([member.id], last_message_id);
|
||||
this.env["discuss.channel.member"]._set_new_message_separator(
|
||||
[member.id],
|
||||
last_message_id + 1
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} ids
|
||||
* @param {number} message_id
|
||||
* @param {boolean} [notify=true]
|
||||
*/
|
||||
_set_last_seen_message(ids, message_id, notify) {
|
||||
const kwargs = getKwArgs(arguments, "ids", "message_id", "notify");
|
||||
ids = kwargs.ids;
|
||||
delete kwargs.ids;
|
||||
message_id = kwargs.message_id;
|
||||
notify = kwargs.notify ?? true;
|
||||
/** @type {import("mock_models").BusBus} */
|
||||
const BusBus = this.env["bus.bus"];
|
||||
/** @type {import("mock_models").DiscussChannel} */
|
||||
const DiscussChannel = this.env["discuss.channel"];
|
||||
/** @type {import("mock_models").DiscussChannelMember} */
|
||||
const DiscussChannelMember = this.env["discuss.channel.member"];
|
||||
/** @type {import("mock_models").ResPartner} */
|
||||
const ResPartner = this.env["res.partner"];
|
||||
|
||||
const [member] = this.browse(ids);
|
||||
if (!member) {
|
||||
return;
|
||||
}
|
||||
DiscussChannelMember.write([member.id], {
|
||||
fetched_message_id: message_id,
|
||||
seen_message_id: message_id,
|
||||
message_unread_counter: DiscussChannelMember._compute_message_unread_counter([
|
||||
member.id,
|
||||
]),
|
||||
});
|
||||
if (notify) {
|
||||
const [channel] = this.search_read([["id", "in", ids]]);
|
||||
const [partner, guest] = ResPartner._get_current_persona();
|
||||
let target = guest ?? partner;
|
||||
if (DiscussChannel._types_allowing_seen_infos().includes(channel.channel_type)) {
|
||||
target = channel;
|
||||
}
|
||||
BusBus._sendone(
|
||||
target,
|
||||
"mail.record/insert",
|
||||
new mailDataHelpers.Store(
|
||||
DiscussChannelMember.browse(member.id),
|
||||
[
|
||||
mailDataHelpers.Store.one(
|
||||
"channel_id",
|
||||
makeKwArgs({ as_thread: true, only_id: true })
|
||||
),
|
||||
|
||||
"seen_message_id",
|
||||
].concat(this._to_store_persona())
|
||||
).get_result()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} ids
|
||||
* @param {number} message_id
|
||||
*/
|
||||
_set_new_message_separator(ids, message_id) {
|
||||
const kwargs = getKwArgs(arguments, "ids", "message_id", "sync");
|
||||
ids = kwargs.ids;
|
||||
delete kwargs.ids;
|
||||
message_id = kwargs.message_id;
|
||||
|
||||
/** @type {import("mock_models").DiscussChannelMember} */
|
||||
const DiscussChannelMember = this.env["discuss.channel.member"];
|
||||
|
||||
const [member] = DiscussChannelMember.browse(ids);
|
||||
if (!member) {
|
||||
return;
|
||||
}
|
||||
this.env["discuss.channel.member"].write([member.id], {
|
||||
new_message_separator: message_id,
|
||||
});
|
||||
const message_unread_counter = this._compute_message_unread_counter([member.id]);
|
||||
this.env["discuss.channel.member"].write([member.id], { message_unread_counter });
|
||||
}
|
||||
|
||||
set_custom_notifications(ids, custom_notifications) {
|
||||
const kwargs = getKwArgs(arguments, "ids", "custom_notifications");
|
||||
ids = kwargs.ids;
|
||||
delete kwargs.ids;
|
||||
custom_notifications = kwargs.custom_notifications;
|
||||
|
||||
/** @type {import("mock_models").DiscussChannelMember} */
|
||||
const DiscussChannelMember = this.env["discuss.channel.member"];
|
||||
|
||||
const channelMememberId = ids[0]; // simulate ensure_one.
|
||||
DiscussChannelMember.write([channelMememberId], { custom_notifications });
|
||||
|
||||
const [partner, guest] = this.env["res.partner"]._get_current_persona();
|
||||
this.env["bus.bus"]._sendone(
|
||||
guest ?? partner,
|
||||
"mail.record/insert",
|
||||
new mailDataHelpers.Store(
|
||||
DiscussChannelMember.browse(channelMememberId),
|
||||
"custom_notifications"
|
||||
).get_result()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
import { mailDataHelpers } from "@mail/../tests/mock_server/mail_mock_server";
|
||||
|
||||
import { getKwArgs, makeKwArgs, models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class DiscussChannelRtcSession extends models.ServerModel {
|
||||
_name = "discuss.channel.rtc.session";
|
||||
|
||||
create() {
|
||||
/** @type {import("mock_models").BusBus} */
|
||||
const BusBus = this.env["bus.bus"];
|
||||
/** @type {import("mock_models").DiscussChannel} */
|
||||
const DiscussChannel = this.env["discuss.channel"];
|
||||
/** @type {import("mock_models").DiscussChannelMember} */
|
||||
const DiscussChannelMember = this.env["discuss.channel.member"];
|
||||
|
||||
const sessionIds = super.create(...arguments);
|
||||
const rtcSessions = this.browse(sessionIds);
|
||||
/** @type {Record<string, DiscussChannelRtcSession>} */
|
||||
const sessionsByChannelId = {};
|
||||
for (const session of rtcSessions) {
|
||||
const [member] = DiscussChannelMember.browse(session.channel_member_id);
|
||||
if (!sessionsByChannelId[member.channel_id]) {
|
||||
sessionsByChannelId[member.channel_id] = [];
|
||||
}
|
||||
sessionsByChannelId[member.channel_id].push(session);
|
||||
}
|
||||
const notifications = [];
|
||||
for (const [channelId, sessions] of Object.entries(sessionsByChannelId)) {
|
||||
const [channel] = DiscussChannel.search_read([["id", "=", Number(channelId)]]);
|
||||
notifications.push([
|
||||
channel,
|
||||
"mail.record/insert",
|
||||
new mailDataHelpers.Store(DiscussChannel.browse(channel.id), {
|
||||
rtc_session_ids: mailDataHelpers.Store.many(
|
||||
this.browse(sessions.map((session) => session.id)),
|
||||
makeKwArgs({ mode: "ADD" })
|
||||
),
|
||||
}).get_result(),
|
||||
]);
|
||||
}
|
||||
for (const record of rtcSessions) {
|
||||
const [channel] = DiscussChannel.browse(record.channel_id);
|
||||
if (channel.rtc_session_ids.length === 1) {
|
||||
DiscussChannel.message_post(
|
||||
channel.id,
|
||||
makeKwArgs({
|
||||
body: `<div data-oe-type="call" class="o_mail_notification"></div>`,
|
||||
message_type: "notification",
|
||||
subtype_xmlid: "mail.mt_comment",
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
BusBus._sendmany(notifications);
|
||||
return sessionIds;
|
||||
}
|
||||
|
||||
unlink(ids) {
|
||||
/** @type {import("mock_models").BusBus} */
|
||||
const BusBus = this.env["bus.bus"];
|
||||
/** @type {import("mock_models").ResPartner} */
|
||||
const ResPartner = this.env["res.partner"];
|
||||
/** @type {import("mock_models").DiscussChannel} */
|
||||
const DiscussChannel = this.env["discuss.channel"];
|
||||
const sessions = this.browse(ids);
|
||||
for (const session of sessions) {
|
||||
const [partner] = ResPartner.search_read([["id", "=", session.partner_id]]);
|
||||
BusBus._sendmany([
|
||||
[
|
||||
partner,
|
||||
"discuss.channel.rtc.session/ended",
|
||||
{
|
||||
sessionId: session.id,
|
||||
},
|
||||
],
|
||||
[
|
||||
partner,
|
||||
"mail.record/insert",
|
||||
new mailDataHelpers.Store(DiscussChannel.browse(Number(session.channel_id)), {
|
||||
rtc_session_ids: mailDataHelpers.Store.many(
|
||||
sessions,
|
||||
makeKwArgs({ only_id: true, mode: "DELETE" })
|
||||
),
|
||||
}).get_result(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
super.unlink(...arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
* @param {{ extra?; boolean }} options
|
||||
*/
|
||||
_to_store(store, fields, extra) {
|
||||
const kwargs = getKwArgs(arguments, "store", "fields", "extra");
|
||||
fields = kwargs.fields;
|
||||
extra = kwargs.extra ?? false;
|
||||
|
||||
store._add_record_fields(this, []);
|
||||
for (const rtcSession of this) {
|
||||
let data = [
|
||||
mailDataHelpers.Store.one(
|
||||
"channel_member_id",
|
||||
makeKwArgs({
|
||||
fields: ["channel"].concat(
|
||||
this.env["discuss.channel.member"]._to_store_persona([
|
||||
"name",
|
||||
"im_status",
|
||||
])
|
||||
),
|
||||
})
|
||||
),
|
||||
];
|
||||
if (extra) {
|
||||
data = data.concat(["is_camera_on", "is_deaf", "is_muted", "is_screen_sharing_on"]);
|
||||
}
|
||||
store._add_record_fields(this.browse(rtcSession.id), data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
* @param {object} values
|
||||
*/
|
||||
_update_and_broadcast(id, values) {
|
||||
const kwargs = getKwArgs(arguments, "id", "values");
|
||||
id = kwargs.id;
|
||||
delete kwargs.id;
|
||||
values = kwargs.values;
|
||||
|
||||
/** @type {import("mock_models").BusBus} */
|
||||
const BusBus = this.env["bus.bus"];
|
||||
/** @type {import("mock_models").DiscussChannel} */
|
||||
const DiscussChannel = this.env["discuss.channel"];
|
||||
/** @type {import("mock_models").DiscussChannelMember} */
|
||||
const DiscussChannelMember = this.env["discuss.channel.member"];
|
||||
/** @type {import("mock_models").DiscussChannelRtcSession} */
|
||||
const DiscussChannelRtcSession = this.env["discuss.channel.rtc.session"];
|
||||
|
||||
this.write([id], values);
|
||||
const [session] = DiscussChannelRtcSession.browse(id);
|
||||
const [member] = DiscussChannelMember.browse(session.channel_member_id);
|
||||
const [channel] = DiscussChannel.search_read([["id", "=", member.channel_id]]);
|
||||
BusBus._sendone(channel, "discuss.channel.rtc.session/update_and_broadcast", {
|
||||
data: new mailDataHelpers.Store(
|
||||
DiscussChannelRtcSession.browse(id),
|
||||
makeKwArgs({ extra: true })
|
||||
).get_result(),
|
||||
channelId: channel.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class DiscussGifFavorite extends models.ServerModel {
|
||||
_name = "discuss.gif.favorite";
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class DiscussVoiceMetadata extends models.ServerModel {
|
||||
_name = "discuss.voice.metadata";
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
import { mailDataHelpers } from "@mail/../tests/mock_server/mail_mock_server";
|
||||
|
||||
import { getKwArgs, makeKwArgs, webModels } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class IrAttachment extends webModels.IrAttachment {
|
||||
/**
|
||||
* @param {number} ids
|
||||
* @param {boolean} [force]
|
||||
*/
|
||||
register_as_main_attachment(ids, force) {
|
||||
const kwargs = getKwArgs(arguments, "ids", "force");
|
||||
ids = kwargs.ids;
|
||||
delete kwargs.ids;
|
||||
force = kwargs.force ?? true;
|
||||
|
||||
const [attachment] = this.browse(ids);
|
||||
if (!attachment.res_model) {
|
||||
return true; // dummy value for mock server
|
||||
}
|
||||
if (!this.env[attachment.res_model]._fields.message_main_attachment_id) {
|
||||
return true; // dummy value for mock server
|
||||
}
|
||||
const [record] = this.env[attachment.res_model].search_read([
|
||||
["id", "=", attachment.res_id],
|
||||
]);
|
||||
if (force || !record.message_main_attachment_id) {
|
||||
this.env[attachment.res_model].write([record.id], {
|
||||
message_main_attachment_id: attachment.id,
|
||||
});
|
||||
}
|
||||
return true; // dummy value for mock server
|
||||
}
|
||||
|
||||
/** @param {number} ids */
|
||||
_to_store(store, fields) {
|
||||
const kwargs = getKwArgs(arguments, "store", "fields");
|
||||
fields = kwargs.fields;
|
||||
|
||||
for (const attachment of this) {
|
||||
const [data] = this._read_format(
|
||||
attachment.id,
|
||||
fields.filter((field) => field !== "thread"),
|
||||
false
|
||||
);
|
||||
if (fields.includes("thread")) {
|
||||
data.thread =
|
||||
attachment.model !== "mail.compose.message" && attachment.res_id
|
||||
? mailDataHelpers.Store.one(
|
||||
this.env[attachment.res_model].browse(attachment.res_id),
|
||||
makeKwArgs({
|
||||
as_thread: true,
|
||||
only_id: true,
|
||||
})
|
||||
)
|
||||
: false;
|
||||
}
|
||||
store._add_record_fields(this.browse(attachment.id), data);
|
||||
}
|
||||
}
|
||||
|
||||
get _to_store_defaults() {
|
||||
return [
|
||||
"checksum",
|
||||
"create_date",
|
||||
"mimetype",
|
||||
"name",
|
||||
"res_name",
|
||||
"thread",
|
||||
"type",
|
||||
"url",
|
||||
"voice_ids",
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import { busModels } from "@bus/../tests/bus_test_helpers";
|
||||
|
||||
import { makeKwArgs } from "@web/../tests/web_test_helpers";
|
||||
import { isIterable } from "@web/core/utils/arrays";
|
||||
|
||||
export class IrWebSocket extends busModels.IrWebSocket {
|
||||
/**
|
||||
* @override
|
||||
* @type {typeof busModels.IrWebSocket["prototype"]["_build_bus_channel_list"]}
|
||||
*/
|
||||
_build_bus_channel_list(channels) {
|
||||
/** @type {import("mock_models").DiscussChannel} */
|
||||
const DiscussChannel = this.env["discuss.channel"];
|
||||
/** @type {import("mock_models").DiscussChannelMember} */
|
||||
const DiscussChannelMember = this.env["discuss.channel.member"];
|
||||
/** @type {import("mock_models").MailGuest} */
|
||||
const MailGuest = this.env["mail.guest"];
|
||||
/** @type {import("mock_models").ResPartner} */
|
||||
const ResPartner = this.env["res.partner"];
|
||||
|
||||
channels = [...super._build_bus_channel_list(channels)];
|
||||
const guest = MailGuest._get_guest_from_context();
|
||||
const authenticatedUserId = this.env.cookie.get("authenticated_user_sid");
|
||||
const [authenticatedPartner] = authenticatedUserId
|
||||
? ResPartner.search_read(
|
||||
[["user_ids", "in", [authenticatedUserId]]],
|
||||
makeKwArgs({ context: { active_test: false } })
|
||||
)
|
||||
: [];
|
||||
if (!authenticatedPartner && !guest) {
|
||||
return channels;
|
||||
}
|
||||
if (guest) {
|
||||
channels.push({ model: "mail.guest", id: guest.id });
|
||||
}
|
||||
const discussChannelIds = channels
|
||||
.filter((c) => typeof c === "string" && c.startsWith("discuss.channel_"))
|
||||
.map((c) => Number(c.split("_")[1]));
|
||||
|
||||
channels = channels.filter(
|
||||
(c) => typeof c !== "string" || !c.startsWith("discuss.channel_")
|
||||
);
|
||||
const allChannels = DiscussChannel.search_read([
|
||||
[
|
||||
"id",
|
||||
"in",
|
||||
DiscussChannelMember.search_read([
|
||||
"|",
|
||||
guest
|
||||
? ["guest_id", "=", guest.id]
|
||||
: ["partner_id", "=", authenticatedPartner.id],
|
||||
["channel_id", "in", discussChannelIds],
|
||||
]).map((member) =>
|
||||
isIterable(member.channel_id) ? member.channel_id[0] : member.channel_id
|
||||
),
|
||||
],
|
||||
]);
|
||||
for (const channel of allChannels) {
|
||||
channels.push(channel);
|
||||
}
|
||||
return channels;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { fields, models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class M2xAvatarUser extends models.Model {
|
||||
_name = "m2x.avatar.user";
|
||||
|
||||
user_id = fields.Many2one({ relation: "res.users" });
|
||||
partner_id = fields.Many2one({ relation: "res.partner" });
|
||||
user_ids = fields.Many2many({ relation: "res.users", string: "Users" });
|
||||
}
|
||||
|
|
@ -0,0 +1,286 @@
|
|||
import { mailDataHelpers } from "@mail/../tests/mock_server/mail_mock_server";
|
||||
|
||||
import { fields, getKwArgs, makeKwArgs, models, serverState } from "@web/../tests/web_test_helpers";
|
||||
import { Domain } from "@web/core/domain";
|
||||
import { deserializeDate, serializeDate, today } from "@web/core/l10n/dates";
|
||||
import { groupBy, sortBy, unique } from "@web/core/utils/arrays";
|
||||
|
||||
const { DateTime } = luxon;
|
||||
|
||||
export class MailActivity extends models.ServerModel {
|
||||
_name = "mail.activity";
|
||||
|
||||
activity_type_id = fields.Many2one({
|
||||
relation: "mail.activity.type",
|
||||
default() {
|
||||
return this.env["mail.activity.type"][0].id;
|
||||
},
|
||||
});
|
||||
user_id = fields.Many2one({ relation: "res.users", default: () => serverState.userId });
|
||||
chaining_type = fields.Generic({ default: "suggest" });
|
||||
activity_category = fields.Generic({ related: false }); // removes related from server to ease creating activities
|
||||
res_model = fields.Char({ string: "Related Document Model", related: false }); // removes related from server to ease creating activities
|
||||
|
||||
/** @param {number[]} ids */
|
||||
action_feedback(ids) {
|
||||
this.write(ids, { active: false, date_done: serializeDate(today()), state: "done" });
|
||||
}
|
||||
|
||||
/** @param {number[]} ids */
|
||||
action_feedback_schedule_next(ids) {
|
||||
this._action_done(ids);
|
||||
return {
|
||||
name: "Schedule an Activity",
|
||||
view_mode: "form",
|
||||
res_model: "mail.activity",
|
||||
views: [[false, "form"]],
|
||||
type: "ir.actions.act_window",
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {number[]} ids */
|
||||
activity_format(ids) {
|
||||
return new mailDataHelpers.Store(this.browse(ids)).get_result();
|
||||
}
|
||||
|
||||
/** @param {number[]} ids */
|
||||
_to_store(store, fields) {
|
||||
/** @type {import("mock_models").MailActivityType} */
|
||||
const MailActivityType = this.env["mail.activity.type"];
|
||||
/** @type {import("mock_models").MailTemplate} */
|
||||
const MailTemplate = this.env["mail.template"];
|
||||
store._add_record_fields(
|
||||
this,
|
||||
fields.filter((f) => !["activity_type_id"].includes(f))
|
||||
);
|
||||
|
||||
for (const activity of this) {
|
||||
const [data] = this._read_format(
|
||||
activity.id,
|
||||
["activity_type_id", "summary"].filter((f) => fields.includes(f))
|
||||
);
|
||||
// simulate computes
|
||||
const activityType = data.activity_type_id
|
||||
? MailActivityType.find((r) => r.id === data.activity_type_id[0])
|
||||
: false;
|
||||
if (activityType) {
|
||||
data.display_name = activityType.name;
|
||||
data.icon = activityType.icon;
|
||||
data.mail_template_ids = activityType.mail_template_ids.map((template_id) => {
|
||||
const [template] = MailTemplate.browse(template_id);
|
||||
return {
|
||||
id: template.id,
|
||||
name: template.name,
|
||||
};
|
||||
});
|
||||
}
|
||||
if (data.summary) {
|
||||
data.display_name = data.summary;
|
||||
}
|
||||
store._add_record_fields(this.browse(activity.id), data);
|
||||
}
|
||||
}
|
||||
|
||||
get _to_store_defaults() {
|
||||
return [
|
||||
"activity_category",
|
||||
"activity_type_id",
|
||||
mailDataHelpers.Store.many(
|
||||
"attachment_ids",
|
||||
makeKwArgs({
|
||||
fields: ["name"],
|
||||
})
|
||||
),
|
||||
"can_write",
|
||||
"chaining_type",
|
||||
"create_date",
|
||||
"create_uid",
|
||||
"date_deadline",
|
||||
"date_done",
|
||||
mailDataHelpers.Store.attr("note", (activity) => ["markup", activity.note]),
|
||||
"res_id",
|
||||
"res_model",
|
||||
"state",
|
||||
"summary",
|
||||
mailDataHelpers.Store.one(
|
||||
"user_id",
|
||||
makeKwArgs({
|
||||
fields: [mailDataHelpers.Store.one("partner_id")],
|
||||
})
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} res_model
|
||||
* @param {string} domain
|
||||
* @param {number} limit
|
||||
* @param {number} offset
|
||||
* @param {boolean} fetch_done
|
||||
*/
|
||||
get_activity_data(res_model, domain, limit = 0, offset = 0, fetch_done) {
|
||||
const kwargs = getKwArgs(arguments, "res_model", "domain", "limit", "offset", "fetch_done");
|
||||
res_model = kwargs.res_model;
|
||||
domain = kwargs.domain;
|
||||
limit = kwargs.limit || 0;
|
||||
offset = kwargs.offset || 0;
|
||||
fetch_done = kwargs.fetch_done ?? false;
|
||||
|
||||
/** @type {import("mock_models").IrAttachment} */
|
||||
const IrAttachment = this.env["ir.attachment"];
|
||||
/** @type {import("mock_models").MailActivityType} */
|
||||
const MailActivityType = this.env["mail.activity.type"];
|
||||
/** @type {import("mock_models").MailTemplate} */
|
||||
const MailTemplate = this.env["mail.template"];
|
||||
|
||||
// 1. Retrieve all ongoing and completed activities according to the parameters
|
||||
const activityTypes = MailActivityType._filter([
|
||||
"|",
|
||||
["res_model", "=", res_model],
|
||||
["res_model", "=", false],
|
||||
]);
|
||||
// Remove domain term used to filter record having "done" activities (not understood by the _filter mock)
|
||||
domain = Domain.removeDomainLeaves(new Domain(domain ?? []).toList(), [
|
||||
"activity_ids.active",
|
||||
]).toList();
|
||||
const allRecords = this.env[res_model]._filter(domain ?? []);
|
||||
const records = limit ? allRecords.slice(offset, offset + limit) : allRecords;
|
||||
const activityDomain = [["res_model", "=", res_model]];
|
||||
const isFiltered = domain || limit || offset;
|
||||
const domainResIds = records.map((r) => r.id);
|
||||
if (isFiltered) {
|
||||
activityDomain.push(["res_id", "in", domainResIds]);
|
||||
}
|
||||
const allActivities = this._filter(activityDomain, { active_test: !res_model });
|
||||
const allOngoing = allActivities.filter((a) => a.active);
|
||||
const allCompleted = allActivities.filter((a) => !a.active);
|
||||
// 2. Get attachment of completed activities
|
||||
let attachmentsById;
|
||||
if (allCompleted.length) {
|
||||
const attachmentIds = allCompleted.map((a) => a.attachment_ids).flat();
|
||||
attachmentsById = attachmentIds.length
|
||||
? Object.fromEntries(IrAttachment.browse(attachmentIds).map((a) => [a.id, a]))
|
||||
: {};
|
||||
} else {
|
||||
attachmentsById = {};
|
||||
}
|
||||
// 3. Group activities per records and activity type
|
||||
const groupedCompleted = groupBy(allCompleted, (a) => [a.res_id, a.activity_type_id]);
|
||||
const groupedOngoing = groupBy(allOngoing, (a) => [a.res_id, a.activity_type_id]);
|
||||
// 4. Format data
|
||||
const resIdToDeadline = {};
|
||||
const resIdToDateDone = {};
|
||||
const groupedActivities = {};
|
||||
for (const resIdStrTuple of new Set([
|
||||
...Object.keys(groupedCompleted),
|
||||
...Object.keys(groupedOngoing),
|
||||
])) {
|
||||
const [resId, activityTypeId] = resIdStrTuple.split(",").map((n) => Number(n));
|
||||
const ongoing = groupedOngoing[resIdStrTuple] || [];
|
||||
const completed = groupedCompleted[resIdStrTuple] || [];
|
||||
const dateDone = completed.length
|
||||
? DateTime.max(...completed.map((a) => deserializeDate(a.date_done)))
|
||||
: false;
|
||||
const dateDeadline = ongoing.length
|
||||
? DateTime.min(...ongoing.map((a) => deserializeDate(a.date_deadline)))
|
||||
: false;
|
||||
if (
|
||||
dateDeadline &&
|
||||
(resIdToDeadline[resId] === undefined || dateDeadline < resIdToDeadline[resId])
|
||||
) {
|
||||
resIdToDeadline[resId] = dateDeadline;
|
||||
}
|
||||
if (
|
||||
dateDone &&
|
||||
(resIdToDateDone[resId] === undefined || dateDone > resIdToDateDone[resId])
|
||||
) {
|
||||
resIdToDateDone[resId] = dateDone;
|
||||
}
|
||||
const userAssignedIds = unique(
|
||||
sortBy(
|
||||
ongoing.filter((a) => a.user_id),
|
||||
(a) => a.date_deadline
|
||||
).map((a) => a.user_id)
|
||||
);
|
||||
const reportingDate = ongoing.length ? dateDeadline : dateDone;
|
||||
const attachments = completed
|
||||
.map((act) => act.attachment_ids)
|
||||
.flat()
|
||||
.map((attachmentId) => attachmentsById[attachmentId]);
|
||||
const attachmentsInfo = {};
|
||||
if (attachments.length) {
|
||||
const lastAttachmentCreateDate = DateTime.max(
|
||||
...attachments.map((a) => deserializeDate(a.create_date))
|
||||
);
|
||||
const mostRecentAttachment = attachments.find((a) =>
|
||||
lastAttachmentCreateDate.equals(deserializeDate(a.create_date))
|
||||
);
|
||||
attachmentsInfo.attachments = {
|
||||
most_recent_id: mostRecentAttachment.id,
|
||||
most_recent_name: mostRecentAttachment.name,
|
||||
count: attachments.length,
|
||||
};
|
||||
}
|
||||
if (!(resId in groupedActivities)) {
|
||||
groupedActivities[resId] = {};
|
||||
}
|
||||
groupedActivities[resId][activityTypeId] = {
|
||||
count_by_state: {
|
||||
...Object.fromEntries(
|
||||
Object.entries(
|
||||
groupBy(ongoing, (a) =>
|
||||
this._compute_state_from_date(deserializeDate(a.date_deadline))
|
||||
)
|
||||
).map(([state, activities]) => [state, activities.length])
|
||||
),
|
||||
...(completed.length ? { done: completed.length } : {}),
|
||||
},
|
||||
ids: ongoing.map((a) => a.id).concat(completed.map((a) => a.id)),
|
||||
reporting_date: reportingDate ? reportingDate.toFormat("yyyy-LL-dd") : false,
|
||||
state: ongoing.length ? this._compute_state_from_date(dateDeadline) : "done",
|
||||
user_assigned_ids: userAssignedIds,
|
||||
summaries: ongoing.map((a) => (a.summary ? a.summary : "")),
|
||||
...attachmentsInfo,
|
||||
};
|
||||
}
|
||||
const ongoingResIds = sortBy(Object.keys(resIdToDeadline), (item) => resIdToDeadline[item]);
|
||||
const completedResIds = sortBy(
|
||||
Object.keys(resIdToDateDone).filter((resId) => !(resId in resIdToDeadline)),
|
||||
(item) => resIdToDateDone[item]
|
||||
);
|
||||
return {
|
||||
activity_types: activityTypes.map((type) => {
|
||||
const templates = (type.mail_template_ids || []).map((template_id) => {
|
||||
const { id, name } = MailTemplate.browse(template_id)[0];
|
||||
return { id, name };
|
||||
});
|
||||
return {
|
||||
id: type.id,
|
||||
name: type.display_name,
|
||||
template_ids: templates,
|
||||
};
|
||||
}),
|
||||
activity_res_ids: ongoingResIds.concat(completedResIds).map((idStr) => Number(idStr)),
|
||||
grouped_activities: groupedActivities,
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {number[]} ids */
|
||||
_action_done(ids) {
|
||||
this.action_feedback(ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DateTime} date_deadline to convert into state
|
||||
* @returns {"today" | "planned" | "overdue"}
|
||||
*/
|
||||
_compute_state_from_date(date_deadline) {
|
||||
const now = DateTime.now();
|
||||
if (date_deadline.hasSame(now, "day")) {
|
||||
return "today";
|
||||
} else if (date_deadline > now) {
|
||||
return "planned";
|
||||
}
|
||||
return "overdue";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class MailActivitySchedule extends models.ServerModel {
|
||||
_name = "mail.activity.schedule";
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { fields, models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class MailActivityType extends models.ServerModel {
|
||||
_name = "mail.activity.type";
|
||||
|
||||
chaining_type = fields.Generic({ default: "suggest" });
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
icon: "fa-envelope",
|
||||
name: "Email",
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
category: "phonecall",
|
||||
icon: "fa-phone",
|
||||
name: "Call",
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
id: 28,
|
||||
icon: "fa-upload",
|
||||
name: "Upload Document",
|
||||
active: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import { mailDataHelpers } from "@mail/../tests/mock_server/mail_mock_server";
|
||||
|
||||
import { getKwArgs, makeKwArgs, models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class MailCannedResponse extends models.ServerModel {
|
||||
_name = "mail.canned.response";
|
||||
|
||||
_views = {
|
||||
list: `
|
||||
<list>
|
||||
<field name="source" widget="shortcut"/>
|
||||
</list>
|
||||
`,
|
||||
form: `
|
||||
<form>
|
||||
<field name="source" widget="shortcut"/>
|
||||
</form>
|
||||
`,
|
||||
kanban: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="source" widget="shortcut"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
`,
|
||||
};
|
||||
|
||||
create() {
|
||||
const cannedReponseIds = super.create(...arguments);
|
||||
this._broadcast(cannedReponseIds);
|
||||
return cannedReponseIds;
|
||||
}
|
||||
|
||||
write(ids) {
|
||||
const res = super.write(...arguments);
|
||||
this._broadcast(ids);
|
||||
return res;
|
||||
}
|
||||
|
||||
unlink(ids) {
|
||||
this._broadcast(ids, makeKwArgs({ delete: true }));
|
||||
return super.unlink(...arguments);
|
||||
}
|
||||
|
||||
_broadcast(ids, _delete) {
|
||||
const kwargs = getKwArgs(arguments, "ids", "delete");
|
||||
_delete = kwargs.delete;
|
||||
const notifications = [];
|
||||
const [partner] = this.env["res.partner"].read(this.env.user.partner_id);
|
||||
for (const cannedResponse of this.browse(ids)) {
|
||||
notifications.push([
|
||||
partner,
|
||||
"mail.record/insert",
|
||||
new mailDataHelpers.Store(
|
||||
this.browse(cannedResponse.id),
|
||||
makeKwArgs({ delete: _delete })
|
||||
).get_result(),
|
||||
]);
|
||||
}
|
||||
if (notifications.length) {
|
||||
this.env["bus.bus"]._sendmany(notifications);
|
||||
}
|
||||
}
|
||||
|
||||
get _to_store_defaults() {
|
||||
return ["source", "substitution"];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class MailComposeMessage extends models.ServerModel {
|
||||
_name = "mail.compose.message";
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import { mailDataHelpers } from "@mail/../tests/mock_server/mail_mock_server";
|
||||
|
||||
import { getKwArgs, makeKwArgs, models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class MailFollowers extends models.ServerModel {
|
||||
_name = "mail.followers";
|
||||
|
||||
_compute_display_name() {
|
||||
for (const record of this) {
|
||||
const [partner] = this.env["res.partner"].browse(record.partner_id);
|
||||
record.display_name = partner.display_name;
|
||||
}
|
||||
}
|
||||
|
||||
_to_store(store, fields) {
|
||||
const kwargs = getKwArgs(arguments, "store", "fields");
|
||||
store = kwargs.store;
|
||||
fields = kwargs.fields;
|
||||
|
||||
store._add_record_fields(
|
||||
this,
|
||||
fields.filter((field) => field !== "subtype_ids")
|
||||
);
|
||||
|
||||
for (const follower of this) {
|
||||
const data = {};
|
||||
if (fields.includes("subtype_ids")) {
|
||||
data.subtype_ids = mailDataHelpers.Store.many(
|
||||
this.env["mail.message.subtype"].browse(follower.subtype_ids)
|
||||
);
|
||||
}
|
||||
if (Object.keys(data).length) {
|
||||
store._add_record_fields(this.browse(follower.id), data);
|
||||
}
|
||||
}
|
||||
}
|
||||
get _to_store_defaults() {
|
||||
return [
|
||||
"display_name",
|
||||
"email",
|
||||
"is_active",
|
||||
"name",
|
||||
mailDataHelpers.Store.one("partner_id"),
|
||||
mailDataHelpers.Store.attr("thread", (follower) =>
|
||||
mailDataHelpers.Store.one(
|
||||
this.env[follower.res_model].browse(follower.res_id),
|
||||
makeKwArgs({ as_thread: true, only_id: true })
|
||||
)
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { getKwArgs, models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class MailGuest extends models.ServerModel {
|
||||
_name = "mail.guest";
|
||||
|
||||
_get_guest_from_context() {
|
||||
const guestId = this.env.cookie.get("dgid");
|
||||
return guestId ? this.search_read([["id", "=", guestId]])[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number[]} ids
|
||||
* @returns {Record<string, ModelRecord>}
|
||||
*/
|
||||
_to_store(store, fields) {
|
||||
const kwargs = getKwArgs(arguments, "store", "fields");
|
||||
fields = kwargs.fields;
|
||||
store._add_record_fields(
|
||||
this,
|
||||
fields.filter((field) => !["avatar_128"].includes(field))
|
||||
);
|
||||
for (const guest of this) {
|
||||
const data = {};
|
||||
if (fields.includes("avatar_128")) {
|
||||
data.avatar_128_access_token = guest.id;
|
||||
data.write_date = guest.write_date;
|
||||
}
|
||||
if (fields.includes("im_status")) {
|
||||
data.im_status = "offline";
|
||||
data.im_status_access_token = guest.id;
|
||||
}
|
||||
if (Object.keys(data).length) {
|
||||
store._add_record_fields(this.browse(guest.id), data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get _to_store_defaults() {
|
||||
return ["avatar_128", "im_status", "name"];
|
||||
}
|
||||
|
||||
_set_auth_cookie(guestId) {
|
||||
this.env.cookie.set("dgid", guestId);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class MailLinkPreview extends models.ServerModel {
|
||||
_name = "mail.link.preview";
|
||||
|
||||
get _to_store_defaults() {
|
||||
return [
|
||||
"image_mimetype",
|
||||
"message_id",
|
||||
"og_description",
|
||||
"og_image",
|
||||
"og_mimetype",
|
||||
"og_title",
|
||||
"og_type",
|
||||
"source_url",
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,664 @@
|
|||
import { mailDataHelpers } from "@mail/../tests/mock_server/mail_mock_server";
|
||||
|
||||
import {
|
||||
Command,
|
||||
fields,
|
||||
getKwArgs,
|
||||
makeKwArgs,
|
||||
models,
|
||||
serverState,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { Domain } from "@web/core/domain";
|
||||
|
||||
/** @typedef {import("@web/core/domain").DomainListRepr} DomainListRepr */
|
||||
|
||||
export class MailMessage extends models.ServerModel {
|
||||
_name = "mail.message";
|
||||
|
||||
author_id = fields.Generic({ default: () => serverState.partnerId });
|
||||
pinned_at = fields.Generic({ default: false });
|
||||
|
||||
/** @param {DomainListRepr} [domain] */
|
||||
mark_all_as_read(domain) {
|
||||
({ domain } = getKwArgs(arguments, "domain"));
|
||||
|
||||
/** @type {import("mock_models").BusBus} */
|
||||
const BusBus = this.env["bus.bus"];
|
||||
/** @type {import("mock_models").MailNotification} */
|
||||
const MailNotification = this.env["mail.notification"];
|
||||
/** @type {import("mock_models").ResPartner} */
|
||||
const ResPartner = this.env["res.partner"];
|
||||
|
||||
const notifDomain = [
|
||||
["res_partner_id", "=", this.env.user.partner_id],
|
||||
["is_read", "=", false],
|
||||
];
|
||||
if (domain) {
|
||||
const messages = this._filter(domain);
|
||||
const ids = messages.map((messages) => messages.id);
|
||||
this.set_message_done(ids);
|
||||
return ids;
|
||||
}
|
||||
const notifications = MailNotification._filter(notifDomain);
|
||||
MailNotification.write(
|
||||
notifications.map((notification) => notification.id),
|
||||
{ is_read: true }
|
||||
);
|
||||
const messageIds = [];
|
||||
for (const notification of notifications) {
|
||||
if (!messageIds.includes(notification.mail_message_id)) {
|
||||
messageIds.push(notification.mail_message_id);
|
||||
}
|
||||
}
|
||||
const messages = this.browse(messageIds);
|
||||
// simulate compute that should be done based on notifications
|
||||
for (const message of messages) {
|
||||
this.write([message.id], {
|
||||
needaction: false,
|
||||
});
|
||||
}
|
||||
const [partner] = ResPartner.read(this.env.user.partner_id);
|
||||
BusBus._sendone(partner, "mail.message/mark_as_read", {
|
||||
message_ids: messageIds,
|
||||
needaction_inbox_counter: ResPartner._get_needaction_count(this.env.user.partner_id),
|
||||
});
|
||||
return messageIds;
|
||||
}
|
||||
|
||||
/** @param {number[]} ids */
|
||||
_to_store(store, fields, for_current_user, add_followers) {
|
||||
const kwargs = getKwArgs(arguments, "store", "fields", "for_current_user", "add_followers");
|
||||
store = kwargs.store;
|
||||
fields = kwargs.fields;
|
||||
for_current_user = kwargs.for_current_user ?? false;
|
||||
add_followers = kwargs.add_followers ?? false;
|
||||
|
||||
/** @type {import("mock_models").MailFollowers} */
|
||||
const MailFollowers = this.env["mail.followers"];
|
||||
/** @type {import("mock_models").MailMessageLinkPreview} */
|
||||
const MailMessageLinkPreview = this.env["mail.message.link.preview"];
|
||||
/** @type {import("mock_models").MailNotification} */
|
||||
const MailNotification = this.env["mail.notification"];
|
||||
/** @type {import("mock_models").MailThread} */
|
||||
const MailThread = this.env["mail.thread"];
|
||||
/** @type {import("mock_models").MailTrackingValue} */
|
||||
const MailTrackingValue = this.env["mail.tracking.value"];
|
||||
/** @type {import("mock_models").ResFake} */
|
||||
const ResFake = this.env["res.fake"];
|
||||
|
||||
const notifications = MailNotification._filtered_for_web_client(
|
||||
MailNotification._filter([["mail_message_id", "in", this.map((m) => m.id)]]).map(
|
||||
(n) => n.id
|
||||
)
|
||||
);
|
||||
store._add_record_fields(
|
||||
this,
|
||||
fields.filter((field) => !["notification_ids", "mail_link_preview_ids"].includes(field))
|
||||
);
|
||||
for (const message of this) {
|
||||
const thread = message.model && this.env[message.model].browse(message.res_id)[0];
|
||||
if (thread) {
|
||||
const thread_data = {
|
||||
display_name: thread.name ?? thread.display_name,
|
||||
module_icon: "/base/static/description/icon.png",
|
||||
};
|
||||
if (for_current_user && add_followers) {
|
||||
thread_data.selfFollower = mailDataHelpers.Store.one(
|
||||
MailFollowers.browse(
|
||||
MailFollowers.search([
|
||||
["res_model", "=", message.model],
|
||||
["res_id", "=", message.res_id],
|
||||
["partner_id", "=", this.env.user.partner_id],
|
||||
])
|
||||
),
|
||||
makeKwArgs({
|
||||
fields: ["is_active", mailDataHelpers.Store.one("partner_id", [])],
|
||||
})
|
||||
);
|
||||
}
|
||||
store._add_record_fields(
|
||||
this.env[message.model].browse(message.res_id),
|
||||
thread_data,
|
||||
makeKwArgs({ as_thread: true })
|
||||
);
|
||||
}
|
||||
const data = {
|
||||
default_subject:
|
||||
message.model &&
|
||||
message.res_id &&
|
||||
(message.model === "res.fake"
|
||||
? ResFake._message_compute_subject([message.res_id])
|
||||
: MailThread._message_compute_subject([message.res_id])
|
||||
).get(message.res_id),
|
||||
record_name: thread?.name ?? thread?.display_name,
|
||||
scheduledDatetime: false,
|
||||
thread: mailDataHelpers.Store.one(
|
||||
message.model && this.env[message.model].browse(message.res_id),
|
||||
makeKwArgs({ as_thread: true, only_id: true })
|
||||
),
|
||||
};
|
||||
if (fields.includes("message_link_preview_ids")) {
|
||||
data.message_link_preview_ids = mailDataHelpers.Store.many(
|
||||
MailMessageLinkPreview.browse(message.message_link_preview_ids).filter(
|
||||
(lpm) => !lpm.is_hidden
|
||||
)
|
||||
);
|
||||
}
|
||||
if (fields.includes("notification_ids")) {
|
||||
data.notification_ids = mailDataHelpers.Store.many(
|
||||
notifications.filter(
|
||||
(notification) => notification.mail_message_id == message.id
|
||||
)
|
||||
);
|
||||
}
|
||||
if (for_current_user) {
|
||||
data["needaction"] = Boolean(
|
||||
this.env.user &&
|
||||
MailNotification.search([
|
||||
["mail_message_id", "=", message.id],
|
||||
["is_read", "=", false],
|
||||
["res_partner_id", "=", this.env.user.partner_id],
|
||||
]).length
|
||||
);
|
||||
data["starred"] = message.starred_partner_ids?.includes(this.env.user?.partner_id);
|
||||
const trackingValues = MailTrackingValue.browse(message.tracking_value_ids);
|
||||
const formattedTrackingValues =
|
||||
MailTrackingValue._tracking_value_format(trackingValues);
|
||||
data["trackingValues"] = formattedTrackingValues;
|
||||
}
|
||||
store._add_record_fields(this.browse(message.id), data);
|
||||
}
|
||||
this._author_to_store(store);
|
||||
this._store_add_linked_messages(store);
|
||||
}
|
||||
|
||||
get _to_store_defaults() {
|
||||
return [
|
||||
mailDataHelpers.Store.many(
|
||||
"attachment_ids",
|
||||
makeKwArgs({
|
||||
sort: (a1, a2) => a1.id - a2.id,
|
||||
})
|
||||
),
|
||||
mailDataHelpers.Store.attr("body", (m) => ["markup", m.body]),
|
||||
"create_date",
|
||||
"date",
|
||||
"message_type",
|
||||
"model",
|
||||
"message_link_preview_ids",
|
||||
"notification_ids",
|
||||
mailDataHelpers.Store.one("parent_id", makeKwArgs({ format_reply: false })),
|
||||
mailDataHelpers.Store.many("partner_ids", makeKwArgs({ fields: ["name"] })),
|
||||
"pinned_at",
|
||||
mailDataHelpers.Store.attr("reactions", (m) =>
|
||||
mailDataHelpers.Store.many(this.env["mail.message.reaction"].browse(m.reaction_ids))
|
||||
),
|
||||
"res_id",
|
||||
"subject",
|
||||
"write_date",
|
||||
mailDataHelpers.Store.one(
|
||||
"subtype_id",
|
||||
makeKwArgs({
|
||||
fields: ["description"],
|
||||
predicate: (m) => m.subtype_id,
|
||||
})
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
_author_to_store(store) {
|
||||
/** @type {import("mock_models").MailGuest} */
|
||||
const MailGuest = this.env["mail.guest"];
|
||||
/** @type {import("mock_models").MailMessage} */
|
||||
const MailMessage = this.env["mail.message"];
|
||||
/** @type {import("mock_models").ResPartner} */
|
||||
const ResPartner = this.env["res.partner"];
|
||||
|
||||
for (const message of this) {
|
||||
const data = {
|
||||
author_id: false,
|
||||
author_guest_id: false,
|
||||
email_from: message.email_from,
|
||||
};
|
||||
if (message.author_guest_id) {
|
||||
data.author_guest_id = mailDataHelpers.Store.one(
|
||||
MailGuest.browse(message.author_guest_id),
|
||||
makeKwArgs({ fields: ["avatar_128", "name"] })
|
||||
);
|
||||
} else if (message.author_id) {
|
||||
data.author_id = mailDataHelpers.Store.one(
|
||||
ResPartner.browse(message.author_id),
|
||||
makeKwArgs({ fields: ["avatar_128", "is_company", "name", "user"] })
|
||||
);
|
||||
}
|
||||
store._add_record_fields(MailMessage.browse(message.id), data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates `set_message_done` on `mail.message`, which turns provided
|
||||
* needaction message to non-needaction (i.e. they are marked as read from
|
||||
* from the Inbox mailbox). Also notify on the longpoll bus that the
|
||||
* messages have been marked as read, so that UI is updated.
|
||||
*
|
||||
* @param {number[]} ids
|
||||
*/
|
||||
set_message_done(ids) {
|
||||
/** @type {import("mock_models").BusBus} */
|
||||
const BusBus = this.env["bus.bus"];
|
||||
/** @type {import("mock_models").MailNotification} */
|
||||
const MailNotification = this.env["mail.notification"];
|
||||
/** @type {import("mock_models").ResPartner} */
|
||||
const ResPartner = this.env["res.partner"];
|
||||
|
||||
if (!this.env.user) {
|
||||
return;
|
||||
}
|
||||
const messages = this.browse(ids);
|
||||
const notifications = MailNotification._filter([
|
||||
["res_partner_id", "=", this.env.user.partner_id],
|
||||
["is_read", "=", false],
|
||||
["mail_message_id", "in", messages.map((messages) => messages.id)],
|
||||
]);
|
||||
if (notifications.length === 0) {
|
||||
return;
|
||||
}
|
||||
MailNotification.write(
|
||||
notifications.map((notification) => notification.id),
|
||||
{ is_read: true }
|
||||
);
|
||||
// simulate compute that should be done based on notifications
|
||||
for (const message of messages) {
|
||||
this.write([message.id], {
|
||||
needaction: false,
|
||||
});
|
||||
const [partner] = ResPartner.read(this.env.user.partner_id);
|
||||
BusBus._sendone(partner, "mail.message/mark_as_read", {
|
||||
message_ids: [message.id],
|
||||
needaction_inbox_counter: ResPartner._get_needaction_count(
|
||||
this.env.user.partner_id
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
unlink() {
|
||||
const messageByPartnerId = {};
|
||||
for (const message of this) {
|
||||
for (const partnerId of message.partner_ids) {
|
||||
messageByPartnerId[partnerId] ??= [];
|
||||
messageByPartnerId[partnerId].push(message);
|
||||
}
|
||||
if (
|
||||
this.env["mail.notification"]
|
||||
.browse(message.notification_ids)
|
||||
.some(({ failure_type }) => Boolean(failure_type))
|
||||
) {
|
||||
messageByPartnerId[message.author_id] ??= [];
|
||||
messageByPartnerId[message.author_id].push(message);
|
||||
}
|
||||
}
|
||||
for (const [partnerId, messages] of Object.entries(messageByPartnerId)) {
|
||||
const [partner] = this.env["res.partner"].browse(parseInt(partnerId));
|
||||
this.env["bus.bus"]._sendone(partner, "mail.message/delete", {
|
||||
message_ids: messages.map(({ id }) => id),
|
||||
});
|
||||
}
|
||||
return super.unlink(...arguments);
|
||||
}
|
||||
|
||||
/** @param {number[]} ids */
|
||||
toggle_message_starred(ids) {
|
||||
/** @type {import("mock_models").BusBus} */
|
||||
const BusBus = this.env["bus.bus"];
|
||||
/** @type {import("mock_models").ResPartner} */
|
||||
const ResPartner = this.env["res.partner"];
|
||||
|
||||
const messages = this.browse(ids);
|
||||
const store = new mailDataHelpers.Store();
|
||||
for (const message of messages) {
|
||||
const wasStarred = message.starred_partner_ids.includes(this.env.user.partner_id);
|
||||
this.write([message.id], {
|
||||
starred_partner_ids: [
|
||||
wasStarred
|
||||
? Command.unlink(this.env.user.partner_id)
|
||||
: Command.link(this.env.user.partner_id),
|
||||
],
|
||||
});
|
||||
const [partner] = ResPartner.read(this.env.user.partner_id);
|
||||
BusBus._sendone(partner, "mail.message/toggle_star", {
|
||||
message_ids: [message.id],
|
||||
starred: !wasStarred,
|
||||
});
|
||||
store.add(this.browse(message.id), { starred: !wasStarred });
|
||||
}
|
||||
return store.get_result();
|
||||
}
|
||||
|
||||
unstar_all() {
|
||||
/** @type {import("mock_models").BusBus} */
|
||||
const BusBus = this.env["bus.bus"];
|
||||
/** @type {import("mock_models").ResPartner} */
|
||||
const ResPartner = this.env["res.partner"];
|
||||
|
||||
const messages = this._filter([["starred_partner_ids", "in", this.env.user.partner_id]]);
|
||||
this.write(
|
||||
messages.map((message) => message.id),
|
||||
{ starred_partner_ids: [Command.unlink(this.env.user.partner_id)] }
|
||||
);
|
||||
const [partner] = ResPartner.read(this.env.user.partner_id);
|
||||
BusBus._sendone(partner, "mail.message/toggle_star", {
|
||||
message_ids: messages.map((message) => message.id),
|
||||
starred: false,
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {number} id */
|
||||
_bus_notification_target(id) {
|
||||
/** @type {import("mock_models").DiscussChannel} */
|
||||
const DiscussChannel = this.env["discuss.channel"];
|
||||
/** @type {import("mock_models").MailGuest} */
|
||||
const MailGuest = this.env["mail.guest"];
|
||||
/** @type {import("mock_models").ResPartner} */
|
||||
const ResPartner = this.env["res.partner"];
|
||||
/** @type {import("mock_models").ResUsers} */
|
||||
const ResUsers = this.env["res.users"];
|
||||
|
||||
const [message] = this.search_read([["id", "=", id]]);
|
||||
if (message.model === "discuss.channel") {
|
||||
return DiscussChannel.search_read([["id", "=", message.res_id]])[0];
|
||||
}
|
||||
if (ResUsers._is_public(this.env.uid)) {
|
||||
MailGuest._get_guest_from_context();
|
||||
}
|
||||
return ResPartner.read(this.env.user.partner_id)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
* @param {string} content
|
||||
* @param {number} partner_id
|
||||
* @param {number} guest_id
|
||||
* @param {string} action
|
||||
* @param {import("@mail/../tests/mock_server/mail_mock_server").mailDataHelpers.Store} store
|
||||
*/
|
||||
_message_reaction(id, content, partner_id, guest_id, action, store) {
|
||||
({ id, content, partner_id, guest_id, action, store } = getKwArgs(
|
||||
arguments,
|
||||
"id",
|
||||
"content",
|
||||
"partner_id",
|
||||
"guest_id",
|
||||
"action",
|
||||
"store"
|
||||
));
|
||||
|
||||
/** @type {import("mock_models").MailMessageReaction} */
|
||||
const MailMessageReaction = this.env["mail.message.reaction"];
|
||||
|
||||
const [reaction] = MailMessageReaction.search_read([
|
||||
["content", "=", content],
|
||||
["message_id", "=", id],
|
||||
["partner_id", "=", partner_id],
|
||||
["guest_id", "=", guest_id],
|
||||
]);
|
||||
if (action === "add" && !reaction) {
|
||||
MailMessageReaction.create({
|
||||
content,
|
||||
message_id: id,
|
||||
partner_id,
|
||||
guest_id,
|
||||
});
|
||||
}
|
||||
if (action === "remove" && reaction) {
|
||||
MailMessageReaction.unlink(reaction.id);
|
||||
}
|
||||
this._reaction_group_to_store(id, store, content);
|
||||
this._bus_send_reaction_group(id, content);
|
||||
}
|
||||
|
||||
_bus_send_reaction_group(id, content) {
|
||||
/** @type {import("mock_models").BusBus} */
|
||||
const BusBus = this.env["bus.bus"];
|
||||
const store = new mailDataHelpers.Store();
|
||||
this._reaction_group_to_store(id, store, content);
|
||||
BusBus._sendone(
|
||||
this._bus_notification_target(id),
|
||||
"mail.record/insert",
|
||||
store.get_result()
|
||||
);
|
||||
}
|
||||
|
||||
_reaction_group_to_store(id, store, content) {
|
||||
/** @type {import("mock_models").MailMessageReaction} */
|
||||
const MailMessageReaction = this.env["mail.message.reaction"];
|
||||
|
||||
const reactions = MailMessageReaction.search([
|
||||
["message_id", "=", id],
|
||||
["content", "=", content],
|
||||
]);
|
||||
let reaction_group = mailDataHelpers.Store.many(
|
||||
MailMessageReaction.browse(reactions),
|
||||
makeKwArgs({ mode: "ADD" })
|
||||
);
|
||||
if (reactions.length === 0) {
|
||||
reaction_group = [["DELETE", { message: this.browse(id), content: content }]];
|
||||
}
|
||||
store.add(this.browse(id), { reactions: reaction_group });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DomainListRepr} domain
|
||||
* @param {number} [before]
|
||||
* @param {number} [after]
|
||||
* @param {number} [limit=30]
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
_message_fetch(domain, thread, search_term, is_notification, before, after, around, limit) {
|
||||
/** @type {import("mock_models").IrAttachment} */
|
||||
const IrAttachment = this.env["ir.attachment"];
|
||||
/** @type {import("mock_models").MailMessageSubtype} */
|
||||
const MailMessageSubtype = this.env["mail.message.subtype"];
|
||||
/** @type {import("mock_models").MailTrackingValue} */
|
||||
const MailTrackingValue = this.env["mail.tracking.value"];
|
||||
({
|
||||
domain,
|
||||
thread,
|
||||
search_term,
|
||||
is_notification,
|
||||
before,
|
||||
after,
|
||||
around,
|
||||
limit = 30,
|
||||
} = getKwArgs(
|
||||
arguments,
|
||||
"domain",
|
||||
"thread",
|
||||
"search_term",
|
||||
"is_notification",
|
||||
"before",
|
||||
"after",
|
||||
"around",
|
||||
"limit"
|
||||
));
|
||||
const res = {};
|
||||
if (thread) {
|
||||
domain = domain.concat([
|
||||
["res_id", "=", parseInt(thread[0].id)],
|
||||
["model", "=", thread._name],
|
||||
["message_type", "!=", "user_notification"],
|
||||
]);
|
||||
}
|
||||
if (is_notification === true) {
|
||||
domain.push(["message_type", "=", "notification"]);
|
||||
} else if (is_notification === false) {
|
||||
domain.push(["message_type", "!=", "notification"]);
|
||||
}
|
||||
if (search_term) {
|
||||
domain = new Domain(domain || []);
|
||||
search_term = search_term.replace(" ", "%");
|
||||
const subtypeIds = MailMessageSubtype.search([["description", "ilike", search_term]]);
|
||||
const irAttachmentIds = IrAttachment.search([["name", "ilike", search_term]]);
|
||||
let message_domain = Domain.or([
|
||||
[["body", "ilike", search_term]],
|
||||
[["attachment_ids", "in", irAttachmentIds]],
|
||||
[["subject", "ilike", search_term]],
|
||||
[["subtype_ids", "in", subtypeIds]],
|
||||
]);
|
||||
if (thread && is_notification !== false) {
|
||||
const messageIds = this.search([
|
||||
["res_id", "=", parseInt(thread[0].id)],
|
||||
["model", "=", thread._name],
|
||||
]);
|
||||
const trackingValueDomain = Domain.and([
|
||||
[["mail_message_id", "in", messageIds]],
|
||||
this._get_tracking_values_domain(search_term),
|
||||
]).toList();
|
||||
const trackingValueIds = MailTrackingValue.search(trackingValueDomain);
|
||||
const trackingMessageIds = this.search([
|
||||
["tracking_value_ids", "in", trackingValueIds],
|
||||
]);
|
||||
message_domain = Domain.or([
|
||||
message_domain,
|
||||
new Domain([["id", "in", trackingMessageIds]]),
|
||||
]);
|
||||
}
|
||||
domain = Domain.and([domain, message_domain]).toList();
|
||||
res.count = this.search_count(domain);
|
||||
}
|
||||
if (around !== undefined) {
|
||||
const messagesBefore = this._filter(domain.concat([["id", "<=", around]])).sort(
|
||||
(m1, m2) => m2.id - m1.id
|
||||
);
|
||||
messagesBefore.length = Math.min(messagesBefore.length, limit / 2);
|
||||
const messagesAfter = this._filter(domain.concat([["id", ">", around]])).sort(
|
||||
(m1, m2) => m1.id - m2.id
|
||||
);
|
||||
messagesAfter.length = Math.min(messagesAfter.length, limit / 2);
|
||||
const messages = messagesAfter
|
||||
.concat(messagesBefore.reverse())
|
||||
.sort((m1, m2) => m2.id - m1.id);
|
||||
return { ...res, messages };
|
||||
}
|
||||
if (before) {
|
||||
domain.push(["id", "<", before]);
|
||||
}
|
||||
if (after) {
|
||||
domain.push(["id", ">", after]);
|
||||
}
|
||||
const messages = this._filter(domain).sort((m1, m2) => m2.id - m1.id);
|
||||
// pick at most 'limit' messages
|
||||
messages.length = Math.min(messages.length, limit);
|
||||
res.messages = messages;
|
||||
return res;
|
||||
}
|
||||
|
||||
_get_tracking_values_domain(search_term) {
|
||||
let numeric_term = false;
|
||||
const epsilon = 1e-9;
|
||||
numeric_term = parseFloat(search_term);
|
||||
const field_names = [
|
||||
"old_value_char",
|
||||
"new_value_char",
|
||||
"old_value_text",
|
||||
"new_value_text",
|
||||
"old_value_datetime",
|
||||
"new_value_datetime",
|
||||
];
|
||||
let domain = Domain.or(
|
||||
field_names.map((field_name) => new Domain([[field_name, "ilike", search_term]]))
|
||||
);
|
||||
if (numeric_term) {
|
||||
const float_domain = Domain.or(
|
||||
["old_value_float", "new_value_float"].map(
|
||||
(fieldName) =>
|
||||
new Domain([
|
||||
[fieldName, ">=", numeric_term - epsilon],
|
||||
[fieldName, "<=", numeric_term + epsilon],
|
||||
])
|
||||
)
|
||||
);
|
||||
domain = Domain.or([domain, float_domain]);
|
||||
}
|
||||
if (Number.isInteger(numeric_term)) {
|
||||
domain = Domain.or([
|
||||
domain,
|
||||
new Domain([["old_value_integer", "=", numeric_term]]),
|
||||
new Domain([["new_value_integer", "=", numeric_term]]),
|
||||
]);
|
||||
}
|
||||
return domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("@mail/../tests/mock_server/mail_mock_server").mailDataHelpers.Store} store
|
||||
*/
|
||||
_store_add_linked_messages(store) {
|
||||
const mids = [];
|
||||
for (const message of this) {
|
||||
const body = message?.body || "";
|
||||
const doc = new DOMParser().parseFromString(body, "text/html");
|
||||
const anchors = doc.querySelectorAll(
|
||||
'a.o_message_redirect[data-oe-model="mail.message"][data-oe-id]'
|
||||
);
|
||||
for (const a of anchors) {
|
||||
const idStr = a.getAttribute("data-oe-id");
|
||||
const id = parseInt(idStr, 10);
|
||||
if (!Number.isNaN(id)) {
|
||||
mids.push(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const message of this.env["mail.message"]._filter([["id", "in", mids]])) {
|
||||
if (message.model && message.res_id) {
|
||||
const record = this.env[message.model]._filter([["id", "=", message.res_id]]);
|
||||
store.add(
|
||||
this.env["mail.message"].browse(message.id),
|
||||
makeKwArgs({
|
||||
fields: [
|
||||
"model",
|
||||
"res_id",
|
||||
mailDataHelpers.Store.attr(
|
||||
"thread",
|
||||
mailDataHelpers.Store.one(
|
||||
this.env[message.model].browse(record.id),
|
||||
makeKwArgs({ fields: ["display_name"] })
|
||||
)
|
||||
),
|
||||
],
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} ids
|
||||
* @param {import("@mail/../tests/mock_server/mail_mock_server").mailDataHelpers.Store} store
|
||||
*/
|
||||
_message_notifications_to_store(ids, store) {
|
||||
/** @type {import("mock_models").MailNotification} */
|
||||
const MailNotification = this.env["mail.notification"];
|
||||
|
||||
for (const message of this.browse(ids)) {
|
||||
store.add(this.browse(message.id), {
|
||||
author_id: mailDataHelpers.Store.one(
|
||||
this.env["res.partner"].browse(message.author_id),
|
||||
makeKwArgs({ only_id: true })
|
||||
),
|
||||
body: message.body,
|
||||
date: message.date,
|
||||
message_type: message.message_type,
|
||||
notification_ids: mailDataHelpers.Store.many(
|
||||
MailNotification._filtered_for_web_client(
|
||||
MailNotification.search([["mail_message_id", "=", message.id]])
|
||||
)
|
||||
),
|
||||
thread: mailDataHelpers.Store.one(
|
||||
message.model ? this.env[message.model].browse(message.res_id) : false,
|
||||
makeKwArgs({ as_thread: true, fields: ["modelName", "display_name"] })
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { mailDataHelpers } from "@mail/../tests/mock_server/mail_mock_server";
|
||||
import { fields, models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class MailMessageLinkPreview extends models.ServerModel {
|
||||
_name = "mail.message.link.preview";
|
||||
|
||||
link_preview_id = fields.Many2one({ relation: "mail.link.preview" });
|
||||
message_id = fields.Many2one({ relation: "mail.message" });
|
||||
is_hidden = fields.Generic({ default: false });
|
||||
|
||||
get _to_store_defaults() {
|
||||
return [mailDataHelpers.Store.one("link_preview_id"), "message_id"];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import { makeKwArgs, models } from "@web/../tests/web_test_helpers";
|
||||
import { groupBy } from "@web/core/utils/arrays";
|
||||
import { mailDataHelpers } from "../mail_mock_server";
|
||||
|
||||
export class MailMessageReaction extends models.ServerModel {
|
||||
_name = "mail.message.reaction";
|
||||
|
||||
_to_store(store) {
|
||||
/** @type {import("mock_models").MailGuest} */
|
||||
const MailGuest = this.env["mail.guest"];
|
||||
/** @type {import("mock_models").ResPartner} */
|
||||
const ResPartner = this.env["res.partner"];
|
||||
|
||||
const reactionGroups = groupBy(this, (r) => [r.message_id, r.content]);
|
||||
for (const groupId in reactionGroups) {
|
||||
const reactionGroup = reactionGroups[groupId];
|
||||
const { message_id, content } = reactionGroups[groupId][0];
|
||||
const guests = MailGuest.browse(reactionGroup.map((reaction) => reaction.guest_id));
|
||||
const partners = ResPartner.browse(
|
||||
reactionGroup.map((reaction) => reaction.partner_id)
|
||||
);
|
||||
const data = {
|
||||
content: content,
|
||||
count: reactionGroup.length,
|
||||
guests: mailDataHelpers.Store.many(
|
||||
guests,
|
||||
makeKwArgs({ fields: ["avatar_128", "name"] })
|
||||
),
|
||||
message: message_id,
|
||||
partners: mailDataHelpers.Store.many(
|
||||
partners,
|
||||
makeKwArgs({ fields: ["avatar_128", "name"] })
|
||||
),
|
||||
sequence: Math.min(reactionGroup.map((reaction) => reaction.id)),
|
||||
};
|
||||
store.add("MessageReactions", data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { fields, models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class MailMessageSubtype extends models.ServerModel {
|
||||
_name = "mail.message.subtype";
|
||||
|
||||
default = fields.Generic({ default: true });
|
||||
subtype_xmlid = fields.Char();
|
||||
|
||||
_records = [
|
||||
{
|
||||
default: false,
|
||||
internal: true,
|
||||
name: "Activities",
|
||||
sequence: 90,
|
||||
subtype_xmlid: "mail.mt_activities",
|
||||
},
|
||||
{
|
||||
default: false,
|
||||
internal: true,
|
||||
name: "Note",
|
||||
sequence: 100,
|
||||
subtype_xmlid: "mail.mt_note",
|
||||
track_recipients: true,
|
||||
},
|
||||
{
|
||||
name: "Discussions",
|
||||
sequence: 0,
|
||||
subtype_xmlid: "mail.mt_comment",
|
||||
track_recipients: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import { mailDataHelpers } from "@mail/../tests/mock_server/mail_mock_server";
|
||||
|
||||
import { makeKwArgs, models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class MailNotification extends models.ServerModel {
|
||||
_name = "mail.notification";
|
||||
|
||||
/** @param {number[]} ids */
|
||||
_filtered_for_web_client(ids) {
|
||||
/** @type {import("mock_models").MailMessage} */
|
||||
const MailMessage = this.env["mail.message"];
|
||||
/** @type {import("mock_models").MailMessageSubtype} */
|
||||
const MailMessageSubtype = this.env["mail.message.subtype"];
|
||||
/** @type {import("mock_models").ResPartner} */
|
||||
const ResPartner = this.env["res.partner"];
|
||||
|
||||
return this.browse(ids).filter((notification) => {
|
||||
const [partner] = ResPartner.browse(notification.res_partner_id);
|
||||
if (
|
||||
["bounce", "exception", "canceled"].includes(notification.notification_status) ||
|
||||
(partner && partner.partner_share)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const [message] = MailMessage.browse(notification.mail_message_id);
|
||||
const subtypes = message.subtype_id
|
||||
? MailMessageSubtype.browse(message.subtype_id)
|
||||
: [];
|
||||
return subtypes.length === 0 || subtypes[0].track_recipients;
|
||||
});
|
||||
}
|
||||
|
||||
get _to_store_defaults() {
|
||||
return [
|
||||
"failure_type",
|
||||
"mail_email_address",
|
||||
"mail_message_id",
|
||||
"notification_status",
|
||||
"notification_type",
|
||||
mailDataHelpers.Store.one("res_partner_id", makeKwArgs({ fields: ["name", "email"] })),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class MailPushDevice extends models.ServerModel {
|
||||
_name = "mail.push.device";
|
||||
get_web_push_vapid_public_key() {
|
||||
return "BPNWmvXxxCOd-QBNMZeF2pL0CAFcZebRRZJzco-s2C2oadl9kQU59hNJW4IscNmzs9L7q9ID9cLCzSIH1vZpqBY";
|
||||
}
|
||||
unregister_devices() {}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { mailDataHelpers } from "@mail/../tests/mock_server/mail_mock_server";
|
||||
import { fields, models, serverState } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class MailScheduledMessage extends models.ServerModel {
|
||||
_inherit = "mail.scheduled.message";
|
||||
|
||||
author_id = fields.Generic({ default: () => serverState.partnerId });
|
||||
|
||||
_to_store(store) {
|
||||
/** @type {import("mock_models").IrAttachment} */
|
||||
const IrAttachment = this.env["ir.attachment"];
|
||||
/** @type {import("mock_models").ResPartner} */
|
||||
const ResPartner = this.env["res.partner"];
|
||||
|
||||
for (const message of this) {
|
||||
store.add("mail.scheduled.message", {
|
||||
attachment_ids: mailDataHelpers.Store.many(
|
||||
IrAttachment.browse(message.attachment_ids)
|
||||
),
|
||||
author_id: mailDataHelpers.Store.one(ResPartner.browse(message.author_id)),
|
||||
body: ["markup", message.body],
|
||||
id: message.id,
|
||||
scheduled_date: message.scheduled_date,
|
||||
subject: message.subject,
|
||||
is_note: message.is_note,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class MailTemplate extends models.ServerModel {
|
||||
_name = "mail.template";
|
||||
}
|
||||
|
|
@ -0,0 +1,702 @@
|
|||
import { parseEmail } from "@mail/utils/common/format";
|
||||
import { mailDataHelpers } from "@mail/../tests/mock_server/mail_mock_server";
|
||||
|
||||
import {
|
||||
Command,
|
||||
getKwArgs,
|
||||
makeKwArgs,
|
||||
models,
|
||||
unmakeKwArgs,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class MailThread extends models.ServerModel {
|
||||
_name = "mail.thread";
|
||||
_inherit = ["base"];
|
||||
|
||||
/**
|
||||
* @param {number[]} ids
|
||||
* @param {number} [after]
|
||||
* @param {number} [limit=100]
|
||||
* @param {boolean} [filter_recipients]
|
||||
*/
|
||||
message_get_followers(ids, after, limit, filter_recipients) {
|
||||
const kwargs = getKwArgs(arguments, "ids", "after", "limit", "filter_recipients");
|
||||
ids = kwargs.ids;
|
||||
after = kwargs.after || 0;
|
||||
limit = kwargs.limit || 100;
|
||||
filter_recipients = kwargs.filter_recipients || false;
|
||||
/** @type {import("mock_models").MailThread} */
|
||||
const MailThread = this.env["mail.thread"];
|
||||
|
||||
const store = new mailDataHelpers.Store();
|
||||
MailThread._message_followers_to_store.call(
|
||||
this,
|
||||
ids,
|
||||
store,
|
||||
after,
|
||||
limit,
|
||||
filter_recipients
|
||||
);
|
||||
return store.get_result();
|
||||
}
|
||||
|
||||
_message_followers_to_store(ids, store, after, limit, filter_recipients, reset) {
|
||||
const kwargs = getKwArgs(
|
||||
arguments,
|
||||
"ids",
|
||||
"store",
|
||||
"after",
|
||||
"limit",
|
||||
"filter_recipients",
|
||||
"reset"
|
||||
);
|
||||
ids = kwargs.ids;
|
||||
store = kwargs.store;
|
||||
after = kwargs.after || 0;
|
||||
limit = kwargs.limit || 100;
|
||||
filter_recipients = kwargs.filter_recipients || false;
|
||||
reset = kwargs.reset || false;
|
||||
|
||||
/** @type {import("mock_models").MailFollowers} */
|
||||
const MailFollowers = this.env["mail.followers"];
|
||||
|
||||
const domain = [
|
||||
["res_id", "=", ids[0]],
|
||||
["res_model", "=", this._name],
|
||||
["partner_id", "!=", this.env.user.partner_id],
|
||||
];
|
||||
if (after) {
|
||||
domain.push(["id", ">", after]);
|
||||
}
|
||||
if (filter_recipients) {
|
||||
// not implemented for simplicity
|
||||
}
|
||||
const followers = MailFollowers._filter(domain).sort(
|
||||
(f1, f2) => (f1.id < f2.id ? -1 : 1) // sorted from lowest ID to highest ID (i.e. from oldest to youngest)
|
||||
);
|
||||
followers.length = Math.min(followers.length, limit);
|
||||
store.add(
|
||||
this.browse(ids[0]),
|
||||
{
|
||||
[filter_recipients ? "recipients" : "followers"]: mailDataHelpers.Store.many(
|
||||
followers,
|
||||
makeKwArgs({ mode: reset ? "REPLACE" : "ADD" })
|
||||
),
|
||||
},
|
||||
makeKwArgs({ as_thread: true })
|
||||
);
|
||||
}
|
||||
|
||||
/** @param {number[]} ids */
|
||||
message_post(ids) {
|
||||
const kwargs = getKwArgs(arguments, "ids", "subtype_id", "tracking_value_ids");
|
||||
ids = kwargs.ids;
|
||||
delete kwargs.ids;
|
||||
|
||||
/** @type {import("mock_models").IrAttachment} */
|
||||
const IrAttachment = this.env["ir.attachment"];
|
||||
/** @type {import("mock_models").MailGuest} */
|
||||
const MailGuest = this.env["mail.guest"];
|
||||
/** @type {import("mock_models").MailMessage} */
|
||||
const MailMessage = this.env["mail.message"];
|
||||
/** @type {import("mock_models").MailNotification} */
|
||||
const MailNotification = this.env["mail.notification"];
|
||||
/** @type {import("mock_models").MailThread} */
|
||||
const MailThread = this.env["mail.thread"];
|
||||
/** @type {import("mock_models").ResUsers} */
|
||||
const ResUsers = this.env["res.users"];
|
||||
/** @type {import("mock_models").MailMessageSubtype} */
|
||||
const MailMessageSubtype = this.env["mail.message.subtype"];
|
||||
|
||||
const id = ids[0]; // ensure_one
|
||||
if (kwargs.context?.mail_post_autofollow && kwargs.partner_ids?.length) {
|
||||
MailThread.message_subscribe.call(this, ids, kwargs.partner_ids, []);
|
||||
}
|
||||
if (kwargs.attachment_ids) {
|
||||
const attachments = IrAttachment._filter([
|
||||
["id", "in", kwargs.attachment_ids],
|
||||
["res_model", "=", "mail.compose.message"],
|
||||
["res_id", "=", false],
|
||||
]);
|
||||
const attachmentIds = attachments.map((attachment) => attachment.id);
|
||||
IrAttachment.write(attachmentIds, {
|
||||
res_id: id,
|
||||
res_model: this._name,
|
||||
});
|
||||
kwargs.attachment_ids = attachmentIds.map((attachmentId) => Command.link(attachmentId));
|
||||
}
|
||||
let author_id;
|
||||
let email_from;
|
||||
const author_guest_id =
|
||||
ResUsers._is_public(this.env.uid) && MailGuest._get_guest_from_context()?.id;
|
||||
if (!author_guest_id) {
|
||||
[author_id, email_from] = MailThread._message_compute_author.call(
|
||||
this,
|
||||
kwargs.author_id,
|
||||
kwargs.email_from
|
||||
);
|
||||
}
|
||||
email_from ||= false;
|
||||
const message_type = kwargs.message_type || "notification";
|
||||
const values = unmakeKwArgs({
|
||||
...kwargs,
|
||||
author_id,
|
||||
author_guest_id,
|
||||
email_from,
|
||||
message_type,
|
||||
subtype_id: MailMessageSubtype._filter([
|
||||
["subtype_xmlid", "=", kwargs.subtype_xmlid || "mail.mt_note"],
|
||||
])[0]?.id,
|
||||
model: this._name,
|
||||
res_id: id,
|
||||
});
|
||||
delete values.context;
|
||||
delete values.subtype_xmlid;
|
||||
const messageId = MailMessage.create(values);
|
||||
for (const partnerId of kwargs.partner_ids || []) {
|
||||
MailNotification.create({
|
||||
mail_message_id: messageId,
|
||||
notification_type: "inbox",
|
||||
res_partner_id: partnerId,
|
||||
});
|
||||
}
|
||||
MailThread._notify_thread.call(this, ids, messageId, kwargs.context?.temporary_id);
|
||||
return [messageId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} ids
|
||||
* @param {number[]} partner_ids
|
||||
* @param {number[]} subtype_ids
|
||||
*/
|
||||
message_subscribe(ids, partner_ids, subtype_ids) {
|
||||
const kwargs = getKwArgs(arguments, "ids", "partner_ids", "subtype_ids");
|
||||
ids = kwargs.ids;
|
||||
delete kwargs.ids;
|
||||
partner_ids = kwargs.partner_ids || [];
|
||||
subtype_ids = kwargs.subtype_ids || [];
|
||||
|
||||
/** @type {import("mock_models").MailFollowers} */
|
||||
const MailFollowers = this.env["mail.followers"];
|
||||
/** @type {import("mock_models").MailMessageSubtype} */
|
||||
const MailMessageSubtype = this.env["mail.message.subtype"];
|
||||
|
||||
for (const id of ids) {
|
||||
for (const partner_id of partner_ids) {
|
||||
let followerId = MailFollowers.search([["partner_id", "=", partner_id]])[0];
|
||||
if (!followerId) {
|
||||
if (!subtype_ids?.length) {
|
||||
subtype_ids = MailMessageSubtype.search([
|
||||
["default", "=", true],
|
||||
"|",
|
||||
["res_model", "=", this._name],
|
||||
["res_model", "=", false],
|
||||
]);
|
||||
}
|
||||
followerId = MailFollowers.create({
|
||||
is_active: true,
|
||||
partner_id,
|
||||
res_id: id,
|
||||
res_model: this._name,
|
||||
subtype_ids: subtype_ids,
|
||||
});
|
||||
}
|
||||
this.env[this._name].write(ids, {
|
||||
message_follower_ids: [Command.link(followerId)],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} ids
|
||||
* @param {number[]} partner_ids
|
||||
*/
|
||||
message_unsubscribe(ids, partner_ids) {
|
||||
const kwargs = getKwArgs(arguments, "ids", "partner_ids");
|
||||
ids = kwargs.ids;
|
||||
delete kwargs.ids;
|
||||
partner_ids = kwargs.partner_ids || [];
|
||||
|
||||
/** @type {import("mock_models").MailFollowers} */
|
||||
const MailFollowers = this.env["mail.followers"];
|
||||
|
||||
if (!partner_ids.length) {
|
||||
return true;
|
||||
}
|
||||
const followers = MailFollowers.search([
|
||||
["res_model", "=", this._name],
|
||||
["res_id", "in", ids],
|
||||
["partner_id", "in", partner_ids],
|
||||
]);
|
||||
MailFollowers.unlink(followers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that this method is overridden by snailmail module but not simulated here.
|
||||
*
|
||||
* @param {string} notification_type
|
||||
*/
|
||||
notify_cancel_by_type(notification_type) {
|
||||
const kwargs = getKwArgs(arguments, "notification_type");
|
||||
notification_type = kwargs.notification_type;
|
||||
|
||||
/** @type {import("mock_models").BusBus} */
|
||||
const BusBus = this.env["bus.bus"];
|
||||
/** @type {import("mock_models").MailMessage} */
|
||||
const MailMessage = this.env["mail.message"];
|
||||
/** @type {import("mock_models").MailNotification} */
|
||||
const MailNotification = this.env["mail.notification"];
|
||||
/** @type {import("mock_models").ResPartner} */
|
||||
const ResPartner = this.env["res.partner"];
|
||||
|
||||
// Query matching notifications
|
||||
const notifications = MailNotification._filter([
|
||||
["notification_type", "=", notification_type],
|
||||
["notification_status", "in", ["bounce", "exception"]],
|
||||
]).filter((notification) => {
|
||||
const [message] = MailMessage.browse(notification.mail_message_id);
|
||||
return message.model === this._name && message.author_id === this.env.user.partner_id;
|
||||
});
|
||||
// Update notification status
|
||||
MailNotification.write(
|
||||
notifications.map((notification) => notification.id),
|
||||
{ notification_status: "canceled" }
|
||||
);
|
||||
// Send bus notifications to update status of notifications in the web client
|
||||
const [partner] = ResPartner.read(this.env.user.partner_id);
|
||||
const store = new mailDataHelpers.Store();
|
||||
MailMessage._message_notifications_to_store(
|
||||
notifications.map((notification) => notification.mail_message_id),
|
||||
store
|
||||
);
|
||||
BusBus._sendone(partner, "mail.record/insert", store.get_result());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
* @param {Object} result
|
||||
* @param {number} partner
|
||||
* @param {string} email
|
||||
* @param {string} lang
|
||||
* @param {string} reason
|
||||
* @param {string} name
|
||||
*/
|
||||
_message_add_suggested_recipient(id, result, partner, email, lang, reason = "", name) {
|
||||
const kwargs = getKwArgs(
|
||||
arguments,
|
||||
"id",
|
||||
"result",
|
||||
"partner",
|
||||
"email",
|
||||
"lang",
|
||||
"reason",
|
||||
"name"
|
||||
);
|
||||
id = kwargs.id;
|
||||
delete kwargs.id;
|
||||
result = kwargs.result;
|
||||
partner = kwargs.partner;
|
||||
email = kwargs.email;
|
||||
lang = kwargs.lang;
|
||||
reason = kwargs.reason;
|
||||
name = kwargs.name;
|
||||
|
||||
/** @type {import("mock_models").ResPartner} */
|
||||
const ResPartner = this.env["res.partner"];
|
||||
|
||||
if (email !== undefined && partner === undefined) {
|
||||
const partnerInfo = parseEmail(email);
|
||||
partner = ResPartner._filter([["email", "=", partnerInfo[1]]])[0];
|
||||
}
|
||||
if (partner) {
|
||||
result.push({
|
||||
partner_id: partner.id,
|
||||
name: partner.display_name,
|
||||
email: partner.email,
|
||||
lang,
|
||||
reason,
|
||||
create_values: {},
|
||||
});
|
||||
} else {
|
||||
const partnerCreateValues = this._get_customer_information(id);
|
||||
result.push({
|
||||
email,
|
||||
name,
|
||||
lang,
|
||||
reason,
|
||||
create_values: partnerCreateValues,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
_get_customer_information(id) {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} [author_id]
|
||||
* @param {string} [email_from]
|
||||
*/
|
||||
_message_compute_author(author_id, email_from) {
|
||||
const kwargs = getKwArgs(arguments, "author_id", "email_from");
|
||||
author_id = kwargs.author_id;
|
||||
email_from = kwargs.email_from;
|
||||
|
||||
/** @type {import("mock_models").ResPartner} */
|
||||
const ResPartner = this.env["res.partner"];
|
||||
|
||||
if (!author_id) {
|
||||
// For simplicity partner is not guessed from email_from here, but
|
||||
// that would be the first step on the server.
|
||||
const [author] = ResPartner.browse(this.env.user.partner_id);
|
||||
author_id = author.id;
|
||||
email_from = `${author.display_name} <${author.email}>`;
|
||||
}
|
||||
if (!email_from && author_id) {
|
||||
const [author] = ResPartner.browse(author_id);
|
||||
email_from = `${author.display_name} <${author.email}>`;
|
||||
}
|
||||
if (email_from === undefined) {
|
||||
if (author_id) {
|
||||
const [author] = ResPartner.browse(author_id);
|
||||
email_from = `${author.display_name} <${author.email}>`;
|
||||
}
|
||||
}
|
||||
if (!email_from) {
|
||||
throw Error("Unable to log message due to missing author email.");
|
||||
}
|
||||
return [author_id, email_from];
|
||||
}
|
||||
|
||||
/** @param {number[]} ids */
|
||||
_message_compute_subject(ids) {
|
||||
const records = this.browse(ids);
|
||||
return new Map(records.map((record) => [record.id, record.name || ""]));
|
||||
}
|
||||
|
||||
/** @param {number[]} ids */
|
||||
_message_get_suggested_recipients(ids, additional_partners = [], primary_email = false) {
|
||||
/** @type {import("mock_models").MailThread} */
|
||||
const MailThread = this.env["mail.thread"];
|
||||
/** @type {import("mock_models").ResFake} */
|
||||
const ResFake = this.env["res.fake"];
|
||||
/** @type {import("mock_models").ResPartner} */
|
||||
const ResPartner = this.env["res.partner"];
|
||||
/** @type {import("mock_models").ResUsers} */
|
||||
const ResUsers = this.env["res.users"];
|
||||
|
||||
if (this._name === "res.fake") {
|
||||
return ResFake._message_get_suggested_recipients(
|
||||
ids,
|
||||
additional_partners,
|
||||
primary_email
|
||||
);
|
||||
}
|
||||
const result = ids.reduce((result, id) => (result[id] = []), {});
|
||||
const model = this.env[this._name];
|
||||
for (const record in model.browse(ids)) {
|
||||
if (record.user_id) {
|
||||
const user = ResUsers.browse(record.user_id);
|
||||
if (user.partner_id) {
|
||||
const reason = model._fields["user_id"].string;
|
||||
const partner = ResPartner.browse(user.partner_id);
|
||||
MailThread._message_add_suggested_recipient.call(
|
||||
this,
|
||||
result,
|
||||
makeKwArgs({
|
||||
email: partner.email,
|
||||
partner: user.partner_id,
|
||||
reason,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified version that sends notification to author and channel.
|
||||
*
|
||||
* @param {number[]} ids
|
||||
* @param {number} message_id
|
||||
* @param {number} [temporary_id]
|
||||
*/
|
||||
_notify_thread(ids, message_id, temporary_id) {
|
||||
const kwargs = getKwArgs(arguments, "ids", "message_id", "temporary_id");
|
||||
ids = kwargs.ids;
|
||||
delete kwargs.ids;
|
||||
message_id = kwargs.message_id;
|
||||
temporary_id = kwargs.temporary_id;
|
||||
|
||||
/** @type {import("mock_models").BusBus} */
|
||||
const BusBus = this.env["bus.bus"];
|
||||
/** @type {import("mock_models").DiscussChannel} */
|
||||
const DiscussChannel = this.env["discuss.channel"];
|
||||
/** @type {import("mock_models").MailMessage} */
|
||||
const MailMessage = this.env["mail.message"];
|
||||
/** @type {import("mock_models").ResPartner} */
|
||||
const ResPartner = this.env["res.partner"];
|
||||
/** @type {import("mock_models").ResUsers} */
|
||||
const ResUsers = this.env["res.users"];
|
||||
|
||||
const [message] = MailMessage.browse(message_id);
|
||||
const notifications = [];
|
||||
if (this._name === "discuss.channel") {
|
||||
// members
|
||||
const channels = DiscussChannel.browse(message.res_id);
|
||||
for (const channel of channels) {
|
||||
notifications.push([
|
||||
channel,
|
||||
"discuss.channel/new_message",
|
||||
{
|
||||
data: new mailDataHelpers.Store(
|
||||
MailMessage.browse(message_id)
|
||||
).get_result(),
|
||||
id: channel.id,
|
||||
temporary_id,
|
||||
},
|
||||
]);
|
||||
const memberOfCurrentUser = this._find_or_create_member_for_self(ids[0]);
|
||||
if (memberOfCurrentUser) {
|
||||
this.env["discuss.channel.member"]._set_last_seen_message(
|
||||
[memberOfCurrentUser.id],
|
||||
message.id,
|
||||
false
|
||||
);
|
||||
this.env["discuss.channel.member"]._set_new_message_separator(
|
||||
[memberOfCurrentUser.id],
|
||||
message.id + 1,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (message.partner_ids) {
|
||||
for (const partner_id of message.partner_ids) {
|
||||
const [partner] = ResPartner.search_read([["id", "=", partner_id]]);
|
||||
if (partner.user_ids.length > 0) {
|
||||
const [user] = ResUsers.search_read([["id", "=", partner.user_ids[0]]]);
|
||||
if (user.notification_type === "inbox") {
|
||||
notifications.push([
|
||||
partner,
|
||||
"mail.message/inbox",
|
||||
{
|
||||
message_id: message.id,
|
||||
store_data: new mailDataHelpers.Store(
|
||||
MailMessage.browse(message.id),
|
||||
makeKwArgs({ for_current_user: true, add_followers: true })
|
||||
).get_result(),
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BusBus._sendmany(notifications);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} fields_iter
|
||||
* @param {Object} initial_values_dict
|
||||
*/
|
||||
_message_track(fields_iter, initial_values_dict) {
|
||||
const kwargs = getKwArgs(arguments, "fields_iter", "initial_values_dict");
|
||||
fields_iter = kwargs.fields_iter;
|
||||
initial_values_dict = kwargs.initial_values_dict;
|
||||
|
||||
/** @type {import("mock_models").Base} */
|
||||
const Base = this.env["base"];
|
||||
/** @type {import("mock_models").MailThread} */
|
||||
const MailThread = this.env["mail.thread"];
|
||||
|
||||
const trackFieldNamesToField = this.env[this._name].fields_get(fields_iter);
|
||||
const tracking = {};
|
||||
const model = this.env[this._name];
|
||||
for (const record of model) {
|
||||
tracking[record.id] = Base._mail_track.call(
|
||||
this,
|
||||
trackFieldNamesToField,
|
||||
initial_values_dict[record.id],
|
||||
record
|
||||
);
|
||||
}
|
||||
for (const record of model) {
|
||||
const { trackingValueIds, changedFieldNames } = tracking[record.id] || {};
|
||||
if (!changedFieldNames || !changedFieldNames.length) {
|
||||
continue;
|
||||
}
|
||||
const changedFieldsInitialValues = {};
|
||||
const initialFieldValues = initial_values_dict[record.id];
|
||||
for (const fname in changedFieldNames) {
|
||||
changedFieldsInitialValues[fname] = initialFieldValues[fname];
|
||||
}
|
||||
const subtype = MailThread._track_subtype.call(this, changedFieldsInitialValues);
|
||||
MailThread.message_post.call(this, [record.id], subtype.id, trackingValueIds);
|
||||
}
|
||||
return tracking;
|
||||
}
|
||||
|
||||
/** @param {Object} initial_values */
|
||||
_track_finalize(initial_values) {
|
||||
const kwargs = getKwArgs(arguments, "initial_values");
|
||||
initial_values = kwargs.initial_values;
|
||||
|
||||
/** @type {import("mock_models").MailThread} */
|
||||
const MailThread = this.env["mail.thread"];
|
||||
|
||||
MailThread._message_track.call(
|
||||
this,
|
||||
MailThread._track_get_fields.call(this),
|
||||
initial_values
|
||||
);
|
||||
}
|
||||
|
||||
_track_get_fields() {
|
||||
return Object.entries(this.env[this._name]._fields).reduce((prev, next) => {
|
||||
if (next[1].tracking) {
|
||||
prev.push(next[0]);
|
||||
}
|
||||
return prev;
|
||||
}, []);
|
||||
}
|
||||
|
||||
_track_prepare() {
|
||||
/** @type {import("mock_models").MailThread} */
|
||||
const MailThread = this.env["mail.thread"];
|
||||
|
||||
const trackedFieldNames = MailThread._track_get_fields.call(this);
|
||||
if (!trackedFieldNames.length) {
|
||||
return;
|
||||
}
|
||||
const initialTrackedFieldValuesByRecordId = {};
|
||||
for (const record of this.env[this._name]) {
|
||||
const values = {};
|
||||
initialTrackedFieldValuesByRecordId[record.id] = values;
|
||||
for (const fname of trackedFieldNames) {
|
||||
values[fname] = record[fname];
|
||||
}
|
||||
}
|
||||
return initialTrackedFieldValuesByRecordId;
|
||||
}
|
||||
|
||||
/** @param {Object} initial_values */
|
||||
_track_subtype(initial_values) {
|
||||
return false;
|
||||
}
|
||||
|
||||
_thread_to_store(store, fields, request_list) {
|
||||
const kwargs = getKwArgs(arguments, "store", "fields", "request_list");
|
||||
store = kwargs.store;
|
||||
fields = kwargs.fields;
|
||||
request_list = kwargs.request_list || [];
|
||||
|
||||
/** @type {import("mock_models").IrAttachment} */
|
||||
const IrAttachment = this.env["ir.attachment"];
|
||||
/** @type {import("mock_models").MailActivity} */
|
||||
const MailActivity = this.env["mail.activity"];
|
||||
/** @type {import("mock_models").MailFollowers} */
|
||||
const MailFollowers = this.env["mail.followers"];
|
||||
/** @type {import("mock_models").MailThread} */
|
||||
const MailThread = this.env["mail.thread"];
|
||||
/** @type {import("mock_models").MailScheduledMessage} */
|
||||
const MailScheduledMessage = this.env["mail.scheduled.message"];
|
||||
|
||||
if (!fields) {
|
||||
fields = [];
|
||||
}
|
||||
const thread = this[0];
|
||||
store._add_record_fields(this.env[this._name].browse(thread.id), fields, true);
|
||||
const res = {};
|
||||
if (request_list) {
|
||||
res.hasReadAccess = true;
|
||||
res.hasWriteAccess = thread.hasWriteAccess ?? true; // mimic user with write access by default
|
||||
res["canPostOnReadonly"] = this._mail_post_access === "read";
|
||||
}
|
||||
const model = this.env[this._name];
|
||||
|
||||
if (request_list.includes("activities") && model.has_activities) {
|
||||
res["activities"] = mailDataHelpers.Store.many(
|
||||
MailActivity.browse(thread.activity_ids)
|
||||
);
|
||||
}
|
||||
if (request_list.includes("attachments")) {
|
||||
res["attachments"] = mailDataHelpers.Store.many(
|
||||
IrAttachment._filter([
|
||||
["res_id", "=", thread.id],
|
||||
["res_model", "=", this._name],
|
||||
]).sort((a1, a2) => a1.id - a2.id)
|
||||
);
|
||||
res["areAttachmentsLoaded"] = true;
|
||||
res["isLoadingAttachments"] = false;
|
||||
// Specific implementation of mail.thread.main.attachment
|
||||
if (this.env[this._name]._fields.message_main_attachment_id) {
|
||||
res["message_main_attachment_id"] = mailDataHelpers.Store.one(
|
||||
IrAttachment.browse(thread.message_main_attachment_id),
|
||||
makeKwArgs({ only_id: true })
|
||||
);
|
||||
}
|
||||
}
|
||||
if (request_list.includes("contact_fields")) {
|
||||
res.primary_email_field = this.env[this._name]._primary_email;
|
||||
res.partner_fields = this.env[this._name]._mail_get_partner_fields?.();
|
||||
}
|
||||
if (request_list.includes("display_name")) {
|
||||
res.display_name = thread.display_name;
|
||||
}
|
||||
if (fields.includes("display_name")) {
|
||||
res.name = thread.display_name ?? thread.name;
|
||||
}
|
||||
if (request_list.includes("followers")) {
|
||||
res["followersCount"] = this.env["mail.followers"].search_count([
|
||||
["res_id", "=", thread.id],
|
||||
["res_model", "=", this._name],
|
||||
]);
|
||||
res["selfFollower"] = mailDataHelpers.Store.one(
|
||||
MailFollowers.browse(
|
||||
MailFollowers.search([
|
||||
["res_id", "=", thread.id],
|
||||
["res_model", "=", this._name],
|
||||
["partner_id", "=", this.env.user.partner_id],
|
||||
])
|
||||
)
|
||||
);
|
||||
MailThread._message_followers_to_store.call(
|
||||
this,
|
||||
[thread.id],
|
||||
store,
|
||||
makeKwArgs({ reset: true })
|
||||
);
|
||||
res["recipientsCount"] = this.env["mail.followers"].search_count([
|
||||
["res_id", "=", thread.id],
|
||||
["res_model", "=", this._name],
|
||||
["partner_id", "!=", this.env.user.partner_id],
|
||||
// subtype and partner active checks not done here for simplicity
|
||||
]);
|
||||
MailThread._message_followers_to_store.call(
|
||||
this,
|
||||
[thread.id],
|
||||
store,
|
||||
makeKwArgs({ filter_recipients: true, reset: true })
|
||||
);
|
||||
}
|
||||
if (fields.includes("modelName")) {
|
||||
res.modelName = this._description;
|
||||
}
|
||||
if (request_list.includes("suggestedRecipients")) {
|
||||
res["suggestedRecipients"] = MailThread._message_get_suggested_recipients.call(this, [
|
||||
thread.id,
|
||||
]);
|
||||
}
|
||||
if (request_list.includes("scheduledMessages")) {
|
||||
res["scheduledMessages"] = mailDataHelpers.Store.many(
|
||||
MailScheduledMessage.filter(
|
||||
(message) => message.model === this._name && message.res_id === thread.id
|
||||
)
|
||||
);
|
||||
}
|
||||
store._add_record_fields(this.env[this._name].browse(thread.id), res, true);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
import { getKwArgs, models } from "@web/../tests/web_test_helpers";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { capitalize } from "@web/core/utils/strings";
|
||||
|
||||
patch(models.ServerModel.prototype, {
|
||||
/**
|
||||
* @override
|
||||
* @type {typeof models.Model["prototype"]["write"]}
|
||||
*/
|
||||
write() {
|
||||
/** @type {import("mock_models").MailThread} */
|
||||
const MailThread = this.env["mail.thread"];
|
||||
|
||||
const initialTrackedFieldValuesByRecordId = MailThread._track_prepare.call(this);
|
||||
const result = super.write(...arguments);
|
||||
if (initialTrackedFieldValuesByRecordId) {
|
||||
MailThread._track_finalize.call(this, initialTrackedFieldValuesByRecordId);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
});
|
||||
/**
|
||||
* @typedef {import("@web/../tests/web_test_helpers").ModelRecord} ModelRecord
|
||||
*/
|
||||
|
||||
export class MailTrackingValue extends models.ServerModel {
|
||||
_name = "mail.tracking.value";
|
||||
|
||||
/**
|
||||
* @param {ModelRecord} initial_value
|
||||
* @param {ModelRecord} new_value
|
||||
* @param {string} col_name
|
||||
* @param {Object} col_info
|
||||
* @param {models.ServerModel} record
|
||||
*/
|
||||
_create_tracking_values(initial_value, new_value, col_name, col_info, record) {
|
||||
const kwargs = getKwArgs(
|
||||
arguments,
|
||||
"initial_value",
|
||||
"new_value",
|
||||
"col_name",
|
||||
"col_info",
|
||||
"record"
|
||||
);
|
||||
initial_value = kwargs.initial_value;
|
||||
new_value = kwargs.new_value;
|
||||
col_name = kwargs.col_name;
|
||||
col_info = kwargs.col_info;
|
||||
record = kwargs.record;
|
||||
|
||||
/** @type {import("mock_models").IrModelFields} */
|
||||
const IrModelFields = this.env["ir.model.fields"];
|
||||
|
||||
let isTracked = true;
|
||||
const irField = IrModelFields.find(
|
||||
(field) => field.model === record._name && field.name === col_name
|
||||
);
|
||||
if (!irField) {
|
||||
return;
|
||||
}
|
||||
const values = { field_id: irField.id };
|
||||
switch (irField.ttype) {
|
||||
case "char":
|
||||
case "datetime":
|
||||
case "float":
|
||||
case "integer":
|
||||
case "text":
|
||||
values[`old_value_${irField.ttype}`] = initial_value;
|
||||
values[`new_value_${irField.ttype}`] = new_value;
|
||||
break;
|
||||
case "date":
|
||||
values["old_value_datetime"] = initial_value;
|
||||
values["new_value_datetime"] = new_value;
|
||||
break;
|
||||
case "boolean":
|
||||
values["old_value_integer"] = initial_value ? 1 : 0;
|
||||
values["new_value_integer"] = new_value ? 1 : 0;
|
||||
break;
|
||||
case "monetary": {
|
||||
values["old_value_float"] = initial_value;
|
||||
values["new_value_float"] = new_value;
|
||||
let currencyField = col_info.currency_field;
|
||||
// see get_currency_field in python fields
|
||||
if (!currencyField && "currency_id" in record._fields) {
|
||||
currencyField = "currency_id";
|
||||
}
|
||||
values[`currency_id`] = record[0][currencyField];
|
||||
break;
|
||||
}
|
||||
case "selection":
|
||||
values["old_value_char"] = initial_value;
|
||||
values["new_value_char"] = new_value;
|
||||
break;
|
||||
case "many2one":
|
||||
initial_value = initial_value
|
||||
? this.env[col_info.relation].search_read([["id", "=", initial_value]])[0]
|
||||
: initial_value;
|
||||
new_value = new_value
|
||||
? this.env[col_info.relation].search_read([["id", "=", new_value]])[0]
|
||||
: new_value;
|
||||
values["old_value_integer"] = initial_value ? initial_value.id : 0;
|
||||
values["new_value_integer"] = new_value ? new_value.id : 0;
|
||||
values["old_value_char"] = initial_value ? initial_value.display_name : "";
|
||||
values["new_value_char"] = new_value ? new_value.display_name : "";
|
||||
break;
|
||||
default:
|
||||
isTracked = false;
|
||||
}
|
||||
if (isTracked) {
|
||||
return this.create(values);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @param {ModelRecord[]} trackingValues */
|
||||
_tracking_value_format(trackingValues) {
|
||||
/** @type {import("mock_models").IrModelFields} */
|
||||
const IrModelFields = this.env["ir.model.fields"];
|
||||
|
||||
return trackingValues.map((tracking) => {
|
||||
const irField = IrModelFields.find((field) => field.id === tracking.field_id);
|
||||
return {
|
||||
id: tracking.id,
|
||||
fieldInfo: {
|
||||
changedField: capitalize(irField.ttype),
|
||||
currencyId: tracking.currency_id,
|
||||
fieldType: irField.ttype,
|
||||
floatPrecision: this.env[irField.model]._fields[irField.name].digits,
|
||||
},
|
||||
newValue: this._format_display_value(tracking, "new"),
|
||||
oldValue: this._format_display_value(tracking, "old"),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ModelRecord} record
|
||||
* @param {"new" | "old"} field_type
|
||||
*/
|
||||
_format_display_value(record, field_type) {
|
||||
const kwargs = getKwArgs(arguments, "record", "field_type");
|
||||
record = kwargs.record;
|
||||
field_type = kwargs.field_type;
|
||||
|
||||
/** @type {import("mock_models").IrModelFields} */
|
||||
const IrModelFields = this.env["ir.model.fields"];
|
||||
|
||||
const irField = IrModelFields.find((field) => field.id === record.field_id);
|
||||
switch (irField.ttype) {
|
||||
case "float":
|
||||
case "integer":
|
||||
case "text":
|
||||
return record[`${field_type}_value_${irField.ttype}`];
|
||||
case "datetime":
|
||||
if (record[`${field_type}_value_datetime`]) {
|
||||
const datetime = record[`${field_type}_value_datetime`];
|
||||
return `${datetime}Z`;
|
||||
} else {
|
||||
return record[`${field_type}_value_datetime`];
|
||||
}
|
||||
case "date":
|
||||
if (record[`${field_type}_value_datetime`]) {
|
||||
return record[`${field_type}_value_datetime`];
|
||||
} else {
|
||||
return record[`${field_type}_value_datetime`];
|
||||
}
|
||||
case "boolean":
|
||||
return !!record[`${field_type}_value_integer`];
|
||||
case "monetary":
|
||||
return record[`${field_type}_value_float`];
|
||||
default:
|
||||
return record[`${field_type}_value_char`];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { webModels } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ResCountry extends webModels.ResCountry {
|
||||
get _to_store_defaults() {
|
||||
return ["code"];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
import { parseEmail } from "@mail/utils/common/format";
|
||||
import { fields, makeKwArgs, models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ResFake extends models.Model {
|
||||
_name = "res.fake";
|
||||
_primary_email = "email_cc";
|
||||
|
||||
_views = {
|
||||
form: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
<field name="partner_id" />
|
||||
<field name="email_cc" />
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>`,
|
||||
};
|
||||
|
||||
name = fields.Char({ string: "Name" });
|
||||
activity_ids = fields.One2many({ relation: "mail.activity", string: "Activities" });
|
||||
email_from = fields.Char({ string: "Email" });
|
||||
email_cc = fields.Char();
|
||||
message_ids = fields.One2many({ relation: "mail.message" });
|
||||
message_follower_ids = fields.Many2many({ relation: "mail.followers", string: "Followers" });
|
||||
partner_ids = fields.One2many({ relation: "res.partner", string: "Related partners" });
|
||||
phone = fields.Char({ string: "Phone number" });
|
||||
partner_id = fields.Many2one({ relation: "res.partner", string: "contact partner" });
|
||||
|
||||
_mail_get_partner_fields() {
|
||||
return ["partner_id"];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {integer[]} ids
|
||||
* @returns {Object}
|
||||
*/
|
||||
_get_customer_information(ids) {
|
||||
const record = this.browse(ids)[0];
|
||||
if (!record.email_cc) {
|
||||
return;
|
||||
}
|
||||
const [name, email] = parseEmail(record.email_cc);
|
||||
return {
|
||||
name,
|
||||
email,
|
||||
phone: record.phone,
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {number[]} ids */
|
||||
_message_get_suggested_recipients(ids, additional_partners = [], primary_email = false) {
|
||||
/** @type {import("mock_models").MailThread} */
|
||||
const MailThread = this.env["mail.thread"];
|
||||
/** @type {import("mock_models").ResPartner} */
|
||||
const ResPartner = this.env["res.partner"];
|
||||
|
||||
const result = [];
|
||||
const records = this.browse(ids);
|
||||
for (const id of ids) {
|
||||
const record = records.find((record) => record.id === id);
|
||||
if (record.email_cc) {
|
||||
MailThread._message_add_suggested_recipient.call(
|
||||
this,
|
||||
id,
|
||||
result,
|
||||
makeKwArgs({
|
||||
name: record.email_cc,
|
||||
email: record.email_cc,
|
||||
partner: undefined,
|
||||
reason: "CC email",
|
||||
})
|
||||
);
|
||||
}
|
||||
if (primary_email) {
|
||||
MailThread._message_add_suggested_recipient.call(
|
||||
this,
|
||||
id,
|
||||
result,
|
||||
makeKwArgs({
|
||||
name: primary_email,
|
||||
email: primary_email,
|
||||
partner: undefined,
|
||||
reason: "CC email",
|
||||
})
|
||||
);
|
||||
}
|
||||
const partners = ResPartner.browse(record.partner_ids);
|
||||
if (partners.length) {
|
||||
for (const partner of partners) {
|
||||
MailThread._message_add_suggested_recipient.call(
|
||||
this,
|
||||
id,
|
||||
result,
|
||||
makeKwArgs({
|
||||
email: partner.email,
|
||||
partner,
|
||||
reason: "Email partner",
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
const partner_id = additional_partners.length ? additional_partners : record.partner_id;
|
||||
const [partner] = ResPartner.browse(partner_id);
|
||||
if (partner) {
|
||||
MailThread._message_add_suggested_recipient.call(
|
||||
this,
|
||||
id,
|
||||
result,
|
||||
makeKwArgs({
|
||||
email: partner.email,
|
||||
partner,
|
||||
reason: "contact partner",
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** @param {number[]} ids */
|
||||
_message_compute_subject(ids) {
|
||||
return new Map(ids.map((id) => [id, "Custom Default Subject"]));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ResLang extends models.ServerModel {
|
||||
_name = "res.lang";
|
||||
|
||||
get _to_store_defaults() {
|
||||
return ["name"];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,453 @@
|
|||
import { mailDataHelpers } from "@mail/../tests/mock_server/mail_mock_server";
|
||||
import {
|
||||
fields,
|
||||
getKwArgs,
|
||||
makeKwArgs,
|
||||
serverState,
|
||||
webModels,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
/** @typedef {import("@web/../tests/web_test_helpers").ModelRecord} ModelRecord */
|
||||
|
||||
export class ResPartner extends webModels.ResPartner {
|
||||
_inherit = ["mail.thread"];
|
||||
|
||||
description = fields.Char({ string: "Description" });
|
||||
hasWriteAccess = fields.Boolean({ default: true });
|
||||
message_main_attachment_id = fields.Many2one({
|
||||
relation: "ir.attachment",
|
||||
string: "Main attachment",
|
||||
});
|
||||
is_in_call = fields.Boolean({ compute: "_compute_is_in_call" });
|
||||
|
||||
_views = {
|
||||
form: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>`,
|
||||
};
|
||||
|
||||
_compute_is_in_call() {
|
||||
for (const partner of this) {
|
||||
partner.is_in_call =
|
||||
this.env["discuss.channel.member"].search([
|
||||
["rtc_session_ids", "!=", []],
|
||||
["partner_id", "=", partner.id],
|
||||
]).length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [search]
|
||||
* @param {number} [limit]
|
||||
*/
|
||||
get_mention_suggestions(search, limit = 8) {
|
||||
const kwargs = getKwArgs(arguments, "search", "limit");
|
||||
search = kwargs.search || "";
|
||||
limit = kwargs.limit || 8;
|
||||
|
||||
/** @type {import("mock_models").ResUsers} */
|
||||
const ResUsers = this.env["res.users"];
|
||||
|
||||
search = search.toLowerCase();
|
||||
/**
|
||||
* Returns the given list of partners after filtering it according to
|
||||
* the logic of the Python method `get_mention_suggestions` for the
|
||||
* given search term. The result is truncated to the given limit and
|
||||
* formatted as expected by the original method.
|
||||
*
|
||||
* @param {ModelRecord[]} partners
|
||||
* @param {string} search
|
||||
* @param {number} limit
|
||||
*/
|
||||
const mentionSuggestionsFilter = (partners, search, limit) => {
|
||||
const matchingPartnerIds = partners
|
||||
.filter((partner) => {
|
||||
// no search term is considered as return all
|
||||
if (!search) {
|
||||
return true;
|
||||
}
|
||||
// otherwise name or email must match search term
|
||||
if (partner.name && partner.name.toLowerCase().includes(search)) {
|
||||
return true;
|
||||
}
|
||||
if (partner.email && partner.email.toLowerCase().includes(search)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.map((partner) => partner.id);
|
||||
// reduce results to max limit
|
||||
matchingPartnerIds.length = Math.min(matchingPartnerIds.length, limit);
|
||||
return matchingPartnerIds;
|
||||
};
|
||||
|
||||
// add main suggestions based on users
|
||||
const partnersFromUsers = ResUsers._filter([])
|
||||
.map((user) => this.browse(user.partner_id)[0])
|
||||
.filter((partner) => partner);
|
||||
const mainMatchingPartnerIds = mentionSuggestionsFilter(partnersFromUsers, search, limit);
|
||||
|
||||
let extraMatchingPartnerIds = [];
|
||||
// if not enough results add extra suggestions based on partners
|
||||
const remainingLimit = limit - mainMatchingPartnerIds.length;
|
||||
if (mainMatchingPartnerIds.length < limit) {
|
||||
const partners = this._filter([["id", "not in", mainMatchingPartnerIds]]);
|
||||
extraMatchingPartnerIds = mentionSuggestionsFilter(partners, search, remainingLimit);
|
||||
}
|
||||
|
||||
const store = new mailDataHelpers.Store(
|
||||
this.browse(mainMatchingPartnerIds.concat(extraMatchingPartnerIds))
|
||||
);
|
||||
const roleIds = this.env["res.role"].search(
|
||||
[["name", "ilike", search || ""]],
|
||||
makeKwArgs({ limit: limit || 8 })
|
||||
);
|
||||
store.add("res.role", this.env["res.role"]._read_format(roleIds, ["name"], false));
|
||||
|
||||
return store.get_result();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} [channel_id]
|
||||
* @param {string} [search]
|
||||
* @param {number} [limit]
|
||||
*/
|
||||
get_mention_suggestions_from_channel(channel_id, search, limit = 8) {
|
||||
const kwargs = getKwArgs(arguments, "channel_id", "search", "limit");
|
||||
channel_id = kwargs.channel_id;
|
||||
search = kwargs.search || "";
|
||||
limit = kwargs.limit || 8;
|
||||
|
||||
/** @type {import("mock_models").DiscussChannelMember} */
|
||||
const DiscussChannelMember = this.env["discuss.channel.member"];
|
||||
/** @type {import("mock_models").ResUsers} */
|
||||
const ResUsers = this.env["res.users"];
|
||||
/** @type {import("mock_models").DiscussChannel} */
|
||||
const channel = this.env["discuss.channel"].browse(channel_id)[0];
|
||||
const searchLower = search.toLowerCase();
|
||||
|
||||
const extraDomain = [
|
||||
["user_ids", "!=", false],
|
||||
["active", "=", true],
|
||||
["partner_share", "=", false],
|
||||
];
|
||||
const parent_channel = this.browse(channel.parent_channel_id);
|
||||
const allowed_group = parent_channel?.group_public_id ?? channel.group_public_id;
|
||||
if (allowed_group) {
|
||||
extraDomain.push(["group_ids", "in", allowed_group]);
|
||||
}
|
||||
const baseDomain = search
|
||||
? ["|", ["name", "ilike", searchLower], ["email", "ilike", searchLower]]
|
||||
: [];
|
||||
const partners = this._search_mention_suggestions(
|
||||
baseDomain,
|
||||
limit,
|
||||
channel_id,
|
||||
extraDomain
|
||||
);
|
||||
const store = new mailDataHelpers.Store();
|
||||
const memberIds = DiscussChannelMember.search([
|
||||
["channel_id", "in", [channel.id, channel.parent_channel_id]],
|
||||
["partner_id", "in", partners],
|
||||
]);
|
||||
const users = ResUsers.search([["partner_id", "in", partners]]).reduce((map, userId) => {
|
||||
const [user] = ResUsers.browse(userId);
|
||||
map[user.partner_id] = user;
|
||||
return map;
|
||||
}, {});
|
||||
for (const memberId of memberIds) {
|
||||
const [member] = DiscussChannelMember.browse(memberId);
|
||||
store.add(this.browse(member.partner_id));
|
||||
store.add(
|
||||
DiscussChannelMember.browse(member.id),
|
||||
makeKwArgs({ fields: ["channel", "persona"] })
|
||||
);
|
||||
}
|
||||
for (const partnerId of partners) {
|
||||
const data = {
|
||||
name: users[partnerId]?.name,
|
||||
group_ids: users[partnerId]?.group_ids.includes(allowed_group)
|
||||
? allowed_group
|
||||
: undefined,
|
||||
};
|
||||
store.add(this.browse(partnerId), data);
|
||||
}
|
||||
const roleIds = this.env["res.role"].search(
|
||||
[["name", "ilike", searchLower || ""]],
|
||||
makeKwArgs({ limit: limit || 8 })
|
||||
);
|
||||
store.add("res.role", this.env["res.role"]._read_format(roleIds, ["name"], false));
|
||||
return store.get_result();
|
||||
}
|
||||
|
||||
compute_im_status(partner) {
|
||||
if (partner.im_status) {
|
||||
return partner.im_status;
|
||||
}
|
||||
if (partner.id === serverState.odoobotId) {
|
||||
return "bot";
|
||||
}
|
||||
if (!partner.user_ids.length) {
|
||||
return "im_status";
|
||||
}
|
||||
return "offline";
|
||||
}
|
||||
|
||||
/* override */
|
||||
_compute_display_name() {
|
||||
super._compute_display_name();
|
||||
for (const record of this) {
|
||||
if (record.parent_id && !record.name) {
|
||||
const [parent] = this.env["res.partner"].browse(record.parent_id);
|
||||
const type = this._fields.type.selection.find((item) => item[0] === record.type);
|
||||
record.display_name = `${parent.name}, ${type[1]}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array} domain
|
||||
* @param {number} limit
|
||||
* @param {number} channel_id
|
||||
* @param {Array} extraDomain
|
||||
* @returns {Array}
|
||||
*/
|
||||
_search_mention_suggestions(domain, limit, channel_id, extraDomain) {
|
||||
const DiscussChannelMember = this.env["discuss.channel.member"];
|
||||
const ResUsers = this.env["res.users"];
|
||||
|
||||
const channel = this.env["discuss.channel"].browse(channel_id)[0];
|
||||
let partnerIds = [];
|
||||
if (!domain?.length && channel) {
|
||||
partnerIds = DiscussChannelMember.search([
|
||||
["channel_id", "in", [channel.id, channel.parent_channel_id]],
|
||||
]).map((memberId) => DiscussChannelMember.browse(memberId)[0].partner_id);
|
||||
} else {
|
||||
partnerIds = ResUsers.search(domain).map(
|
||||
(userId) => ResUsers.browse(userId)[0].partner_id
|
||||
);
|
||||
}
|
||||
if (extraDomain?.length) {
|
||||
const usersWithAccess = ResUsers.search(extraDomain).map(
|
||||
(userId) => ResUsers.browse(userId)[0].partner_id
|
||||
);
|
||||
partnerIds.push(...usersWithAccess);
|
||||
}
|
||||
return Array.from(new Set(partnerIds)).slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} ids
|
||||
* @returns {Record<string, ModelRecord>}
|
||||
*/
|
||||
_to_store(store, fields, extra_fields) {
|
||||
const kwargs = getKwArgs(arguments, "store", "fields", "extra_fields");
|
||||
fields = kwargs.fields;
|
||||
extra_fields = kwargs.extra_fields ?? [];
|
||||
fields = fields.concat(extra_fields);
|
||||
|
||||
/** @type {import("mock_models").ResCountry} */
|
||||
const ResCountry = this.env["res.country"];
|
||||
/** @type {import("mock_models").ResUsers} */
|
||||
const ResUsers = this.env["res.users"];
|
||||
|
||||
this._compute_main_user_id(); // compute not automatically triggering when necessary
|
||||
store._add_record_fields(
|
||||
this,
|
||||
fields.filter(
|
||||
(field) =>
|
||||
![
|
||||
"avatar_128",
|
||||
"country_id",
|
||||
"display_name",
|
||||
"is_admin",
|
||||
"notification_type",
|
||||
"signature",
|
||||
"user",
|
||||
].includes(field)
|
||||
)
|
||||
);
|
||||
for (const partner of this) {
|
||||
const data = {};
|
||||
if (fields.includes("avatar_128")) {
|
||||
data.avatar_128_access_token = partner.id;
|
||||
data.write_date = partner.write_date;
|
||||
}
|
||||
if (fields.includes("country_id")) {
|
||||
const [country_id] = ResCountry.browse(partner.country_id);
|
||||
data.country_id = country_id || false;
|
||||
}
|
||||
if (fields.includes("display_name")) {
|
||||
data.displayName = partner.display_name || partner.name;
|
||||
}
|
||||
if (fields.includes("im_status")) {
|
||||
data.im_status = this.compute_im_status(partner);
|
||||
data.im_status_access_token = partner.id;
|
||||
}
|
||||
if (fields.includes("user")) {
|
||||
data.main_user_id = partner.main_user_id;
|
||||
if (partner.main_user_id) {
|
||||
store._add_record_fields(ResUsers.browse(partner.main_user_id), ["share"]);
|
||||
}
|
||||
if (partner.main_user_id && fields.includes("is_admin")) {
|
||||
const users = ResUsers.search([["login", "=", "admin"]]);
|
||||
store._add_record_fields(ResUsers.browse(partner.main_user_id), {
|
||||
is_admin:
|
||||
this.env.cookie.get("authenticated_user_sid") ===
|
||||
(Number.isInteger(users?.[0]) ? users?.[0] : users?.[0]?.id) ??
|
||||
false,
|
||||
}); // mock server simplification
|
||||
}
|
||||
if (partner.main_user_id && fields.includes("notification_type")) {
|
||||
store._add_record_fields(
|
||||
ResUsers.browse(partner.main_user_id),
|
||||
makeKwArgs({ fields: ["notification_type"] })
|
||||
);
|
||||
}
|
||||
if (partner.main_user_id && fields.includes("signature")) {
|
||||
store._add_record_fields(
|
||||
ResUsers.browse(partner.main_user_id),
|
||||
makeKwArgs({ fields: ["signature"] })
|
||||
);
|
||||
}
|
||||
}
|
||||
if (Object.keys(data).length) {
|
||||
store._add_record_fields(this.browse(partner.id), data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get _to_store_defaults() {
|
||||
return [
|
||||
"avatar_128",
|
||||
"name",
|
||||
"email",
|
||||
"active",
|
||||
"im_status",
|
||||
"is_company",
|
||||
mailDataHelpers.Store.one("main_user_id", ["share"]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [search_term]
|
||||
* @param {number} [channel_id]
|
||||
* @param {number} [limit]
|
||||
*/
|
||||
search_for_channel_invite(search_term, channel_id, limit = 30) {
|
||||
const kwargs = getKwArgs(arguments, "search_term", "channel_id", "limit");
|
||||
const store = new mailDataHelpers.Store();
|
||||
const channel_invites = this._search_for_channel_invite(
|
||||
store,
|
||||
kwargs.search_term,
|
||||
kwargs.channel_id,
|
||||
kwargs.limit
|
||||
);
|
||||
return { store_data: store.get_result(), ...channel_invites };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [search_term]
|
||||
* @param {number} [channel_id]
|
||||
* @param {number} [limit]
|
||||
*/
|
||||
_search_for_channel_invite(store, search_term, channel_id, limit = 30) {
|
||||
const kwargs = getKwArgs(arguments, "store", "search_term", "channel_id", "limit");
|
||||
search_term = kwargs.search_term || "";
|
||||
channel_id = kwargs.channel_id;
|
||||
limit = kwargs.limit || 30;
|
||||
|
||||
/** @type {import("mock_models").DiscussChannelMember} */
|
||||
const DiscussChannelMember = this.env["discuss.channel.member"];
|
||||
/** @type {import("mock_models").ResUsers} */
|
||||
const ResUsers = this.env["res.users"];
|
||||
|
||||
search_term = search_term.toLowerCase(); // simulates ILIKE
|
||||
let memberPartnerIds;
|
||||
if (channel_id) {
|
||||
memberPartnerIds = new Set(
|
||||
DiscussChannelMember._filter([["channel_id", "=", channel_id]]).map(
|
||||
(member) => member.partner_id
|
||||
)
|
||||
);
|
||||
}
|
||||
// simulates domain with relational parts (not supported by mock server)
|
||||
const matchingPartnersIds = ResUsers._filter([])
|
||||
.filter((user) => {
|
||||
const [partner] = this.browse(user.partner_id);
|
||||
// user must have a partner
|
||||
if (!partner) {
|
||||
return false;
|
||||
}
|
||||
// not current partner
|
||||
if (!channel_id && partner.id === this.env.user.partner_id) {
|
||||
return false;
|
||||
}
|
||||
// user should not already be a member of the channel
|
||||
if (channel_id && memberPartnerIds.has(partner.id)) {
|
||||
return false;
|
||||
}
|
||||
// no name is considered as return all
|
||||
if (!search_term) {
|
||||
return true;
|
||||
}
|
||||
if (partner.name && partner.name.toLowerCase().includes(search_term)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.map((user) => user.partner_id)
|
||||
.reduce((ids, partnerId) => {
|
||||
if (!ids.includes(partnerId)) {
|
||||
ids.push(partnerId);
|
||||
}
|
||||
return ids;
|
||||
}, []);
|
||||
const count = matchingPartnersIds.length;
|
||||
matchingPartnersIds.length = Math.min(count, limit);
|
||||
this._search_for_channel_invite_to_store(matchingPartnersIds, store, channel_id);
|
||||
return {
|
||||
count,
|
||||
partner_ids: matchingPartnersIds,
|
||||
};
|
||||
}
|
||||
|
||||
_search_for_channel_invite_to_store(ids, store, channel_id) {
|
||||
store.add(this.browse(ids));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
* @returns {number}
|
||||
*/
|
||||
_get_needaction_count(id) {
|
||||
/** @type {import("mock_models").MailNotification} */
|
||||
const MailNotification = this.env["mail.notification"];
|
||||
|
||||
const [partner] = this.browse(id);
|
||||
return MailNotification._filter([
|
||||
["res_partner_id", "=", partner.id],
|
||||
["is_read", "=", false],
|
||||
]).length;
|
||||
}
|
||||
|
||||
_get_current_persona() {
|
||||
/** @type {import("mock_models").MailGuest} */
|
||||
const MailGuest = this.env["mail.guest"];
|
||||
/** @type {import("mock_models").ResUsers} */
|
||||
const ResUsers = this.env["res.users"];
|
||||
|
||||
if (ResUsers._is_public(this.env.uid)) {
|
||||
return [null, MailGuest._get_guest_from_context()];
|
||||
}
|
||||
return [this.browse(this.env.user.partner_id)[0], null];
|
||||
}
|
||||
|
||||
_get_store_avatar_card_fields() {
|
||||
return ["email", "partner_share", "name", "phone"];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { fields, models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ResRole extends models.ServerModel {
|
||||
_name = "res.role";
|
||||
|
||||
name = fields.Char();
|
||||
}
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
import { DISCUSS_ACTION_ID, mailDataHelpers } from "@mail/../tests/mock_server/mail_mock_server";
|
||||
|
||||
import { fields, makeKwArgs, serverState, webModels } from "@web/../tests/web_test_helpers";
|
||||
import { serializeDate, today } from "@web/core/l10n/dates";
|
||||
|
||||
export class ResUsers extends webModels.ResUsers {
|
||||
im_status = fields.Char({ default: "online" });
|
||||
notification_type = fields.Selection({
|
||||
selection: [
|
||||
["email", "Handle by Emails"],
|
||||
["inbox", "Handle in Odoo"],
|
||||
],
|
||||
default: "email",
|
||||
});
|
||||
role_ids = fields.Many2many({ relation: "res.role", string: "Roles" });
|
||||
|
||||
/** Simulates `_init_store_data` on `res.users`. */
|
||||
_init_store_data(store) {
|
||||
/** @type {import("mock_models").DiscussChannel} */
|
||||
const DiscussChannel = this.env["discuss.channel"];
|
||||
/** @type {import("mock_models").MailGuest} */
|
||||
const MailGuest = this.env["mail.guest"];
|
||||
/** @type {import("mock_models").ResPartner} */
|
||||
const ResPartner = this.env["res.partner"];
|
||||
/** @type {import("mock_models").ResUsersSettings} */
|
||||
const ResUsersSettings = this.env["res.users.settings"];
|
||||
/** @type {import("mock_models").MailMessageSubtype} */
|
||||
const MailMessageSubtype = this.env["mail.message.subtype"];
|
||||
store.add({
|
||||
action_discuss_id: DISCUSS_ACTION_ID,
|
||||
channel_types_with_seen_infos: DiscussChannel._types_allowing_seen_infos(),
|
||||
hasGifPickerFeature: true,
|
||||
hasLinkPreviewFeature: true,
|
||||
hasMessageTranslationFeature: true,
|
||||
mt_comment: MailMessageSubtype._filter([["subtype_xmlid", "=", "mail.mt_comment"]])[0]
|
||||
.id,
|
||||
mt_note: MailMessageSubtype._filter([["subtype_xmlid", "=", "mail.mt_note"]])[0].id,
|
||||
odoobot: mailDataHelpers.Store.one(ResPartner.browse(serverState.odoobotId)),
|
||||
});
|
||||
if (!this._is_public(this.env.uid)) {
|
||||
const userSettings = ResUsersSettings._find_or_create_for_user(this.env.uid);
|
||||
store.add({
|
||||
self_partner: mailDataHelpers.Store.one(
|
||||
ResPartner.browse(this.env.user.partner_id),
|
||||
makeKwArgs({
|
||||
fields: [
|
||||
"active",
|
||||
"avatar_128",
|
||||
"im_status",
|
||||
"is_admin",
|
||||
mailDataHelpers.Store.one("main_user_id", ["notification_type"]),
|
||||
mailDataHelpers.Store.one("main_user_id", ["signature"]),
|
||||
"name",
|
||||
"notification_type",
|
||||
"signature",
|
||||
"user",
|
||||
],
|
||||
})
|
||||
),
|
||||
settings: ResUsersSettings.res_users_settings_format(userSettings.id),
|
||||
});
|
||||
} else if (this.env.cookie.get("dgid")) {
|
||||
store.add({
|
||||
self_guest: mailDataHelpers.Store.one(
|
||||
MailGuest.browse(this.env.cookie.get("dgid")),
|
||||
makeKwArgs({ fields: ["avatar_128", "name"] })
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
systray_get_activities() {
|
||||
/** @type {import("mock_models").MailActivity} */
|
||||
const MailActivity = this.env["mail.activity"];
|
||||
|
||||
const activities = MailActivity.search_read([]);
|
||||
const userActivitiesByModelName = {};
|
||||
for (const activity of activities) {
|
||||
const day = serializeDate(today());
|
||||
if (day === activity["date_deadline"]) {
|
||||
activity["states"] = "today";
|
||||
} else if (day > activity["date_deadline"]) {
|
||||
activity["states"] = "overdue";
|
||||
} else {
|
||||
activity["states"] = "planned";
|
||||
}
|
||||
}
|
||||
for (const activity of activities) {
|
||||
const modelName = activity["res_model"];
|
||||
if (!userActivitiesByModelName[modelName]) {
|
||||
userActivitiesByModelName[modelName] = {
|
||||
id: modelName, // for simplicity
|
||||
model: modelName,
|
||||
name: modelName,
|
||||
overdue_count: 0,
|
||||
planned_count: 0,
|
||||
today_count: 0,
|
||||
total_count: 0,
|
||||
type: "activity",
|
||||
};
|
||||
}
|
||||
userActivitiesByModelName[modelName][`${activity["states"]}_count`] += 1;
|
||||
userActivitiesByModelName[modelName]["total_count"] += 1;
|
||||
userActivitiesByModelName[modelName].actions = [
|
||||
{
|
||||
icon: "fa-clock-o",
|
||||
name: "Summary",
|
||||
},
|
||||
];
|
||||
}
|
||||
return Object.values(userActivitiesByModelName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} ids
|
||||
* @param {import("@mail/../tests/mock_server/mail_mock_server").mailDataHelpers.Store} store
|
||||
**/
|
||||
_init_messaging(ids, store) {
|
||||
/** @type {import("mock_models").DiscussChannel} */
|
||||
const DiscussChannel = this.env["discuss.channel"];
|
||||
/** @type {import("mock_models").DiscussChannelMember} */
|
||||
const DiscussChannelMember = this.env["discuss.channel.member"];
|
||||
/** @type {import("mock_models").MailMessage} */
|
||||
const MailMessage = this.env["mail.message"];
|
||||
/** @type {import("mock_models").ResPartner} */
|
||||
const ResPartner = this.env["res.partner"];
|
||||
/** @type {import("mock_models").ResUsers} */
|
||||
const ResUsers = this.env["res.users"];
|
||||
|
||||
const [user] = ResUsers.browse(ids);
|
||||
const channels = DiscussChannel._get_channels_as_member();
|
||||
const members = DiscussChannelMember._filter([
|
||||
["channel_id", "in", channels.map((channel) => channel.id)],
|
||||
["partner_id", "=", user.partner_id],
|
||||
]);
|
||||
const bus_last_id = this.env["bus.bus"].lastBusNotificationId;
|
||||
store.add({
|
||||
inbox: {
|
||||
counter: ResPartner._get_needaction_count(user.partner_id),
|
||||
counter_bus_id: bus_last_id,
|
||||
id: "inbox",
|
||||
model: "mail.box",
|
||||
},
|
||||
starred: {
|
||||
counter: MailMessage._filter([["starred_partner_ids", "in", user.partner_id]])
|
||||
.length,
|
||||
counter_bus_id: bus_last_id,
|
||||
id: "starred",
|
||||
model: "mail.box",
|
||||
},
|
||||
initChannelsUnreadCounter: members.filter((member) => member.message_unread_counter)
|
||||
.length,
|
||||
});
|
||||
}
|
||||
|
||||
_get_activity_groups() {
|
||||
/** @type {import("mock_models").MailActivity} */
|
||||
const MailActivity = this.env["mail.activity"];
|
||||
|
||||
const activities = MailActivity.search_read([]);
|
||||
const userActivitiesByModelName = {};
|
||||
for (const activity of activities) {
|
||||
const day = serializeDate(today());
|
||||
if (day === activity["date_deadline"]) {
|
||||
activity["states"] = "today";
|
||||
} else if (day > activity["date_deadline"]) {
|
||||
activity["states"] = "overdue";
|
||||
} else {
|
||||
activity["states"] = "planned";
|
||||
}
|
||||
}
|
||||
for (const activity of activities) {
|
||||
const modelName = activity["res_model"];
|
||||
if (!userActivitiesByModelName[modelName]) {
|
||||
userActivitiesByModelName[modelName] = {
|
||||
id: modelName, // for simplicity
|
||||
model: modelName,
|
||||
name: modelName,
|
||||
domain:
|
||||
modelName && "active" in this.env[modelName]._fields
|
||||
? [["active", "in", [true, false]]]
|
||||
: [],
|
||||
overdue_count: 0,
|
||||
planned_count: 0,
|
||||
today_count: 0,
|
||||
total_count: 0,
|
||||
type: "activity",
|
||||
};
|
||||
}
|
||||
userActivitiesByModelName[modelName][`${activity["states"]}_count`] += 1;
|
||||
userActivitiesByModelName[modelName]["total_count"] += 1;
|
||||
userActivitiesByModelName[modelName].actions = [
|
||||
{
|
||||
icon: "fa-clock-o",
|
||||
name: "Summary",
|
||||
},
|
||||
];
|
||||
}
|
||||
return Object.values(userActivitiesByModelName);
|
||||
}
|
||||
|
||||
_get_store_avatar_card_fields() {
|
||||
return [
|
||||
"share",
|
||||
mailDataHelpers.Store.one(
|
||||
"partner_id",
|
||||
this.env["res.partner"]._get_store_avatar_card_fields()
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
import { fields, getKwArgs, webModels } from "@web/../tests/web_test_helpers";
|
||||
import { ensureArray } from "@web/core/utils/arrays";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {import("@web/../tests/web_test_helpers").KwArgs<T>} KwArgs
|
||||
*/
|
||||
|
||||
export class ResUsersSettings extends webModels.ResUsersSettings {
|
||||
is_discuss_sidebar_category_channel_open = fields.Generic({ default: true });
|
||||
is_discuss_sidebar_category_chat_open = fields.Generic({ default: true });
|
||||
|
||||
/**
|
||||
* @param {number} guest_id
|
||||
* @param {number} partner_id
|
||||
* @param {number} volume
|
||||
*/
|
||||
set_volume_setting(ids, partner_id, volume, guest_id = false) {
|
||||
const kwargs = getKwArgs(arguments, "ids", "partner_id", "volume", "guest_id");
|
||||
ids = kwargs.ids;
|
||||
partner_id = kwargs.partner_id;
|
||||
volume = kwargs.volume;
|
||||
guest_id = kwargs.guest_id;
|
||||
|
||||
/** @type {import("mock_models").BusBus} */
|
||||
const BusBus = this.env["bus.bus"];
|
||||
/** @type {import("mock_models").ResPartner} */
|
||||
const ResPartner = this.env["res.partner"];
|
||||
/** @type {import("mock_models").ResUsersSettingsVolumes} */
|
||||
const ResUsersSettingsVolumes = this.env["res.users.settings.volumes"];
|
||||
|
||||
const id = ids[0]; // ensure_one
|
||||
let [volumeSettings] = ResUsersSettingsVolumes.search_read([
|
||||
["user_setting_id", "=", id],
|
||||
partner_id ? ["partner_id", "=", partner_id] : ["guest_id", "=", guest_id],
|
||||
]);
|
||||
if (!volumeSettings) {
|
||||
volumeSettings = ResUsersSettingsVolumes.create({
|
||||
partner_id,
|
||||
guest_id,
|
||||
volume,
|
||||
});
|
||||
} else {
|
||||
ResUsersSettingsVolumes.write(volumeSettings.id, { volume });
|
||||
}
|
||||
const [partner] = ResPartner.read(this.env.user.partner_id);
|
||||
BusBus._sendone(partner, "res.users.settings.volumes", {
|
||||
...ResUsersSettingsVolumes.discuss_users_settings_volume_format(volumeSettings.id),
|
||||
});
|
||||
return volumeSettings;
|
||||
}
|
||||
|
||||
set_custom_notifications(ids, custom_notifications) {
|
||||
const kwargs = getKwArgs(arguments, "ids", "custom_notifications");
|
||||
ids = kwargs.ids;
|
||||
delete kwargs.ids;
|
||||
custom_notifications = kwargs.custom_notifications;
|
||||
this.set_res_users_settings(ids, { channel_notifications: custom_notifications });
|
||||
}
|
||||
}
|
||||
|
||||
patch(webModels.ResUsersSettings.prototype, {
|
||||
res_users_settings_format(id, fields_to_format) {
|
||||
const kwargs = getKwArgs(arguments, "id", "fields_to_format");
|
||||
id = kwargs.id;
|
||||
delete kwargs.id;
|
||||
fields_to_format = kwargs.fields_to_format;
|
||||
const res = super.res_users_settings_format(id, fields_to_format);
|
||||
|
||||
/** @type {import("mock_models").ResUsersSettingsVolumes} */
|
||||
const ResUsersSettingsVolumes = this.env["res.users.settings.volumes"];
|
||||
|
||||
const [settings] = this.browse(id);
|
||||
if (Reflect.ownKeys(res).includes("volume_settings_ids")) {
|
||||
const volumeSettings = ResUsersSettingsVolumes.discuss_users_settings_volume_format(
|
||||
settings.volume_settings_ids
|
||||
);
|
||||
res.volumes = [["ADD", volumeSettings]];
|
||||
}
|
||||
return res;
|
||||
},
|
||||
set_res_users_settings(idOrIds, new_settings) {
|
||||
const kwargs = getKwArgs(arguments, "idOrIds", "new_settings");
|
||||
idOrIds = kwargs.idOrIds;
|
||||
delete kwargs.idOrIds;
|
||||
new_settings = kwargs.new_settings || {};
|
||||
const changedSettings = super.set_res_users_settings(idOrIds, new_settings);
|
||||
|
||||
/** @type {import("mock_models").BusBus} */
|
||||
const BusBus = this.env["bus.bus"];
|
||||
/** @type {import("mock_models").ResPartner} */
|
||||
const ResPartner = this.env["res.partner"];
|
||||
/** @type {import("mock_models").ResUsers} */
|
||||
const ResUsers = this.env["res.users"];
|
||||
|
||||
const [id] = ensureArray(idOrIds);
|
||||
const [oldSettings] = this.browse(id);
|
||||
const [relatedUser] = ResUsers.search_read([["id", "=", oldSettings.user_id]]);
|
||||
const [relatedPartner] = ResPartner.search_read([["id", "=", relatedUser.partner_id[0]]]);
|
||||
BusBus._sendone(relatedPartner, "res.users.settings", {
|
||||
...changedSettings,
|
||||
id,
|
||||
});
|
||||
return changedSettings;
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ResUsersSettingsVolumes extends models.ServerModel {
|
||||
_name = "res.users.settings.volumes";
|
||||
|
||||
/** @param {number[]} ids */
|
||||
discuss_users_settings_volume_format(ids) {
|
||||
/** @type {import("mock_models").MailGuest} */
|
||||
const MailGuest = this.env["mail.guest"];
|
||||
/** @type {import("mock_models").ResPartner} */
|
||||
const ResPartner = this.env["res.partner"];
|
||||
|
||||
return this.browse(ids).map((volumeSettingsRecord) => {
|
||||
const [relatedGuest] = MailGuest.browse(volumeSettingsRecord.guest_id);
|
||||
const [relatedPartner] = ResPartner.browse(volumeSettingsRecord.partner_id);
|
||||
let partner_id, guest_id;
|
||||
if (relatedPartner) {
|
||||
partner_id = {
|
||||
id: relatedPartner.id,
|
||||
name: relatedPartner.name,
|
||||
};
|
||||
}
|
||||
if (relatedGuest) {
|
||||
guest_id = {
|
||||
id: relatedGuest.id,
|
||||
name: relatedGuest.name,
|
||||
};
|
||||
}
|
||||
return {
|
||||
partner_id,
|
||||
guest_id,
|
||||
id: volumeSettingsRecord.id,
|
||||
volume: volumeSettingsRecord.volume,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue