+
@@ -234,7 +273,6 @@
-
@@ -243,6 +281,7 @@
+
@@ -284,6 +323,11 @@
+
+
diff --git a/addons/crm/crm_phonecall.py b/addons/crm/crm_phonecall.py
index 9418b05b9ea..13991245eda 100644
--- a/addons/crm/crm_phonecall.py
+++ b/addons/crm/crm_phonecall.py
@@ -288,14 +288,6 @@ class crm_phonecall(base_state, osv.osv):
# ----------------------------------------
# OpenChatter
# ----------------------------------------
-
- def get_needaction_user_ids(self, cr, uid, ids, context=None):
- result = dict.fromkeys(ids)
- for obj in self.browse(cr, uid, ids, context=context):
- result[obj.id] = []
- if (obj.state == 'draft' and obj.user_id):
- result[obj.id] = [obj.user_id.id]
- return result
def case_get_note_msg_prefix(self, cr, uid, id, context=None):
return 'Phonecall'
diff --git a/addons/crm/crm_phonecall_view.xml b/addons/crm/crm_phonecall_view.xml
index 68905d80165..6d09b2f2947 100644
--- a/addons/crm/crm_phonecall_view.xml
+++ b/addons/crm/crm_phonecall_view.xml
@@ -2,8 +2,44 @@
-
+
+
+ Mark unread
+ True
+ ir.actions.server
+
+ code
+ self.message_check_and_set_unread(cr, uid, context.get('active_ids'), context=context)
+
+
+ action_crm_phonecall_unread
+
+
+ action
+
+ crm.phonecall
+ client_action_multi
+
+
+ Mark read
+ True
+ ir.actions.server
+
+ code
+ self.message_check_and_set_read(cr, uid, context.get('active_ids'), context=context)
+
+
+ action_crm_phonecall_read
+
+
+ action
+
+ crm.phonecall
+ client_action_multi
+
+
+
Phonecall Categoriescrm.case.categ
@@ -34,6 +70,7 @@
+
-
+
-
-
+
CRM - Phone Calls Searchcrm.phonecallsearch
+
+
diff --git a/addons/crm/crm_view.xml b/addons/crm/crm_view.xml
index a733ef511f9..fc3cb3a3d2e 100644
--- a/addons/crm/crm_view.xml
+++ b/addons/crm/crm_view.xml
@@ -81,11 +81,16 @@
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/addons/crm/res_partner_view.xml b/addons/crm/res_partner_view.xml
index 19bb151b9b3..0c3750a66ac 100644
--- a/addons/crm/res_partner_view.xml
+++ b/addons/crm/res_partner_view.xml
@@ -99,10 +99,9 @@
context="{'default_partner_id': [active_id], 'default_duration': 4.0}"/>
+ context="{'search_default_partner_id': [active_id], 'default_duration': 1.0, 'default_partner_id': active_id}" />
+ name="%(crm.crm_case_category_act_oppor11)d" context="{'search_default_partner_id': [active_id]}"/>
diff --git a/addons/crm/wizard/crm_phonecall_to_phonecall_view.xml b/addons/crm/wizard/crm_phonecall_to_phonecall_view.xml
index a9819d63886..7319c1b0437 100644
--- a/addons/crm/wizard/crm_phonecall_to_phonecall_view.xml
+++ b/addons/crm/wizard/crm_phonecall_to_phonecall_view.xml
@@ -17,15 +17,12 @@
-
-
-
diff --git a/addons/crm_caldav/crm_caldav_view.xml b/addons/crm_caldav/crm_caldav_view.xml
index 287e301532e..5746a3af0aa 100644
--- a/addons/crm_caldav/crm_caldav_view.xml
+++ b/addons/crm_caldav/crm_caldav_view.xml
@@ -2,7 +2,7 @@
- Caldav Browse
+ Synchronize Your Meetingsir.actions.act_windowuser.preference
diff --git a/addons/crm_claim/crm_claim.py b/addons/crm_claim/crm_claim.py
index 808ad2e2055..5b98258240c 100644
--- a/addons/crm_claim/crm_claim.py
+++ b/addons/crm_claim/crm_claim.py
@@ -183,59 +183,53 @@ class crm_claim(base_stage, osv.osv):
}
address = self.pool.get('res.partner').browse(cr, uid, part)
return {'value': {'email_from': address.email, 'partner_phone': address.phone}}
-
- def message_new(self, cr, uid, msg, custom_values=None, context=None):
- """Automatically called when new email message arrives"""
- res_id = super(crm_claim,self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
- subject = msg.get('subject')
- body = msg.get('body_text')
- msg_from = msg.get('from')
- priority = msg.get('priority')
- vals = {
- 'name': subject,
- 'email_from': msg_from,
- 'email_cc': msg.get('cc'),
- 'description': body,
- 'user_id': False,
- }
- if priority:
- vals['priority'] = priority
- vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
- self.write(cr, uid, [res_id], vals, context=context)
- return res_id
- def message_update(self, cr, uid, ids, msg, vals={}, default_act='pending', context=None):
+ # -------------------------------------------------------
+ # Mail gateway
+ # -------------------------------------------------------
+
+ def message_new(self, cr, uid, msg, custom_values=None, context=None):
+ """ Overrides mail_thread message_new that is called by the mailgateway
+ through message_process.
+ This override updates the document according to the email.
+ """
+ if custom_values is None: custom_values = {}
+ custom_values.update({
+ 'name': msg.get('subject') or _("No Subject"),
+ 'description': msg.get('body_text'),
+ 'email_from': msg.get('from'),
+ 'email_cc': msg.get('cc'),
+ })
+ if msg.get('priority'):
+ custom_values['priority'] = msg.get('priority')
+ custom_values.update(self.message_partner_by_email(cr, uid, msg.get('from'), context=context))
+ return super(crm_claim,self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
+
+ def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
+ """ Overrides mail_thread message_update that is called by the mailgateway
+ through message_process.
+ This method updates the document according to the email.
+ """
if isinstance(ids, (str, int, long)):
ids = [ids]
-
- res_id = super(crm_claim,self).message_update(cr, uid, ids, msg, context=context)
+ if update_vals is None: update_vals = {}
if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
- vals['priority'] = msg.get('priority')
+ update_vals['priority'] = msg.get('priority')
maps = {
'cost':'planned_cost',
'revenue': 'planned_revenue',
'probability':'probability'
}
- vls = {}
for line in msg['body_text'].split('\n'):
line = line.strip()
res = tools.misc.command_re.match(line)
if res and maps.get(res.group(1).lower()):
key = maps.get(res.group(1).lower())
- vls[key] = res.group(2).lower()
- vals.update(vls)
+ update_vals[key] = res.group(2).lower()
- # Unfortunately the API is based on lists
- # but we want to update the state based on the
- # previous state, so we have to loop:
- for case in self.browse(cr, uid, ids, context=context):
- values = dict(vals)
- if case.state in CRM_CLAIM_PENDING_STATES:
- values.update(state=crm.AVAILABLE_STATES[1][0]) #re-open
- res = self.write(cr, uid, [case.id], values, context=context)
- return res
+ return super(crm_claim,self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
# ---------------------------------------------------
# OpenChatter methods and notifications
diff --git a/addons/crm_fundraising/crm_fundraising.py b/addons/crm_fundraising/crm_fundraising.py
index 73f72979730..20d90795ffc 100644
--- a/addons/crm_fundraising/crm_fundraising.py
+++ b/addons/crm_fundraising/crm_fundraising.py
@@ -127,21 +127,26 @@ class crm_fundraising(base_stage, osv.osv):
self.create_send_note(cr, uid, [obj_id], context=context)
return obj_id
+ # -------------------------------------------------------
+ # Mail gateway
+ # -------------------------------------------------------
+
def message_new(self, cr, uid, msg, custom_values=None, context=None):
- """Automatically called when new email message arrives"""
- res_id = super(crm_fundraising,self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
- vals = {
- 'name': msg.get('subject'),
+ """ Overrides mail_thread message_new that is called by the mailgateway
+ through message_process.
+ This override also updates the document according to the email.
+ """
+ if custom_values is None: custom_values = {}
+ custom_values.update({
+ 'name': msg.get('subject') or _("No Subject"),
+ 'description': msg.get('body_text'),
'email_from': msg.get('from'),
'email_cc': msg.get('cc'),
- 'description': msg.get('body_text'),
- }
- priority = msg.get('priority')
- if priority:
- vals['priority'] = priority
- vals.update(self.message_partner_by_email(cr, uid, msg.get('from')))
- self.write(cr, uid, [res_id], vals, context=context)
- return res_id
+ })
+ if msg.get('priority'):
+ custom_values['priority'] = priority
+ custom_values.update(self.message_partner_by_email(cr, uid, msg.get('from'), context=context))
+ return super(crm_fundraising,self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
# ---------------------------------------------------
# OpenChatter methods and notifications
diff --git a/addons/crm_fundraising/crm_fundraising_view.xml b/addons/crm_fundraising/crm_fundraising_view.xml
index aefb64db4e6..212ef29a6a1 100644
--- a/addons/crm_fundraising/crm_fundraising_view.xml
+++ b/addons/crm_fundraising/crm_fundraising_view.xml
@@ -72,38 +72,48 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
-
diff --git a/addons/crm_helpdesk/crm_helpdesk.py b/addons/crm_helpdesk/crm_helpdesk.py
index f0d98efebb3..fe7b0581e9b 100644
--- a/addons/crm_helpdesk/crm_helpdesk.py
+++ b/addons/crm_helpdesk/crm_helpdesk.py
@@ -95,55 +95,51 @@ class crm_helpdesk(base_state, osv.osv):
self.create_send_note(cr, uid, [obj_id], context=context)
return obj_id
- def message_new(self, cr, uid, msg_dict, custom_values=None, context=None):
- """Automatically called when new email message arrives"""
- res_id = super(crm_helpdesk,self).message_new(cr, uid, msg_dict, custom_values=custom_values, context=context)
- subject = msg_dict.get('subject') or _("No Subject")
- body = msg_dict.get('body_text')
- msg_from = msg_dict.get('from')
- vals = {
- 'name': subject,
- 'email_from': msg_from,
- 'email_cc': msg_dict.get('cc'),
- 'description': body,
- 'user_id': False,
- }
- vals.update(self.message_partner_by_email(cr, uid, msg_from))
- self.write(cr, uid, [res_id], vals, context)
- return res_id
+ # -------------------------------------------------------
+ # Mail gateway
+ # -------------------------------------------------------
- def message_update(self, cr, uid, ids, msg, vals={}, default_act='pending', context=None):
+ def message_new(self, cr, uid, msg, custom_values=None, context=None):
+ """ Overrides mail_thread message_new that is called by the mailgateway
+ through message_process.
+ This override updates the document according to the email.
+ """
+ if custom_values is None: custom_values = {}
+ custom_values.update({
+ 'name': msg.get('subject') or _("No Subject"),
+ 'description': msg.get('body_text'),
+ 'email_from': msg.get('from'),
+ 'email_cc': msg.get('cc'),
+ 'user_id': False,
+ })
+ custom_values.update(self.message_partner_by_email(cr, uid, msg.get('from'), context=context))
+ return super(crm_helpdesk,self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
+
+ def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
+ """ Overrides mail_thread message_update that is called by the mailgateway
+ through message_process.
+ This method updates the document according to the email.
+ """
if isinstance(ids, (str, int, long)):
ids = [ids]
-
- super(crm_helpdesk,self).message_update(cr, uid, ids, msg, context=context)
+ if update_vals is None: update_vals = {}
if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
- vals['priority'] = msg.get('priority')
+ update_vals['priority'] = msg.get('priority')
maps = {
'cost':'planned_cost',
'revenue': 'planned_revenue',
'probability':'probability'
}
- vls = {}
for line in msg['body_text'].split('\n'):
line = line.strip()
res = tools.misc.command_re.match(line)
if res and maps.get(res.group(1).lower()):
key = maps.get(res.group(1).lower())
- vls[key] = res.group(2).lower()
- vals.update(vls)
+ update_vals[key] = res.group(2).lower()
- # Unfortunately the API is based on lists
- # but we want to update the state based on the
- # previous state, so we have to loop:
- for case in self.browse(cr, uid, ids, context=context):
- values = dict(vals)
- if case.state in CRM_HELPDESK_STATES:
- values.update(state=crm.AVAILABLE_STATES[1][0]) #re-open
- res = self.write(cr, uid, [case.id], values, context=context)
- return res
+ return super(crm_helpdesk,self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
# ******************************
# OpenChatter
diff --git a/addons/event/event.py b/addons/event/event.py
index a348e005d06..26fef61d2b5 100644
--- a/addons/event/event.py
+++ b/addons/event/event.py
@@ -271,13 +271,6 @@ class event_event(osv.osv):
# OpenChatter methods and notifications
# ----------------------------------------
- def get_needaction_user_ids(self, cr, uid, ids, context=None):
- result = dict.fromkeys(ids, [])
- for obj in self.browse(cr, uid, ids, context=context):
- if obj.state == 'draft' and obj.user_id:
- result[obj.id] = [obj.user_id.id]
- return result
-
def create_send_note(self, cr, uid, ids, context=None):
message = _("Event has been created.")
self.message_append_note(cr, uid, ids, body=message, context=context)
@@ -451,13 +444,6 @@ class event_registration(osv.osv):
# OpenChatter methods and notifications
# ----------------------------------------
- def get_needaction_user_ids(self, cr, uid, ids, context=None):
- result = dict.fromkeys(ids, [])
- for obj in self.browse(cr, uid, ids, context=context):
- if obj.state == 'draft' and obj.user_id:
- result[obj.id] = [obj.user_id.id]
- return result
-
def create_send_note(self, cr, uid, ids, context=None):
message = _("Registration has been created.")
self.message_append_note(cr, uid, ids, body=message, context=context)
diff --git a/addons/event/event_view.xml b/addons/event/event_view.xml
index 0e9647d496d..fcf8231f5f7 100644
--- a/addons/event/event_view.xml
+++ b/addons/event/event_view.xml
@@ -47,6 +47,43 @@
+
+
+ Mark unread
+ True
+ ir.actions.server
+
+ code
+ self.message_check_and_set_unread(cr, uid, context.get('active_ids'), context=context)
+
+
+ action_event_event_unread
+
+
+ action
+
+ event.event
+ client_action_multi
+
+
+
+ Mark read
+ True
+ ir.actions.server
+
+ code
+ self.message_check_and_set_read(cr, uid, context.get('active_ids'), context=context)
+
+
+ action_event_event_read
+
+
+ action
+
+ event.event
+ client_action_multi
+
+
Eventsevent.event
@@ -144,7 +181,7 @@
event.eventtree
-
+
@@ -155,6 +192,7 @@
+
@@ -264,6 +302,11 @@
search
+
+
@@ -330,12 +373,49 @@
+
+
+ Mark unread
+ True
+ ir.actions.server
+
+ code
+ self.message_check_and_set_unread(cr, uid, context.get('active_ids'), context=context)
+
+
+ action_event_registration_unread
+
+
+ action
+
+ event.registration
+ client_action_multi
+
+
+
+ Mark read
+ True
+ ir.actions.server
+
+ code
+ self.message_check_and_set_read(cr, uid, context.get('active_ids'), context=context)
+
+
+ action_event_registration_read
+
+
+ action
+
+ event.registration
+ client_action_multi
+
+
event.registration.treeevent.registrationtree
-
+
@@ -345,6 +425,7 @@
+
@@ -449,6 +530,11 @@
+
+
diff --git a/addons/hr_holidays/hr_holidays.py b/addons/hr_holidays/hr_holidays.py
index f5e8ba69e5d..a1ab0ae04f3 100644
--- a/addons/hr_holidays/hr_holidays.py
+++ b/addons/hr_holidays/hr_holidays.py
@@ -359,7 +359,7 @@ class hr_holidays(osv.osv):
# -----------------------------
def get_needaction_user_ids(self, cr, uid, ids, context=None):
- result = dict.fromkeys(ids, [])
+ result = super(hr_holidays, self).get_needaction_user_ids(cr, uid, ids, context=context)
for obj in self.browse(cr, uid, ids, context=context):
if obj.state == 'confirm' and obj.employee_id.parent_id:
result[obj.id] = [obj.employee_id.parent_id.user_id.id]
@@ -371,14 +371,16 @@ class hr_holidays(osv.osv):
hr_manager_group = self.pool.get('res.groups').read(cr, uid, [obj_id], ['users'], context=context)[0]
result[obj.id] = hr_manager_group['users']
return result
-
+
def message_get_subscribers(self, cr, uid, ids, context=None):
- sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
- # add the employee and its manager if specified to the subscribed users
+ """ Override to add employee and its manager. """
+ user_ids = super(hr_holidays, self).message_get_subscribers(cr, uid, ids, context=context)
for obj in self.browse(cr, uid, ids, context=context):
- if obj.employee_id.parent_id:
- sub_ids.append(obj.employee_id.parent_id.user_id.id)
- return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
+ if obj.user_id and not obj.user_id.id in user_ids:
+ user_ids.append(obj.user_id.id)
+ if obj.employee_id.parent_id and not obj.employee_id.parent_id.user_id.id in user_ids:
+ user_ids.append(obj.employee_id.parent_id.user_id.id)
+ return user_ids
def create_notificate(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids, context=context):
diff --git a/addons/hr_holidays/hr_holidays_view.xml b/addons/hr_holidays/hr_holidays_view.xml
index 31c50e86bdf..bda18f1bc6c 100644
--- a/addons/hr_holidays/hr_holidays_view.xml
+++ b/addons/hr_holidays/hr_holidays_view.xml
@@ -1,6 +1,43 @@
+
+
+ Mark unread
+ True
+ ir.actions.server
+
+ code
+ self.message_check_and_set_unread(cr, uid, context.get('active_ids'), context=context)
+
+
+ action_holidays_unread
+
+
+ action
+
+ hr.holidays
+ client_action_multi
+
+
+
+ Mark read
+ True
+ ir.actions.server
+
+ code
+ self.message_check_and_set_read(cr, uid, context.get('active_ids'), context=context)
+
+
+ action_holidays_read
+
+
+ action
+
+ hr.holidays
+ client_action_multi
+
+
hr.holidays.filterhr.holidays
@@ -10,6 +47,11 @@
+
+
diff --git a/addons/hr_recruitment/hr_recruitment.py b/addons/hr_recruitment/hr_recruitment.py
index c893755c7c0..7b0090d60ea 100644
--- a/addons/hr_recruitment/hr_recruitment.py
+++ b/addons/hr_recruitment/hr_recruitment.py
@@ -357,54 +357,53 @@ class hr_applicant(base_stage, osv.Model):
return value
def message_new(self, cr, uid, msg, custom_values=None, context=None):
- """Automatically called when new email message arrives"""
- res_id = super(hr_applicant,self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
- subject = msg.get('subject') or _("No Subject")
- body = msg.get('body_text')
- msg_from = msg.get('from')
- priority = msg.get('priority')
- vals = {
- 'name': subject,
- 'email_from': msg_from,
+ """ Overrides mail_thread message_new that is called by the mailgateway
+ through message_process.
+ This override updates the document according to the email.
+ """
+ if custom_values is None: custom_values = {}
+ custom_values.update({
+ 'name': msg.get('subject') or _("No Subject"),
+ 'description': msg.get('body_text'),
+ 'email_from': msg.get('from'),
'email_cc': msg.get('cc'),
- 'description': body,
'user_id': False,
- }
- if priority:
- vals['priority'] = priority
- vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
- self.write(cr, uid, [res_id], vals, context)
- return res_id
+ })
+ if msg.get('priority'):
+ custom_values['priority'] = msg.get('priority')
+ custom_values.update(self.message_partner_by_email(cr, uid, msg.get('from', False), context=context))
+ return super(hr_applicant,self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
- def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
+ def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
+ """ Override mail_thread message_update that is called by the mailgateway
+ through message_process.
+ This method updates the document according to the email.
+ """
if isinstance(ids, (str, int, long)):
ids = [ids]
- if vals is None:
- vals = {}
- msg_from = msg['from']
- vals.update({
- 'description': msg['body_text']
+ if update_vals is None: vals = {}
+
+ update_vals.update({
+ 'description': msg.get('body'),
+ 'email_from': msg.get('from'),
+ 'email_cc': msg.get('cc'),
})
- if msg.get('priority', False):
- vals['priority'] = msg.get('priority')
+ if msg.get('priority'):
+ update_vals['priority'] = msg.get('priority')
maps = {
- 'cost':'planned_cost',
+ 'cost': 'planned_cost',
'revenue': 'planned_revenue',
- 'probability':'probability'
+ 'probability': 'probability',
}
- vls = { }
- for line in msg['body_text'].split('\n'):
+ for line in msg.get('body_text', '').split('\n'):
line = line.strip()
res = tools.misc.command_re.match(line)
if res and maps.get(res.group(1).lower(), False):
key = maps.get(res.group(1).lower())
- vls[key] = res.group(2).lower()
+ update_vals[key] = res.group(2).lower()
- vals.update(vls)
- res = self.write(cr, uid, ids, vals, context=context)
- self.message_append_dict(cr, uid, ids, msg, context=context)
- return res
+ return super(hr_applicant, self).message_update(cr, uids, ids, update_vals=update_vals, context=context)
def create(self, cr, uid, vals, context=None):
obj_id = super(hr_applicant, self).create(cr, uid, vals, context=context)
@@ -493,20 +492,14 @@ class hr_applicant(base_stage, osv.Model):
# -------------------------------------------------------
# OpenChatter methods and notifications
# -------------------------------------------------------
-
- def message_get_subscribers(self, cr, uid, ids, context=None):
- sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
- for obj in self.browse(cr, uid, ids, context=context):
- if obj.user_id:
- sub_ids.append(obj.user_id.id)
- return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
- def get_needaction_user_ids(self, cr, uid, ids, context=None):
- result = dict.fromkeys(ids, [])
+ def message_get_subscribers(self, cr, uid, ids, context=None):
+ """ Override to add responsible user. """
+ user_ids = super(hr_applicant, self).message_get_subscribers(cr, uid, ids, context=context)
for obj in self.browse(cr, uid, ids, context=context):
- if obj.state == 'draft' and obj.user_id:
- result[obj.id] = [obj.user_id.id]
- return result
+ if obj.user_id and not obj.user_id.id in user_ids:
+ user_ids.append(obj.user_id.id)
+ return user_ids
def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
""" Override of the (void) default notification method. """
diff --git a/addons/hr_recruitment/hr_recruitment_view.xml b/addons/hr_recruitment/hr_recruitment_view.xml
index 86cb259dad4..00b300cb6f6 100644
--- a/addons/hr_recruitment/hr_recruitment_view.xml
+++ b/addons/hr_recruitment/hr_recruitment_view.xml
@@ -45,6 +45,43 @@
# Jobs
# ------------------------------------------------------
+
+
+ Mark unread
+ True
+ ir.actions.server
+
+ code
+ self.message_check_and_set_unread(cr, uid, context.get('active_ids'), context=context)
+
+
+ action_project_unread
+
+
+ action
+
+ hr.applicant
+ client_action_multi
+
+
+
+ Mark read
+ True
+ ir.actions.server
+
+ code
+ self.message_check_and_set_read(cr, uid, context.get('active_ids'), context=context)
+
+
+ action_project_read
+
+
+ action
+
+ hr.applicant
+ client_action_multi
+
+
Applicantshr.applicant
@@ -192,6 +229,12 @@
search
+
+
+ form
diff --git a/addons/mail/__init__.py b/addons/mail/__init__.py
index d9835f86fc5..805c27386f3 100644
--- a/addons/mail/__init__.py
+++ b/addons/mail/__init__.py
@@ -23,6 +23,7 @@ import mail_message
import mail_thread
import mail_group
import mail_subscription
+import ir_needaction
import res_users
import res_partner
import report
diff --git a/addons/mail/__openerp__.py b/addons/mail/__openerp__.py
index 8ba3c02b0b3..f501cc58d2c 100644
--- a/addons/mail/__openerp__.py
+++ b/addons/mail/__openerp__.py
@@ -86,7 +86,7 @@ The main features are:
'static/src/css/mail_group.css',
],
'js': [
- 'static/src/js/jquery.expander.min.js',
+ 'static/lib/jquery.expander/jquery.expander.js',
'static/src/js/mail.js',
],
'qweb': [
diff --git a/addons/mail/doc/index.rst.inc b/addons/mail/doc/index.rst.inc
index 822d855d5b4..c4b87cc18f9 100644
--- a/addons/mail/doc/index.rst.inc
+++ b/addons/mail/doc/index.rst.inc
@@ -10,3 +10,4 @@ Mail Module documentation topics
mail_openchatter_howto
mail_needaction_howto
mail_partner
+ mail_state
diff --git a/addons/mail/doc/mail_state.rst b/addons/mail/doc/mail_state.rst
new file mode 100644
index 00000000000..527a31ba835
--- /dev/null
+++ b/addons/mail/doc/mail_state.rst
@@ -0,0 +1,28 @@
+.. _mail_state:
+
+message_state
+=============
+
+``message_state`` is a boolean field that states whether the document
+has unread messages. In previous versions, some documents were going
+back to ``pending`` state when receiving an email through the mail
+gateway. Now the state related to messages differs from the state or
+stage of the document itself.
+
+message_state and need action mechanism
++++++++++++++++++++++++++++++++++++++++
+
+The ``mail`` module introduces a default behavior for the need_action
+mechanism [REF].
+
+::
+
+ def get_needaction_user_ids(self, cr, uid, ids, context=None):
+ """ Returns the user_ids that have to perform an action
+ :return: dict { record_id: [user_ids], }
+ """
+ result = super(ir_needaction_mixin, self).get_needaction_user_ids(cr, uid, ids, context=context)
+ for obj in self.browse(cr, uid, ids, context=context):
+ if obj.message_state == False and obj.user_id:
+ result[obj.id].append(obj.user_id.id)
+ return result
diff --git a/addons/mail/ir_needaction.py b/addons/mail/ir_needaction.py
new file mode 100644
index 00000000000..8193ac4f385
--- /dev/null
+++ b/addons/mail/ir_needaction.py
@@ -0,0 +1,46 @@
+# -*- 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
+
+class ir_needaction_mixin(osv.Model):
+ """ Update of ir.needaction_mixin class
+ - override the get_needaction_user_ids method to define the default
+ mail gateway need_action: when the object is unread, the object
+ responsible has an action to perform.
+ """
+ _name = 'ir.needaction_mixin'
+ _inherit = ['ir.needaction_mixin']
+
+ def get_needaction_user_ids(self, cr, uid, ids, context=None):
+ """ Returns the user_ids that have to perform an action. It the
+ document mail state is unread (False), return object.user_id.id
+ as need_action uid.
+ :return: dict { record_id: [user_ids], }
+ """
+ result = super(ir_needaction_mixin, self).get_needaction_user_ids(cr, uid, ids, context=context)
+ for obj in self.browse(cr, uid, ids, context=context):
+ if obj.message_state == False and obj.user_id:
+ result[obj.id].append(obj.user_id.id)
+ return result
+
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/mail/mail_group.py b/addons/mail/mail_group.py
index 0a8677d7862..f691d75e0bc 100644
--- a/addons/mail/mail_group.py
+++ b/addons/mail/mail_group.py
@@ -87,7 +87,7 @@ class mail_group(osv.osv):
result = dict.fromkeys(ids)
for id in ids:
result[id] = {}
- result[id]['member_ids'] = self.message_get_subscribers_ids(cr, uid, [id], context=context)
+ result[id]['member_ids'] = self.message_get_subscribers(cr, uid, [id], context=context)
result[id]['member_count'] = len(result[id]['member_ids'])
result[id]['is_subscriber'] = uid in result[id]['member_ids']
return result
diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py
index d0af1ed8841..5668709af17 100644
--- a/addons/mail/mail_thread.py
+++ b/addons/mail/mail_thread.py
@@ -34,7 +34,7 @@ import xmlrpclib
_logger = logging.getLogger(__name__)
-class mail_thread(osv.osv):
+class mail_thread(osv.Model):
'''Mixin model, meant to be inherited by any model that needs to
act as a discussion topic on which messages can be attached.
Public methods are prefixed with ``message_`` in order to avoid
@@ -62,10 +62,15 @@ class mail_thread(osv.osv):
_name = 'mail.thread'
_description = 'Email Thread'
- def _get_message_ids(self, cr, uid, ids, name, arg, context=None):
+ def _get_message_ids(self, cr, uid, ids, name, args, context=None):
res = {}
for id in ids:
- res[id] = self.message_load_ids(cr, uid, [id], context=context)
+ message_ids = self.message_load_ids(cr, uid, [id], context=context)
+ subscriber_ids = self.message_get_subscribers(cr, uid, [id], context=context)
+ res[id] = {
+ 'message_ids': message_ids,
+ 'message_summary': "Msg: %d . Fol: %d" % (len(message_ids), len(subscriber_ids)),
+ }
return res
def _search_message_ids(self, cr, uid, obj, name, args, context=None):
@@ -73,10 +78,23 @@ class mail_thread(osv.osv):
msg_ids = msg_obj.search(cr, uid, ['&', ('res_id', 'in', args[0][2]), ('model', '=', self._name)], context=context)
return [('id', 'in', msg_ids)]
- # OpenChatter: message_ids is a dummy field that should not be used
_columns = {
- 'message_ids': fields.function(_get_message_ids, method=True, fnct_search=_search_message_ids,
- type='one2many', obj='mail.message', string='Temp messages', _fields_id = 'res_id'),
+ 'message_ids': fields.function(_get_message_ids, method=True,
+ fnct_search=_search_message_ids,
+ type='one2many', obj='mail.message', _fields_id = 'res_id',
+ string='Temp messages', multi="_get_message_ids",
+ help="Functional field holding messages related to the current document."),
+ 'message_state': fields.boolean('Read',
+ help="When checked, new messages require your attention."),
+ 'message_summary': fields.function(_get_message_ids, method=True,
+ type='text', string='Summary', multi="_get_message_ids",
+ help="Holds the Chatter summary (number of messages, ...). "\
+ "This summary is directly in html format in order to "\
+ "be inserted in kanban views."),
+ }
+
+ _defaults = {
+ 'message_state': True,
}
#------------------------------------------------------
@@ -84,10 +102,11 @@ class mail_thread(osv.osv):
#------------------------------------------------------
def create(self, cr, uid, vals, context=None):
- """Automatically subscribe the creator"""
- thread_id = super(mail_thread, self).create(cr, uid, vals, context=context);
- self.message_subscribe(cr, uid, [thread_id], [uid], context=context)
- return thread_id;
+ """Automatically subscribe the creator """
+ thread_id = super(mail_thread, self).create(cr, uid, vals, context=context)
+ if thread_id:
+ self.message_subscribe(cr, uid, [thread_id], [uid], context=context)
+ return thread_id
def write(self, cr, uid, ids, vals, context=None):
"""Automatically subscribe the writer"""
@@ -106,8 +125,6 @@ class mail_thread(osv.osv):
a foreign key with a 'cascade' ondelete attribute.
Notifications will be deleted with messages
"""
- if context is None:
- context = {}
subscr_obj = self.pool.get('mail.subscription')
msg_obj = self.pool.get('mail.message')
# delete subscriptions
@@ -120,17 +137,18 @@ class mail_thread(osv.osv):
return super(mail_thread, self).unlink(cr, uid, ids, context=context)
#------------------------------------------------------
- # Generic message api
+ # mail.message wrappers and tools
#------------------------------------------------------
def message_create(self, cr, uid, thread_id, vals, context=None):
- """OpenSocial: wrapper of mail.message create method
+ """ OpenChatter: wrapper of mail.message create method
- creates the mail.message
- automatically subscribe the message writer
- push the message to subscribed users
"""
if context is None:
context = {}
+
message_obj = self.pool.get('mail.message')
subscription_obj = self.pool.get('mail.subscription')
notification_obj = self.pool.get('mail.notification')
@@ -167,7 +185,10 @@ class mail_thread(osv.osv):
# create message
msg_id = message_obj.create(cr, uid, vals, context=context)
-
+
+ # Set as unread if writer is not the document responsible
+ self.message_create_set_unread(cr, uid, [thread_id], context=context)
+
# special: if install mode, do not push demo data
if context.get('install_mode', False):
return True
@@ -186,7 +207,7 @@ class mail_thread(osv.osv):
body = new_msg_vals.get('body_html', '') if new_msg_vals.get('subtype', 'plain') == 'html' else new_msg_vals.get('body_text', '')
for thread_id in thread_ids:
# add subscribers
- notif_user_ids += [user['id'] for user in self.message_get_subscribers(cr, uid, [thread_id], context=context)]
+ notif_user_ids += self.message_get_subscribers(cr, uid, [thread_id], context=context)
# add users requested via parsing message (@login)
notif_user_ids += self.message_parse_users(cr, uid, [thread_id], body, context=context)
# add users requested to perform an action (need_action mechanism)
@@ -214,6 +235,10 @@ class mail_thread(osv.osv):
user_ids = self.pool.get('res.users').search(cr, uid, [('login', 'in', login_lst)], context=context)
return user_ids
+ #------------------------------------------------------
+ # Generic message api
+ #------------------------------------------------------
+
def message_capable_models(self, cr, uid, context=None):
ret_dict = {}
for model_name in self.pool.obj_list():
@@ -376,7 +401,6 @@ class mail_thread(osv.osv):
original = msg_dict.get('original'),
context = context)
- # Message loading
def _message_add_ancestor_ids(self, cr, uid, ids, child_ids, root_ids, context=None):
""" Given message child_ids
Find their ancestors until root ids"""
@@ -420,6 +444,9 @@ class mail_thread(osv.osv):
msg_ids = self.message_load_ids(cr, uid, ids, limit, offset, domain, ascent, root_ids, context=context)
msgs = self.pool.get('mail.message').read(cr, uid, msg_ids, [], context=context)
+ # Set as read
+ self.message_check_and_set_read(cr, uid, ids, context=context)
+
""" Retrieve all attachments names """
map_id_to_name = {}
@@ -474,7 +501,7 @@ class mail_thread(osv.osv):
return msgs
#------------------------------------------------------
- # Email specific
+ # Mail gateway
#------------------------------------------------------
# message_process will call either message_new or message_update.
@@ -549,7 +576,7 @@ class mail_thread(osv.osv):
if res_id:
res_id = res_id.group(1)
if res_id:
- res_id = int(res_id)
+ res_id = res_id
if model_pool.exists(cr, uid, res_id):
if hasattr(model_pool, 'message_update'):
model_pool.message_update(cr, uid, [res_id], msg, {}, context=context)
@@ -558,7 +585,7 @@ class mail_thread(osv.osv):
res_id = False
if not res_id:
res_id = create_record(msg)
- #To forward the email to other followers
+ # To forward the email to other followers
self.message_forward(cr, uid, model, [res_id], msg_txt, context=context)
return res_id
@@ -594,29 +621,34 @@ class mail_thread(osv.osv):
fields = model_pool.fields_get(cr, uid, context=context)
data = model_pool.default_get(cr, uid, fields, context=context)
if 'name' in fields and not data.get('name'):
- data['name'] = msg_dict.get('from','')
+ data['name'] = msg_dict.get('from', '')
if custom_values and isinstance(custom_values, dict):
data.update(custom_values)
res_id = model_pool.create(cr, uid, data, context=context)
self.message_append_dict(cr, uid, [res_id], msg_dict, context=context)
return res_id
- def message_update(self, cr, uid, ids, msg_dict, vals={}, default_act=None, context=None):
- """Called by ``message_process`` when a new message is received
- for an existing thread. The default behavior is to create a
- new mail.message in the given thread (by calling
- ``message_append_dict``)
- Additional behavior may be implemented by overriding this
- method.
+ def message_update(self, cr, uid, ids, msg_dict, update_vals=None, context=None):
+ """ Called by ``message_process`` when a new message is received
+ for an existing thread. The default behavior is to create a
+ new mail.message in the given thread (by calling
+ ``message_append_dict``)
+ Additional behavior may be implemented by overriding this
+ method.
- :param dict msg_dict: a map containing the email details and
+ :param dict msg_dict: a map containing the email details and
attachments. See ``message_process`` and
``mail.message.parse()`` for details.
- :param dict context: if a ``thread_model`` value is present
+ :param dict vals: a dict containing values to update records
+ given their ids; if the dict is None or is
+ void, no write operation is performed.
+ :param dict context: if a ``thread_model`` value is present
in the context, its value will be used
to determine the model of the thread to
update (instead of the current model).
"""
+ if update_vals:
+ self.write(cr, uid, ids, update_vals, context=context)
return self.message_append_dict(cr, uid, ids, msg_dict, context=context)
def message_thread_followers(self, cr, uid, ids, context=None):
@@ -760,51 +792,77 @@ class mail_thread(osv.osv):
# Subscription mechanism
#------------------------------------------------------
- def message_get_subscribers_ids(self, cr, uid, ids, context=None):
+ def message_get_subscribers(self, cr, uid, ids, context=None):
+ """ Returns the current document followers. Basically this method
+ checks in mail.subscription for entries with matching res_model,
+ res_id.
+
+ :param get_ids: if set to True, return the ids of users; if set
+ to False, returns the result of a read in res.users
+ """
subscr_obj = self.pool.get('mail.subscription')
subscr_ids = subscr_obj.search(cr, uid, ['&', ('res_model', '=', self._name), ('res_id', 'in', ids)], context=context)
- subs = subscr_obj.read(cr, uid, subscr_ids, context=context)
- return [sub['user_id'][0] for sub in subs]
+ return [sub['user_id'][0] for sub in subscr_obj.read(cr, uid, subscr_ids, ['user_id'], context=context)]
- def message_get_subscribers(self, cr, uid, ids, context=None):
- user_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context)
- users = self.pool.get('res.users').read(cr, uid, user_ids, fields=['id', 'name', 'avatar'], context=context)
- return users
+ def message_read_subscribers(self, cr, uid, ids, fields=['id', 'name', 'avatar'], context=None):
+ """ Returns the current document followers as a read result. Used
+ mainly for Chatter having only one method to call to have
+ details about users.
+ """
+ user_ids = self.message_get_subscribers(cr, uid, ids, context=context)
+ return self.pool.get('res.users').read(cr, uid, user_ids, fields=fields, context=context)
def message_is_subscriber(self, cr, uid, ids, user_id = None, context=None):
- users = self.message_get_subscribers(cr, uid, ids, context=context)
+ """ Check if uid or user_id (if set) is a subscriber to the current
+ document.
+
+ :param user_id: if set, check is done on user_id; if not set
+ check is done on uid
+ """
sub_user_id = uid if user_id is None else user_id
- if sub_user_id in [user['id'] for user in users]:
+ if sub_user_id in self.message_get_subscribers(cr, uid, ids, context=context):
return True
return False
def message_subscribe(self, cr, uid, ids, user_ids = None, context=None):
+ """ Subscribe the user (or user_ids) to the current document.
+
+ :param user_ids: a list of user_ids; if not set, subscribe
+ uid instead
+ """
subscription_obj = self.pool.get('mail.subscription')
to_subscribe_uids = [uid] if user_ids is None else user_ids
create_ids = []
for id in ids:
+ already_subscribed_user_ids = self.message_get_subscribers(cr, uid, [id], context=context)
for user_id in to_subscribe_uids:
- if self.message_is_subscriber(cr, uid, [id], user_id=user_id, context=context): continue
+ if user_id in already_subscribed_user_ids: continue
create_ids.append(subscription_obj.create(cr, uid, {'res_model': self._name, 'res_id': id, 'user_id': user_id}, context=context))
return create_ids
def message_unsubscribe(self, cr, uid, ids, user_ids = None, context=None):
- if not user_ids and not uid in self.message_get_subscribers_ids(cr, uid, ids, context=context):
+ """ Unsubscribe the user (or user_ids) from the current document.
+
+ :param user_ids: a list of user_ids; if not set, subscribe
+ uid instead
+ """
+ # Trying to unsubscribe somebody not in subscribers: returns False
+ # if special management is needed; allows to know that an automatically
+ # subscribed user tries to unsubscribe and allows to warn him
+ mail_thread_model = self.pool.get('mail.thread')
+ if not user_ids and not uid in mail_thread_model.message_get_subscribers(cr, uid, ids, context=context):
return False
subscription_obj = self.pool.get('mail.subscription')
to_unsubscribe_uids = [uid] if user_ids is None else user_ids
to_delete_sub_ids = subscription_obj.search(cr, uid,
['&', '&', ('res_model', '=', self._name), ('res_id', 'in', ids), ('user_id', 'in', to_unsubscribe_uids)], context=context)
- subscription_obj.unlink(cr, uid, to_delete_sub_ids, context=context)
- return True
+ return subscription_obj.unlink(cr, uid, to_delete_sub_ids, context=context)
#------------------------------------------------------
# Notification API
#------------------------------------------------------
def message_remove_pushed_notifications(self, cr, uid, ids, msg_ids, remove_childs=True, context=None):
- if context is None:
- context = {}
notif_obj = self.pool.get('mail.notification')
msg_obj = self.pool.get('mail.message')
if remove_childs:
@@ -814,4 +872,37 @@ class mail_thread(osv.osv):
to_del_notif_ids = notif_obj.search(cr, uid, ['&', ('user_id', '=', uid), ('message_id', 'in', notif_msg_ids)], context=context)
return notif_obj.unlink(cr, uid, to_del_notif_ids, context=context)
+ #------------------------------------------------------
+ # Thread_state
+ #------------------------------------------------------
+
+ def message_create_set_unread(self, cr, uid, ids, context=None):
+ """ When creating a new message, set as unread if uid is not the
+ object responsible. """
+ for obj in self.browse(cr, uid, ids, context=context):
+ if obj.message_state and hasattr(obj, 'user_id') and (not obj.user_id or obj.user_id.id != uid):
+ self.message_mark_as_unread(cr, uid, [obj.id], context=context)
+
+ def message_check_and_set_unread(self, cr, uid, ids, context=None):
+ """ Set unread if uid is the object responsible or if the object has
+ no responsible. """
+ for obj in self.browse(cr, uid, ids, context=context):
+ if obj.message_state and hasattr(obj, 'user_id') and (not obj.user_id or obj.user_id.id == uid):
+ self.message_mark_as_unread(cr, uid, [obj.id], context=context)
+
+ def message_mark_as_unread(self, cr, uid, ids, context=None):
+ """ Set as unread. """
+ return self.write(cr, uid, ids, {'message_state': False}, context=context)
+
+ def message_check_and_set_read(self, cr, uid, ids, context=None):
+ """ Set read if uid is the object responsible. """
+ for obj in self.browse(cr, uid, ids, context=context):
+ if not obj.message_state and hasattr(obj, 'user_id') and obj.user_id and obj.user_id.id == uid:
+ self.message_mark_as_read(cr, uid, [obj.id], context=context)
+
+ def message_mark_as_read(self, cr, uid, ids, context=None):
+ """ Set as read. """
+ return self.write(cr, uid, ids, {'message_state': True}, context=context)
+
+
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/mail/static/lib/jquery.expander/jquery.expander.js b/addons/mail/static/lib/jquery.expander/jquery.expander.js
new file mode 100644
index 00000000000..91cc03db918
--- /dev/null
+++ b/addons/mail/static/lib/jquery.expander/jquery.expander.js
@@ -0,0 +1,385 @@
+/*!
+ * jQuery Expander Plugin v1.4.2
+ *
+ * Date: Fri Mar 16 14:29:56 2012 EDT
+ * Requires: jQuery v1.3+
+ *
+ * Copyright 2011, Karl Swedberg
+ * Dual licensed under the MIT and GPL licenses (just like jQuery):
+ * http://www.opensource.org/licenses/mit-license.php
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ *
+ *
+ *
+*/
+
+(function($) {
+ $.expander = {
+ version: '1.4.2',
+ defaults: {
+ // the number of characters at which the contents will be sliced into two parts.
+ slicePoint: 100,
+
+ // whether to keep the last word of the summary whole (true) or let it slice in the middle of a word (false)
+ preserveWords: true,
+
+ // a threshold of sorts for whether to initially hide/collapse part of the element's contents.
+ // If after slicing the contents in two there are fewer words in the second part than
+ // the value set by widow, we won't bother hiding/collapsing anything.
+ widow: 4,
+
+ // text displayed in a link instead of the hidden part of the element.
+ // clicking this will expand/show the hidden/collapsed text
+ expandText: 'read more',
+ expandPrefix: '… ',
+
+ expandAfterSummary: false,
+
+ // class names for summary element and detail element
+ summaryClass: 'summary',
+ detailClass: 'details',
+
+ // class names for around "read-more" link and "read-less" link
+ moreClass: 'read-more',
+ lessClass: 'read-less',
+
+ // number of milliseconds after text has been expanded at which to collapse the text again.
+ // when 0, no auto-collapsing
+ collapseTimer: 0,
+
+ // effects for expanding and collapsing
+ expandEffect: 'fadeIn',
+ expandSpeed: 250,
+ collapseEffect: 'fadeOut',
+ collapseSpeed: 200,
+
+ // allow the user to re-collapse the expanded text.
+ userCollapse: true,
+
+ // text to use for the link to re-collapse the text
+ userCollapseText: 'read less',
+ userCollapsePrefix: ' ',
+
+
+ // all callback functions have the this keyword mapped to the element in the jQuery set when .expander() is called
+
+ onSlice: null, // function() {}
+ beforeExpand: null, // function() {},
+ afterExpand: null, // function() {},
+ onCollapse: null // function(byUser) {}
+ }
+ };
+
+ $.fn.expander = function(options) {
+ var meth = 'init';
+
+ if (typeof options == 'string') {
+ meth = options;
+ options = {};
+ }
+
+ var opts = $.extend({}, $.expander.defaults, options),
+ rSelfClose = /^<(?:area|br|col|embed|hr|img|input|link|meta|param).*>$/i,
+ rAmpWordEnd = opts.wordEnd || /(&(?:[^;]+;)?|[a-zA-Z\u00C0-\u0100]+)$/,
+ rOpenCloseTag = /<\/?(\w+)[^>]*>/g,
+ rOpenTag = /<(\w+)[^>]*>/g,
+ rCloseTag = /<\/(\w+)>/g,
+ rLastCloseTag = /(<\/[^>]+>)\s*$/,
+ rTagPlus = /^<[^>]+>.?/,
+ delayedCollapse;
+
+ var methods = {
+ init: function() {
+ this.each(function() {
+ var i, l, tmp, newChar, summTagless, summOpens, summCloses,
+ lastCloseTag, detailText, detailTagless,
+ $thisDetails, $readMore,
+ openTagsForDetails = [],
+ closeTagsForsummaryText = [],
+ defined = {},
+ thisEl = this,
+ $this = $(this),
+ $summEl = $([]),
+ o = $.meta ? $.extend({}, opts, $this.data()) : opts,
+ hasDetails = !!$this.find('.' + o.detailClass).length,
+ hasBlocks = !!$this.find('*').filter(function() {
+ var display = $(this).css('display');
+ return (/^block|table|list/).test(display);
+ }).length,
+ el = hasBlocks ? 'div' : 'span',
+ detailSelector = el + '.' + o.detailClass,
+ moreSelector = 'span.' + o.moreClass,
+ expandSpeed = o.expandSpeed || 0,
+ allHtml = $.trim( $this.html() ),
+ allText = $.trim( $this.text() ),
+ summaryText = allHtml.slice(0, o.slicePoint);
+
+ // bail out if we've already set up the expander on this element
+ if ( $.data(this, 'expander') ) {
+ return;
+ }
+
+ $.data(this, 'expander', true);
+
+ // determine which callback functions are defined
+ $.each(['onSlice','beforeExpand', 'afterExpand', 'onCollapse'], function(index, val) {
+ defined[val] = $.isFunction(o[val]);
+ });
+
+ // back up if we're in the middle of a tag or word
+ summaryText = backup(summaryText);
+
+ // summary text sans tags length
+ summTagless = summaryText.replace(rOpenCloseTag, '').length;
+
+ // add more characters to the summary, one for each character in the tags
+ while (summTagless < o.slicePoint) {
+ newChar = allHtml.charAt(summaryText.length);
+ if (newChar == '<') {
+ newChar = allHtml.slice(summaryText.length).match(rTagPlus)[0];
+ }
+ summaryText += newChar;
+ summTagless++;
+ }
+
+ summaryText = backup(summaryText, o.preserveWords);
+
+ // separate open tags from close tags and clean up the lists
+ summOpens = summaryText.match(rOpenTag) || [];
+ summCloses = summaryText.match(rCloseTag) || [];
+
+ // filter out self-closing tags
+ tmp = [];
+ $.each(summOpens, function(index, val) {
+ if ( !rSelfClose.test(val) ) {
+ tmp.push(val);
+ }
+ });
+ summOpens = tmp;
+
+ // strip close tags to just the tag name
+ l = summCloses.length;
+ for (i = 0; i < l; i++) {
+ summCloses[i] = summCloses[i].replace(rCloseTag, '$1');
+ }
+
+ // tags that start in summary and end in detail need:
+ // a). close tag at end of summary
+ // b). open tag at beginning of detail
+ $.each(summOpens, function(index, val) {
+ var thisTagName = val.replace(rOpenTag, '$1');
+ var closePosition = $.inArray(thisTagName, summCloses);
+ if (closePosition === -1) {
+ openTagsForDetails.push(val);
+ closeTagsForsummaryText.push('' + thisTagName + '>');
+
+ } else {
+ summCloses.splice(closePosition, 1);
+ }
+ });
+
+ // reverse the order of the close tags for the summary so they line up right
+ closeTagsForsummaryText.reverse();
+
+ // create necessary summary and detail elements if they don't already exist
+ if ( !hasDetails ) {
+
+ // end script if there is no detail text or if detail has fewer words than widow option
+ detailText = allHtml.slice(summaryText.length);
+ detailTagless = $.trim( detailText.replace(rOpenCloseTag, '') );
+
+ if ( detailTagless === '' || detailTagless.split(/\s+/).length < o.widow ) {
+ return;
+ }
+ // otherwise, continue...
+ lastCloseTag = closeTagsForsummaryText.pop() || '';
+ summaryText += closeTagsForsummaryText.join('');
+ detailText = openTagsForDetails.join('') + detailText;
+
+ } else {
+ // assume that even if there are details, we still need readMore/readLess/summary elements
+ // (we already bailed out earlier when readMore el was found)
+ // but we need to create els differently
+
+ // remove the detail from the rest of the content
+ detailText = $this.find(detailSelector).remove().html();
+
+ // The summary is what's left
+ summaryText = $this.html();
+
+ // allHtml is the summary and detail combined (this is needed when content has block-level elements)
+ allHtml = summaryText + detailText;
+
+ lastCloseTag = '';
+ }
+ o.moreLabel = $this.find(moreSelector).length ? '' : buildMoreLabel(o);
+
+ if (hasBlocks) {
+ detailText = allHtml;
+ }
+ summaryText += lastCloseTag;
+
+ // onSlice callback
+ o.summary = summaryText;
+ o.details = detailText;
+ o.lastCloseTag = lastCloseTag;
+
+ if (defined.onSlice) {
+ // user can choose to return a modified options object
+ // one last chance for user to change the options. sneaky, huh?
+ // but could be tricky so use at your own risk.
+ tmp = o.onSlice.call(thisEl, o);
+
+ // so, if the returned value from the onSlice function is an object with a details property, we'll use that!
+ o = tmp && tmp.details ? tmp : o;
+ }
+
+ // build the html with summary and detail and use it to replace old contents
+ var html = buildHTML(o, hasBlocks);
+
+ $this.html( html );
+
+ // set up details and summary for expanding/collapsing
+ $thisDetails = $this.find(detailSelector);
+ $readMore = $this.find(moreSelector);
+ $thisDetails.hide();
+ $readMore.find('a').unbind('click.expander').bind('click.expander', expand);
+
+ $summEl = $this.find('div.' + o.summaryClass);
+
+ if ( o.userCollapse && !$this.find('span.' + o.lessClass).length ) {
+ $this
+ .find(detailSelector)
+ .append('' + o.userCollapsePrefix + '' + o.userCollapseText + '');
+ }
+
+ $this
+ .find('span.' + o.lessClass + ' a')
+ .unbind('click.expander')
+ .bind('click.expander', function(event) {
+ event.preventDefault();
+ clearTimeout(delayedCollapse);
+ var $detailsCollapsed = $(this).closest(detailSelector);
+ reCollapse(o, $detailsCollapsed);
+ if (defined.onCollapse) {
+ o.onCollapse.call(thisEl, true);
+ }
+ });
+
+ function expand(event) {
+ event.preventDefault();
+ $readMore.hide();
+ $summEl.hide();
+ if (defined.beforeExpand) {
+ o.beforeExpand.call(thisEl);
+ }
+
+ $thisDetails.stop(false, true)[o.expandEffect](expandSpeed, function() {
+ $thisDetails.css({zoom: ''});
+ if (defined.afterExpand) {o.afterExpand.call(thisEl);}
+ delayCollapse(o, $thisDetails, thisEl);
+ });
+ }
+
+ }); // this.each
+ },
+ destroy: function() {
+ if ( !this.data('expander') ) {
+ return;
+ }
+ this.removeData('expander');
+ this.each(function() {
+ var $this = $(this),
+ o = $.meta ? $.extend({}, opts, $this.data()) : opts,
+ details = $this.find('.' + o.detailClass).contents();
+
+ $this.find('.' + o.moreClass).remove();
+ $this.find('.' + o.summaryClass).remove();
+ $this.find('.' + o.detailClass).after(details).remove();
+ $this.find('.' + o.lessClass).remove();
+
+ });
+ }
+ };
+
+ // run the methods (almost always "init")
+ if ( methods[meth] ) {
+ methods[ meth ].call(this);
+ }
+
+ // utility functions
+ function buildHTML(o, blocks) {
+ var el = 'span',
+ summary = o.summary;
+ if ( blocks ) {
+ el = 'div';
+ // if summary ends with a close tag, tuck the moreLabel inside it
+ if ( rLastCloseTag.test(summary) && !o.expandAfterSummary) {
+ summary = summary.replace(rLastCloseTag, o.moreLabel + '$1');
+ } else {
+ // otherwise (e.g. if ends with self-closing tag) just add moreLabel after summary
+ // fixes #19
+ summary += o.moreLabel;
+ }
+
+ // and wrap it in a div
+ summary = '