portal.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. import werkzeug
  4. from odoo import http, fields, tools
  5. from odoo.addons.http_routing.models.ir_http import slug
  6. from odoo.addons.portal.controllers.portal import pager as portal_pager
  7. from odoo.exceptions import AccessError
  8. from odoo.http import request
  9. from odoo.osv import expression
  10. class PortalMailGroup(http.Controller):
  11. _thread_per_page = 20
  12. _replies_per_page = 5
  13. def _get_website_domain(self):
  14. # Base group domain in addition to the security access rules
  15. # Do not show rejected message on the portal view even for admin
  16. return [('moderation_status', '!=', 'rejected')]
  17. def _get_archives(self, group_id):
  18. """Return the different date range and message count for the group messages."""
  19. domain = expression.AND([self._get_website_domain(), [('mail_group_id', '=', group_id)]])
  20. results = request.env['mail.group.message']._read_group_raw(
  21. domain,
  22. ['subject', 'create_date'],
  23. groupby=['create_date'], orderby='create_date')
  24. date_groups = []
  25. for result in results:
  26. (dates_range, label) = result['create_date']
  27. start, end = dates_range.split('/')
  28. date_groups.append({
  29. 'date': label,
  30. 'date_begin': fields.Date.to_string(fields.Date.to_date(start)),
  31. 'date_end': fields.Date.to_string(fields.Date.to_date(end)),
  32. 'messages_count': result['create_date_count'],
  33. })
  34. thread_domain = expression.AND([domain, [('group_message_parent_id', '=', False)]])
  35. threads_count = request.env['mail.group.message'].search_count(thread_domain)
  36. return {
  37. 'threads_count': threads_count,
  38. 'threads_time_data': date_groups,
  39. }
  40. # ------------------------------------------------------------
  41. # MAIN PAGE
  42. # ------------------------------------------------------------
  43. @http.route('/groups', type='http', auth='public', sitemap=True, website=True)
  44. def groups_index(self, email='', **kw):
  45. """View of the group lists. Allow the users to subscribe and unsubscribe."""
  46. if kw.get('group_id') and kw.get('token'):
  47. group_id = int(kw.get('group_id'))
  48. token = kw.get('token')
  49. group = request.env['mail.group'].browse(group_id).exists().sudo()
  50. if not group:
  51. raise werkzeug.exceptions.NotFound()
  52. if token != group._generate_group_access_token():
  53. raise werkzeug.exceptions.NotFound()
  54. mail_groups = group
  55. else:
  56. mail_groups = request.env['mail.group'].search([]).sudo()
  57. if not request.env.user._is_public():
  58. # Force the email if the user is logged
  59. email_normalized = request.env.user.email_normalized
  60. partner_id = request.env.user.partner_id.id
  61. else:
  62. email_normalized = tools.email_normalize(email)
  63. partner_id = None
  64. members_data = mail_groups._find_members(email_normalized, partner_id)
  65. return request.render('mail_group.mail_groups', {
  66. 'mail_groups': [{
  67. 'group': group,
  68. 'is_member': bool(members_data.get(group.id, False)),
  69. } for group in mail_groups],
  70. 'email': email_normalized,
  71. 'is_mail_group_manager': request.env.user.has_group('mail_group.group_mail_group_manager'),
  72. })
  73. # ------------------------------------------------------------
  74. # THREAD DISPLAY / MANAGEMENT
  75. # ------------------------------------------------------------
  76. @http.route([
  77. '/groups/<model("mail.group"):group>',
  78. '/groups/<model("mail.group"):group>/page/<int:page>',
  79. ], type='http', auth='public', sitemap=True, website=True)
  80. def group_view_messages(self, group, page=1, mode='thread', date_begin=None, date_end=None, **post):
  81. GroupMessage = request.env['mail.group.message']
  82. domain = expression.AND([self._get_website_domain(), [('mail_group_id', '=', group.id)]])
  83. if mode == 'thread':
  84. domain = expression.AND([domain, [('group_message_parent_id', '=', False)]])
  85. if date_begin and date_end:
  86. domain = expression.AND([domain, [('create_date', '>', date_begin), ('create_date', '<=', date_end)]])
  87. # SUDO after the search to apply access rules but be able to read attachments
  88. messages_sudo = GroupMessage.search(
  89. domain, limit=self._thread_per_page,
  90. offset=(page - 1) * self._thread_per_page).sudo()
  91. pager = portal_pager(
  92. url=f'/groups/{slug(group)}',
  93. total=GroupMessage.search_count(domain),
  94. page=page,
  95. step=self._thread_per_page,
  96. scope=5,
  97. url_args={'date_begin': date_begin, 'date_end': date_end, 'mode': mode}
  98. )
  99. self._generate_attachments_access_token(messages_sudo)
  100. return request.render('mail_group.group_messages', {
  101. 'page_name': 'groups',
  102. 'group': group,
  103. 'messages': messages_sudo,
  104. 'archives': self._get_archives(group.id),
  105. 'date_begin': date_begin,
  106. 'date_end': date_end,
  107. 'pager': pager,
  108. 'replies_per_page': self._replies_per_page,
  109. 'mode': mode,
  110. })
  111. @http.route('/groups/<model("mail.group"):group>/<model("mail.group.message"):message>',
  112. type='http', auth='public', sitemap=True, website=True)
  113. def group_view_message(self, group, message, mode='thread', date_begin=None, date_end=None, **post):
  114. if group != message.mail_group_id:
  115. raise werkzeug.exceptions.NotFound()
  116. GroupMessage = request.env['mail.group.message']
  117. base_domain = expression.AND([
  118. self._get_website_domain(),
  119. [('mail_group_id', '=', group.id),
  120. ('group_message_parent_id', '=', message.group_message_parent_id.id)],
  121. ])
  122. next_message = GroupMessage.search(
  123. expression.AND([base_domain, [('id', '>', message.id)]]),
  124. order='id ASC', limit=1)
  125. prev_message = GroupMessage.search(
  126. expression.AND([base_domain, [('id', '<', message.id)]]),
  127. order='id DESC', limit=1)
  128. message_sudo = message.sudo()
  129. self._generate_attachments_access_token(message_sudo)
  130. values = {
  131. 'page_name': 'groups',
  132. 'message': message_sudo,
  133. 'group': group,
  134. 'mode': mode,
  135. 'archives': self._get_archives(group.id),
  136. 'date_begin': date_begin,
  137. 'date_end': date_end,
  138. 'replies_per_page': self._replies_per_page,
  139. 'next_message': next_message,
  140. 'prev_message': prev_message,
  141. }
  142. return request.render('mail_group.group_message', values)
  143. @http.route('/groups/<model("mail.group"):group>/<model("mail.group.message"):message>/get_replies',
  144. type='json', auth='public', methods=['POST'], website=True)
  145. def group_message_get_replies(self, group, message, last_displayed_id, **post):
  146. if group != message.mail_group_id:
  147. raise werkzeug.exceptions.NotFound()
  148. replies_domain = expression.AND([
  149. self._get_website_domain(),
  150. [('id', '>', int(last_displayed_id)), ('group_message_parent_id', '=', message.id)],
  151. ])
  152. # SUDO after the search to apply access rules but be able to read attachments
  153. replies_sudo = request.env['mail.group.message'].search(replies_domain, limit=self._replies_per_page).sudo()
  154. message_count = request.env['mail.group.message'].search_count(replies_domain)
  155. if not replies_sudo:
  156. return
  157. message_sudo = message.sudo()
  158. self._generate_attachments_access_token(message_sudo | replies_sudo)
  159. values = {
  160. 'group': group,
  161. 'parent_message': message_sudo,
  162. 'messages': replies_sudo,
  163. 'msg_more_count': message_count - self._replies_per_page,
  164. 'replies_per_page': self._replies_per_page,
  165. }
  166. return request.env['ir.qweb']._render('mail_group.messages_short', values)
  167. # ------------------------------------------------------------
  168. # SUBSCRIPTION
  169. # ------------------------------------------------------------
  170. @http.route('/group/subscribe', type='json', auth='public', website=True)
  171. def group_subscribe(self, group_id=0, email=None, token=None, **kw):
  172. """Subscribe the current logged user or the given email address to the mailing list.
  173. If the user is logged, the action is automatically done.
  174. But if the user is not logged (public user) an email will be send with a token
  175. to confirm the action.
  176. :param group_id: Id of the group
  177. :param email: Email to add in the member list
  178. :param token: An access token to bypass the <mail.group> access rule
  179. :return:
  180. 'added'
  181. if the member was added in the mailing list
  182. 'email_sent'
  183. if we send a confirmation email
  184. 'is_already_member'
  185. if we try to subscribe but we are already member
  186. """
  187. group_sudo, is_member, partner_id = self._group_subscription_get_group(group_id, email, token)
  188. if is_member:
  189. return 'is_already_member'
  190. if not request.env.user._is_public():
  191. # For logged user, automatically join / leave without sending a confirmation email
  192. group_sudo._join_group(request.env.user.email, partner_id)
  193. return 'added'
  194. # For non-logged user, send an email with a token to confirm the action
  195. group_sudo._send_subscribe_confirmation_email(email)
  196. return 'email_sent'
  197. @http.route('/group/unsubscribe', type='json', auth='public', website=True)
  198. def group_unsubscribe(self, group_id=0, email=None, token=None, **kw):
  199. """Unsubscribe the current logged user or the given email address to the mailing list.
  200. If the user is logged, the action is automatically done.
  201. But if the user is not logged (public user) an email will be send with a token
  202. to confirm the action.
  203. :param group_id: Id of the group
  204. :param email: Email to add in the member list
  205. :param token: An access token to bypass the <mail.group> access rule
  206. :return:
  207. 'removed'
  208. if the member was removed from the mailing list
  209. 'email_sent'
  210. if we send a confirmation email
  211. 'is_not_member'
  212. if we try to unsubscribe but we are not member
  213. """
  214. group_sudo, is_member, partner_id = self._group_subscription_get_group(group_id, email, token)
  215. if not is_member:
  216. return 'is_not_member'
  217. if not request.env.user._is_public():
  218. # For logged user, automatically join / leave without sending a confirmation email
  219. group_sudo._leave_group(request.env.user.email, partner_id)
  220. return 'removed'
  221. # For non-logged user, send an email with a token to confirm the action
  222. group_sudo._send_unsubscribe_confirmation_email(email)
  223. return 'email_sent'
  224. def _group_subscription_get_group(self, group_id, email, token):
  225. """Check the given token and return,
  226. :return:
  227. - The group sudo-ed
  228. - True if the email is member of the group
  229. - The partner of the current user
  230. :raise NotFound: if the given token is not valid
  231. """
  232. group = request.env['mail.group'].browse(int(group_id)).exists()
  233. if not group:
  234. raise werkzeug.exceptions.NotFound()
  235. # SUDO to have access to field of the many2one
  236. group_sudo = group.sudo()
  237. if token and token != group_sudo._generate_group_access_token():
  238. raise werkzeug.exceptions.NotFound()
  239. elif not token:
  240. try:
  241. # Check that the current user has access to the group
  242. group.check_access_rights('read')
  243. group.check_access_rule('read')
  244. except AccessError:
  245. raise werkzeug.exceptions.NotFound()
  246. partner_id = None
  247. if not request.env.user._is_public():
  248. partner_id = request.env.user.partner_id.id
  249. is_member = bool(group_sudo._find_member(email, partner_id))
  250. return group_sudo, is_member, partner_id
  251. @http.route('/group/subscribe-confirm', type='http', auth='public', website=True)
  252. def group_subscribe_confirm(self, group_id, email, token, **kw):
  253. """Confirm the subscribe / unsubscribe action which was sent by email."""
  254. group = self._group_subscription_confirm_get_group(group_id, email, token, 'subscribe')
  255. if not group:
  256. return request.render('mail_group.invalid_token_subscription')
  257. partners = request.env['mail.thread'].sudo()._mail_find_partner_from_emails([email])
  258. partner_id = partners[0].id if partners else None
  259. group._join_group(email, partner_id)
  260. return request.render('mail_group.confirmation_subscription', {
  261. 'group': group,
  262. 'email': email,
  263. 'subscribing': True,
  264. })
  265. @http.route('/group/unsubscribe-confirm', type='http', auth='public', website=True)
  266. def group_unsubscribe_confirm(self, group_id, email, token, **kw):
  267. """Confirm the subscribe / unsubscribe action which was sent by email."""
  268. group = self._group_subscription_confirm_get_group(group_id, email, token, 'unsubscribe')
  269. if not group:
  270. return request.render('mail_group.invalid_token_subscription')
  271. group._leave_group(email, all_members=True)
  272. return request.render('mail_group.confirmation_subscription', {
  273. 'group': group,
  274. 'email': email,
  275. 'subscribing': False,
  276. })
  277. def _group_subscription_confirm_get_group(self, group_id, email, token, action):
  278. """Retrieve the group and check the token use to perform the given action."""
  279. if not group_id or not email or not token:
  280. return False
  281. # Here we can SUDO because the token will be checked
  282. group = request.env['mail.group'].browse(int(group_id)).exists().sudo()
  283. if not group:
  284. raise werkzeug.exceptions.NotFound()
  285. excepted_token = group._generate_action_token(email, action)
  286. return group if token == excepted_token else False
  287. def _generate_attachments_access_token(self, messages):
  288. for message in messages:
  289. if message.attachment_ids:
  290. message.attachment_ids.generate_access_token()
  291. self._generate_attachments_access_token(message.group_message_child_ids)