mirror of
https://github.com/OCA/server-backend.git
synced 2025-02-18 09:52:42 +02:00
[IMP] base_ical: Allow advanced snippets. Use apikeys with scope.
This commit is contained in:
@@ -1,70 +1,80 @@
|
||||
# Copyright 2023 Hunki Enterprises BV
|
||||
# Copyright 2024 initOS GmbH
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl-3.0)
|
||||
|
||||
|
||||
import datetime
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
import logging
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import pytz
|
||||
import vobject
|
||||
from dateutil import relativedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.tools import html2plaintext
|
||||
from odoo.tools.safe_eval import safe_eval, wrap_module
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
vobject_wrapped = wrap_module(__import__("vobject"), ["iCalendar", "readOne"])
|
||||
|
||||
|
||||
class BaseIcal(models.Model):
|
||||
_name = "base.ical"
|
||||
_description = "Definition of an iCal export"
|
||||
|
||||
def _get_operating_modes(self):
|
||||
return [
|
||||
("simple", _("Simple")),
|
||||
("advanced", _("Advanced")),
|
||||
]
|
||||
|
||||
active = fields.Boolean(default=True)
|
||||
name = fields.Char(required=True, translate=True)
|
||||
mode = fields.Selection("_get_operating_modes", required=True, default="simple")
|
||||
model_id = fields.Many2one("ir.model", required=True, ondelete="cascade")
|
||||
model = fields.Char(related="model_id.model")
|
||||
model = fields.Char("Model Name", related="model_id.model")
|
||||
domain = fields.Char(
|
||||
required=True, default="[]", help="You can use variables `env` and `user` here"
|
||||
)
|
||||
preview = fields.Text(compute="_compute_preview")
|
||||
code = fields.Text()
|
||||
expression_dtstamp = fields.Char(
|
||||
required=True,
|
||||
vevent_field="dtstamp",
|
||||
string="DTSTAMP",
|
||||
help="You can use variables `record` and `user` here",
|
||||
default="record.write_date",
|
||||
)
|
||||
expression_uid = fields.Char(
|
||||
required=True,
|
||||
vevent_field="uid",
|
||||
string="UID",
|
||||
help="You can use variables `record` and `user` here",
|
||||
)
|
||||
expression_dtstart = fields.Char(
|
||||
required=True,
|
||||
vevent_field="dtstart",
|
||||
string="DTSTART",
|
||||
help="You can use variables `record` and `user` here",
|
||||
)
|
||||
expression_dtend = fields.Char(
|
||||
required=True,
|
||||
vevent_field="dtend",
|
||||
string="DTEND",
|
||||
help="You can use variables `record` and `user` here",
|
||||
)
|
||||
expression_summary = fields.Char(
|
||||
required=True,
|
||||
vevent_field="summary",
|
||||
string="SUMMARY",
|
||||
help="You can use variables `record` and `user` here",
|
||||
default="record.display_name",
|
||||
)
|
||||
user_url = fields.Char(compute="_compute_user_fields", string="URL")
|
||||
user_active = fields.Boolean(compute="_compute_user_fields")
|
||||
allowed_users_ids = fields.Many2many("res.users")
|
||||
auto = fields.Boolean(
|
||||
"Enable automatically",
|
||||
"Allow automatically",
|
||||
copy=False,
|
||||
help="If you check this, the calendar will be enabled for all current and "
|
||||
"future users. Not that unchecking this will not disable existing calendar "
|
||||
"subscriptions",
|
||||
)
|
||||
help_text = fields.Html(compute="_compute_help")
|
||||
|
||||
def _valid_field_parameter(self, field, name):
|
||||
return super()._valid_field_parameter(field, name) or name == "vevent_field"
|
||||
@@ -77,24 +87,23 @@ class BaseIcal(models.Model):
|
||||
"expression_dtstart",
|
||||
"expression_dtend",
|
||||
"expression_summary",
|
||||
"code",
|
||||
"mode",
|
||||
)
|
||||
def _compute_preview(self):
|
||||
for this in self:
|
||||
this.preview = this._get_ical()
|
||||
this.preview = this._get_ical(limit=5)
|
||||
|
||||
def _compute_user_fields(self):
|
||||
base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url")
|
||||
@api.depends("mode")
|
||||
def _compute_help(self):
|
||||
for this in self:
|
||||
token = this._get_user_tokens()[:1]
|
||||
vals = {"user_url": False, "user_active": False}
|
||||
if token:
|
||||
vals.update(
|
||||
user_url=urlunparse(
|
||||
urlparse(base_url)._replace(path="/base_ical/%s" % token.token)
|
||||
),
|
||||
user_active=token.active,
|
||||
)
|
||||
this.update(vals)
|
||||
variables = this.default_variables()
|
||||
lines = []
|
||||
for var, desc in sorted(variables.items()):
|
||||
var = (f"<code>{v.strip()}</code>" for v in var.split(","))
|
||||
lines.append(f"<li>{', '.join(sorted(var))}: {desc}</li>")
|
||||
|
||||
this.help_text = "<ul>" + "\n".join(lines) + "</ul>"
|
||||
|
||||
@api.onchange("model_id")
|
||||
def _onchange_model_id(self):
|
||||
@@ -117,6 +126,8 @@ class BaseIcal(models.Model):
|
||||
"expression_dtstart",
|
||||
"expression_dtend",
|
||||
"expression_summary",
|
||||
"code",
|
||||
"mode",
|
||||
)
|
||||
def _check_domain(self):
|
||||
for this in self:
|
||||
@@ -136,25 +147,34 @@ class BaseIcal(models.Model):
|
||||
self._enable_all_users()
|
||||
return result
|
||||
|
||||
def _get_user_tokens(self):
|
||||
return (
|
||||
self.env["base.ical.token"]
|
||||
.with_context(active_test=False)
|
||||
.search(
|
||||
[
|
||||
("user_id", "=", self.env.user.id),
|
||||
("ical_id", "in", self.ids),
|
||||
]
|
||||
def default_variables(self):
|
||||
self.ensure_one()
|
||||
variables = {
|
||||
"datetime, relativedelta, time, timedelta": "useful Python libraries",
|
||||
"record": "Record to export",
|
||||
"user": "Current user record",
|
||||
}
|
||||
if self.mode == "advanced":
|
||||
variables.update(
|
||||
{
|
||||
"calendar": "Output: Calendar e.g. from `_get_ics_file`",
|
||||
"dict2ical": "Function to add the key-values of dict to ical component",
|
||||
"event": "Output: Dictionary of an VEVENT",
|
||||
"todo": "Output: Dictionary of an VTODO",
|
||||
"vobject": "vobject python library",
|
||||
"html2plaintext": "Converts HTML to plain text",
|
||||
}
|
||||
)
|
||||
)
|
||||
return variables
|
||||
|
||||
def _get_eval_expression_context(self, record):
|
||||
def _get_eval_expression_context(self):
|
||||
"""Return the evaluation context for expression evaluation"""
|
||||
return {
|
||||
"record": record,
|
||||
"user": self.env.user,
|
||||
"timedelta": datetime.timedelta,
|
||||
"datetime": datetime.datetime,
|
||||
"date": datetime.date,
|
||||
"relativedelta": relativedelta.relativedelta,
|
||||
"timedelta": datetime.timedelta,
|
||||
"user": self.env.user,
|
||||
}
|
||||
|
||||
def _get_eval_domain_context(self):
|
||||
@@ -164,36 +184,103 @@ class BaseIcal(models.Model):
|
||||
"env": self.env,
|
||||
}
|
||||
|
||||
def _get_events(self):
|
||||
def _get_items(self, limit=None):
|
||||
"""Return events based on model_id and domain"""
|
||||
self.ensure_one()
|
||||
return self.env[self.model_id.sudo().model].search(
|
||||
safe_eval(self.domain, self._get_eval_domain_context())
|
||||
safe_eval(self.domain, self._get_eval_domain_context()),
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
def _get_ical(self):
|
||||
def _get_ical(self, records=None, limit=None):
|
||||
"""Return the vcalendar as text"""
|
||||
if self.mode == "simple":
|
||||
return self._get_ical_simple(records=records, limit=limit)
|
||||
|
||||
return self._get_ical_advanced(records=records, limit=limit)
|
||||
|
||||
def _get_ical_simple(self, records=None, limit=None):
|
||||
if not all(
|
||||
self[field_name]
|
||||
for field_name, field in self._fields.items()
|
||||
if hasattr(field, "vevent_field") and field.required
|
||||
if hasattr(field, "vevent_field")
|
||||
):
|
||||
return False
|
||||
return ""
|
||||
|
||||
calendar = vobject.iCalendar()
|
||||
for record in self._get_events():
|
||||
for record in records or self._get_items(limit):
|
||||
event = calendar.add("vevent")
|
||||
for field_name, field in self._fields.items():
|
||||
if not hasattr(field, "vevent_field"):
|
||||
continue
|
||||
value = safe_eval(
|
||||
self[field_name], self._get_eval_expression_context(record)
|
||||
)
|
||||
|
||||
if not self[field_name]:
|
||||
continue
|
||||
|
||||
ctx = self._get_eval_expression_context()
|
||||
ctx["record"] = record
|
||||
|
||||
value = safe_eval(self[field_name], ctx)
|
||||
event.add(field.vevent_field).value = self._format_ical_value(
|
||||
field, value
|
||||
value, field=field
|
||||
)
|
||||
return calendar.serialize()
|
||||
|
||||
def _format_ical_value(self, field, value):
|
||||
def _get_ical_advanced(self, records=None, limit=None):
|
||||
if not self.code:
|
||||
return ""
|
||||
|
||||
context = self._get_eval_expression_context()
|
||||
context.update(
|
||||
{
|
||||
"dict2ical": self._dict_to_ical_component,
|
||||
"html2plaintext": html2plaintext,
|
||||
"vobject": vobject_wrapped,
|
||||
}
|
||||
)
|
||||
|
||||
calendar = vobject.iCalendar()
|
||||
tz = pytz.timezone(self.env.user.tz or "UTC")
|
||||
calendar.add(vobject.icalendar.TimezoneComponent(tz))
|
||||
for record in records or self._get_items(limit):
|
||||
context.update(
|
||||
{"record": record, "calendar": None, "event": None, "todo": None}
|
||||
)
|
||||
safe_eval(self.code, context, mode="exec", nocopy=True)
|
||||
|
||||
cal = context.get("calendar")
|
||||
if cal:
|
||||
# Support for `_get_ics_file`
|
||||
if isinstance(cal, dict) and record.id in cal:
|
||||
cal = cal[record.id]
|
||||
|
||||
if isinstance(cal, bytes):
|
||||
cal = cal.decode()
|
||||
|
||||
if isinstance(cal, str):
|
||||
cal = vobject.readOne(cal)
|
||||
|
||||
self._copy_ical_calendar(calendar, cal)
|
||||
|
||||
event, todo = map(context.get, ("event", "todo"))
|
||||
if event:
|
||||
self._dict_to_ical_component(calendar.add("vevent"), event)
|
||||
|
||||
if todo:
|
||||
self._dict_to_ical_component(calendar.add("vtodo"), todo)
|
||||
|
||||
return calendar.serialize()
|
||||
|
||||
def _dict_to_ical_component(self, component, data):
|
||||
for key, value in data.items():
|
||||
component.add(key).value = self._format_ical_value(value)
|
||||
|
||||
def _copy_ical_calendar(self, dst_calendar, src_calendar):
|
||||
for item in src_calendar.getChildren():
|
||||
if item.name.lower() in ("vevent", "vtodo"):
|
||||
dst_calendar.add(item)
|
||||
|
||||
def _format_ical_value(self, value, field=None):
|
||||
"""Add timezone to datetime values"""
|
||||
if isinstance(value, datetime.datetime):
|
||||
return pytz.utc.localize(value).astimezone(
|
||||
@@ -203,24 +290,19 @@ class BaseIcal(models.Model):
|
||||
|
||||
def _enable_all_users(self, users=None):
|
||||
"""Enable calendar for all users"""
|
||||
for this in self:
|
||||
for user in users or self.env["res.users"].search(
|
||||
[("groups_id", "=", self.env.ref("base.group_user").id)]
|
||||
):
|
||||
this.with_user(user).action_enable()
|
||||
users = users or self.env.ref("base.group_user").users
|
||||
self.write({"allowed_users_ids": users})
|
||||
|
||||
def action_enable(self):
|
||||
def action_new_url(self):
|
||||
"""Create or activate current user's token"""
|
||||
token = self._get_user_tokens()[:1]
|
||||
if token and not token.active:
|
||||
token.active = True
|
||||
elif not token:
|
||||
self.env["base.ical.token"].create(
|
||||
{"ical_id": self.id, "user_id": self.env.user.id}
|
||||
)
|
||||
|
||||
def action_disable(self):
|
||||
"""Deactivate current user's token"""
|
||||
token = self._get_user_tokens()[:1]
|
||||
if token and token.active:
|
||||
token.active = False
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "base.ical.url.description",
|
||||
"name": "iCalendar URL",
|
||||
"views": [(False, "form")],
|
||||
"target": "new",
|
||||
"context": {
|
||||
"default_calendar_id": self.id,
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user