19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:27 +01:00
parent d1963a3c3a
commit 2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions

View file

@ -0,0 +1,157 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import pytz
import uuid
from datetime import datetime, timedelta
from odoo.tools import consteq, get_lang
from odoo import _, api, fields, models
from odoo.http import request
from odoo.addons.base.models.res_partner import _tz_get
from odoo.exceptions import UserError
from odoo.tools.misc import limited_field_access_token
from odoo.addons.mail.tools.discuss import Store
class MailGuest(models.Model):
_name = 'mail.guest'
_description = "Guest"
_inherit = ["avatar.mixin", "bus.listener.mixin"]
_avatar_name_field = "name"
_cookie_name = 'dgid'
_cookie_separator = '|'
@api.model
def _lang_get(self):
return self.env['res.lang'].get_installed()
name = fields.Char(string="Name", required=True)
access_token = fields.Char(string="Access Token", default=lambda self: str(uuid.uuid4()), groups='base.group_system', required=True, readonly=True, copy=False)
country_id = fields.Many2one(string="Country", comodel_name='res.country')
email = fields.Char()
lang = fields.Selection(string="Language", selection=_lang_get)
timezone = fields.Selection(string="Timezone", selection=_tz_get)
channel_ids = fields.Many2many(string="Channels", comodel_name='discuss.channel', relation='discuss_channel_member', column1='guest_id', column2='channel_id', copy=False)
presence_ids = fields.One2many("mail.presence", "guest_id", groups="base.group_system")
# sudo: mail.guest - can access presence of accessible guest
im_status = fields.Char("IM Status", compute="_compute_im_status", compute_sudo=True)
offline_since = fields.Datetime("Offline since", compute="_compute_im_status", compute_sudo=True)
@api.depends("presence_ids.status")
def _compute_im_status(self):
for guest in self:
guest.im_status = guest.presence_ids.status or "offline"
guest.offline_since = (
guest.presence_ids.last_poll
if guest.im_status == "offline"
else None
)
def _get_guest_from_token(self, token=""):
"""Returns the guest record for the given token, if applicable."""
guest = self.env["mail.guest"]
parts = token.split(self._cookie_separator)
if len(parts) == 2:
guest_id, guest_access_token = parts
# sudo: mail.guest: guests need sudo to read their access_token
guest = self.browse(int(guest_id)).sudo().exists()
if not guest or not guest.access_token or not consteq(guest.access_token, guest_access_token):
guest = self.env["mail.guest"]
return guest.sudo(False)
def _get_guest_from_context(self):
"""Returns the current guest record from the context, if applicable."""
guest = self.env.context.get('guest')
if isinstance(guest, self.pool['mail.guest']):
assert len(guest) <= 1, "Context guest should be empty or a single record."
return guest.sudo(False).with_context(guest=guest)
return self.env['mail.guest']
def _get_or_create_guest(self, *, guest_name, country_code, timezone):
if not (guest := self._get_guest_from_context()):
guest = self.create(
{
"country_id": self.env["res.country"].search([("code", "=", country_code)]).id,
"lang": get_lang(self.env).code,
"name": guest_name,
"timezone": timezone,
}
)
guest._set_auth_cookie()
return guest.sudo(False)
def _get_timezone_from_request(self, request):
timezone = request.cookies.get('tz')
return timezone if timezone in pytz.all_timezones else False
def _update_name(self, name):
self.ensure_one()
name = name.strip()
if len(name) < 1:
raise UserError(_("Guest's name cannot be empty."))
if len(name) > 512:
raise UserError(_("Guest's name is too long."))
self.name = name
for channel in self.channel_ids:
Store(bus_channel=channel).add(self, ["avatar_128", "name"]).bus_send()
Store(bus_channel=self).add(self, ["avatar_128", "name"]).bus_send()
def _update_timezone(self, timezone):
query = """
UPDATE mail_guest
SET timezone = %s
WHERE id IN (
SELECT id FROM mail_guest WHERE id = %s
FOR NO KEY UPDATE SKIP LOCKED
)
"""
self.env.cr.execute(query, (timezone, self.id))
def _get_im_status_access_token(self):
"""Return a scoped access token for the `im_status` field. The token is used in
`ir_websocket._prepare_subscribe_data` to grant access to presence channels.
:rtype: str
"""
self.ensure_one()
return limited_field_access_token(self, "im_status", scope="mail.presence")
def _field_store_repr(self, field_name):
if field_name == "avatar_128":
return [
Store.Attr("avatar_128_access_token", lambda g: g._get_avatar_128_access_token()),
"write_date",
]
if field_name == "im_status":
return [
"im_status",
Store.Attr("im_status_access_token", lambda g: g._get_im_status_access_token()),
]
return [field_name]
def _to_store_defaults(self, target):
return ["avatar_128", "im_status", "name"]
def _set_auth_cookie(self):
"""Add a cookie to the response to identify the guest. Every route
that expects a guest will make use of it to authenticate the guest
through `add_guest_to_context`.
"""
self.ensure_one()
expiration_date = datetime.now() + timedelta(days=365)
request.future_response.set_cookie(
self._cookie_name,
self._format_auth_cookie(),
httponly=True,
expires=expiration_date,
)
request.update_context(guest=self.sudo(False))
def _format_auth_cookie(self):
"""Format the cookie value for the given guest.
:return: formatted cookie value
:rtype: str
"""
self.ensure_one()
return f"{self.id}{self._cookie_separator}{self.access_token}"