diff --git a/addons/account/account_invoice.py b/addons/account/account_invoice.py index a1c53eff4b1..c543b493831 100644 --- a/addons/account/account_invoice.py +++ b/addons/account/account_invoice.py @@ -1308,17 +1308,19 @@ class account_invoice(osv.osv): def create_send_note(self, cr, uid, ids, context=None): for obj in self.browse(cr, uid, ids, context=context): - self.message_post(cr, uid, [obj.id], body=_("%s created.") % (_(self._get_document_type(obj.type))), context=context) + self.message_post(cr, uid, [obj.id], body=_("%s created.") % (self._get_document_type(obj.type)), + subtype="account.mt_invoice_new", context=context) def confirm_paid_send_note(self, cr, uid, ids, context=None): - for obj in self.browse(cr, uid, ids, context=context): - self.message_post(cr, uid, [obj.id], body=_("%s paid.") % (_(self._get_document_type(obj.type))), context=context) + for obj in self.browse(cr, uid, ids, context=context): + self.message_post(cr, uid, [obj.id], body=_("%s paid.") % (self._get_document_type(obj.type)), + subtype="account.mt_invoice_paid", context=context) def invoice_cancel_send_note(self, cr, uid, ids, context=None): for obj in self.browse(cr, uid, ids, context=context): - self.message_post(cr, uid, [obj.id], body=_("%s cancelled.") % (_(self._get_document_type(obj.type))), context=context) + self.message_post(cr, uid, [obj.id], body=_("%s cancelled.") % (self._get_document_type(obj.type)), + context=context) -account_invoice() class account_invoice_line(osv.osv): diff --git a/addons/account/account_invoice_view.xml b/addons/account/account_invoice_view.xml index 4a587eee6fe..17ebfe1f744 100644 --- a/addons/account/account_invoice_view.xml +++ b/addons/account/account_invoice_view.xml @@ -278,7 +278,6 @@
-
@@ -438,7 +437,6 @@
-
diff --git a/addons/account/data/account_data.xml b/addons/account/data/account_data.xml index e8899d0348a..dd27cfbab4e 100644 --- a/addons/account/data/account_data.xml +++ b/addons/account/data/account_data.xml @@ -518,7 +518,6 @@ - Account Reconcile account.reconcile @@ -554,8 +553,6 @@ - - @@ -564,7 +561,14 @@ account.invoice - - + + + created + account.invoice + + + paid + account.invoice + diff --git a/addons/account_voucher/account_voucher.py b/addons/account_voucher/account_voucher.py index 75de84c0277..4b0365e1a11 100644 --- a/addons/account_voucher/account_voucher.py +++ b/addons/account_voucher/account_voucher.py @@ -1304,17 +1304,17 @@ class account_voucher(osv.osv): def create_send_note(self, cr, uid, ids, context=None): for obj in self.browse(cr, uid, ids, context=context): message = "%s created." % self._document_type[obj.type or False] - self.message_post(cr, uid, [obj.id], body=message, context=context) + self.message_post(cr, uid, [obj.id], body=message, subtype="account_voucher.mt_voucher", context=context) def post_send_note(self, cr, uid, ids, context=None): for obj in self.browse(cr, uid, ids, context=context): message = "%s '%s' is posted." % (self._document_type[obj.type or False], obj.move_id.name) - self.message_post(cr, uid, [obj.id], body=message, context=context) + self.message_post(cr, uid, [obj.id], body=message, subtype="account_voucher.mt_voucher", context=context) def reconcile_send_note(self, cr, uid, ids, context=None): for obj in self.browse(cr, uid, ids, context=context): message = "%s reconciled." % self._document_type[obj.type or False] - self.message_post(cr, uid, [obj.id], body=message, context=context) + self.message_post(cr, uid, [obj.id], body=message, subtype="account_voucher.mt_voucher", context=context) account_voucher() diff --git a/addons/account_voucher/account_voucher_data.xml b/addons/account_voucher/account_voucher_data.xml index fd25fec9493..3d4252e697d 100644 --- a/addons/account_voucher/account_voucher_data.xml +++ b/addons/account_voucher/account_voucher_data.xml @@ -1,6 +1,7 @@ + mail.group @@ -13,5 +14,12 @@

