File: portal.py

package info (click to toggle)
odoo 18.0.0%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 878,716 kB
  • sloc: javascript: 927,937; python: 685,670; xml: 388,524; sh: 1,033; sql: 415; makefile: 26
file content (389 lines) | stat: -rw-r--r-- 16,454 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

import babel.dates
import werkzeug

from odoo import http, fields, tools, models
from odoo.addons.portal.controllers.portal import pager as portal_pager
from odoo.exceptions import AccessError
from odoo.http import request, Response
from odoo.osv import expression
from odoo.tools import consteq
from odoo.tools.misc import get_lang


class PortalMailGroup(http.Controller):
    _thread_per_page = 20
    _replies_per_page = 5

    def _get_website_domain(self):
        # Base group domain in addition to the security access rules
        # Do not show rejected message on the portal view even for admin
        return [('moderation_status', '!=', 'rejected')]

    def _get_archives(self, group_id):
        """Return the different date range and message count for the group messages."""
        domain = expression.AND([self._get_website_domain(), [('mail_group_id', '=', group_id)]])
        results = request.env['mail.group.message']._read_group(
            domain,
            groupby=['create_date:month'],
            aggregates=['__count'],
        )

        date_groups = []

        locale = get_lang(request.env).code
        fmt = models.READ_GROUP_DISPLAY_FORMAT['month']
        interval = models.READ_GROUP_TIME_GRANULARITY['month']
        for start, count in results:
            label = babel.dates.format_datetime(start, format=fmt, locale=locale)
            date_groups.append({
                'date': label,
                'date_begin': fields.Date.to_string(start),
                'date_end': fields.Date.to_string(start + interval),
                'messages_count': count,
            })

        thread_domain = expression.AND([domain, [('group_message_parent_id', '=', False)]])
        threads_count = request.env['mail.group.message'].search_count(thread_domain)

        return {
            'threads_count': threads_count,
            'threads_time_data': date_groups,
        }

    # ------------------------------------------------------------
    # MAIN PAGE
    # ------------------------------------------------------------

    @http.route('/groups', type='http', auth='public', sitemap=True, website=True)
    def groups_index(self, email='', **kw):
        """View of the group lists. Allow the users to subscribe and unsubscribe."""
        if kw.get('group_id') and kw.get('token'):
            group_id = int(kw.get('group_id'))
            token = kw.get('token')
            group = request.env['mail.group'].browse(group_id).exists().sudo()
            if not group:
                raise werkzeug.exceptions.NotFound()

            if token != group._generate_group_access_token():
                raise werkzeug.exceptions.NotFound()

            mail_groups = group

        else:
            mail_groups = request.env['mail.group'].search([]).sudo()

        if not request.env.user._is_public():
            # Force the email if the user is logged
            email_normalized = request.env.user.email_normalized
            partner_id = request.env.user.partner_id.id
        else:
            email_normalized = tools.email_normalize(email)
            partner_id = None

        members_data = mail_groups._find_members(email_normalized, partner_id)

        return request.render('mail_group.mail_groups', {
            'mail_groups': [{
                'group': group,
                'is_member': bool(members_data.get(group.id, False)),
            } for group in mail_groups],
            'email': email_normalized,
            'is_mail_group_manager': request.env.user.has_group('mail_group.group_mail_group_manager'),
        })

    # ------------------------------------------------------------
    # THREAD DISPLAY / MANAGEMENT
    # ------------------------------------------------------------

    @http.route([
        '/groups/<model("mail.group"):group>',
        '/groups/<model("mail.group"):group>/page/<int:page>',
    ], type='http', auth='public', sitemap=True, website=True)
    def group_view_messages(self, group, page=1, mode='thread', date_begin=None, date_end=None, **post):
        GroupMessage = request.env['mail.group.message']

        domain = expression.AND([self._get_website_domain(), [('mail_group_id', '=', group.id)]])
        if mode == 'thread':
            domain = expression.AND([domain, [('group_message_parent_id', '=', False)]])

        if date_begin and date_end:
            domain = expression.AND([domain, [('create_date', '>', date_begin), ('create_date', '<=', date_end)]])

        # SUDO after the search to apply access rules but be able to read attachments
        messages_sudo = GroupMessage.search(
            domain, limit=self._thread_per_page,
            offset=(page - 1) * self._thread_per_page).sudo()

        pager = portal_pager(
            url=f'/groups/{request.env["ir.http"]._slug(group)}',
            total=GroupMessage.search_count(domain),
            page=page,
            step=self._thread_per_page,
            scope=5,
            url_args={'date_begin': date_begin, 'date_end': date_end, 'mode': mode}
        )

        self._generate_attachments_access_token(messages_sudo)

        return request.render('mail_group.group_messages', {
            'page_name': 'groups',
            'group': group,
            'messages': messages_sudo,
            'archives': self._get_archives(group.id),
            'date_begin': date_begin,
            'date_end': date_end,
            'pager': pager,
            'replies_per_page': self._replies_per_page,
            'mode': mode,
        })

    @http.route('/groups/<model("mail.group"):group>/<model("mail.group.message"):message>',
                type='http', auth='public', sitemap=False, website=True)
    def group_view_message(self, group, message, mode='thread', date_begin=None, date_end=None, **post):
        if group != message.mail_group_id:
            raise werkzeug.exceptions.NotFound()

        GroupMessage = request.env['mail.group.message']
        base_domain = expression.AND([
            self._get_website_domain(),
            [('mail_group_id', '=', group.id),
             ('group_message_parent_id', '=', message.group_message_parent_id.id)],
        ])

        next_message = GroupMessage.search(
            expression.AND([base_domain, [('id', '>', message.id)]]),
            order='id ASC', limit=1)
        prev_message = GroupMessage.search(
            expression.AND([base_domain, [('id', '<', message.id)]]),
            order='id DESC', limit=1)

        message_sudo = message.sudo()
        self._generate_attachments_access_token(message_sudo)

        values = {
            'page_name': 'groups',
            'message': message_sudo,
            'group': group,
            'mode': mode,
            'archives': self._get_archives(group.id),
            'date_begin': date_begin,
            'date_end': date_end,
            'replies_per_page': self._replies_per_page,
            'next_message': next_message,
            'prev_message': prev_message,
        }
        return request.render('mail_group.group_message', values)

    @http.route('/groups/<model("mail.group"):group>/<model("mail.group.message"):message>/get_replies',
                type='json', auth='public', methods=['POST'], website=True)
    def group_message_get_replies(self, group, message, last_displayed_id, **post):
        if group != message.mail_group_id:
            raise werkzeug.exceptions.NotFound()

        replies_domain = expression.AND([
            self._get_website_domain(),
            [('id', '>', int(last_displayed_id)), ('group_message_parent_id', '=', message.id)],
        ])
        # SUDO after the search to apply access rules but be able to read attachments
        replies_sudo = request.env['mail.group.message'].search(replies_domain, limit=self._replies_per_page).sudo()
        message_count = request.env['mail.group.message'].search_count(replies_domain)

        if not replies_sudo:
            return

        message_sudo = message.sudo()

        self._generate_attachments_access_token(message_sudo | replies_sudo)

        values = {
            'group': group,
            'parent_message': message_sudo,
            'messages': replies_sudo,
            'msg_more_count': message_count - self._replies_per_page,
            'replies_per_page': self._replies_per_page,
        }
        return request.env['ir.qweb']._render('mail_group.messages_short', values)

    # ------------------------------------------------------------
    # SUBSCRIPTION
    # ------------------------------------------------------------

    # csrf is disabled here because it will be called by the MUA with unpredictable session at that time
    @http.route('/group/<int:group_id>/unsubscribe_oneclick', website=True, type='http', auth='public',
           methods=['POST'], csrf=False)
    def group_unsubscribe_oneclick(self, group_id, token, email):
        """ Unsubscribe a given user from a given group. One-click unsubscribe
        allow mail user agent to propose a one click button to the user to
        unsubscribe as defined in rfc8058. Only POST method is allowed preventing
        the risk that anti-spam trigger unwanted unsubscribe (scenario explained
        in the same rfc).

        :param int group_id: group ID from which user wants to unsubscribe;
        :param str token: optional access token ensuring security;
        :param email: email to unsubscribe;
        """
        group_sudo = request.env['mail.group'].sudo().browse(group_id).exists()
        # new route parameters
        if group_sudo and token and email:
            correct_token = group_sudo._generate_email_access_token(email)
            if not consteq(correct_token, token):
                raise werkzeug.exceptions.NotFound()
            group_sudo._leave_group(email)
        else:
            raise werkzeug.exceptions.NotFound()
        return Response(status=200)

    @http.route('/group/subscribe', type='json', auth='public', website=True)
    def group_subscribe(self, group_id=0, email=None, token=None, **kw):
        """Subscribe the current logged user or the given email address to the mailing list.

        If the user is logged, the action is automatically done.

        But if the user is not logged (public user) an email will be send with a token
        to confirm the action.

        :param group_id: Id of the group
        :param email: Email to add in the member list
        :param token: An access token to bypass the <mail.group> access rule
        :return:
            'added'
                if the member was added in the mailing list
            'email_sent'
                if we send a confirmation email
            'is_already_member'
                if we try to subscribe but we are already member
        """
        group_sudo, is_member, partner_id = self._group_subscription_get_group(group_id, email, token)

        if is_member:
            return 'is_already_member'

        if not request.env.user._is_public():
            # For logged user, automatically join / leave without sending a confirmation email
            group_sudo._join_group(request.env.user.email, partner_id)
            return 'added'

        # For non-logged user, send an email with a token to confirm the action
        group_sudo._send_subscribe_confirmation_email(email)
        return 'email_sent'

    @http.route('/group/unsubscribe', type='json', auth='public', website=True)
    def group_unsubscribe(self, group_id=0, email=None, token=None, **kw):
        """Unsubscribe the current logged user or the given email address to the mailing list.

        If the user is logged, the action is automatically done.

        But if the user is not logged (public user) an email will be send with a token
        to confirm the action.

        :param group_id: Id of the group
        :param email: Email to add in the member list
        :param token: An access token to bypass the <mail.group> access rule
        :return:
            'removed'
                if the member was removed from the mailing list
            'email_sent'
                if we send a confirmation email
            'is_not_member'
                if we try to unsubscribe but we are not member
        """
        group_sudo, is_member, partner_id = self._group_subscription_get_group(group_id, email, token)

        if not is_member:
            return 'is_not_member'

        if not request.env.user._is_public():
            # For logged user, automatically join / leave without sending a confirmation email
            group_sudo._leave_group(request.env.user.email, partner_id)
            return 'removed'

        # For non-logged user, send an email with a token to confirm the action
        group_sudo._send_unsubscribe_confirmation_email(email)
        return 'email_sent'

    def _group_subscription_get_group(self, group_id, email, token):
        """Check the given token and return,

        :return:
            - The group sudo-ed
            - True if the email is member of the group
            - The partner of the current user
        :raise NotFound: if the given token is not valid
        """
        group = request.env['mail.group'].browse(int(group_id)).exists()
        if not group:
            raise werkzeug.exceptions.NotFound()

        # SUDO to have access to field of the many2one
        group_sudo = group.sudo()

        if token and token != group_sudo._generate_group_access_token():
            raise werkzeug.exceptions.NotFound()

        elif not token:
            try:
                # Check that the current user has access to the group
                group.check_access('read')
            except AccessError:
                raise werkzeug.exceptions.NotFound()

        partner_id = None
        if not request.env.user._is_public():
            partner_id = request.env.user.partner_id.id

        is_member = bool(group_sudo._find_member(email, partner_id))

        return group_sudo, is_member, partner_id

    @http.route('/group/subscribe-confirm', type='http', auth='public', website=True)
    def group_subscribe_confirm(self, group_id, email, token, **kw):
        """Confirm the subscribe / unsubscribe action which was sent by email."""
        group = self._group_subscription_confirm_get_group(group_id, email, token, 'subscribe')
        if not group:
            return request.render('mail_group.invalid_token_subscription')

        partners = request.env['mail.thread'].sudo()._mail_find_partner_from_emails([email])
        partner_id = partners[0].id if partners else None
        group._join_group(email, partner_id)

        return request.render('mail_group.confirmation_subscription', {
            'group': group,
            'email': email,
            'subscribing': True,
        })

    @http.route('/group/unsubscribe-confirm', type='http', auth='public', website=True)
    def group_unsubscribe_confirm(self, group_id, email, token, **kw):
        """Confirm the subscribe / unsubscribe action which was sent by email."""
        group = self._group_subscription_confirm_get_group(group_id, email, token, 'unsubscribe')
        if not group:
            return request.render('mail_group.invalid_token_subscription')

        group._leave_group(email, all_members=True)

        return request.render('mail_group.confirmation_subscription', {
            'group': group,
            'email': email,
            'subscribing': False,
        })

    def _group_subscription_confirm_get_group(self, group_id, email, token, action):
        """Retrieve the group and check the token use to perform the given action."""
        if not group_id or not email or not token:
            return False
        # Here we can SUDO because the token will be checked
        group = request.env['mail.group'].browse(int(group_id)).exists().sudo()
        if not group:
            raise werkzeug.exceptions.NotFound()

        excepted_token = group._generate_action_token(email, action)
        return group if token == excepted_token else False

    def _generate_attachments_access_token(self, messages):
        for message in messages:
            if message.attachment_ids:
                message.attachment_ids.generate_access_token()
            self._generate_attachments_access_token(message.group_message_child_ids)