You can track customer payments easily and automate follow-ups. You get an overview of the discussion with your customers on each invoice for easier traceability. For advanced accounting features, you should install the "Accounting and Finance" module.

]]>
+ + + + Status Change + account.voucher + +
diff --git a/addons/account_voucher/account_voucher_view.xml b/addons/account_voucher/account_voucher_view.xml index 697d2affcf0..5e26c366778 100644 --- a/addons/account_voucher/account_voucher_view.xml +++ b/addons/account_voucher/account_voucher_view.xml @@ -108,7 +108,6 @@
-
diff --git a/addons/account_voucher/voucher_payment_receipt_view.xml b/addons/account_voucher/voucher_payment_receipt_view.xml index 8e86e44942a..78b76e5c9b7 100644 --- a/addons/account_voucher/voucher_payment_receipt_view.xml +++ b/addons/account_voucher/voucher_payment_receipt_view.xml @@ -243,7 +243,6 @@
-
@@ -519,7 +518,6 @@
-
diff --git a/addons/account_voucher/voucher_sales_purchase_view.xml b/addons/account_voucher/voucher_sales_purchase_view.xml index d3449cf6027..5bcecfa268e 100644 --- a/addons/account_voucher/voucher_sales_purchase_view.xml +++ b/addons/account_voucher/voucher_sales_purchase_view.xml @@ -146,7 +146,6 @@
-
@@ -302,7 +301,6 @@
-
diff --git a/addons/analytic/__openerp__.py b/addons/analytic/__openerp__.py index 417d2b29b20..6227cd369d3 100644 --- a/addons/analytic/__openerp__.py +++ b/addons/analytic/__openerp__.py @@ -38,7 +38,8 @@ that have no counterpart in the general financial accounts. 'security/analytic_security.xml', 'security/ir.model.access.csv', 'analytic_sequence.xml', - 'analytic_view.xml' + 'analytic_view.xml', + 'analytic_data.xml' ], 'demo': [], 'installable': True, diff --git a/addons/analytic/analytic.py b/addons/analytic/analytic.py index 269bf24ed61..ff09349b8a2 100644 --- a/addons/analytic/analytic.py +++ b/addons/analytic/analytic.py @@ -284,7 +284,8 @@ class account_analytic_account(osv.osv): def create_send_note(self, cr, uid, ids, context=None): for obj in self.browse(cr, uid, ids, context=context): - self.message_post(cr, uid, [obj.id], body=_("Contract for %s has been created.") % (obj.partner_id.name), context=context) + self.message_post(cr, uid, [obj.id], body=_("Contract for %s has been created.") % (obj.partner_id.name), + subtype="analytic.mt_account_status", context=context) account_analytic_account() diff --git a/addons/analytic/analytic_data.xml b/addons/analytic/analytic_data.xml new file mode 100644 index 00000000000..67940fb2063 --- /dev/null +++ b/addons/analytic/analytic_data.xml @@ -0,0 +1,12 @@ + + + + + + + Status Change + account.analytic.account + + + + diff --git a/addons/analytic/analytic_view.xml b/addons/analytic/analytic_view.xml index bed588015ad..51f5428002f 100644 --- a/addons/analytic/analytic_view.xml +++ b/addons/analytic/analytic_view.xml @@ -53,7 +53,6 @@
-
diff --git a/addons/base_calendar/crm_meeting_view.xml b/addons/base_calendar/crm_meeting_view.xml index 8373d32ab0d..adcfc23a520 100644 --- a/addons/base_calendar/crm_meeting_view.xml +++ b/addons/base_calendar/crm_meeting_view.xml @@ -223,7 +223,6 @@
-
diff --git a/addons/base_status/base_stage.py b/addons/base_status/base_stage.py index 2cb5a4c1859..f5c9d779896 100644 --- a/addons/base_status/base_stage.py +++ b/addons/base_status/base_stage.py @@ -359,6 +359,9 @@ class base_stage(object): """ return '' + def find_subtype(self, cr, uid, ids, name, context=None): + return "mail.mt_comment" + def stage_set_send_note(self, cr, uid, ids, stage_id, context=None): """ Send a notification when the stage changes. This method has to be overriden, because each document will have its particular @@ -370,19 +373,22 @@ class base_stage(object): def case_open_send_note(self, cr, uid, ids, context=None): for id in ids: msg = _('%s has been opened.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context)) - self.message_post(cr, uid, [id], body=msg, context=context) + xml_id = self.find_subtype(cr, uid, ids, context=context) + self.message_post(cr, uid, [id], body=msg, subtype=xml_id, context=context) return True def case_close_send_note(self, cr, uid, ids, context=None): for id in ids: msg = _('%s has been closed.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context)) - self.message_post(cr, uid, [id], body=msg, context=context) + xml_id = self.find_subtype(cr, uid, ids, context=context) + self.message_post(cr, uid, [id], body=msg, subtype=xml_id, context=context) return True def case_cancel_send_note(self, cr, uid, ids, context=None): for id in ids: - msg = _('%s has been canceled.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context)) - self.message_post(cr, uid, [id], body=msg, context=context) + msg = _('%s has been cancelled.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context)) + xml_id = self.find_subtype(cr, uid, ids, context=context) + self.message_post(cr, uid, [id], body=msg, subtype=xml_id, context=context) return True def case_pending_send_note(self, cr, uid, ids, context=None): diff --git a/addons/crm/crm_lead_view.xml b/addons/crm/crm_lead_view.xml index f0311f8fce5..f91a8756598 100644 --- a/addons/crm/crm_lead_view.xml +++ b/addons/crm/crm_lead_view.xml @@ -223,7 +223,6 @@
-
@@ -526,7 +525,6 @@
-
diff --git a/addons/crm/crm_phonecall_view.xml b/addons/crm/crm_phonecall_view.xml index b8bc6b3a13e..ea20624d738 100644 --- a/addons/crm/crm_phonecall_view.xml +++ b/addons/crm/crm_phonecall_view.xml @@ -149,7 +149,6 @@
-
diff --git a/addons/crm_claim/crm_claim_view.xml b/addons/crm_claim/crm_claim_view.xml index b319339bcfd..9643a56bfe2 100644 --- a/addons/crm_claim/crm_claim_view.xml +++ b/addons/crm_claim/crm_claim_view.xml @@ -181,7 +181,6 @@
-
diff --git a/addons/crm_helpdesk/crm_helpdesk_view.xml b/addons/crm_helpdesk/crm_helpdesk_view.xml index d2be33e4584..52f006ca7b2 100644 --- a/addons/crm_helpdesk/crm_helpdesk_view.xml +++ b/addons/crm_helpdesk/crm_helpdesk_view.xml @@ -95,7 +95,6 @@
-
diff --git a/addons/email_template/tests/test_mail.py b/addons/email_template/tests/test_mail.py index 1091c34ad5c..69afd5bc167 100644 --- a/addons/email_template/tests/test_mail.py +++ b/addons/email_template/tests/test_mail.py @@ -167,8 +167,9 @@ class test_message_compose(test_mail.TestMailMockups): # Test: partner_ids: p_a_id (default) + 3 newly created partners message_pigs_pids = [partner.id for partner in message_pigs.partner_ids] message_bird_pids = [partner.id for partner in message_bird.partner_ids] - partner_ids = self.res_partner.search(cr, uid, [('email', 'in', ['b@b.b', 'c@c.c', 'd@d.d'])]) + [p_a_id] + partner_ids = self.res_partner.search(cr, uid, [('email', 'in', ['b@b.b', 'c@c.c', 'd@d.d'])]) self.assertEqual(len(message_pigs_pids), len(partner_ids), 'mail.message on pigs incorrect number of partner_ids') - self.assertEqual(len(message_bird_pids), len(partner_ids), 'mail.message on bird partner_ids incorrect') self.assertEqual(set(message_pigs_pids), set(partner_ids), 'mail.message on pigs incorrect number of partner_ids') + + self.assertEqual(len(message_bird_pids), len(partner_ids), 'mail.message on bird partner_ids incorrect') self.assertEqual(set(message_bird_pids), set(partner_ids), 'mail.message on bird partner_ids incorrect') diff --git a/addons/event/__openerp__.py b/addons/event/__openerp__.py index 9fa2dfc3e04..aa9f209df88 100644 --- a/addons/event/__openerp__.py +++ b/addons/event/__openerp__.py @@ -45,6 +45,7 @@ Key Features 'security/ir.model.access.csv', 'wizard/event_confirm_view.xml', 'event_view.xml', + 'event_data.xml', 'report/report_event_registration_view.xml', 'board_association_view.xml', 'res_partner_view.xml', diff --git a/addons/event/event.py b/addons/event/event.py index 6512139faee..0eb2dda8312 100644 --- a/addons/event/event.py +++ b/addons/event/event.py @@ -360,8 +360,10 @@ class event_registration(osv.osv): return self.write(cr, uid, ids, {'state': 'draft'}, context=context) def confirm_registration(self, cr, uid, ids, context=None): - self.message_post(cr, uid, ids, body=_('State set to open'), context=context) - return self.write(cr, uid, ids, {'state': 'open'}, context=context) + for reg in self.browse(cr, uid, ids, context=context or {}): + self.pool.get('event.event').message_post(cr, uid, [reg.event_id.id], body=_('New registration confirmed: %s.') % (reg.name or '', ),subtype="event.mt_event_registration", context=context) + self.message_post(cr, uid, ids, body=_('Registration confirmed.'), context=context) + return self.write(cr, uid, ids, {'state': 'open'},context=context) def create(self, cr, uid, vals, context=None): obj_id = super(event_registration, self).create(cr, uid, vals, context) @@ -392,7 +394,7 @@ class event_registration(osv.osv): self.write(cr, uid, ids, values) self.message_post(cr, uid, ids, body=_('State set to Done'), context=context) else: - raise osv.except_osv(_('Error!'),_("You must wait for the starting day of the event to do this action.") ) + raise osv.except_osv(_('Error!'), _("You must wait for the starting day of the event to do this action.")) return True def button_reg_cancel(self, cr, uid, ids, context=None, *args): diff --git a/addons/event/event_data.xml b/addons/event/event_data.xml index 5e34ddcfe02..47efca17d6d 100644 --- a/addons/event/event_data.xml +++ b/addons/event/event_data.xml @@ -12,14 +12,19 @@ open - - - mail.group - - notification - Events Organisation application installed! - From the top Events menu, you can organize events, manage registrations, automate communication around your event and sell events through your quotations. + + + New Registrations + event.event + + + + + + From the top menu Events, you can organize events, manage registrations, automate communication around your event and sell events through your quotations. + Module Events has been installed + diff --git a/addons/event/event_view.xml b/addons/event/event_view.xml index f786592d06c..6ad8fb20467 100644 --- a/addons/event/event_view.xml +++ b/addons/event/event_view.xml @@ -196,7 +196,6 @@
-
@@ -479,7 +478,6 @@
-
diff --git a/addons/hr_expense/hr_expense_view.xml b/addons/hr_expense/hr_expense_view.xml index f35547927fa..388e5262f6e 100644 --- a/addons/hr_expense/hr_expense_view.xml +++ b/addons/hr_expense/hr_expense_view.xml @@ -139,7 +139,6 @@
-
diff --git a/addons/hr_holidays/hr_holidays.py b/addons/hr_holidays/hr_holidays.py index 76d4e42818a..306e5201829 100644 --- a/addons/hr_holidays/hr_holidays.py +++ b/addons/hr_holidays/hr_holidays.py @@ -350,46 +350,43 @@ class hr_holidays(osv.osv): # ----------------------------- def needaction_domain_get(self, cr, uid, ids, context=None): - # to be tested, otherwise convert into employee_id in ... emp_obj = self.pool.get('hr.employee') - empids = emp_obj.search(cr, uid, [('parent_id.user_id','=',uid)], context=context) - dom = [ - '&', ('state','=','confirm'),('employee_id', 'in', empids) - ] + empids = emp_obj.search(cr, uid, [('parent_id.user_id', '=', uid)], context=context) + dom = ['&', ('state', '=', 'confirm'), ('employee_id', 'in', empids)] # if this user is a hr.manager, he should do second validations if self.pool.get('res.users').has_group(cr, uid, 'base.group_hr_manager'): - dom = ['|'] + dom + [ ('state','=','validate1') ] + dom = ['|'] + dom + [('state', '=', 'validate1')] return dom def create_notificate(self, cr, uid, ids, context=None): for obj in self.browse(cr, uid, ids, context=context): - self.message_post(cr, uid, ids, - _("The request has been created and is waiting confirmation."), context=context) + self.message_post(cr, uid, ids, + _("Request created, waiting confirmation."), context=context) return True - + def holidays_confirm_notificate(self, cr, uid, ids, context=None): for obj in self.browse(cr, uid, ids): self.message_post(cr, uid, [obj.id], - _("The request has been submitted and is waiting for validation by the manager."), context=context) - + _("Request submitted, waiting for validation by the manager."), context=context) + def holidays_first_validate_notificate(self, cr, uid, ids, context=None): for obj in self.browse(cr, uid, ids, context=context): self.message_post(cr, uid, [obj.id], - _("The request has been approved. A second validation is necessary and is now pending."), context=context) - + _("Request approved, waiting second validation."), context=context) + def holidays_validate_notificate(self, cr, uid, ids, context=None): for obj in self.browse(cr, uid, ids): if obj.double_validation: - self.message_post(cr, uid, [obj.id], - _("The request has been double validated. The validation process is now over."), context=context) + self.message_post(cr, uid, [obj.id], + _("Request validated."), context=context) else: self.message_post(cr, uid, [obj.id], - _("The request has been approved. The validation process is now over."), context=context) - + _("The request has been approved."), context=context) + def holidays_refuse_notificate(self, cr, uid, ids, context=None): for obj in self.browse(cr, uid, ids): self.message_post(cr, uid, [obj.id], - _("The request has been refused. The validation process is now over."), context=context) + _("Request refused"), context=context) class resource_calendar_leaves(osv.osv): diff --git a/addons/hr_holidays/hr_holidays_view.xml b/addons/hr_holidays/hr_holidays_view.xml index 0a2054b14d2..347e2b17ca5 100644 --- a/addons/hr_holidays/hr_holidays_view.xml +++ b/addons/hr_holidays/hr_holidays_view.xml @@ -121,7 +121,6 @@
-
@@ -159,7 +158,6 @@
-
diff --git a/addons/hr_recruitment/hr_recruitment.py b/addons/hr_recruitment/hr_recruitment.py index 902b436ed29..31959d7e9c7 100644 --- a/addons/hr_recruitment/hr_recruitment.py +++ b/addons/hr_recruitment/hr_recruitment.py @@ -461,7 +461,7 @@ class hr_applicant(base_stage, osv.Model): """ Override of the (void) default notification method. """ if not stage_id: return True stage_name = self.pool.get('hr.recruitment.stage').name_get(cr, uid, [stage_id], context=context)[0][1] - return self.message_post(cr, uid, ids, body= _("Stage changed to %s.") % (stage_name), context=context) + return self.message_post(cr, uid, ids, body=_("Stage changed to %s.") % (stage_name), context=context) def case_get_note_msg_prefix(self, cr, uid, id, context=None): return 'Applicant' @@ -474,6 +474,8 @@ class hr_applicant(base_stage, osv.Model): if context is None: context = {} for applicant in self.browse(cr, uid, ids, context=context): + if applicant.job_id: + self.pool.get('hr.job').message_post(cr, uid, [applicant.job_id.id], body=_('New employee joined the company %s.')%(applicant.name,), subtype="hr_recruitment.mt_hired", context=context) if applicant.emp_id: message = _("Applicant has been hired and created as an employee.") self.message_post(cr, uid, [applicant.id], body=message, context=context) @@ -492,6 +494,9 @@ class hr_applicant(base_stage, osv.Model): def create_send_note(self, cr, uid, ids, context=None): message = _("Applicant has been created.") + for applicant in self.browse(cr, uid, ids, context=context): + if applicant.job_id: + self.pool.get('hr.job').message_post(cr, uid, [applicant.job_id.id], body=message, subtype="hr_recruitment.mt_applicant_new", context=context) return self.message_post(cr, uid, ids, body=message, context=context) class hr_job(osv.osv): diff --git a/addons/hr_recruitment/hr_recruitment_data.xml b/addons/hr_recruitment/hr_recruitment_data.xml index 84289da7bae..288231a4000 100644 --- a/addons/hr_recruitment/hr_recruitment_data.xml +++ b/addons/hr_recruitment/hr_recruitment_data.xml @@ -460,6 +460,15 @@ You can automatically receive job application though an email gateway, see the H
- + + + + Employee Hired + hr.job + + + New Applicant + hr.job + diff --git a/addons/hr_recruitment/hr_recruitment_view.xml b/addons/hr_recruitment/hr_recruitment_view.xml index 32c26573e8c..08e5f31a2d2 100644 --- a/addons/hr_recruitment/hr_recruitment_view.xml +++ b/addons/hr_recruitment/hr_recruitment_view.xml @@ -183,7 +183,6 @@
-
diff --git a/addons/hr_timesheet_invoice/hr_timesheet_invoice.py b/addons/hr_timesheet_invoice/hr_timesheet_invoice.py index 53a90aa26ed..b9574b553d1 100644 --- a/addons/hr_timesheet_invoice/hr_timesheet_invoice.py +++ b/addons/hr_timesheet_invoice/hr_timesheet_invoice.py @@ -85,7 +85,7 @@ class account_analytic_account(osv.osv): return res def on_change_partner_id(self, cr, uid, ids, partner_id, name, context=None): - res = super(account_analytic_account,self).on_change_partner_id(cr, uid, ids,partner_id, name, context=context) + res = super(account_analytic_account, self).on_change_partner_id(cr, uid, ids, partner_id, name, context=context) part = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context) pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False if pricelist: @@ -93,25 +93,25 @@ class account_analytic_account(osv.osv): return res def set_close(self, cr, uid, ids, context=None): - self.write(cr, uid, ids, {'state':'close'}, context=context) + self.write(cr, uid, ids, {'state': 'close'}, context=context) message = _("Contract has been closed.") - self.message_post(cr, uid, ids, body=message, context=context) + self.message_post(cr, uid, ids, body=message, subtype="mt_account_closed", context=context) return True def set_cancel(self, cr, uid, ids, context=None): - self.write(cr, uid, ids, {'state':'cancelled'}, context=context) - message = _("Contract has been cancelled.") - self.message_post(cr, uid, ids, body=message, context=context) + self.write(cr, uid, ids, {'state': 'cancelled'}, context=context) + message = _("Contract has been canceled.") + self.message_post(cr, uid, ids, body=message, subtype="mt_account_canceled", context=context) return True def set_open(self, cr, uid, ids, context=None): - self.write(cr, uid, ids, {'state':'open'}, context=context) + self.write(cr, uid, ids, {'state': 'open'}, context=context) message = _("Contract has been opened.") self.message_post(cr, uid, ids, body=message, context=context) return True def set_pending(self, cr, uid, ids, context=None): - self.write(cr, uid, ids, {'state':'pending'}, context=context) + self.write(cr, uid, ids, {'state': 'pending'}, context=context) message = _("Contract has been set as pending.") self.message_post(cr, uid, ids, body=message, context=context) return True diff --git a/addons/idea/idea.py b/addons/idea/idea.py index 9b4a9c580a9..a382948e15c 100644 --- a/addons/idea/idea.py +++ b/addons/idea/idea.py @@ -71,19 +71,17 @@ class idea_idea(osv.osv): self.message_post(cr, uid, ids, body=_('Idea canceled.'), context=context) return True - def idea_open(self, cr, uid, ids, context=None): - self.write(cr, uid, ids, { 'state': 'open'}) + def idea_open(self, cr, uid, ids, context={}): + self.write(cr, uid, ids, {'state': 'open'}, context=context) self.message_post(cr, uid, ids, body=_('Idea accepted.'), context=context) return True - def idea_close(self, cr, uid, ids, context=None): - self.message_post(cr, uid, ids, body=_('Idea done.'), context=context) - self.write(cr, uid, ids, { 'state': 'close' }) + def idea_close(self, cr, uid, ids, context={}): + self.write(cr, uid, ids, {'state': 'close'}, context=context) + self.message_post(cr, uid, ids, body=_('Idea closed.'), context=context) return True - def idea_draft(self, cr, uid, ids, context=None): + def idea_draft(self, cr, uid, ids, context={}): + self.write(cr, uid, ids, {'state': 'draft'}, context=context) self.message_post(cr, uid, ids, body=_('Idea reset to draft.'), context=context) - self.write(cr, uid, ids, { 'state': 'draft' }) return True - -idea_idea() diff --git a/addons/idea/idea_data.xml b/addons/idea/idea_data.xml index d63eb907ef8..421cb6a9eef 100644 --- a/addons/idea/idea_data.xml +++ b/addons/idea/idea_data.xml @@ -15,6 +15,7 @@ + diff --git a/addons/idea/idea_view.xml b/addons/idea/idea_view.xml index 8547761b4c3..5a1482830e1 100644 --- a/addons/idea/idea_view.xml +++ b/addons/idea/idea_view.xml @@ -79,7 +79,6 @@
-
diff --git a/addons/mail/__init__.py b/addons/mail/__init__.py index b384613d174..92e882ae6d8 100644 --- a/addons/mail/__init__.py +++ b/addons/mail/__init__.py @@ -19,6 +19,7 @@ # ############################################################################## +import mail_message_subtype import mail_alias import mail_followers import mail_message diff --git a/addons/mail/__openerp__.py b/addons/mail/__openerp__.py index 4e42b74597e..39a3f1bee13 100644 --- a/addons/mail/__openerp__.py +++ b/addons/mail/__openerp__.py @@ -49,6 +49,7 @@ Main Features 'data': [ 'wizard/invite_view.xml', 'wizard/mail_compose_message_view.xml', + 'mail_message_subtype.xml', 'res_config_view.xml', 'mail_message_view.xml', 'mail_mail_view.xml', diff --git a/addons/mail/data/mail_data.xml b/addons/mail/data/mail_data.xml index c8e8b1a488e..5fe1ee45b1d 100644 --- a/addons/mail/data/mail_data.xml +++ b/addons/mail/data/mail_data.xml @@ -12,5 +12,9 @@ + + + comment + diff --git a/addons/mail/doc/index.rst.inc b/addons/mail/doc/index.rst.inc index c4b87cc18f9..4b01d3ec7bd 100644 --- a/addons/mail/doc/index.rst.inc +++ b/addons/mail/doc/index.rst.inc @@ -11,3 +11,4 @@ Mail Module documentation topics mail_needaction_howto mail_partner mail_state + mail_subtype diff --git a/addons/mail/doc/mail_subtype.rst b/addons/mail/doc/mail_subtype.rst new file mode 100644 index 00000000000..e63b67936cd --- /dev/null +++ b/addons/mail/doc/mail_subtype.rst @@ -0,0 +1,70 @@ +.. _mail_message_subtype: + +OpenChatter Pi (3.1415): Message Subtype +======================================== + + To overcome the problems of crowdy walls in system notification, We have added features of **Message Subtype** in mail. + +mail.message.subtype +++++++++++++++++++++ +``mail.message.subtype`` has following fields: + + - ``Name``: fields.char(' Message Subtype ', size = 128,required = True,help = 'Subtype Of Message'), + - ``model_ids``: fields.many2many('ir.model','mail_message_subtyp_message_rel','message_subtype_id', 'model_id', 'Model',help = "link some subtypes to several models, for projet/task"), + - ``default``: fields.boolean('Default', help = "When subscribing to the document, users will receive by default messages related to this subtype unless they uncheck this subtype"), + +mail.followers +++++++++++++++ + +In ``mail.followers`` we have added additional many2many field subtype ids : + + - ``subtype_ids``: fields.many2many('mail.message.subtype','mail_message_subtyp_rel','subscription_id', 'subtype_id', 'Subtype',help = "linking some subscription to several subtype for projet/task") + +mail.message +++++++++++++ + +In mail_message we have added additional field subtype_id which Indicates the Type of Message + + - ``subtype_id``: fields.many2one('mail.message.subtype', 'Subtype') + +mail.thread ++++++++++++ + + - In **message_post** method add the *subtype_id* field as parameter and set as default subtype 'Other'. + + def message_post(self, cr, uid, thread_id, body='', subject=False, msg_type='notification', parent_id=False, attachments=None, subtype='other', context=None, ``**kwargs``): + + - In **message_subscribe** method add the *subtype_ids* field as parameter.In this method if subtype_ids is None, it fatch the default true subtypes in mail.message.subtypes otherwise pass selected subtypes. + For update subtypes call **message_subscribe_udpate_subtypes** method + + def message_subscribe(self, cr, uid, ids, partner_ids,subtype_ids = None, context=None): + + - Add **message_subscribe_udpate_subtypes** method to update the subtype_ids in followers. + + def message_subscribe_udpate_subtypes(self, cr, uid, ids, user_id, subtype_ids,context=None): + followers_obj = self.pool.get('mail.followers') + followers_ids = followers_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', 'in', ids)]) + return followers_obj.write(cr, uid, followers_ids, {'subtype_ids': [(6, 0 , subtype_ids)]}, context = context) + +For Each Addons: +++++++++++++++++ + + - Add data of subtypes for each addons module. + - Add subtype field as parameter in **message_post** Method for each addons module. + +How It Works: ++++++++++++++ + + - In addons module when we Follow a Perticular document It display under the followers button. + - In sybtypes there are 3 default subtypes for each addons + 1) Email + 2) Comment + 3) Other + - In document display a default subtypes(which are true) related a perticular model_ids wise. + + Example:- + If I have open crm.lead, It display only subtypes of crm.lead + + - When we select subtype it update subtype_ids(which are checked) in mail.follower where match res_model & res_id of the current documents. + - when message created update subtype_id of that message in mail.message. + - In Feeds display only those notifications of documents which subtypes are selected diff --git a/addons/mail/mail_followers.py b/addons/mail/mail_followers.py index 23417850f2a..6f122a87ba5 100644 --- a/addons/mail/mail_followers.py +++ b/addons/mail/mail_followers.py @@ -46,6 +46,8 @@ class mail_followers(osv.Model): help='Id of the followed resource'), 'partner_id': fields.many2one('res.partner', string='Related Partner', ondelete='cascade', required=True, select=1), + 'subtype_ids': fields.many2many('mail.message.subtype', string='Subtype', + help="Message subtypes followed, meaning subtypes that will be pushed onto the user's Wall."), } @@ -81,18 +83,27 @@ class mail_notification(osv.Model): return super(mail_notification, self).create(cr, uid, vals, context=context) return False - def set_message_read(self, cr, uid, msg_id, context=None): - partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id - notif_ids = self.search(cr, uid, [('partner_id', '=', partner_id), ('message_id', '=', msg_id)], context=context) - return self.write(cr, uid, notif_ids, {'read': True}, context=context) + def set_message_read(self, cr, uid, msg_ids, read=None, context=None): + if msg_ids == None: + return False + if type(msg_ids) is not list: + msg_ids=[msg_ids] - def get_partners_to_notify(self, cr, uid, partner_ids, message, context=None): + partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id + notif_ids = self.search(cr, uid, [('partner_id', '=', partner_id), ('message_id', 'in', msg_ids)], context=context) + + return self.write(cr, uid, notif_ids, {'read': read}, context=context) + + def get_partners_to_notify(self, cr, uid, message, context=None): """ Return the list of partners to notify, based on their preferences. :param browse_record message: mail.message to notify """ notify_pids = [] - for partner in self.pool.get('res.partner').browse(cr, SUPERUSER_ID, partner_ids, context=context): + for notification in message.notification_ids: + if notification.read: + continue + partner = notification.partner_id # Do not send an email to the writer if partner.user_ids and partner.user_ids[0].id == uid: continue @@ -111,15 +122,15 @@ class mail_notification(osv.Model): notify_pids.append(partner.id) return notify_pids - def notify(self, cr, uid, partner_ids, msg_id, context=None): + def _notify(self, cr, uid, msg_id, context=None): """ Send by email the notification depending on the user preferences """ context = context or {} # mail_noemail (do not send email) or no partner_ids: do not send, return - if context.get('mail_noemail') or not partner_ids: + if context.get('mail_noemail'): return True msg = self.pool.get('mail.message').browse(cr, uid, msg_id, context=context) - notify_partner_ids = self.get_partners_to_notify(cr, uid, partner_ids, msg, context=context) + notify_partner_ids = self.get_partners_to_notify(cr, uid, msg, context=context) if not notify_partner_ids: return True diff --git a/addons/mail/mail_followers_view.xml b/addons/mail/mail_followers_view.xml index 36f08b3814a..9fc8a545a20 100644 --- a/addons/mail/mail_followers_view.xml +++ b/addons/mail/mail_followers_view.xml @@ -16,6 +16,28 @@ + + mail.followers.form + mail.followers + form + +
+ + + + + + + + + + + + +
+
+
+ mail.notification.tree diff --git a/addons/mail/mail_group.py b/addons/mail/mail_group.py index 60e3b9bd3af..3febe34be51 100644 --- a/addons/mail/mail_group.py +++ b/addons/mail/mail_group.py @@ -30,6 +30,7 @@ class mail_group(osv.Model): group. The group mechanics are based on the followers. """ _description = 'Discussion group' _name = 'mail.group' + _mail_autothread = False _inherit = ['mail.thread'] _inherits = {'mail.alias': 'alias_id', 'ir.ui.menu': 'menu_id'} @@ -157,7 +158,7 @@ class mail_group(osv.Model): def write(self, cr, uid, ids, vals, context=None): result = super(mail_group, self).write(cr, uid, ids, vals, context=context) if vals.get('group_ids'): - self._subscribe_users(cr, uid, ids, vals.get('group_ids'), context=context) + self._subscribe_users(cr, uid, ids, context=context) return result def action_follow(self, cr, uid, ids, context=None): diff --git a/addons/mail/mail_group_view.xml b/addons/mail/mail_group_view.xml index 94b9f8f9b0b..6268cd425b1 100644 --- a/addons/mail/mail_group_view.xml +++ b/addons/mail/mail_group_view.xml @@ -15,9 +15,10 @@ - + +
@@ -81,7 +82,6 @@
- diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py index 7d9c7b3085f..bbf68509277 100644 --- a/addons/mail/mail_message.py +++ b/addons/mail/mail_message.py @@ -131,6 +131,7 @@ class mail_message(osv.Model): 'unread': fields.function(_get_unread, fnct_search=_search_unread, type='boolean', string='Unread', help='Functional field to search for unread messages linked to uid'), + 'subtype_id': fields.many2one('mail.message.subtype', 'Subtype'), 'vote_user_ids': fields.many2many('res.users', 'mail_vote', 'message_id', 'user_id', string='Votes', help='Users that voted for this message'), } @@ -167,7 +168,7 @@ class mail_message(osv.Model): self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(4, user_id)]}, context=context) else: self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(3, user_id)]}, context=context) - return True + return not(has_voted) or False #------------------------------------------------------ # Message loading for web interface @@ -179,6 +180,7 @@ class mail_message(osv.Model): fields allow to have the foreign record name without having to check external access rights). """ + child_nbr = len(msg.child_ids) has_voted = False vote_ids = self.pool.get('res.users').name_get(cr, SUPERUSER_ID, [user.id for user in msg.vote_user_ids], context=context) for vote in vote_ids: @@ -191,7 +193,7 @@ class mail_message(osv.Model): attachment_ids = [] try: author_id = self.pool.get('res.partner').name_get(cr, uid, [msg.author_id.id], context=context)[0] - is_author = uid in msg.author_id.user_ids + is_author = uid == msg.author_id.user_ids[0].id except (orm.except_orm, osv.except_osv): author_id = False is_author = False @@ -199,6 +201,7 @@ class mail_message(osv.Model): partner_ids = self.pool.get('res.partner').name_get(cr, uid, [x.id for x in msg.partner_ids], context=context) except (orm.except_orm, osv.except_osv): partner_ids = [] + return { 'id': msg.id, 'type': msg.type, @@ -212,12 +215,30 @@ class mail_message(osv.Model): 'author_id': author_id, 'is_author': is_author, 'partner_ids': partner_ids, - 'child_ids': [], + 'parent_id': msg.parent_id and msg.parent_id.id or False, 'vote_user_ids': vote_ids, - 'has_voted': has_voted + 'has_voted': has_voted, + 'unread': msg.unread and msg.unread['unread'] or False } - def message_read_tree_flatten(self, cr, uid, messages, current_level, level, context=None): + def message_read_tree_get_expandable(self, cr, uid, parent_message, last_message, domain=[], current_level=0, level=0, context=None): + """ . """ + base_domain = [('id', '<', last_message['id'])] + if parent_message and current_level < level: + base_domain += [('parent_id', '=', parent_message['id'])] + elif parent_message: + base_domain += [('id', 'child_of', parent_message['id']), ('id', '!=', parent_message['id'])] + if domain: + base_domain += domain + extension = { 'type': 'expandable', + 'domain': base_domain, + 'thread_level': current_level, + 'context': context, + 'id': -1, + } + return extension + + def message_read_tree_flatten(self, cr, uid, parent_message, messages, domain=[], level=0, current_level=0, context=None, limit=None, add_expandable=True): """ Given a tree with several roots of following structure : [ {'id': 1, 'child_ids': [ {'id': 11, 'child_ids': [...] },], @@ -236,69 +257,143 @@ class mail_message(osv.Model): child_ids = msg_dict.pop('child_ids', []) msg_dict['child_ids'] = [] return [msg_dict] + child_ids - # return sorted([msg_dict] + child_ids, key=itemgetter('id'), reverse=True) + context = context or {} + limit = limit or self._message_read_limit + # Depth-first flattening for message in messages: if message.get('type') == 'expandable': continue - message['child_ids'] = self.message_read_tree_flatten(cr, uid, message['child_ids'], current_level + 1, level, context=context) + message['child_ids'] = self.message_read_tree_flatten(cr, uid, message, message['child_ids'], domain, level, current_level + 1, context=context, limit=limit) + for child in message['child_ids']: + if child.get('type') == 'expandable': + continue + message['child_nbr'] += child['child_nbr'] # Flatten if above maximum depth if current_level < level: return_list = messages else: - return_list = [] - for message in messages: - for flat_message in _flatten(message): - return_list.append(flat_message) - return sorted(return_list, key=itemgetter(context.get('sort_key', 'id')), reverse=context.get('sort_reverse', True)) + return_list = [flat_message for message in messages for flat_message in _flatten(message)] - def message_read(self, cr, uid, ids=False, domain=[], thread_level=0, limit=None, context=None): - """ If IDs are provided, fetch these records. Otherwise use the domain - to fetch the matching records. - After having fetched the records provided by IDs, it will fetch the - parents to have well-formed threads. + # Add expandable + return_list = sorted(return_list, key=itemgetter(context.get('sort_key', 'id')), reverse=context.get('sort_reverse', True)) + if return_list and current_level == 0 and add_expandable: + expandable = self.message_read_tree_get_expandable(cr, uid, parent_message, return_list and return_list[-1] or parent_message, domain, current_level, level, context=context) + return_list.append(expandable) + elif return_list and current_level <= level and add_expandable: + expandable = self.message_read_tree_get_expandable(cr, uid, parent_message, return_list and return_list[-1] or parent_message, domain, current_level, level, context=context) + return_list.append(expandable) + return return_list + + def message_read(self, cr, uid, ids=False, domain=[], level=0, context=None, parent_id=False, limit=None): + """ Read messages from mail.message, and get back a structured tree + of messages to be displayed as discussion threads. If IDs is set, + fetch these records. Otherwise use the domain to fetch messages. + After having fetch messages, their parents will be added to obtain + well formed threads. + + :param domain: optional domain for searching ids + :param level: level of threads to display, 0 being flat + :param limit: number of messages to fetch + :param parent_id: if parent_id reached, stop searching for + further parents :return list: list of trees of messages """ + + message_loaded = context and context.get('message_loaded') or [0] + + # don't read the message display by .js, in context message_loaded list + if context and context.get('message_loaded'): + domain += [ ['id','not in',message_loaded] ]; + limit = limit or self._message_read_limit context = context or {} - if not ids: - ids = self.search(cr, SUPERUSER_ID, domain, context=context, limit=limit) - messages = self.browse(cr, uid, ids, context=context) + tree = [] result = [] - tree = {} # key: ID, value: record - for msg in messages: - if len(result) < (limit - 1): - record = self._message_dict_get(cr, uid, msg, context=context) - if thread_level and msg.parent_id: - while msg.parent_id: - if msg.parent_id.id in tree: - record_parent = tree[msg.parent_id.id] - else: - record_parent = self._message_dict_get(cr, uid, msg.parent_id, context=context) - if msg.parent_id.parent_id: - tree[msg.parent_id.id] = record_parent - if record['id'] not in [x['id'] for x in record_parent['child_ids']]: - record_parent['child_ids'].append(record) - record = record_parent - msg = msg.parent_id - if msg.id not in tree: - result.append(record) - tree[msg.id] = record - else: - result.append({ - 'type': 'expandable', - 'domain': [('id', '<=', msg.id)] + domain, - 'context': context, - 'thread_level': thread_level, # should be improve accodting to level of records - 'id': -1, - }) - break + record = None + + # select ids + if ids: + for msg in self.browse(cr, uid, ids, context=context): + result.append(self._message_dict_get(cr, uid, msg, context=context)) + return result + + # key: ID, value: record + ids = self.search(cr, SUPERUSER_ID, domain, context=context, limit=limit) + for msg in self.browse(cr, uid, ids, context=context): + # if not in record and not in message_loded list + if msg.id not in tree and msg.id not in message_loaded : + record = self._message_dict_get(cr, uid, msg, context=context) + tree.append(msg.id) + result.append(record) + + while msg.parent_id and msg.parent_id.id != parent_id: + parent_id = msg.parent_id.id + if msg.parent_id.id not in tree: + msg = msg.parent_id + tree.append(msg.id) + # if not in record and not in message_loded list + if msg.id not in message_loaded : + record = self._message_dict_get(cr, uid, msg, context=context) + result.append(record) + + result = sorted(result, key=lambda k: k['id']) + + + tree_not = [] + # expandable for not show message + for id_msg in tree: + # get all childs + not_loaded_ids = self.search(cr, SUPERUSER_ID, [['parent_id','=',id_msg],['id','not in',message_loaded]], None, limit=1000) + # group childs not read + id_min=None + id_max=None + nb=0 + for not_loaded_id in not_loaded_ids: + if not_loaded_id not in tree: + nb+=1 + if id_min==None or id_min>not_loaded_id: + id_min=not_loaded_id + if id_max==None or id_max0: + result.append({ + 'domain': [['id','>=',id_min],['id','<=',id_max],['parent_id','=',id_msg]], + 'nb_messages': nb, + 'type': 'expandable', + 'parent_id': id_msg, + 'id': id_min + }) + nb=0 + if nb>0: + result.append({ + 'domain': [['id','>=',id_min],['parent_id','=',id_msg]], + 'nb_messages': nb, + 'type': 'expandable', + 'parent_id': id_msg, + 'id': id_min + }) + + + # expandable for limit max + ids = self.search(cr, SUPERUSER_ID, domain+[['id','not in',message_loaded+tree+tree_not]], context=context, limit=1) + if len(ids) > 0: + result.append( + { + 'domain': domain, + 'nb_messages': 0, + 'type': 'expandable', + 'parent_id': parent_id, + 'id': -1 + }); + + + result = sorted(result, key=lambda k: k['id']) - # Flatten the result - if thread_level > 0: - result = self.message_read_tree_flatten(cr, uid, result, 0, thread_level, context=context) return result #------------------------------------------------------ @@ -409,7 +504,7 @@ class mail_message(osv.Model): if not values.get('message_id') and values.get('res_id') and values.get('model'): values['message_id'] = tools.generate_tracking_message_id('%(model)s-%(res_id)s' % values) newid = super(mail_message, self).create(cr, uid, values, context) - self.notify(cr, uid, newid, context=context) + self._notify(cr, 1, newid, context=context) return newid def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'): @@ -431,26 +526,39 @@ class mail_message(osv.Model): self.pool.get('ir.attachment').unlink(cr, uid, attachments_to_delete, context=context) return super(mail_message, self).unlink(cr, uid, ids, context=context) - def notify(self, cr, uid, newid, context=None): + def _notify(self, cr, uid, newid, context=None): """ Add the related record followers to the destination partner_ids. Call mail_notification.notify to manage the email sending """ message = self.browse(cr, uid, newid, context=context) partners_to_notify = set([]) - # add all partner_ids of the message + # message has no subtype_id: pure log message -> no partners, no one notified + if not message.subtype_id: + message.write({'partner_ids': [5]}) + return True + # all partner_ids of the mail.message have to be notified if message.partner_ids: partners_to_notify |= set(partner.id for partner in message.partner_ids) - # add all followers and set add them in partner_ids + # all followers of the mail.message document have to be added as partners and notified if message.model and message.res_id: - record = self.pool.get(message.model).browse(cr, SUPERUSER_ID, message.res_id, context=context) - extra_notified = set(partner.id for partner in record.message_follower_ids) + fol_obj = self.pool.get("mail.followers") + fol_ids = fol_obj.search(cr, uid, [('res_model', '=', message.model), ('res_id', '=', message.res_id), ('subtype_ids', 'in', message.subtype_id.id)], context=context) + fol_objs = fol_obj.browse(cr, uid, fol_ids, context=context) + extra_notified = set(fol.partner_id.id for fol in fol_objs) missing_notified = extra_notified - partners_to_notify + missing_notified = missing_notified if missing_notified: self.write(cr, SUPERUSER_ID, [newid], {'partner_ids': [(4, p_id) for p_id in missing_notified]}, context=context) partners_to_notify |= extra_notified - # # remove uid from partners - self.write(cr, SUPERUSER_ID, [newid], {'partner_ids': [(3, uid)]}, context=context) - self.pool.get('mail.notification').notify(cr, uid, list(partners_to_notify), newid, context=context) + + # add myself if I wrote on my wall, + # unless remove myself author + if ((message.model=="res.partner" and message.res_id==message.author_id.id)): + self.write(cr, SUPERUSER_ID, [newid], {'partner_ids': [(4, message.author_id.id)]}, context=context) + else: + self.write(cr, SUPERUSER_ID, [newid], {'partner_ids': [(3, message.author_id.id)]}, context=context) + + self.pool.get('mail.notification')._notify(cr, uid, newid, context=context) def copy(self, cr, uid, id, default=None, context=None): """Overridden to avoid duplicating fields that are unique to each email""" diff --git a/addons/mail/mail_message_subtype.py b/addons/mail/mail_message_subtype.py new file mode 100644 index 00000000000..32b20d9c11e --- /dev/null +++ b/addons/mail/mail_message_subtype.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2012-today OpenERP SA () +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see +# +############################################################################## + +from osv import osv +from osv import fields + + +class mail_message_subtype(osv.osv): + """ Class holding subtype definition for messages. Subtypes allow to tune + the follower subscription, allowing only some subtypes to be pushed + on the Wall. """ + _name = 'mail.message.subtype' + _description = 'mail_message_subtype' + _columns = { + 'name': fields.char('Message Type', required=True, translate=True, + help='Message subtype, gives a more precise type on the message, '\ + 'especially for system notifications. For example, it can be '\ + 'a notification related to a new record (New), or to a stage '\ + 'change in a process (Stage change). Message subtypes allow to '\ + 'precisely tune the notifications the user want to receive on its wall.'), + 'res_model': fields.char('Model', help="link subtype to model"), + 'default': fields.boolean('Default', + help="When subscribing to the document, this subtype will be checked by default."), + } + _defaults = { + 'default': True, + } diff --git a/addons/mail/mail_message_subtype.xml b/addons/mail/mail_message_subtype.xml new file mode 100644 index 00000000000..c1cd510642d --- /dev/null +++ b/addons/mail/mail_message_subtype.xml @@ -0,0 +1,43 @@ + + + + + + mail.message.subtype.tree + mail.message.subtype + 10 + + + + + + + + + + + mail.message.subtype.form + mail.message.subtype + +
+ + + + + + + +
+
+
+ + + Subtypes + mail.message.subtype + form + tree,form + + + +
+
diff --git a/addons/mail/mail_message_view.xml b/addons/mail/mail_message_view.xml index 8e7129e8165..cc0419352af 100644 --- a/addons/mail/mail_message_view.xml +++ b/addons/mail/mail_message_view.xml @@ -31,6 +31,7 @@ + @@ -55,16 +56,18 @@ - + - - - @@ -75,20 +78,28 @@ form tree,form + {'search_default_unread_message':True} - - News Feed + + Inbox mail.wall - - - My Posts + + Archives + mail.wall + + + + + Sent mail.wall diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 061647c388a..943bc5f0164 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -96,6 +96,7 @@ class many2many_reference(fields.many2many): else: return super(many2many_reference, self).set(cr, model, id, name, values, user, context) + class mail_thread(osv.AbstractModel): ''' mail_thread model is meant to be inherited by any model that needs to act as a discussion topic on which messages can be attached. Public @@ -116,10 +117,13 @@ class mail_thread(osv.AbstractModel): ''' _name = 'mail.thread' _description = 'Email Thread' + _mail_autothread = True def _get_message_data(self, cr, uid, ids, name, args, context=None): + """ Computes: + - message_unread: has uid unread message for the document + - message_summary: html snippet summarizing the Chatter for kanban views """ res = dict((id, dict(message_unread=False, message_summary='')) for id in ids) - partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0] # search for unread messages, by reading directly mail.notification, as SUPERUSER notif_obj = self.pool.get('mail.notification') @@ -132,10 +136,41 @@ class mail_thread(osv.AbstractModel): for notif in notif_obj.browse(cr, SUPERUSER_ID, notif_ids, context=context): res[notif.message_id.res_id]['message_unread'] = True - for thread in self.read(cr, uid, ids, ['message_follower_ids', 'message_comment_ids', 'message_ids'], context=context): - cls = res[thread['id']]['message_unread'] and ' class="oe_kanban_mail_new"' or '' - res[thread['id']]['message_summary'] = "9 %d + %d" % (cls, len(thread['message_comment_ids']), len(thread['message_follower_ids'])) - res[thread['id']]['message_is_follower'] = partner_id in thread['message_follower_ids'] + for thread in self.browse(cr, uid, ids, context=context): + cls = res[thread.id]['message_unread'] and ' class="oe_kanban_mail_new"' or '' + res[thread.id]['message_summary'] = "9 %d + %d" % (cls, len(thread.message_comment_ids), len(thread.message_follower_ids)) + + return res + + def _get_subscription_data(self, cr, uid, ids, name, args, context=None): + """ Computes: + - message_is_follower: is uid in the document followers + - message_subtype_data: data about document subtypes: which are + available, which are followed if any """ + res = dict((id, dict(message_subtype_data='', message_is_follower=False)) for id in ids) + user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0] + + # find current model subtypes, add them to a dictionary + subtype_obj = self.pool.get('mail.message.subtype') + subtype_ids = subtype_obj.search(cr, uid, ['|', ('res_model', '=', self._name), ('res_model', '=', False)], context=context) + subtype_dict = dict((subtype.name, dict(default=subtype.default, followed=False, id=subtype.id)) for subtype in subtype_obj.browse(cr, uid, subtype_ids, context=context)) + for id in ids: + res[id]['message_subtype_data'] = subtype_dict.copy() + + # find the document followers, update the data + fol_obj = self.pool.get('mail.followers') + fol_ids = fol_obj.search(cr, uid, [ + ('partner_id', '=', user_pid), + ('res_id', 'in', ids), + ('res_model', '=', self._name), + ], context=context) + for fol in fol_obj.browse(cr, uid, fol_ids, context=context): + thread_subtype_dict = res[fol.res_id]['message_subtype_data'] + res[fol.res_id]['message_is_follower'] = True + for subtype in fol.subtype_ids: + thread_subtype_dict[subtype.name]['followed'] = True + res[fol.res_id]['message_subtype_data'] = thread_subtype_dict + return res def _search_unread(self, cr, uid, obj=None, name=None, domain=None, context=None): @@ -152,8 +187,13 @@ class mail_thread(osv.AbstractModel): return [('id', 'in', res.keys())] _columns = { - 'message_is_follower': fields.function(_get_message_data, - type='boolean', string='Is a Follower', multi='_get_message_data'), + 'message_is_follower': fields.function(_get_subscription_data, + type='boolean', string='Is a Follower', multi='_get_subscription_data,'), + 'message_subtype_data': fields.function(_get_subscription_data, + type='text', string='Subscription data', multi="_get_subscription_data", + help="Holds data about the subtypes. The content of this field "\ + "is a structure holding the current model subtypes, and the "\ + "current document followed subtypes."), 'message_follower_ids': many2many_reference('res.partner', 'mail_followers', 'res_id', 'partner_id', reference_column='res_model', string='Followers'), @@ -571,12 +611,12 @@ class mail_thread(osv.AbstractModel): "now deprecated res.log.") self.message_post(cr, uid, [id], message, context=context) - def message_post(self, cr, uid, thread_id, body='', subject=False, - type='notification', parent_id=False, attachments=None, context=None, **kwargs): + def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification', + subtype=None, parent_id=False, attachments=None, context=None, **kwargs): """ Post a new message in an existing thread, returning the new mail.message ID. Extra keyword arguments will be used as default column values for the new mail.message record. - + Auto link messages for same id and object :param int thread_id: thread ID to post into, or list with one ID :param str body: body of the message, usually raw HTML that will be sanitized @@ -587,9 +627,10 @@ class mail_thread(osv.AbstractModel): ``(name,content)``, where content is NOT base64 encoded :return: ID of newly created mail.message """ + context = context or {} attachments = attachments or [] - assert (not thread_id) or isinstance(thread_id, (int,long)) or \ + assert (not thread_id) or isinstance(thread_id, (int, long)) or \ (isinstance(thread_id, (list, tuple)) and len(thread_id) == 1), "Invalid thread_id" if isinstance(thread_id, (list, tuple)): thread_id = thread_id and thread_id[0] @@ -608,39 +649,86 @@ class mail_thread(osv.AbstractModel): } attachment_ids.append((0, 0, data_attach)) + # get subtype + if not subtype: + subtype = 'mail.mt_comment' + s = subtype.split('.') + if len(s)==1: + s = ('mail', s[0]) + ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, s[0], s[1]) + subtype_id = ref and ref[1] or False + + model = context.get('thread_model', self._name) if thread_id else False + messages = self.pool.get('mail.message') + + #auto link messages for same id and object + if self._mail_autothread and thread_id: + message_ids = messages.search(cr, uid, ['&',('res_id', '=', thread_id),('model','=',model)], context=context) + if len(message_ids): + parent_id = min(message_ids) + + values = kwargs values.update({ - 'model': context.get('thread_model', self._name) if thread_id else False, + 'model': model, 'res_id': thread_id or False, 'body': body, - 'subject': subject, + 'subject': subject or False, 'type': type, 'parent_id': parent_id, 'attachment_ids': attachment_ids, + 'subtype_id': subtype_id, }) - for x in ('from', 'to', 'cc'): values.pop(x, None) # Avoid warnings - return self.pool.get('mail.message').create(cr, uid, values, context=context) + # Avoid warnings about non-existing fields + for x in ('from', 'to', 'cc'): + values.pop(x, None) + + return messages.create(cr, uid, values, context=context) #------------------------------------------------------ # Followers API #------------------------------------------------------ - def message_subscribe_users(self, cr, uid, ids, user_ids=None, context=None): + def message_post_api(self, cr, uid, thread_id, body='', subject=False, type='notification', + subtype=None, parent_id=False, attachments=None, context=None, **kwargs): + added_message_id = self.message_post(cr, uid, thread_id=thread_id, body=body, subject=subject, type=type, + subtype=subtype, parent_id=parent_id, attachments=attachments, context=context) + added_message = self.pool.get('mail.message').message_read(cr, uid, [added_message_id]) + + return added_message + + def get_message_subtypes(self, cr, uid, ids, context=None): + """ message_subtype_data: data about document subtypes: which are + available, which are followed if any """ + return self._get_subscription_data(cr, uid, ids, None, None, context=context) + + def message_subscribe_users(self, cr, uid, ids, user_ids=None, subtype_ids=None, context=None): """ Wrapper on message_subscribe, using users. If user_ids is not provided, subscribe uid instead. """ - if not user_ids: user_ids = [uid] - partner_ids = [user['partner_id'][0] for user in self.pool.get('res.users').read(cr, uid, user_ids, ['partner_id'], context=context)] - return self.message_subscribe(cr, uid, ids, partner_ids, context=context) + if not user_ids: + user_ids = [uid] + partner_ids = [user.partner_id.id for user in self.pool.get('res.users').browse(cr, uid, user_ids, context=context)] + return self.message_subscribe(cr, uid, ids, partner_ids, subtype_ids=subtype_ids, context=context) - def message_subscribe(self, cr, uid, ids, partner_ids, context=None): + def message_subscribe(self, cr, uid, ids, partner_ids, subtype_ids=None, context=None): """ Add partners to the records followers. """ - return self.write(cr, uid, ids, {'message_follower_ids': [(4, pid) for pid in partner_ids]}, context=context) + self.write(cr, uid, ids, {'message_follower_ids': [(4, pid) for pid in partner_ids]}, context=context) + # if subtypes are not specified (and not set to a void list), fetch default ones + if subtype_ids is None: + subtype_obj = self.pool.get('mail.message.subtype') + subtype_ids = subtype_obj.search(cr, uid, [('default', '=', True), '|', ('res_model', '=', self._name), ('res_model', '=', False)], context=context) + # update the subscriptions + fol_obj = self.pool.get('mail.followers') + fol_ids = fol_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', 'in', ids), ('partner_id', 'in', partner_ids)], context=context) + fol_obj.write(cr, uid, fol_ids, {'subtype_ids': [(6, 0, subtype_ids)]}, context=context) + return True def message_unsubscribe_users(self, cr, uid, ids, user_ids=None, context=None): """ Wrapper on message_subscribe, using users. If user_ids is not provided, unsubscribe uid instead. """ - if not user_ids: user_ids = [uid] - partner_ids = [user['partner_id'][0] for user in self.pool.get('res.users').read(cr, uid, user_ids, ['partner_id'], context=context)] + if not user_ids: + user_ids = [uid] + partner_ids = [user.partner_id.id for user in self.pool.get('res.users').browse(cr, uid, user_ids, context=context)] return self.message_unsubscribe(cr, uid, ids, partner_ids, context=context) def message_unsubscribe(self, cr, uid, ids, partner_ids, context=None): @@ -674,3 +762,5 @@ class mail_thread(osv.AbstractModel): partner_id = %s ''', (ids, self._name, partner_id)) return True + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/mail/mail_thread_view.xml b/addons/mail/mail_thread_view.xml index b4b1aade755..3507e7c449f 100644 --- a/addons/mail/mail_thread_view.xml +++ b/addons/mail/mail_thread_view.xml @@ -12,16 +12,22 @@ - - My Feeds + + Inbox - + - - My Posts + + Archives - + + + + + Sent + + diff --git a/addons/mail/res_partner.py b/addons/mail/res_partner.py index 2988cee3d9c..363ea74d16c 100644 --- a/addons/mail/res_partner.py +++ b/addons/mail/res_partner.py @@ -25,6 +25,7 @@ class res_partner_mail(osv.Model): """ Update partner to add a field about notification preferences """ _name = "res.partner" _inherit = ['res.partner', 'mail.thread'] + _mail_autothread = False _columns = { 'notification_email_send': fields.selection([ @@ -41,5 +42,4 @@ class res_partner_mail(osv.Model): 'notification_email_send': lambda *args: 'comment' } - # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/mail/res_partner_view.xml b/addons/mail/res_partner_view.xml index 5c051fc89b3..2a8430926d5 100644 --- a/addons/mail/res_partner_view.xml +++ b/addons/mail/res_partner_view.xml @@ -9,7 +9,6 @@
- diff --git a/addons/mail/security/ir.model.access.csv b/addons/mail/security/ir.model.access.csv index 26048e7a40c..b68a9ab7443 100644 --- a/addons/mail/security/ir.model.access.csv +++ b/addons/mail/security/ir.model.access.csv @@ -11,4 +11,7 @@ access_mail_group_all,mail.group.all,model_mail_group,,1,0,0,0 access_mail_group_user,mail.group.user,model_mail_group,base.group_user,1,1,1,1 access_mail_alias_all,mail.alias.all,model_mail_alias,,1,0,0,0 access_mail_alias_user,mail.alias,model_mail_alias,base.group_user,1,1,1,0 +access_mail_alias_system,mail.alias,model_mail_alias,base.group_system,1,1,1,1 +access_mail_message_subtype,mail.message.subtype,model_mail_message_subtype,,1,1,1,1 +access_mail_mail_user,mail.mail,model_mail_mail,base.group_user,1,1,1,0 access_mail_vote_all,mail.vote.all,model_mail_vote,,1,1,1,1 diff --git a/addons/mail/static/src/css/mail.css b/addons/mail/static/src/css/mail.css index e69eaadbcdb..364ac7e3165 100644 --- a/addons/mail/static/src/css/mail.css +++ b/addons/mail/static/src/css/mail.css @@ -14,7 +14,6 @@ margin: 0; } - /* ------------------------------------------------------------ */ /* Wall /* ------------------------------------------------------------ */ @@ -37,6 +36,21 @@ list-style-type: none; } +/* ------------------------------------------------------------ */ +/* Followers +/* ------------------------------------------------------------ */ + +.openerp div.oe_mail_recthread_aside h4 { + display: inline-block; +} +.openerp div.oe_mail_recthread_aside button { + position: relative; +} +.openerp div.oe_mail_recthread_aside label, +.openerp div.oe_mail_recthread_aside input { + cursor:pointer; +} + /* Specific display of threads in the wall */ /* ------------------------------------------------------------ */ @@ -50,22 +64,12 @@ border-top: 0; } -.openerp div.oe_mail_thread_subthread img { +.openerp div.oe_thread_placeholder img { width: 28px; height: 28px; } -.openerp div.oe_mail_msg_content { - position: relative; - width: 486px; -} - -.openerp div.oe_mail_msg_content > li { - float: left; - margin-right: 3px; -} - -.openerp div.oe_mail_thread_subthread div.oe_mail_msg_content { +.openerp div.oe_thread_placeholder div.oe_mail_msg_content { width: 440px; } @@ -78,7 +82,7 @@ overflow: auto; } -.openerp div.oe_mail_recthread_main { +.openerp .oe_mail_record_wall > .oe_mail_wall_threads { float: left; width: 560px; } @@ -96,7 +100,7 @@ width: 120px; } -.openerp button.oe_mail_button_mouseout { +.openerp .oe_mail_recthread_aside .oe_follower.oe_follow { color: white; background-color: #8a89ba; background-image: -webkit-gradient(linear, left top, left bottom, from(#8a89ba), to(#807fb4)); @@ -106,9 +110,7 @@ background-image: -o-linear-gradient(top, #8a89ba, #807fb4); background-image: linear-gradient(to bottom, #8a89ba, #807fb4); } - -.openerp button.oe_mail_button_mouseover { - display: none; +.openerp .oe_mail_recthread_aside .oe_follower.oe_following { color: white; background-color: #dc5f59; background-image: -webkit-gradient(linear, left top, left bottom, from(#dc5f59), to(#b33630)); @@ -119,17 +121,38 @@ background-image: linear-gradient(to bottom, #dc5f59, #b33630); } + +.openerp .oe_mail_recthread_aside .oe_follower span { + display:none; +} +.openerp .oe_mail_recthread_aside .oe_following span.oe_following, +.openerp .oe_mail_recthread_aside .oe_notfollow span.oe_follow { + display:block; +} + .openerp div.oe_mail_recthread_followers { margin-bottom: 8px; } /* ------------------------------------------------------------ */ -/* Followers +/* subtypes /* ------------------------------------------------------------ */ -.openerp div.oe_mail_recthread_aside h4 { - display: inline-block; +.openerp .oe_mouse_subtypes { + display:inline-block; + position: relative; + z-index: 5; +} +.openerp .oe_mouse_subtypes .oe_recthread_subtypes { + position: absolute; + z-index: 2; +} +.openerp .oe_mouse_subtypes.oe_mouseout .oe_recthread_subtypes { + display: none; +} +.openerp .oe_mouse_subtypes.oe_mouseover .oe_recthread_subtypes { + display: block; } /* ------------------------------------------------------------ */ @@ -140,6 +163,8 @@ display: none; white-space: normal; padding: 8px; + z-index:5; + background: #fff; } .openerp div.oe_mail_thread_action:after { @@ -168,15 +193,20 @@ -box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6); } +.openerp .oe_mail_vote_count, +.openerp .oe_mail_msg_vote{ + vertical-align: bottom; +} + .openerp div.oe_mail_thread_display { white-space: normal; } -.openerp div.oe_mail_thread_subthread { +.openerp div.oe_thread_placeholder { margin-left: 66px; } -.openerp div.oe_mail_thread_subthread li.oe_mail_thread_msg:last-child { +.openerp div.oe_thread_placeholder li.oe_mail_thread_msg:last-child { margin-bottom: 8px; } @@ -191,6 +221,15 @@ clear: both; } +.openerp li.oe_mail_thread_msg.oe_mail_read, +.openerp li.oe_mail_thread_msg.oe_mail_read div { + background-color: #F0F0F0; +} +.openerp li.oe_mail_thread_msg.oe_mail_read li.oe_mail_thread_msg.oe_mail_unread, +.openerp li.oe_mail_thread_msg.oe_mail_read li.oe_mail_thread_msg.oe_mail_unread div { + background-color: #F6F6F6; +} + .openerp li.oe_mail_thread_msg > div:after { content: ""; display: block; @@ -202,18 +241,15 @@ margin: 0 0 4px 0; } -.openerp .oe_mail_msg_notification, -.openerp .oe_mail_msg_comment, +.openerp .oe_mail_msg_notification, +.openerp .oe_mail_msg_expandable, +.openerp .oe_mail_msg_comment, .openerp .oe_mail_msg_email { padding: 8px; background: white; border-top: 1px solid #ebebeb; } -.openerp div.oe_mail_thread_subthread .oe_mail_msg_comment { - background: #eee; -} - .openerp .oe_mail_msg_notification:after, .openerp .oe_mail_msg_comment:after, .openerp .oe_mail_msg_email:after { @@ -222,8 +258,15 @@ clear: both; } -.openerp .oe_mail_msg_content { +.openerp div.oe_mail_msg_content { + float: right; + position: relative; + width: 486px; +} + +.openerp div.oe_mail_msg_content > li { float: left; + margin-right: 3px; } .openerp .oe_mail_msg_content:after { @@ -266,7 +309,6 @@ display: none; } - /* ------------------------------------------------------------ */ /* mail.compose.message form view & OpenERP hacks /* ------------------------------------------------------------ */ @@ -371,15 +413,38 @@ } /* Dropdown menu */ -.openerp .oe_mail_msg_content .oe_dropdown_toggle { +/*.openerp .oe_mail_msg_content .oe_dropdown_toggle { position: absolute; top: 0px; right: 3px; +}*/ + +.openerp .oe_mail .oe_semantic_html_override { + position: relative; } + +.openerp .oe_mail ul.oe_header { + position: absolute; + right: 3px; + top: -6px; + display: none; + z-index: 10; + height: 18px; +} + +.openerp .oe_mail ul.oe_header a { + text-decoration: none; +} + +.openerp .oe_mail .oe_semantic_html_override:hover > ul.oe_header { + display: block; +} + +.openerp .oe_mail ul.oe_header>li { + display: inline-block; +} + .openerp .oe_mail_msg_content .oe_dropdown_arrow:after { - border-top: 4px solid transparent; -} -.openerp .oe_mail_msg_content:hover .oe_dropdown_arrow:after { border-top: 4px solid #404040; } diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js index 196c54c8296..e0c9ac9b7fc 100644 --- a/addons/mail/static/src/js/mail.js +++ b/addons/mail/static/src/js/mail.js @@ -22,8 +22,9 @@ openerp.mail = function(session) { action.context && action.context.redirect == true && this.fields && this.fields.message_ids && this.fields.message_ids.view.get("actual_mode") != 'create') { var thread = this.fields.message_ids.thread; - thread.refresh_composition_form(action.context); - return true; + + thread.refresh(action.context); + return false; } else { return this._super(action, on_close); @@ -110,8 +111,8 @@ openerp.mail = function(session) { this._super.apply(this, arguments); // customize display: add avatar, clean previous content var user_avatar = mail.ChatterUtils.get_image(this.session, 'res.users', 'image_small', this.session.uid); - this.$el.find('img.oe_mail_icon').attr('src', user_avatar); - this.$el.find('div.oe_mail_msg_content').empty(); + this.$('img.oe_mail_icon').attr('src', user_avatar); + this.$('div.oe_mail_msg_content').empty(); // create a context for the dataset and default_get of the wizard var context = _.extend({}, this.options.context); this.ds_compose = new session.web.DataSetSearch(this, 'mail.compose.message', context); @@ -139,21 +140,21 @@ openerp.mail = function(session) { disable_autofocus: true, }); // add the form, bind events, activate the form - var msg_node = this.$el.find('div.oe_mail_msg_content'); + var msg_node = this.$('div.oe_mail_msg_content'); return $.when(this.form_view.appendTo(msg_node)).pipe(this.proxy('postprocess_create_form_view')); }, postprocess_create_form_view: function () { // handle attachment button this.fileupload_id = _.uniqueId('oe_fileupload'); - var button_attach = this.$el.find('button.oe_mail_compose_message_attachment'); + var button_attach = this.$('button.oe_mail_compose_message_attachment'); var rendered = session.web.qweb.render('mail.compose_message.add_attachment', {'widget': this}); $(rendered).insertBefore(button_attach); // move the button inside div.oe_hidden_input_file - var input_node = this.$el.find('input[name=ufile]'); + var input_node = this.$('input[name=ufile]'); button_attach.detach().insertAfter(input_node); // set the function called when attachments are added - this.$el.find('input.oe_form_binary_file').change(this.on_attachment_change); + this.$('input.oe_form_binary_file').change(this.on_attachment_change); this.bind_events(); this.form_view.do_show(); }, @@ -161,7 +162,7 @@ openerp.mail = function(session) { on_attachment_change: function (event) { var $target = $(event.target); if ($target.val() !== '') { - this.$el.find('form.oe_form_binary_form').submit(); + this.$('form.oe_form_binary_form').submit(); session.web.blockUI(); } }, @@ -178,11 +179,11 @@ openerp.mail = function(session) { }, display_attachments: function () { - var attach_node = this.$el.find('div.oe_mail_compose_message_attachments'); + var attach_node = this.$('div.oe_mail_compose_message_attachments'); var rendered = session.web.qweb.render('mail.thread.message.attachments', {'record': this}); attach_node.empty(); $(rendered).appendTo(attach_node); - this.$el.find('.oe_mail_msg_attachments').show(); + this.$('.oe_mail_msg_attachments').show(); var composer_attachment_ids = _.pluck(this.attachment_ids, 'id'); var onchange_like = {'value': {'attachment_ids': composer_attachment_ids}} this.form_view.on_processed_onchange(onchange_like, []); @@ -228,6 +229,379 @@ openerp.mail = function(session) { this.$el.on('click', '.oe_mail_attachment_delete', self.on_attachment_delete); }, }), + + + /** + * ------------------------------------------------------------ + * Thread Message Expandable Widget + * ------------------------------------------------------------ + * + * This widget handles the display the expandable message in a thread. The + * [thread_level] parameter sets the thread level number: + * - thread + * - - visible message + * - - expandable + * - - visible message + * - - visible message + * - - expandable + */ + mail.ThreadExpandable = session.web.Widget.extend({ + template: 'mail.thread.expandable', + + init: function(parent, options) { + this._super(parent); + this.domain = options.domain || []; + this.context = _.extend({ + default_model: 'mail.thread', + default_res_id: 0, + default_parent_id: false }, options.context || {}); + + this.id = -1; + this.parent_id= options.parameters.parent_id || false; + this.nb_messages = options.parameters.nb_messages || 0; + this.type = options.parameters.type || false; + + // record options and data + this.parent_thread= parent.messages!= undefined ? parent : options.options.thread._parents[0] ; + + }, + + + start: function() { + this._super.apply(this, arguments); + this.bind_events(); + }, + + /** + * Bind events in the widget. Each event is slightly described + * in the function. */ + bind_events: function() { + var self = this; + // event: click on 'Vote' button + this.$el.on('click', 'a.oe_mail_fetch_more', self.on_expandable); + }, + + /*The selected thread and all childs (messages/thread) became read + * @param {object} mouse envent + */ + on_expandable: function (event) { + event.stopPropagation(); + this.parent_thread.message_fletch(false, this.domain, this.context); + this.destroy(); + return false; + }, + }); + + /** + * ------------------------------------------------------------ + * Thread Message Widget + * ------------------------------------------------------------ + * This widget handles the display of a messages in a thread. + * Displays a record and performs some formatting on the record : + * - record.date: formatting according to the user timezone + * - record.timerelative: relative time givein by timeago lib + * - record.avatar: image url + * - record.attachment_ids[].url: url of each attachmentThe + * [thread_level] parameter sets the thread level number: + * - root thread + * - - sub message (parent_id = root message) + * - - - sub thread + * - - - - sub sub message (parent id = sub thread) + * - - sub message (parent_id = root message) + * - - - sub thread + */ + mail.ThreadMessage = session.web.Widget.extend({ + template: 'mail.thread.message', + + /** + * @param {Object} parent parent + * @param {Array} [domain] + * @param {Object} [context] context of the thread. It should + contain at least default_model, default_res_id. Please refer to + the ComposeMessage widget for more information about it. + * @param {Object} [options] + * @param {Object} [thread] read obout mail.Thread object + * @param {Object} [message] + * @param {Number} [message_ids=null] ids for message_fletch + * @param {Number} [message_data=null] already formatted message data, + * for subthreads getting data from their parent + * @param {Number} [truncate_limit=250] number of character to + * display before having a "show more" link; note that the text + * will not be truncated if it does not have 110% of the parameter + * @param {Boolean} [show_record_name] + * @param {Boolean} [show_reply] + * @param {Boolean} [show_reply_by_email] + * @param {Boolean} [show_dd_delete] + * @param {Boolean} [show_dd_hide] + */ + init: function(parent, options) { + this._super(parent); + this.domain = options.domain || []; + this.context = _.extend({ + default_model: 'mail.thread', + default_res_id: 0, + default_parent_id: false }, options.context || {}); + + // options + this.options={ + 'thread' : options.options.thread, + 'message' : { + 'message_ids': options.options.message.message_ids || null, + 'message_data': options.options.message.message_data || null, + 'show_record_name': options.options.message.show_record_name != undefined ? options.options.message.show_record_name: true, + 'show_reply': options.options.message.show_reply || false, + 'show_reply_by_email': options.options.message.show_reply_by_email || false, + 'show_dd_delete': options.options.message.show_dd_delete || false, + 'show_dd_hide': options.options.message.show_dd_hide || false, + 'truncate_limit': options.options.message.truncate_limit || 250, + } + }; + // record options and data + this.parent_thread= parent.messages!= undefined ? parent : options.options.thread._parents[0] ; + + var param = options.parameters; + // record parameters + this.id = param.id || -1; + this.model = param.model || false; + this.parent_id= param.parent_id || false; + this.res_id = param.res_id || false; + this.type = param.type || false; + this.is_author = param.is_author || false; + this.subject = param.subject || false; + this.name = param.name || false; + this.record_name = param.record_name || false; + this.body = param.body || false; + this.vote_user_ids =param.vote_user_ids || []; + this.has_voted = param.has_voted || false; + + this.vote_user_ids = param.vote_user_ids || []; + + this.unread = param.unread || false; + this._date = param.date; + this.author_id = param.author_id || []; + this.attachment_ids = param.attachment_ids || []; + + this.thread = false; + + if( param.id > 0 ) { + this.formating_data(); + } + + this.ds_notification = new session.web.DataSetSearch(this, 'mail.notification'); + this.ds_message = new session.web.DataSetSearch(this, 'mail.message'); + }, + + formating_data: function(){ + + //formating and add some fields for render + this.date = session.web.format_value(this._date, {type:"datetime"}); + this.timerelative = $.timeago(this.date); + if (this.type == 'email') { + this.avatar = ('/mail/static/src/img/email_icon.png'); + } else { + this.avatar = mail.ChatterUtils.get_image(this.session, 'res.partner', 'image_small', this.author_id[0]); + } + for (var l in this.attachment_ids) { + var attach = this.attachment_ids[l]; + attach['url'] = mail.ChatterUtils.get_attachment_url(this.session, attach); + } + }, + + start: function() { + this._super.apply(this, arguments); + this.expender(); + this.$el.hide().fadeIn(750); + this.bind_events(); + this.create_thread(); + }, + + /** + * Bind events in the widget. Each event is slightly described + * in the function. */ + bind_events: function() { + var self = this; + // event: click on 'Attachment(s)' in msg + this.$el.on('click', 'a.oe_mail_msg_view_attachments', function (event) { + var act_dom = $(this).parent().parent().parent().find('.oe_mail_msg_attachments'); + act_dom.toggle(); + }); + // event: click on icone 'Read' in header + this.$el.on('click', 'a.oe_read', this.on_message_read_unread); + // event: click on icone 'UnRead' in header + this.$el.on('click', 'a.oe_unread', this.on_message_read_unread); + // event: click on 'Delete' in msg side menu + this.$el.on('click', 'a.oe_mail_msg_delete', this.on_message_delete); + + // event: click on 'Reply' in msg + this.$el.on('click', 'a.oe_reply', this.on_message_reply); + // event: click on 'Reply by email' in msg side menu + this.$el.on('click', 'a.oe_reply_by_email', this.on_message_reply_by_mail); + // event: click on 'Vote' button + this.$el.on('click', 'button.oe_mail_msg_vote', this.on_vote); + }, + + on_message_reply:function(event){ + event.stopPropagation(); + this.thread.on_compose_message($(event.srcElement).hasClass("oe_full_reply"), false); + return false; + }, + + on_message_reply_by_mail:function(event){ + event.stopPropagation(); + this.thread.on_compose_message(true, true); + return false; + }, + + expender: function(){ + this.$('div.oe_mail_msg_body:first').expander({ + slicePoint: this.options.truncate_limit, + expandText: 'read more', + userCollapseText: '[^]', + detailClass: 'oe_mail_msg_tail', + moreClass: 'oe_mail_expand', + lessClass: 'oe_mail_reduce', + }); + }, + + create_thread: function(){ + var self=this; + if(this.thread){ + return false; + } + /*create thread*/ + self.thread = new mail.Thread(self, { + 'domain': self.domain, + 'context':{ + 'default_model': self.model, + 'default_res_id': self.res_id, + 'default_parent_id': self.id + }, + 'options': { + 'thread' : self.options.thread, + 'message' : self.options.message + }, + 'parameters':{ + 'model': self.model, + 'id': self.id, + 'parent_id': self.id + } + } + ); + /*insert thread in parent message*/ + self.thread.appendTo(self.$el.find('div.oe_thread_placeholder')); + }, + + animated_destroy: function(options) { + var self=this; + //graphic effects + if(options && options.fadeTime) { + self.$el.fadeOut(options.fadeTime, function(){ + self.destroy(); + }); + } else { + self.destroy(); + } + for(var i in this.thread.messages){ + this.thread.messages[i].animated_destroy({fadeTime:0}); + } + }, + + on_message_delete: function (event) { + event.stopPropagation(); + if (! confirm(_t("Do you really want to delete this message?"))) { return false; } + + this.animated_destroy({fadeTime:250}); + // delete this message and his childs + var ids = [this.id].concat( this.get_child_ids() ); + this.ds_message.unlink(ids); + this.animated_destroy(); + return false; + }, + + /*The selected thread and all childs (messages/thread) became read + * @param {object} mouse envent + */ + on_message_read_unread: function (event) { + event.stopPropagation(); + this.animated_destroy({fadeTime:250}); + // if this message is read, all childs message display is read + var ids = [this.id].concat( this.get_child_ids() ); + this.ds_notification.call('set_message_read', [ids,$(event.srcElement).hasClass("oe_read")]); + return false; + }, + + /** browse message + * @param {object}{int} option.id + * @param {object}{string} option.model + * @param {object}{boolean} option._go_thread_wall + * private for check the top thread + * @return thread object + */ + browse_message: function(options){ + // goto the wall thread for launch browse + if(!options._go_thread_wall) { + options._go_thread_wall = true; + for(var i in this.options.thread._parents[0].messages){ + var res=this.options.thread._parents[0].messages[i].browse_message(options); + if(res) return res; + } + } + + if(this.id==options.id) + return this; + + for(var i in this.thread.messages){ + if(this.thread.messages[i].thread){ + var res=this.thread.messages[i].browse_message(options); + if(res) return res; + } + } + + return false; + }, + + /* get all child message/thread id linked + */ + get_child_ids: function(){ + var res=[] + if(arguments[0]) res.push(this.id); + if(this.thread){ + res = res.concat( this.thread.get_child_ids(true) ); + } + return res; + }, + + + on_vote: function (event) { + event.stopPropagation(); + var self=this; + return this.ds_message.call('vote_toggle', [[self.id]]).pipe(function(vote){ + + self.has_voted=vote; + if (!self.has_voted) { + var votes=[]; + for(var i in self.vote_user_ids){ + if(self.vote_user_ids[i][0]!=self.session.uid) + vote.push(self.vote_user_ids[i]); + } + self.vote_user_ids=votes; + } + else { + self.vote_user_ids.push([self.session.uid, 'You']); + } + self.display_vote(); + }); + return false; + }, + + // Render vote Display template. + display_vote: function () { + var self = this; + var vote_element = session.web.qweb.render('mail.thread.message.vote', {'widget': self}); + self.$(".placeholder-mail-vote").empty(); + self.$(".placeholder-mail-vote").html(vote_element); + }, + }); /** * ------------------------------------------------------------ @@ -241,7 +615,6 @@ openerp.mail = function(session) { * - - - sub sub message (parent id = sub message) * - - sub message (parent_id = root message) */ - mail.Thread = session.web.Widget.extend({ template: 'mail.thread', @@ -252,66 +625,97 @@ openerp.mail = function(session) { contain at least default_model, default_res_id. Please refer to the ComposeMessage widget for more information about it. * @param {Object} [options] - * @param {Number} [options.message_ids=null] ids for message_fetch - * @param {Number} [options.message_data=null] already formatted message - data, for subthreads getting data from their parent - * @param {Number} [options.thread_level=0] number of thread levels - * @param {Boolean} [options.use_composer] use the advanced composer, or - the default basic textarea if not set - * @param {Number} [options.truncate_limit=250] number of character to - * display before having a "show more" link; note that the text - * will not be truncated if it does not have 110% of the parameter + * @param {Object} [message] read about mail.ThreadMessage object + * @param {Object} [thread] + * @param {Number} [thread_level=0] number of thread levels + * @param {Boolean} [use_composer] use the advanced composer, or + * the default basic textarea if not set + * @param {Number} [expandable_number=5] number message show + * for each click on "show more message" + * @param {Number} [expandable_default_number=5] number message show + * on begin before the first click on "show more message" + * @param {Boolean} [display_on_flat] display all thread + * on the wall thread level (no hierarchy) + * @param {Array} [parents] liked with the parents thread + * use with browse, fletch... [O]= top parent */ - init: function(parent, domain, context, options) { + init: function(parent, options) { this._super(parent); - this.domain = domain || []; + this.domain = options.domain || []; this.context = _.extend({ default_model: 'mail.thread', default_res_id: 0, - default_parent_id: false }, context || {}); + default_parent_id: false }, options.context || {}); + // options - this.options = { - message_ids: options.message_ids || null, - message_data: options.message_data || null, - thread_level: options.thread_level || 0, - use_composer: options.use_composer || false, - show_header_compose: options.show_header_compose != undefined ? options.show_header_compose: true, - show_record_name: options.show_record_name != undefined ? options.show_record_name: true, - show_reply: options.show_reply || false, - show_reply_by_email: options.show_reply_by_email || false, - show_dd_reply_by_email:options.show_dd_reply_by_email != undefined ? options.show_dd_reply_by_email: true, - show_dd_delete: options.show_dd_delete || false, - show_dd_hide: options.show_dd_hide || false, - show_more: options.show_more || false, - truncate_limit: options.truncate_limit || 250, - } + this.options={ + 'thread' : { + 'thread_level': options.options.thread.thread_level || 0, + 'show_header_compose': (options.options.thread.show_header_compose != undefined ? options.options.thread.show_header_compose: false), + 'use_composer': options.options.thread.use_composer || false, + 'expandable_number': options.options.thread.expandable_number || 5, + 'expandable_default_number': options.options.thread.expandable_default_number || 5, + '_expandable_max': options.options.thread.expandable_default_number || 5, + 'display_on_flat': options.options.thread.display_on_flat || false, + '_parents': (options.options.thread._parents != undefined ? options.options.thread._parents : []).concat( [this] ) + }, + 'message' : options.options.message + }; + + // record options and data + this.parent_linked_message= parent.thread!= undefined ? parent : false ; + + var param = options.parameters // datasets and internal vars - this.records = {}; + this.id= param.id || false; + this.model= param.model || false; + this.parent_id= param.parent_id || false; + + this.messages = []; + this.ds_thread = new session.web.DataSetSearch(this, this.context.default_model); - this.ds_notification = new session.web.DataSetSearch(this, 'mail.notification'); this.ds_message = new session.web.DataSetSearch(this, 'mail.message'); }, start: function() { // TDE TODO: check for deferred, not sure it is correct this._super.apply(this, arguments); - this.bind_events(); + + this.list_ul=this.$('ul.oe_mail_thread_display:first'); + this.more_msg=this.$(">.oe_mail_msg_more_message:first"); + this.display_user_avatar(); - // fetch and display message, using message_ids if set - var display_done = $.when(this.message_fetch(true, [], {})).then(this.proxy('do_customize_display')); + var display_done = compose_done = false; + // add message composition form view - if (this.options.show_header_compose && this.options.use_composer) { - var compose_done = this.instantiate_composition_form(); + compose_done = this.instantiate_composition_form(); + + this.bind_events(); + + if(this.options.thread._parents[0]==this){ + this.on_first_thread(); } + return display_done && compose_done; }, - /** Customize the display - * - show_header_compose: show the composition form in the header */ - do_customize_display: function() { - if (this.options.show_header_compose) { - this.$el.find('div.oe_mail_thread_action').eq(0).show(); - } + /** + * Override-hack of do_action: automatically load message on the chatter. + * Normally it should be called only when clicking on 'Post/Send' + * in the composition form. */ + do_action: function(action, on_close) { + this.instantiate_composition_form(); + this.message_fletch(false, false, false, [action.id]); + return this._super(action, on_close); + }, + + /* this method is runing for first parent thread + */ + on_first_thread: function(){ + // fetch and display message, using message_ids if set + display_done = this.message_fletch(true); + //show the first write message + this.$(">.oe_mail_thread_action").show(); }, /** @@ -319,281 +723,266 @@ openerp.mail = function(session) { * in the function. */ bind_events: function() { var self = this; - // event: click on 'More' at bottom of thread - this.$el.on('click', 'button.oe_mail_button_more', this.do_message_fetch_more); // event: writing in basic textarea of composition form (quick reply) - this.$el.find('textarea.oe_mail_compose_textarea').keyup(function (event) { - var charCode = (event.which) ? event.which : window.event.keyCode; - if (event.shiftKey && charCode == 13) { this.value = this.value+"\n"; } - else if (charCode == 13) { return self.message_post(); } - }); - // event: click on 'Reply' in msg - this.$el.on('click', 'a.oe_mail_msg_reply', function (event) { - var act_dom = $(this).parents('li.oe_mail_thread_msg').eq(0).find('div.oe_mail_thread_action:first'); - act_dom.toggle(); - }); - // event: click on 'Attachment(s)' in msg - this.$el.on('click', 'a.oe_mail_msg_view_attachments', function (event) { - var act_dom = $(this).parent().parent().parent().find('.oe_mail_msg_attachments'); - act_dom.toggle(); - }); - // event: click on 'Delete' in msg side menu - this.$el.on('click', 'a.oe_mail_msg_delete', this.on_message_delete); - // event: click on 'Hide' in msg side menu - this.$el.on('click', 'a.oe_mail_msg_hide', this.on_message_read); - // event: click on 'Reply by email' in msg side menu - this.$el.on('click', 'a.oe_mail_msg_reply_by_email', function (event) { - if (! self.compose_message_widget) return true; - evt = event.target || event.srcElement; - var msg_id = evt.dataset.msg_id; - if (! msg_id) return false; - self.compose_message_widget.refresh({ - 'default_composition_mode': 'reply', - 'default_parent_id': parseInt(msg_id), - 'default_content_subtype': 'html'} ); - }); - // event: click on 'Vote' button - this.$el.on('click', 'button.oe_mail_msg_vote', this.on_vote); + // event: onblur for hide 'Reply' + this.$('.oe_mail_compose_textarea:first textarea') + .keyup(function (event) { + var charCode = (event.which) ? event.which : window.event.keyCode; + if (event.shiftKey && charCode == 13) { this.value = this.value+"\n"; } + else if (charCode == 13) { return self.message_post(); } + }) + .blur(function (event) { + $(this).parents('.oe_mail_thread_action:first').hide(); + }); }, - on_message_delete: function (event) { - if (! confirm(_t("Do you really want to delete this message?"))) { return false; } - var msg_id = event.srcElement.dataset.id; - if (! msg_id) return false; - $(event.srcElement).parents('li.oe_mail_thread_msg').eq(0).remove(); - return this.ds_message.unlink([parseInt(msg_id)]); - }, - - on_message_read: function (event) { - //TDE: TODO - evt = event.target || event.srcElement; - var msg_id = evt.dataset.id; - if (! msg_id) return false; - $(evt).parents('li.oe_mail_thread_msg').eq(0).remove(); - return this.ds_notification.call('set_message_read', [parseInt(msg_id)]); - }, - - on_vote: function (event) { - event.stopPropagation(); - var self = this; - var message_id = $(event.srcElement).parent().data().msg_id; - var vote_node = $(event.srcElement).parents('li').eq(0); - if (! message_id) { return false; } - return this.ds_message.call('vote_toggle', [[parseInt(message_id)]]).pipe( - self.toggle_vote(message_id, vote_node)); - }, - - /** - * Override-hack of do_action: automatically reload the chatter. - * Normally it should be called only when clicking on 'Post/Send' - * in the composition form. */ - do_action: function(action, on_close) { - //TDE: TODO: instead of reloading, push the message ? - this.message_clean(); - this.message_fetch(); - if (this.compose_message_widget) { - this.compose_message_widget.refresh({ - 'default_composition_mode': 'comment', - 'default_parent_id': this.context.default_parent_id, - 'default_content_subtype': 'plain'} ); + /* get all child message/thread id linked + */ + get_child_ids: function(){ + var res=[]; + for(var i in this.messages){ + if(this.messages[i].thread){ + res = res.concat( this.messages[i].get_child_ids(true) ); + } } - // return this._super(action, on_close); + return res; + }, + + /** browse thread + * @param {object}{int} option.id + * @param {object}{string} option.model + * @param {object}{boolean} option._go_thread_wall + * private for check the top thread + * @param {object}{boolean} option.default_return_top_thread + * return the top thread (wall) if no thread found + * @return thread object + */ + browse_thread: function(options){ + // goto the wall thread for launch browse + if(!options._go_thread_wall) { + options._go_thread_wall = true; + return this.options.thread._parents[0].browse_thread(options); + } + + if(this.id==options.id){ + return this; + } + + if(options.id) + for(var i in this.messages){ + if(this.messages[i].thread){ + var res=this.messages[i].thread.browse_thread({'id':options.id, '_go_thread_wall':true}); + if(res) return res; + } + } + + //if option default_return_top_thread, return the top if no found thread + if(options.default_return_top_thread){ + return this; + } + + return false; + }, + + /** browse message + * @param {object}{int} option.id + * @param {object}{string} option.model + * @param {object}{boolean} option._go_thread_wall + * private for check the top thread + * @return thread object + */ + browse_message: function(options){ + if(this.options.thread._parents[0].messages[0]) + return this.options.thread._parents[0].messages[0].browse_message(options); }, /** Instantiate the composition form, with every parameters in context - or in the widget context. */ + * or in the widget context. + * @param {object} + * @param {boolean} show_header_compose, force to instantiate form + */ instantiate_composition_form: function(context) { - if (this.compose_message_widget) { - this.compose_message_widget.destroy(); + // add message composition form view + if ((!context || !context.show_header_compose) && + (!this.options.thread.show_header_compose || !this.options.thread.use_composer || + (this.options.thread.show_header_compose <= this.options.thread._parents.length && this.options.thread._parents[0]!=this))) { + this.$("textarea:first").val(""); + return false; + } + if (this.compose_message_widget) { + this.compose_message_widget.refresh(); + } else { + var context = _.extend(context || {}, this.context); + this.compose_message_widget = new mail.ComposeMessage(this, {'context': context}); + var composition_node = this.$('div.oe_mail_thread_action'); + composition_node.empty(); + return this.compose_message_widget.appendTo(composition_node); } - this.compose_message_widget = new mail.ComposeMessage(this, { - 'context': _.extend(context || {}, this.context), - }); - var composition_node = this.$el.find('div.oe_mail_thread_action'); - composition_node.empty(); - var compose_done = this.compose_message_widget.appendTo(composition_node); - return compose_done; }, - refresh_composition_form: function (context) { - if (! this.compose_message_widget) return; - return this.compose_message_widget.refresh(context); + /* this function is launch when a user click on "Reply" button + */ + on_compose_message: function(full_reply, by_mail){ + if(full_reply){ + this.instantiate_composition_form({'show_header_compose':true}); + } + if(by_mail){ + if (!this.compose_message_widget) return true; + this.compose_message_widget.refresh({ + 'default_composition_mode': 'reply', + 'default_parent_id': this.id, + 'default_content_subtype': 'html'} ); + } + this.$('div.oe_mail_thread_action:first').toggle(); + return false; }, - /** Clean the thread */ - message_clean: function() { - this.$el.find('div.oe_mail_thread_display').empty(); + refresh: function (action_context) { + var self=this; + _(this.messages).each(function(){ self.destroy(); }); + self.message_fletch(); + }, + + /*post a message and fletch the message*/ + message_post: function (body) { + var self = this; + if (! body) { + var comment_node = this.$('textarea'); + var body = comment_node.val(); + comment_node.val(''); + } + if(body.match(/\S+/)) { + this.ds_thread.call('message_post_api', [ + [this.context.default_res_id], body, false, 'comment', false, this.context.default_parent_id, undefined]) + .then(this.proxy('switch_new_message')); + } + else { + return false; + } }, /** Fetch messages * @param {Bool} initial_mode: initial mode: try to use message_data or * message_ids, if nothing available perform a message_read; otherwise * directly perform a message_read - * @param {Array} additional_domain: added to this.domain - * @param {Object} additional_context: added to this.context + * @param {Array} replace_domain: added to this.domain + * @param {Object} replace_context: added to this.context */ - message_fetch: function (initial_mode, additional_domain, additional_context) { + message_fletch: function (initial_mode, replace_domain, replace_context, ids) { var self = this; - // domain and context: options + additional - fetch_domain = _.flatten([this.domain, additional_domain || []], true) - fetch_context = _.extend(this.context, additional_context || {}) + // initial mode: try to use message_data or message_ids - if (initial_mode && this.options.message_data) { - return this.message_display(this.options.message_data); + if (initial_mode && this.options.thread.message_data) { + return this.create_message_object(this.options.message_data); } - message_ids = initial_mode && this.options.message_ids != null && this.options.message_ids || false; - return this.ds_message.call('message_read', [message_ids, fetch_domain, this.options.thread_level, undefined, fetch_context] - ).then(this.proxy('message_display')); + // domain and context: options + additional + fetch_domain = replace_domain ? replace_domain : this.domain; + fetch_context = replace_context ? replace_context : this.context; + fetch_context.message_loaded= [this.id||0].concat( self.options.thread._parents[0].get_child_ids() ); + + return this.ds_message.call('message_read', [ids, fetch_domain, (this.options.thread.thread_level+1), fetch_context, this.context.default_parent_id || undefined] + ).then(this.proxy('switch_new_message')); }, - /* Display a list of records - * A specific case is done for 'expandable' messages that are messages - displayed under a 'show more' button form + /* create record object and linked him */ - message_display: function (records) { + create_message_object: function (message) { var self = this; - var _expendable = false; - _(records).each(function (record) { - if (record.type == 'expandable') { - _expendable = true; - self.update_fetch_more(true); - self.fetch_more_domain = record.domain; - self.fetch_more_context = record.context; + + // check if the message is already create + for(var i in this.messages){ + if(this.messages[i].id==message.id){ + this.messages[i].destroy(); + this.messages[i]=self.insert_message(message); + return true; } - else { - self.display_record(record); - self.thread = new mail.Thread(self, self.domain, - { 'default_model': record.model, - 'default_res_id': record.res_id, - 'default_parent_id': record.id }, - { 'message_data': record.child_ids, - 'thread_level': self.options.thread_level - 1, - 'show_header_compose': false, - 'show_reply': self.options.show_reply && self.options.thread_level > 1, - 'show_reply_by_email': self.options.show_reply_by_email, - 'show_dd_hide': self.options.show_dd_hide, - 'show_dd_delete': self.options.show_dd_delete }); - self.$el.find('li.oe_mail_thread_msg:last').append('
'); - self.thread.appendTo(self.$el.find('div.oe_mail_thread_subthread:last')); - } - }); - if (! _expendable) { - this.update_fetch_more(false); } + + self.messages.push( self.insert_message(message) ); + }, - /** Displays a record and performs some formatting on the record : - * - record.date: formatting according to the user timezone - * - record.timerelative: relative time givein by timeago lib - * - record.avatar: image url - * - record.attachment_ids[].url: url of each attachment */ - display_record: function (record) { - // formatting and additional fields - record.date = session.web.format_value(record.date, {type:"datetime"}); - record.timerelative = $.timeago(record.date); - if (record.type == 'email') { - record.avatar = ('/mail/static/src/img/email_icon.png'); - } else { - record.avatar = mail.ChatterUtils.get_image(this.session, 'res.partner', 'image_small', record.author_id[0]); - } - for (var l in record.attachment_ids) { - var attach = record.attachment_ids[l]; - attach['url'] = mail.ChatterUtils.get_attachment_url(this.session, attach); - } - // add to internal storage - this.records[record.id] = record; - // render, add the expand feature - var rendered = session.web.qweb.render('mail.thread.message', {'record': record, 'thread': this, 'options': this.options}); - $(rendered).appendTo(this.$el.children('div.oe_mail_thread_display:first')); - this.$el.find('div.oe_mail_msg_body').expander({ - slicePoint: this.options.truncate_limit, - expandText: 'read more', - userCollapseText: '[^]', - detailClass: 'oe_mail_msg_tail', - moreClass: 'oe_mail_expand', - lessClass: 'oe_mail_reduce', + /** Displays a message or an expandable message */ + insert_message: function (message) { + var self=this; + + if(message.type=='expandable'){ + var message = new mail.ThreadExpandable(self, { + 'domain': message.domain, + 'context': { + 'default_model': message.model, + 'default_res_id': message.res_id, + 'default_parent_id': message.id }, + 'parameters': message }); - }, - - // Render vote Display template. - toggle_vote: function (message_id, vote_node) { - var self = this; - var record = this.records[message_id]; - if (record.has_voted) { - var idx = _.map(record.vote_user_ids, function (x) { return x[0]; }).indexOf(message_id); - record.vote_user_ids.splice(idx, 1); - } - else { - record.vote_user_ids.push([this.session.uid, 'You']); - } - record.has_voted = ! record.has_voted; - var vote_element = session.web.qweb.render('mail.thread.message.vote', {'record': record}); - vote_node.empty(); - vote_node.html(vote_element); - }, - - /** Display 'show more' button */ - update_fetch_more: function (new_value) { - if (new_value) { - this.$el.find('div.oe_mail_thread_more:last').show(); } else { - this.$el.find('div.oe_mail_thread_more:last').hide(); + var message = new mail.ThreadMessage(self, { + 'domain': message.domain, + 'context': { + 'default_model': message.model, + 'default_res_id': message.res_id, + 'default_parent_id': message.id }, + 'options':{ + 'thread': self.options.thread, + 'message': self.options.message + }, + 'parameters': message + }); + } + + var thread = self.options.thread.display_on_flat ? self.options.thread._parents[0] : this; + + // check older and newer message for insert + var parent_newer = false; + var parent_older = false; + for(var i in thread.messages){ + if(thread.messages[i].id > message.id){ + if(!parent_newer || parent_newer.id>thread.messages[i].id) + parent_newer = thread.messages[i]; + } else if(thread.messages[i].id>0 && thread.messages[i].id < message.id) { + if(!parent_older || parent_older.idthis.options._expandable_max){ + this.list_ul.find('>li:gt('+(this.options._expandable_max-1)+')').hide(); + this.more_msg.show(); + } else { + this.list_ul.find('>li').show(); + this.more_msg.hide(); } }, display_user_avatar: function () { var avatar = mail.ChatterUtils.get_image(this.session, 'res.users', 'image_small', this.session.uid); - return this.$el.find('img.oe_mail_icon').attr('src', avatar); + return this.$('img.oe_mail_icon').attr('src', avatar); }, - message_post: function (body) { - var self = this; - if (! body) { - var comment_node = this.$el.find('textarea'); - var body = comment_node.val(); - comment_node.val(''); - } - return this.ds_thread.call('message_post', [ - [this.context.default_res_id], body, false, 'comment', this.context.default_parent_id, undefined] - ).then(self.message_fetch()); + /* Send the records to his parent thread */ + switch_new_message: function(records) { + var self=this; + _(records).each(function(record){ + self.browse_thread({ + 'id': record.parent_id, + 'default_return_top_thread':true + }).create_message_object( record ); + }); }, - - /** Action: 'shows more' to fetch new messages */ - do_message_fetch_more: function () { - return this.message_fetch(false, this.fetch_more_domain, this.fetch_more_context); - }, - - // TDE: keep currently because need something similar - // /** - // * Create a domain to fetch new comments according to - // * comment already present in comments_structure - // * @param {Object} comments_structure (see chatter utils) - // * @returns {Array} fetch_domain (OpenERP domain style) - // */ - // get_fetch_domain: function (comments_structure) { - // var domain = []; - // var ids = comments_structure.root_ids.slice(); - // var ids2 = []; - // // must be child of current parent - // if (this.options.parent_id) { domain.push(['id', 'child_of', this.options.parent_id]); } - // _(comments_structure.root_ids).each(function (id) { // each record - // ids.push(id); - // ids2.push(id); - // }); - // if (this.options.parent_id != false) { - // ids2.push(this.options.parent_id); - // } - // // must not be children of already fetched messages - // if (ids.length > 0) { - // domain.push('&'); - // domain.push('!'); - // domain.push(['id', 'child_of', ids]); - // } - // if (ids2.length > 0) { - // domain.push(['id', 'not in', ids2]); - // } - // return domain; - // }, }); @@ -632,7 +1021,7 @@ openerp.mail = function(session) { var self = this; this._super.apply(this, arguments); if (! this.view.datarecord.id || session.web.BufferedDataSet.virtual_id_regex.test(this.view.datarecord.id)) { - this.$el.find('oe_mail_thread').hide(); + this.$('oe_mail_thread').hide(); return; } // update context @@ -644,14 +1033,31 @@ openerp.mail = function(session) { // create and render Thread widget var show_header_compose = this.view.is_action_enabled('edit') || (this.getParent().fields.message_is_follower && this.getParent().fields.message_is_follower.get_value()); - this.$el.find('div.oe_mail_recthread_main').empty(); - var thread = new mail.Thread(self, domain, this.options.context, - { 'thread_level': this.options.thread_level, - 'show_header_compose': show_header_compose, - 'use_composer': show_header_compose, - 'show_dd_delete': true, - 'show_reply_by_email': show_header_compose }); - return thread.appendTo(this.$el.find('div.oe_mail_recthread_main')); + + this.thread = new mail.Thread(self, { + 'domain': domain, + 'context': this.options.context, + 'options':{ + 'thread':{ + 'thread_level': this.options.thread_level, + 'show_header_compose': show_header_compose, + 'use_composer': show_header_compose, + 'display_on_flat':true + }, + 'message':{ + 'show_dd_delete': true, + 'show_reply_by_email': show_header_compose, + } + }, + 'parameters': {}, + } + ); + + this.$('ul.oe_mail_wall_threads').empty(); + var render_res = session.web.qweb.render('mail.wall_thread_container', {}); + $(render_res).appendTo(this.$('ul.oe_mail_wall_threads')); + + return this.thread.appendTo( this.$('li.oe_mail_wall_thread:last') ); }, }); @@ -703,7 +1109,7 @@ openerp.mail = function(session) { load_searchview: function (defaults, hidden) { var self = this; this.searchview = new session.web.SearchView(this, this.ds_msg, false, defaults || {}, hidden || false); - return this.searchview.appendTo(this.$el.find('.oe_view_manager_view_search')).then(function () { + return this.searchview.appendTo(this.$('.oe_view_manager_view_search')).then(function () { self.searchview.on_search.add(self.do_searchview_search); }); }, @@ -724,25 +1130,41 @@ openerp.mail = function(session) { }, function (results) { self.search_results['context'] = results.context; self.search_results['domain'] = results.domain; - self.search_results['groupby'] = results.group_by; + self.thread.destroy(); return self.message_render(); }); }, /** Clean and display the threads */ - message_render: function () { - this.$el.find('ul.oe_mail_wall_threads').empty(); + message_render: function (search) { var domain = this.options.domain.concat(this.search_results['domain']); - var render_res = session.web.qweb.render('mail.wall_thread_container', {}); - $(render_res).appendTo(this.$el.find('ul.oe_mail_wall_threads')); - var thread = new mail.Thread(this, domain, this.options.context, - { 'thread_level': this.options.thread_level, - 'use_composer': true, - 'show_reply': this.options.thread_level > 0, - 'show_dd_hide': true, + + var domain = _.extend(this.options.domain, search&&search.domain? search.domain : {}); + var context = _.extend(this.options.context, search&&search.context ? search.context : {}); + + this.thread = new mail.Thread(this, { + 'domain' : domain, + 'context' : context, + 'options': { + 'thread' :{ + 'thread_level': this.options.thread_level, + 'use_composer': true, + 'show_header_compose': 1, + }, + 'message': { + 'show_reply': this.options.thread_level > 0, + 'show_dd_hide': true, + }, + }, + 'parameters': {}, } ); - return thread.appendTo(this.$el.find('li.oe_mail_wall_thread:last')); + + this.$('ul.oe_mail_wall_threads').empty(); + var render_res = session.web.qweb.render('mail.wall_thread_container', {}); + $(render_res).appendTo(this.$('ul.oe_mail_wall_threads')); + + return this.thread.appendTo( this.$('li.oe_mail_wall_thread:last') ); }, }); }; diff --git a/addons/mail/static/src/js/mail_followers.js b/addons/mail/static/src/js/mail_followers.js index 743c4704330..953b8cd9fc8 100644 --- a/addons/mail/static/src/js/mail_followers.js +++ b/addons/mail/static/src/js/mail_followers.js @@ -29,7 +29,9 @@ openerp_mail_followers = function(session, mail) { this.options.context = this.node.attrs.context; this.options.comment = this.node.attrs.help || false; this.ds_model = new session.web.DataSetSearch(this, this.view.model); + this.sub_model = new session.web.DataSetSearch(this,'mail.message.subtype'); this.ds_follow = new session.web.DataSetSearch(this, this.field.relation); + this.follower_model = new session.web.DataSetSearch(this,'mail.followers'); }, start: function() { @@ -51,11 +53,33 @@ openerp_mail_followers = function(session, mail) { bind_events: function() { var self = this; - this.$('button.oe_mail_button_unfollow').on('click', function () { self.do_unfollow(); }) - .mouseover(function () { $(this).html('Unfollow').removeClass('oe_mail_button_mouseout').addClass('oe_mail_button_mouseover'); }) - .mouseleave(function () { $(this).html('Following').removeClass('oe_mail_button_mouseover').addClass('oe_mail_button_mouseout'); }); - this.$el.on('click', 'button.oe_mail_button_follow', function () { self.do_follow(); }); - this.$el.on('click', 'a.oe_mail_invite', function(event) { + this.$('div.oe_mouse_subtypes') + .on('mouseover', function () { + $(this).removeClass('oe_mouseout').addClass('oe_mouseover'); + self.display_subtypes(); + }) + .on('mouseleave', function () { + $(this).removeClass('oe_mouseover').addClass('oe_mouseout'); + self.display_subtypes(); + }); + + this.$('button.oe_follower') + .on('click', function () { + if($(this).hasClass('oe_notfollow')) + self.do_follow(); + else + self.do_unfollow(); + }) + .on('mouseover', function () { + $(this).removeClass('oe_mouseout').addClass('oe_mouseover'); + }) + .on('mouseleave', function () { + $(this).removeClass('oe_mouseover').addClass('oe_mouseout'); + }); + + this.$el.on('click', 'ul.oe_subtypes input', function () { self.do_update_subscription(); }) + + this.$el.on('click', 'button.oe_invite', function(event) { action = { type: 'ir.actions.act_window', res_model: 'mail.wizard.invite', @@ -74,28 +98,35 @@ openerp_mail_followers = function(session, mail) { read_value: function() { var self = this; - return this.ds_model.read_ids([this.view.datarecord.id], ['message_is_follower', 'message_follower_ids']).then(function (results) { - self.set_value(results[0].message_follower_ids, results[0].message_is_follower); + return this.ds_model.read_ids([this.view.datarecord.id], ['message_follower_ids']).pipe(function (results) { + self.set_value(results[0].message_follower_ids); }); }, - set_value: function(value_, message_is_follower) { + set_value: function(value_) { this.reinit(); - if (! this.view.datarecord.id || - session.web.BufferedDataSet.virtual_id_regex.test(this.view.datarecord.id)) { - this.$('div.oe_mail_recthread_aside').hide(); - return; + return this.fetch_followers(value_ || this.get_value()); + }, + + set_is_follower: function(value_) { + for(var i in value_){ + if(value_[i]['user_ids'][0]==this.session.uid) + this.message_is_follower=true; + this.display_buttons(); + return true; } - return this.fetch_followers(value_ || this.get_value(), message_is_follower); + this.message_is_follower=false; + this.display_buttons(); + return false; }, - fetch_followers: function (value_, message_is_follower) { - this.value = value_; - this.message_is_follower = message_is_follower || (this.getParent().fields.message_is_follower && this.getParent().fields.message_is_follower.get_value()); - return this.ds_follow.call('read', [value_, ['name', 'user_ids']]).pipe(this.proxy('display_followers'), this.proxy('display_generic')); + fetch_followers: function (value_) { + this.value = value_ || {}; + this.message_is_follower = (this.getParent().fields.message_is_follower && this.getParent().fields.message_is_follower.get_value()); + if(value_) + return this.ds_follow.call('read', [this.value, ['name', 'user_ids']]).pipe(this.proxy('display_followers'), this.proxy('display_generic')); }, - /* Display generic info about follower, for people not having access to res_partner */ display_generic: function (error, event) { event.preventDefault(); @@ -117,32 +148,88 @@ openerp_mail_followers = function(session, mail) { display_followers: function (records) { var self = this; var node_user_list = this.$('ul.oe_mail_followers_display').empty(); - this.$('div.oe_mail_recthread_followers h4').html(this.options.title + ' (' + records.length + ')'); - _(records).each(function (record) { + this.$('div.oe_mail_recthread_followers h4').html(this.options.title + (records.length>=5 ? ' (' + records.length + ')' : '') ); + console.log(records); + for(var i=0; i -
+
-
+
    -
+
-
+
User img
-