diff --git a/addons/account/installer.py b/addons/account/installer.py
index 961b8f9e763..343e22a7632 100644
--- a/addons/account/installer.py
+++ b/addons/account/installer.py
@@ -47,7 +47,7 @@ class account_installer(osv.osv_memory):
# try get the list on apps server
try:
- apps_server = self.pool.get('ir.config_parameter').get_param(cr, uid, 'apps.server', 'https://apps.openerp.com')
+ apps_server = self.pool.get('ir.module.module').get_apps_server(cr, uid, context=context)
up = urlparse.urlparse(apps_server)
url = '{0.scheme}://{0.netloc}/apps/charts?serie={1}'.format(up, serie)
diff --git a/addons/account/report/account_entries_report.py b/addons/account/report/account_entries_report.py
index 6060d97c182..df2d92c9f12 100644
--- a/addons/account/report/account_entries_report.py
+++ b/addons/account/report/account_entries_report.py
@@ -94,7 +94,7 @@ class account_entries_report(osv.osv):
return super(account_entries_report, self).search(cr, uid, args=args, offset=offset, limit=limit, order=order,
context=context, count=count)
- def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False):
+ def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False,lazy=True):
if context is None:
context = {}
fiscalyear_obj = self.pool.get('account.fiscalyear')
@@ -108,7 +108,7 @@ class account_entries_report(osv.osv):
domain.append(['period_id','in',ids])
else:
domain = domain
- return super(account_entries_report, self).read_group(cr, uid, domain, fields, groupby, offset, limit, context, orderby)
+ return super(account_entries_report, self).read_group(cr, uid, domain, fields, groupby, offset, limit, context, orderby,lazy)
def init(self, cr):
tools.drop_view_if_exists(cr, 'account_entries_report')
diff --git a/addons/account_asset/account_asset.py b/addons/account_asset/account_asset.py
index f4e5d17a8e0..7041c3aafbf 100644
--- a/addons/account_asset/account_asset.py
+++ b/addons/account_asset/account_asset.py
@@ -328,7 +328,7 @@ class account_asset_asset(osv.osv):
default = {}
if context is None:
context = {}
- default.update({'depreciation_line_ids': [], 'state': 'draft'})
+ default.update({'depreciation_line_ids': [], 'account_move_line_ids': [], 'history_ids': [], 'state': 'draft'})
return super(account_asset_asset, self).copy(cr, uid, id, default, context=context)
def _compute_entries(self, cr, uid, ids, period_id, context=None):
diff --git a/addons/analytic_user_function/analytic_user_function.py b/addons/analytic_user_function/analytic_user_function.py
index 2671fc92581..5a17ac91e53 100644
--- a/addons/analytic_user_function/analytic_user_function.py
+++ b/addons/analytic_user_function/analytic_user_function.py
@@ -26,6 +26,7 @@ import openerp.addons.decimal_precision as dp
class analytic_user_funct_grid(osv.osv):
_name="analytic.user.funct.grid"
_description= "Price per User"
+ _rec_name="user_id"
_columns={
'user_id': fields.many2one("res.users", "User", required=True,),
'product_id': fields.many2one("product.product", "Service", required=True,),
diff --git a/addons/auth_oauth/controllers/main.py b/addons/auth_oauth/controllers/main.py
index b4ce20d49e3..4fca426ffc0 100644
--- a/addons/auth_oauth/controllers/main.py
+++ b/addons/auth_oauth/controllers/main.py
@@ -2,6 +2,7 @@ import functools
import logging
import simplejson
+import urlparse
import werkzeug.utils
from werkzeug.exceptions import BadRequest
@@ -68,7 +69,8 @@ class OAuthLogin(openerp.addons.web.controllers.main.Home):
def get_state(self, provider):
state = dict(
d=request.session.db,
- p=provider['id']
+ p=provider['id'],
+ r=request.httprequest.full_path
)
token = request.params.get('token')
if token:
@@ -137,8 +139,12 @@ class OAuthController(http.Controller):
cr.commit()
action = state.get('a')
menu = state.get('m')
+ redirect = state.get('r')
url = '/web'
- if action:
+ if redirect and not redirect.startswith('/auth_oauth/signin') and \
+ (not redirect.startswith('/web/login') or 'redirect' in urlparse.urlsplit(redirect).query):
+ url = redirect
+ elif action:
url = '/web#action=%s' % action
elif menu:
url = '/web#menu_id=%s' % menu
diff --git a/addons/auth_signup/auth_signup_data.xml b/addons/auth_signup/auth_signup_data.xml
index f470811d39e..2789fcd8315 100644
--- a/addons/auth_signup/auth_signup_data.xml
+++ b/addons/auth_signup/auth_signup_data.xml
@@ -10,7 +10,7 @@
-
+ _usertemplate
diff --git a/addons/base_gengo/__init__.py b/addons/base_gengo/__init__.py
index 133fa90dc51..ff48d6cc91f 100644
--- a/addons/base_gengo/__init__.py
+++ b/addons/base_gengo/__init__.py
@@ -22,5 +22,6 @@
import res_company
import ir_translation
import wizard
+import controller
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/base_gengo/controller/__init__.py b/addons/base_gengo/controller/__init__.py
new file mode 100644
index 00000000000..4648ae64b0f
--- /dev/null
+++ b/addons/base_gengo/controller/__init__.py
@@ -0,0 +1 @@
+import gengo_callback
diff --git a/addons/base_gengo/controller/gengo_callback.py b/addons/base_gengo/controller/gengo_callback.py
new file mode 100644
index 00000000000..381ac7b7fe9
--- /dev/null
+++ b/addons/base_gengo/controller/gengo_callback.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+
+import openerp
+from openerp.addons.web import http
+from openerp.addons.web.http import request
+
+import json
+
+class website_gengo(http.Controller):
+ @http.route('/website/gengo_callback', type='http', auth='none')
+ def gengo_callback(self,**post):
+ cr, uid, context = request.cr, openerp.SUPERUSER_ID, request.context
+ translation_pool = request.registry['ir.translation']
+ if post and post.get('job'):
+ job = json.loads(post['job'])
+ tid = job.get('custom_data', False)
+ if (job.get('status') == 'approved') and tid:
+ term = translation_pool.browse(cr, uid, int(tid), context=context)
+ if term.job_id <> job.get('job_id'):
+ raise 'Error'
+ vals = {'state': 'translated', 'value': job.get('body_tgt')}
+ translation_pool.write(cr, uid, [int(tid)], vals, context=context)
diff --git a/addons/base_gengo/gengo_sync_schedular_data.xml b/addons/base_gengo/gengo_sync_schedular_data.xml
index e55066e0e74..91a5c8266d8 100644
--- a/addons/base_gengo/gengo_sync_schedular_data.xml
+++ b/addons/base_gengo/gengo_sync_schedular_data.xml
@@ -5,8 +5,8 @@
Gengo Sync Translation (Response)
- 20
- minutes
+ 6
+ hours-1
@@ -17,8 +17,8 @@
Gengo Sync Translation (Request)
- 20
- minutes
+ 6
+ hours-1
diff --git a/addons/base_gengo/wizard/base_gengo_translations.py b/addons/base_gengo/wizard/base_gengo_translations.py
index 8cc7206e63e..f565b520d6c 100644
--- a/addons/base_gengo/wizard/base_gengo_translations.py
+++ b/addons/base_gengo/wizard/base_gengo_translations.py
@@ -33,10 +33,6 @@ try:
from mygengo import MyGengo
except ImportError:
_logger.warning('Gengo library not found, Gengo features disabled. If you plan to use it, please install the mygengo library from http://pypi.python.org/pypi/mygengo')
- class MyGengo(object):
- def __init__(self, *args, **kwargs):
- # no context for translations - so don't bother
- raise ImportError('Gengo library not found, please install mygengo from http://pypi.python.org/pypi/mygengo')
GENGO_DEFAULT_LIMIT = 20
@@ -120,51 +116,45 @@ class base_gengo_translations(osv.osv_memory):
all_translation_ids = translation_pool.search(cr, uid, [('state', '=', 'inprogress'), ('gengo_translation', 'in', ('machine', 'standard', 'pro', 'ultra')), ('job_id', "!=", False)], context=context)
while True:
translation_ids = all_translation_ids[offset:offset + limit]
- if translation_ids:
- offset += limit
- translation_terms = translation_pool.browse(cr, uid, translation_ids, context=context)
- gengo_job_id = [term.job_id for term in translation_terms]
- if gengo_job_id:
- gengo_ids = ','.join(gengo_job_id)
- job_response = gengo.getTranslationJobBatch(id=gengo_ids)
- if job_response['opstat'] == 'ok':
- job_response_dict = dict([(job['job_id'], job) for job in job_response['response']['jobs']])
- for term in translation_terms:
- up_term = up_comment = 0
- vals = {}
- if job_response_dict[term.job_id]['status'] == 'approved':
- vals.update({'state': 'translated',
- 'value': job_response_dict[term.job_id]['body_tgt']})
- up_term += 1
- job_comment = gengo.getTranslationJobComments(id=term.job_id)
- if job_comment['opstat'] == 'ok':
- gengo_comments = ""
- for comment in job_comment['response']['thread']:
- gengo_comments += _('%s\n-- Commented on %s by %s.\n\n') % (comment['body'], time.ctime(comment['ctime']), comment['author'])
- vals.update({'gengo_comment': gengo_comments})
- up_comment += 1
- if vals:
- translation_pool.write(cr, uid, term.id, vals)
- _logger.info("Successfully Updated `%d` terms and %d Comments." % (up_term, up_comment))
- if not len(translation_ids) == limit:
+ offset += limit
+ if not translation_ids:
break
+ translation_terms = translation_pool.browse(cr, uid, translation_ids, context=context)
+ gengo_job_id = [term.job_id for term in translation_terms]
+ if gengo_job_id:
+ gengo_ids = ','.join(gengo_job_id)
+ try:
+ job_response = gengo.getTranslationJobBatch(id=gengo_ids)
+ except:
+ continue
+ if job_response['opstat'] == 'ok':
+ for job in job_response['response'].get('jobs', []):
+ self._update_terms_job(cr, uid, job, context=context)
return True
+ def _update_terms_job(self, cr, uid, job, context=None):
+ translation_pool = self.pool.get('ir.translation')
+ tid = int(job['custom_data'])
+ vals = {}
+ if job.get('job_id', False):
+ vals['job_id'] = job['job_id']
+ vals['state'] = 'inprogress'
+ if job.get('status', False) in ('queued','available','pending','reviewable'):
+ vals['state'] = 'inprogress'
+ if job.get('body_tgt', False) and job.get('status', False)=='approved':
+ vals['value'] = job['body_tgt']
+ if job.get('status', False) in ('approved', 'canceled'):
+ vals['state'] = 'translated'
+ if vals:
+ translation_pool.write(cr, uid, [tid], vals, context=context)
+
def _update_terms(self, cr, uid, response, context=None):
"""
Update the terms after their translation were requested to Gengo
"""
- translation_pool = self.pool.get('ir.translation')
- for jobs in response['jobs']:
+ for jobs in response.get('jobs', []):
for t_id, res in jobs.items():
- vals = {}
- t_id = int(t_id)
- tier = translation_pool.read(cr, uid, [t_id], ['gengo_translation'], context=context)[0]['gengo_translation']
- if tier == "machine":
- vals.update({'value': res['body_tgt'], 'state': 'translated'})
- else:
- vals.update({'job_id': res['job_id'], 'state': 'inprogress'})
- translation_pool.write(cr, uid, [t_id], vals, context=context)
+ self._update_terms_job(cr, uid, res, context=context)
return
def pack_jobs_request(self, cr, uid, term_ids, context=None):
@@ -181,17 +171,22 @@ class base_gengo_translations(osv.osv_memory):
auto_approve = 1 if user.company_id.gengo_auto_approve else 0
for term in translation_pool.browse(cr, uid, term_ids, context=context):
if re.search(r"\w", term.src or ""):
- jobs[term.id] = {'type': 'text',
- 'slug': 'single::English to ' + term.lang,
- 'tier': tools.ustr(term.gengo_translation),
- 'body_src': term.src,
- 'lc_src': 'en',
- 'lc_tgt': translation_pool._get_gengo_corresponding_language(term.lang),
- 'auto_approve': auto_approve,
- 'comment': user.company_id.gengo_comment and "%s %s"%(user.company_id.gengo_comment,term.gengo_comment) or term.gengo_comment,
- 'callback_url': self.pool.get('ir.config_parameter').get_param(cr, uid,'web.base.url') + '/website/gengo_callback/' + str(term.id)
+ comment = user.company_id.gengo_comment or ''
+ if term.gengo_comment:
+ comment+='\n' + term.gengo_comment
+ jobs[time.strftime('%Y%m%d%H%M%S') + '-' + str(term.id)] = {
+ 'type': 'text',
+ 'slug': 'Single :: English to ' + term.lang,
+ 'tier': tools.ustr(term.gengo_translation),
+ 'custom_data': str(term.id),
+ 'body_src': term.src,
+ 'lc_src': 'en',
+ 'lc_tgt': translation_pool._get_gengo_corresponding_language(term.lang),
+ 'auto_approve': auto_approve,
+ 'comment': comment,
+ 'callback_url': self.pool.get('ir.config_parameter').get_param(cr, uid,'web.base.url') + '/website/gengo_callback'
}
- return {'jobs': jobs}
+ return {'jobs': jobs, 'as_group': 1}
def _send_translation_terms(self, cr, uid, term_ids, context=None):
@@ -223,15 +218,14 @@ class base_gengo_translations(osv.osv_memory):
context = {}
language_pool = self.pool.get('res.lang')
translation_pool = self.pool.get('ir.translation')
+ domain = [('state', '=', 'to_translate'), ('gengo_translation', 'in', ('machine', 'standard', 'pro', 'ultra')), ('job_id', "=", False)]
+ if context.get('gengo_language', False):
+ lc = language_pool.browse(cr, uid, context['gengo_language'], context=context).code
+ domain.append( ('lang', '=', lc) )
+
+ all_term_ids = translation_pool.search(cr, uid, domain, context=context)
try:
- #by default, the request will be made for all terms that needs it, whatever the language
- lang_ids = language_pool.search(cr, uid, [], context=context)
- if context.get('gengo_language'):
- #but if this specific key is given, then we restrict the request on terms of this language only
- lang_ids = [context.get('gengo_language')]
- langs = [lang.code for lang in language_pool.browse(cr, uid, lang_ids, context=context)]
offset = 0
- all_term_ids = translation_pool.search(cr, uid, [('state', '=', 'to_translate'), ('gengo_translation', 'in', ('machine', 'standard', 'pro', 'ultra')), ('lang', 'in', langs), ('job_id', "=", False)], context=context)
while True:
#search for the n first terms to translate
term_ids = all_term_ids[offset:offset + limit]
diff --git a/addons/calendar/calendar.py b/addons/calendar/calendar.py
index e928432821b..add0030d76c 100644
--- a/addons/calendar/calendar.py
+++ b/addons/calendar/calendar.py
@@ -1476,7 +1476,7 @@ class calendar_event(osv.Model):
self.create_attendees(cr, uid, [res], context=context)
return res
- def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False):
+ def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False, lazy=True):
if not context:
context = {}
@@ -1484,7 +1484,7 @@ class calendar_event(osv.Model):
raise osv.except_osv(_('Warning!'), _('Group by date is not supported, use the calendar view instead.'))
virtual_id = context.get('virtual_id', True)
context.update({'virtual_id': False})
- res = super(calendar_event, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
+ res = super(calendar_event, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby, lazy=lazy)
for result in res:
#remove the count, since the value is not consistent with the result of the search when expand the group
for groupname in groupby:
diff --git a/addons/document/document.py b/addons/document/document.py
index eb51ad5ac57..9008f32cd20 100644
--- a/addons/document/document.py
+++ b/addons/document/document.py
@@ -2005,7 +2005,7 @@ class nodefd_content(StringIO, node_descriptor):
par = self._get_parent()
uid = par.context.uid
- cr = openerp.registry(par.context.dbname).db.cursor()
+ cr = openerp.registry(par.context.dbname).cursor()
try:
if self.mode in ('w', 'w+', 'r+'):
data = self.getvalue()
@@ -2058,7 +2058,7 @@ class nodefd_static(StringIO, node_descriptor):
par = self._get_parent()
# uid = par.context.uid
- cr = openerp.registry(par.context.dbname).db.cursor()
+ cr = openerp.registry(par.context.dbname).cursor()
try:
if self.mode in ('w', 'w+', 'r+'):
data = self.getvalue()
diff --git a/addons/edi/edi_service.py b/addons/edi/edi_service.py
index dd95196ef4f..0d3edd451d1 100644
--- a/addons/edi/edi_service.py
+++ b/addons/edi/edi_service.py
@@ -34,18 +34,14 @@ def _edi_dispatch(db_name, method_name, *method_args):
try:
registry = openerp.modules.registry.RegistryManager.get(db_name)
assert registry, 'Unknown database %s' % db_name
- edi = registry['edi.edi']
- cr = registry.db.cursor()
- res = None
- res = getattr(edi, method_name)(cr, *method_args)
- cr.commit()
+ with registry.cursor() as cr:
+ edi = registry['edi.edi']
+ return getattr(edi, method_name)(cr, *method_args)
+
except Exception, e:
_logger.exception('Failed to execute EDI method %s with args %r.',
method_name, method_args)
raise
- finally:
- cr.close()
- return res
def exp_import_edi_document(db_name, uid, passwd, edi_document, context=None):
return _edi_dispatch(db_name, 'import_edi', uid, edi_document, None)
diff --git a/addons/event/event_view.xml b/addons/event/event_view.xml
index 07a36ea3a6b..3669b5a1e6f 100644
--- a/addons/event/event_view.xml
+++ b/addons/event/event_view.xml
@@ -105,9 +105,9 @@
-
-
-
+
+
+
@@ -263,21 +263,21 @@
-
+
-
-
+
+
-
+
-
+
-
-
-
-
+
+
+
+
@@ -333,9 +333,9 @@
-
-
-
+
+
+
@@ -367,7 +367,7 @@
+ context= '{"default_email_to":email}' type="action"/>
@@ -420,20 +420,20 @@
-
+
-
-
+
+
-
+
-
-
-
-
+
+
+
+
diff --git a/addons/event_sale/event_sale_view.xml b/addons/event_sale/event_sale_view.xml
index 821a4c19b83..1f42108c993 100644
--- a/addons/event_sale/event_sale_view.xml
+++ b/addons/event_sale/event_sale_view.xml
@@ -17,6 +17,28 @@
+
+ event.registration.ticket.search
+ event.registration
+
+
+
+
+
+
+
+
+
+ event.registration.ticket.tree
+ event.registration
+
+
+
+
+
+
+
+
product.template
diff --git a/addons/idea/models/idea.py b/addons/idea/models/idea.py
index 61934cfc3b2..37fbbe82e1c 100644
--- a/addons/idea/models/idea.py
+++ b/addons/idea/models/idea.py
@@ -91,7 +91,7 @@ class IdeaIdea(osv.Model):
# Technical stuff
#------------------------------------------------------
- def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False):
+ def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False, lazy=True):
""" Override read_group to always display all states. """
if groupby and groupby[0] == "state":
# Default result structure
@@ -103,7 +103,7 @@ class IdeaIdea(osv.Model):
'state_count': 0,
} for state_value, state_name in states]
# Get standard results
- read_group_res = super(IdeaIdea, self).read_group(cr, uid, domain, fields, groupby, offset, limit, context, orderby)
+ read_group_res = super(IdeaIdea, self).read_group(cr, uid, domain, fields, groupby, offset, limit, context, orderby, lazy)
# Update standard results with default results
result = []
for state_value, state_name in states:
@@ -114,7 +114,7 @@ class IdeaIdea(osv.Model):
result.append(res[0])
return result
else:
- return super(IdeaIdea, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
+ return super(IdeaIdea, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby, lazy=lazy)
#------------------------------------------------------
# Workflow / Actions
diff --git a/addons/lunch/security/lunch_security.xml b/addons/lunch/security/lunch_security.xml
index e01fab71ea2..5f2a351472c 100644
--- a/addons/lunch/security/lunch_security.xml
+++ b/addons/lunch/security/lunch_security.xml
@@ -16,5 +16,43 @@
+
+
+ lunch.order: do not see and create other people's order
+
+
+ [('user_id', '=', user.id)]
+
+
+ lunch.order: do not see and create other people's order
+
+
+ [(1, '=', 1)]
+
+
+ lunch.order.line: do not see and create other people's order line
+
+
+ [('user_id', '=', user.id)]
+
+
+ lunch.order.line: do not see and create other people's order line
+
+
+ [(1, '=', 1)]
+
+
+ lunch.cashmove: do not see and create other people's cashmove
+
+
+ [('user_id', '=', user.id)]
+
+
+ lunch.cashmove: do not see and create other people's cashmove
+
+
+ [(1, '=', 1)]
+
+
diff --git a/addons/note/note.py b/addons/note/note.py
index 7f03f43b36a..38b2395a852 100644
--- a/addons/note/note.py
+++ b/addons/note/note.py
@@ -116,7 +116,7 @@ class note_note(osv.osv):
}
_order = 'sequence'
- def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False):
+ def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False, lazy=True):
if groupby and groupby[0]=="stage_id":
#search all stages
@@ -169,7 +169,7 @@ class note_note(osv.osv):
else:
return super(note_note, self).read_group(self, cr, uid, domain, fields, groupby,
- offset=offset, limit=limit, context=context, orderby=orderby)
+ offset=offset, limit=limit, context=context, orderby=orderby,lazy=lazy)
#upgrade config setting page to configure pad, fancy and tags mode
diff --git a/addons/pad/static/src/js/pad.js b/addons/pad/static/src/js/pad.js
index d3a6394ee5d..cf6768e0f1f 100644
--- a/addons/pad/static/src/js/pad.js
+++ b/addons/pad/static/src/js/pad.js
@@ -13,6 +13,7 @@ openerp.pad = function(instance) {
event.preventDefault();
self.set("configured", true);
});
+ this.pad_loading_request = null;
},
initialize_content: function() {
var self = this;
@@ -30,14 +31,14 @@ openerp.pad = function(instance) {
},
render_value: function() {
var self = this;
- this._configured_deferred.always(function() {
+ $.when(this._configured_deferred, this.pad_loading_request).always(function() {
if (! self.get('configured')) {
return;
};
var value = self.get('value');
if (self.get('effective_readonly')) {
if (_.str.startsWith(value, 'http')) {
- this.pad_loading_request = self.view.dataset.call('pad_get_content', {url: value}).done(function(data) {
+ self.pad_loading_request = self.view.dataset.call('pad_get_content', {url: value}).done(function(data) {
self.$('.oe_pad_content').removeClass('oe_pad_loading').html('
');
self.$('.oe_pad_readonly').html(data);
}).fail(function() {
diff --git a/addons/payment/__openerp__.py b/addons/payment/__openerp__.py
index bc5d3896ac2..c192bec7c1b 100644
--- a/addons/payment/__openerp__.py
+++ b/addons/payment/__openerp__.py
@@ -7,11 +7,12 @@
'version': '1.0',
'description': """Payment Acquirer Base Module""",
'author': 'OpenERP SA',
- 'depends': ['mail', 'account'],
+ 'depends': ['account'],
'data': [
'views/payment_acquirer.xml',
'views/res_config_view.xml',
'security/ir.model.access.csv',
],
'installable': True,
+ 'auto_install': True,
}
diff --git a/addons/procurement/schedulers.py b/addons/procurement/schedulers.py
index d82fa94c132..b86a799e19c 100644
--- a/addons/procurement/schedulers.py
+++ b/addons/procurement/schedulers.py
@@ -59,7 +59,7 @@ class procurement_order(osv.osv):
context = {}
try:
if use_new_cursor:
- cr = openerp.registry(use_new_cursor).db.cursor()
+ cr = openerp.registry(use_new_cursor).cursor()
procurement_obj = self.pool.get('procurement.order')
if not skip_exception:
@@ -199,7 +199,7 @@ class procurement_order(osv.osv):
if context is None:
context = {}
if use_new_cursor:
- cr = openerp.registry(use_new_cursor).db.cursor()
+ cr = openerp.registry(use_new_cursor).cursor()
orderpoint_obj = self.pool.get('stock.warehouse.orderpoint')
procurement_obj = self.pool.get('procurement.order')
diff --git a/addons/procurement/wizard/orderpoint_procurement.py b/addons/procurement/wizard/orderpoint_procurement.py
index afc8e2a721f..c8d41c5c655 100644
--- a/addons/procurement/wizard/orderpoint_procurement.py
+++ b/addons/procurement/wizard/orderpoint_procurement.py
@@ -49,7 +49,7 @@ class procurement_compute(osv.osv_memory):
"""
proc_obj = self.pool.get('procurement.order')
#As this function is in a new thread, I need to open a new cursor, because the old one may be closed
- new_cr = self.pool.db.cursor()
+ new_cr = self.pool.cursor()
for proc in self.browse(new_cr, uid, ids, context=context):
proc_obj._procure_orderpoint_confirm(new_cr, uid, automatic=proc.automatic, use_new_cursor=new_cr.dbname, context=context)
#close the new cursor
diff --git a/addons/procurement/wizard/schedulers_all.py b/addons/procurement/wizard/schedulers_all.py
index aab9657ae5d..c0f1cfcc2df 100644
--- a/addons/procurement/wizard/schedulers_all.py
+++ b/addons/procurement/wizard/schedulers_all.py
@@ -45,7 +45,7 @@ class procurement_compute_all(osv.osv_memory):
"""
proc_obj = self.pool.get('procurement.order')
#As this function is in a new thread, i need to open a new cursor, because the old one may be closed
- new_cr = self.pool.db.cursor()
+ new_cr = self.pool.cursor()
for proc in self.browse(new_cr, uid, ids, context=context):
proc_obj.run_scheduler(new_cr, uid, automatic=proc.automatic, use_new_cursor=new_cr.dbname,\
context=context)
diff --git a/addons/product/product_view.xml b/addons/product/product_view.xml
index 4946a71f80b..c547a611cd4 100644
--- a/addons/product/product_view.xml
+++ b/addons/product/product_view.xml
@@ -118,6 +118,7 @@
');
+ transfo.$center = transfo.$markup.find(".transfo-scaler-mc");
+
+ // init setting and get css to set wrap
+ _setOptions($this, transfo);
+ _overwriteOptions ($this, transfo, settings);
+
+ // append controls to container
+ $("body").append(transfo.$markup);
+
+ // set transfo container and markup
+ setTimeout(function () {
+ _targetCss($this, transfo);
+ },0);
+
+ _bind($this, transfo);
+
+ _targetCss($this, transfo);
+ }
+
+ function _overwriteOptions ($this, transfo, settings) {
+ transfo.settings = $.extend(transfo.settings, settings || {});
+ }
+
+ function _setOptions ($this, transfo) {
+ var style = $this.attr("style") || "";
+ var transform = style.match(/transform\s*:([^;]+)/) ? style.match(/transform\s*:([^;]+)/)[1] : "";
+
+ transfo.settings = {};
+
+ transfo.settings.angle= transform.indexOf('rotate') != -1 ? parseFloat(transform.match(/rotate\(([^)]+)deg\)/)[1]) : 0;
+ transfo.settings.scalex= transform.indexOf('scaleX') != -1 ? parseFloat(transform.match(/scaleX\(([^)]+)\)/)[1]) : 1;
+ transfo.settings.scaley= transform.indexOf('scaleY') != -1 ? parseFloat(transform.match(/scaleY\(([^)]+)\)/)[1]) : 1;
+
+ transfo.settings.style = style.replace(/[^;]*transform[^;]+/g, '').replace(/;+/g, ';');
+ $this.attr("style", transfo.settings.style);
+
+ transfo.settings.height = $this.innerHeight();
+ transfo.settings.width = $this.innerWidth();
+
+ var translatex = transform.match(/translateX\(([0-9.-]+)(%|px)\)/);
+ var translatey = transform.match(/translateY\(([0-9.-]+)(%|px)\)/);
+ transfo.settings.translate = "%";
+
+ if (translatex && translatex[2] === "%") {
+ transfo.settings.translatexp = parseFloat(translatex[1]);
+ transfo.settings.translatex = transfo.settings.translatexp / 100 * transfo.settings.width;
+ } else {
+ transfo.settings.translatex = translatex ? parseFloat(translatex[1]) : 0;
+ }
+ if (translatey && translatey[2] === "%") {
+ transfo.settings.translateyp = parseFloat(translatey[1]);
+ transfo.settings.translatey = transfo.settings.translateyp / 100 * transfo.settings.height;
+ } else {
+ transfo.settings.translatey = translatey ? parseFloat(translatey[1]) : 0;
+ }
+
+ transfo.settings.css = window.getComputedStyle($this[0], null);
+ transfo.settings.pos = $this.offset();
+
+ transfo.settings.rotationStep = 5;
+ transfo.settings.hide = false;
+ transfo.settings.callback = function () {};
+ }
+
+ function _bind ($this, transfo) {
+ function mousedown (event) {
+ _mouseDown($this, this, transfo, event);
+ $(document).on("mousemove", mousemove).on("mouseup", mouseup);
+ }
+ function mousemove (event) {
+ _mouseMove($this, this, transfo, event);
+ }
+ function mouseup (event) {
+ _mouseUp($this, this, transfo, event);
+ $(document).off("mousemove", mousemove).off("mouseup", mouseup);
+ }
+
+ transfo.$markup.off().on("mousedown", mousedown);
+ transfo.$markup.find(">:not(.transfo-scaler-mc)").off().on("mousedown", mousedown);
+ }
+
+ function _mouseDown($this, div, transfo, event) {
+ event.preventDefault();
+ if (transfo.active || event.which !== 1) return;
+
+ var type = "position", $e = $(div);
+ if ($e.hasClass("transfo-rotator")) type = "rotator";
+ else if ($e.hasClass("transfo-scaler-tl")) type = "tl";
+ else if ($e.hasClass("transfo-scaler-tr")) type = "tr";
+ else if ($e.hasClass("transfo-scaler-br")) type = "br";
+ else if ($e.hasClass("transfo-scaler-bl")) type = "bl";
+ else if ($e.hasClass("transfo-scaler-tc")) type = "tc";
+ else if ($e.hasClass("transfo-scaler-bc")) type = "bc";
+ else if ($e.hasClass("transfo-scaler-ml")) type = "ml";
+ else if ($e.hasClass("transfo-scaler-mr")) type = "mr";
+
+ transfo.active = {
+ "type": type,
+ "scalex": transfo.settings.scalex,
+ "scaley": transfo.settings.scaley,
+ "pageX": event.pageX,
+ "pageY": event.pageY,
+ "center": transfo.$center.offset(),
+ };
+ }
+ function _mouseUp($this, div, transfo, event) {
+ transfo.active = null;
+ }
+
+ function _mouseMove($this, div, transfo, event) {
+ event.preventDefault();
+ if (!transfo.active) return;
+ var settings = transfo.settings;
+ var center = transfo.active.center;
+ var cdx = center.left - event.pageX;
+ var cdy = center.top - event.pageY;
+
+ if (transfo.active.type == "rotator") {
+ var ang, dang = Math.atan((settings.width * settings.scalex) / (settings.height * settings.scaley)) / rad;
+
+ if (cdy) ang = Math.atan(- cdx / cdy) / rad;
+ else ang = 0;
+ if (event.pageY >= center.top && event.pageX >= center.left) ang += 180;
+ else if (event.pageY >= center.top && event.pageX < center.left) ang += 180;
+ else if (event.pageY < center.top && event.pageX < center.left) ang += 360;
+
+ ang -= dang;
+ if (settings.scaley < 0 && settings.scalex < 0) ang += 180;
+
+ if (!event.ctrlKey) {
+ settings.angle = Math.round(ang / transfo.settings.rotationStep) * transfo.settings.rotationStep;
+ } else {
+ settings.angle = ang;
+ }
+
+ // reset position : don't move center
+ _targetCss($this, transfo);
+ var new_center = transfo.$center.offset();
+ var x = center.left - new_center.left;
+ var y = center.top - new_center.top;
+ var angle = ang * rad;
+ settings.translatex += x*Math.cos(angle) - y*Math.sin(-angle);
+ settings.translatey += - x*Math.sin(angle) + y*Math.cos(-angle);
+ }
+ else if (transfo.active.type == "position") {
+ var angle = settings.angle * rad;
+ var x = event.pageX - transfo.active.pageX;
+ var y = event.pageY - transfo.active.pageY;
+ transfo.active.pageX = event.pageX;
+ transfo.active.pageY = event.pageY;
+ var dx = x*Math.cos(angle) - y*Math.sin(-angle);
+ var dy = - x*Math.sin(angle) + y*Math.cos(-angle);
+
+ settings.translatex += dx;
+ settings.translatey += dy;
+ }
+ else if (transfo.active.type.length === 2) {
+ var angle = settings.angle * rad;
+ var dx = cdx*Math.cos(angle) - cdy*Math.sin(-angle);
+ var dy = - cdx*Math.sin(angle) + cdy*Math.cos(-angle);
+ if (transfo.active.type.indexOf("t") != -1) {
+ settings.scaley = dy / (settings.height/2);
+ }
+ if (transfo.active.type.indexOf("b") != -1) {
+ settings.scaley = - dy / (settings.height/2);
+ }
+ if (transfo.active.type.indexOf("l") != -1) {
+ settings.scalex = dx / (settings.width/2);
+ }
+ if (transfo.active.type.indexOf("r") != -1) {
+ settings.scalex = - dx / (settings.width/2);
+ }
+ if (settings.scaley > 0 && settings.scaley < 0.05) settings.scaley = 0.05;
+ if (settings.scalex > 0 && settings.scalex < 0.05) settings.scalex = 0.05;
+ if (settings.scaley < 0 && settings.scaley > -0.05) settings.scaley = -0.05;
+ if (settings.scalex < 0 && settings.scalex > -0.05) settings.scalex = -0.05;
+
+ if (event.shiftKey &&
+ (transfo.active.type === "tl" || transfo.active.type === "bl" ||
+ transfo.active.type === "tr" || transfo.active.type === "br")) {
+ settings.scaley = settings.scalex;
+ }
+ }
+
+ settings.angle = Math.round(settings.angle);
+ settings.translatex = Math.round(settings.translatex);
+ settings.translatey = Math.round(settings.translatey);
+ settings.scalex = Math.round(settings.scalex*100)/100;
+ settings.scaley = Math.round(settings.scaley*100)/100;
+
+ _targetCss($this, transfo);
+ return false;
+ }
+
+ function _setCss($this, css, settings) {
+ var transform = "";
+ var trans = false;
+ if (settings.angle !== 0) {
+ trans = true;
+ transform += " rotate("+settings.angle+"deg) ";
+ }
+ if (settings.translatex) {
+ trans = true;
+ transform += " translateX("+(settings.translate === "%" ? settings.translatexp+"%" : settings.translatex+"px")+") ";
+ }
+ if (settings.translatey) {
+ trans = true;
+ transform += " translateY("+(settings.translate === "%" ? settings.translateyp+"%" : settings.translatey+"px")+") ";
+ }
+ if (settings.scalex != 1) {
+ trans = true;
+ transform += " scaleX("+settings.scalex+") ";
+ }
+ if (settings.scaley != 1){
+ trans = true;
+ transform += " scaleY("+settings.scaley+") ";
+ }
+
+ if (trans) {
+ css += ";"
+ /* Safari */
+ css += "-webkit-transform:" + transform + ";"
+ /* Firefox */
+ + "-moz-transform:" + transform + ";"
+ /* IE */
+ + "-ms-transform:" + transform + ";"
+ /* Opera */
+ + "-o-transform:" + transform + ";"
+ /* Other */
+ + "transform:" + transform + ";";
+ }
+
+ css = css.replace(/(\s*;)+/g, ';').replace(/^\s*;|;\s*$/g, '');
+
+ $this.attr("style", css);
+ }
+
+ function _targetCss ($this, transfo) {
+ var settings = transfo.settings;
+ var width = parseFloat(settings.css.width);
+ var height = parseFloat(settings.css.height);
+ settings.translatexp = Math.round(settings.translatex/width*1000)/10;
+ settings.translateyp = Math.round(settings.translatey/height*1000)/10;
+
+ _setCss($this, settings.style, settings);
+
+ _setCss(transfo.$markup,
+ "position: absolute;" +
+ "top:" + settings.pos.top + "px;" +
+ "left:" + settings.pos.left + "px;" +
+ "width:" + width + "px;" +
+ "height:" + height + "px;" +
+ "cursor: move;",
+ settings);
+ transfo.$markup.find(">").css("transform", "scaleX("+(1/settings.scalex)+") scaleY("+(1/settings.scaley)+")");
+
+ _showHide($this, transfo);
+
+ transfo.settings.callback.call($this[0], $this);
+ }
+
+ function _showHide ($this, transfo) {
+ transfo.$markup.css("z-index", transfo.settings.hide ? -1 : 1000);
+ if (transfo.settings.hide) {
+ transfo.$markup.find(">").hide();
+ transfo.$markup.find(".transfo-scaler-mc").show();
+ } else {
+ transfo.$markup.find(">").show();
+ }
+ }
+
+ function _destroy ($this) {
+ $this.data('transfo').$markup.remove();
+ $this.removeData('transfo');
+ }
+
+ function _reset ($this) {
+ var transfo = $this.data('transfo');
+ _destroy($this);
+ $this.transfo(transfo.settings);
+ }
+
+})(jQuery);
diff --git a/addons/website/static/src/js/website.editor.js b/addons/website/static/src/js/website.editor.js
index c965c5ef834..365b8cf46ea 100644
--- a/addons/website/static/src/js/website.editor.js
+++ b/addons/website/static/src/js/website.editor.js
@@ -64,7 +64,7 @@
return new website.editor.RTELinkDialog(editor).appendTo(document.body);
}
function image_dialog(editor, image) {
- return new website.editor.RTEImageDialog(editor, image).appendTo(document.body);
+ return new website.editor.MediaDialog(editor, image).appendTo(document.body);
}
// only enable editors manually
@@ -80,14 +80,19 @@
website.init_editor = function () {
CKEDITOR.plugins.add('customdialogs', {
-// requires: 'link,image',
+ // requires: 'link,image',
init: function (editor) {
editor.on('doubleclick', function (evt) {
var element = evt.data.element;
- if (element.is('img') && is_editable_node(element)) {
+ if ((element.is('img') || element.$.className.indexOf(' fa-') != -1) && is_editable_node(element)) {
image_dialog(editor, element);
return;
}
+ var parent = new CKEDITOR.dom.element(element.$.parentNode);
+ if (parent.$.className.indexOf('media_iframe_video') != -1 && is_editable_node(parent)) {
+ image_dialog(editor, parent);
+ return;
+ }
element = get_selected_link(editor) || evt.data.element;
if (!(element.is('a') && is_editable_node(element))) {
@@ -109,7 +114,7 @@
context: 'a',
});
//noinspection JSValidateTypes
- editor.addCommand('image', {
+ editor.addCommand('cimage', {
exec: function (editor) {
image_dialog(editor);
return true;
@@ -126,7 +131,7 @@
});
editor.ui.addButton('Image', {
label: 'Image',
- command: 'image',
+ command: 'cimage',
toolbar: 'insert,10',
});
@@ -400,7 +405,7 @@
init: function () {
this.on('edit', function () {
- new website.editor.FontIconsDialog(editor, this.element.$)
+ new website.editor.MediaDialog(editor, this.element)
.appendTo(document.body);
});
},
@@ -629,22 +634,48 @@
},
/**
- * Creates a "hover" button for image and link edition
+ * Creates a "hover" button for link edition
*
- * @param {String} label the button's label
* @param {Function} editfn edition function, called when clicking the button
- * @param {String} [classes] additional classes to set on the button
* @returns {jQuery}
*/
- make_hover_button: function (label, editfn, classes) {
- return $(openerp.qweb.render('website.editor.hoverbutton', {
- label: label,
- classes: classes,
- })).hide().appendTo(document.body).click(function (e) {
+ make_hover_button_link: function (editfn) {
+ return $(openerp.qweb.render('website.editor.hoverbutton.link', {}))
+ .hide()
+ .click(function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ editfn.call(this, e);
+ })
+ .appendTo(document.body);
+ },
+
+ /**
+ * Creates a "hover" button for image
+ *
+ * @param {Function} editfn edition function, called when clicking the button
+ * @param {Function} stylefn edition style function, called when clicking the button
+ * @returns {jQuery}
+ */
+ make_hover_button_image: function (editfn, stylefn) {
+ var $div = $(openerp.qweb.render('website.editor.hoverbutton.media', {}))
+ .hide()
+ .appendTo(document.body);
+
+ $div.find('[data-toggle="dropdown"]').dropdown();
+ $div.find(".hover-edition-button").click(function (e) {
e.preventDefault();
e.stopPropagation();
editfn.call(this, e);
});
+ if (stylefn) {
+ $div.find(".hover-style-button").click(function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ stylefn.call(this, e);
+ });
+ }
+ return $div;
},
/**
* For UI clarity, during RTE edition when the user hovers links and
@@ -653,23 +684,18 @@
*/
setup_hover_buttons: function () {
var editor = this.rte.editor;
- var $link_button = this.make_hover_button(_t("Change"), function () {
+ var $link_button = this.make_hover_button_link(function () {
var sel = new CKEDITOR.dom.element(previous);
editor.getSelection().selectElement(sel);
if(sel.hasClass('fa')) {
- new website.editor.FontIconsDialog(editor, previous)
+ new website.editor.MediaDialog(editor, previous)
.appendTo(document.body);
} else if (previous.tagName.toUpperCase() === 'A') {
link_dialog(editor);
}
$link_button.hide();
previous = null;
- }, 'btn-xs');
- var $image_button = this.make_hover_button(_t("Change"), function () {
- image_dialog(editor, new CKEDITOR.dom.element(previous));
- $image_button.hide();
- previous = null;
- }, 'btn-sm');
+ });
function is_icons_widget(element) {
var w = editor.widgets.getByElement(element);
@@ -681,7 +707,7 @@
// -ish, because when moving to the button itself ``previous`` is
// still set to the element having triggered showing the button.
var previous;
- $(editor.element.$).on('mouseover', 'a, img, .fa', function () {
+ $(editor.element.$).on('mouseover', 'a', function () {
// Back from edit button -> ignore
if (previous && previous === this) { return; }
@@ -700,34 +726,19 @@
previous = this;
var $selected = $(this);
var position = $selected.offset();
- if ($selected.is('img')) {
- $link_button.hide();
- // center button on image
- $image_button.show().offset({
- top: $selected.outerHeight() / 2
- + position.top
- - $image_button.outerHeight() / 2,
- left: $selected.outerWidth() / 2
- + position.left
- - $image_button.outerWidth() / 2,
- });
- } else {
- $image_button.hide();
- // put button below link, horizontally centered
- $link_button.show().offset({
- top: $selected.outerHeight()
- + position.top,
- left: $selected.outerWidth() / 2
- + position.left
- - $link_button.outerWidth() / 2
- })
- }
+ $link_button.show().offset({
+ top: $selected.outerHeight()
+ + position.top,
+ left: $selected.outerWidth() / 2
+ + position.left
+ - $link_button.outerWidth() / 2
+ })
}).on('mouseleave', 'a, img, .fa', function (e) {
var current = document.elementFromPoint(e.clientX, e.clientY);
- if (current === $link_button[0] || current === $image_button[0]) {
+ if (current === $link_button[0] || $(current).parent()[0] === $link_button[0]) {
return;
}
- $image_button.add($link_button).hide();
+ $link_button.hide();
previous = null;
});
}
@@ -1041,6 +1052,7 @@
},
save: function () {
this.close();
+ this.trigger("saved");
},
cancel: function () {
},
@@ -1290,6 +1302,128 @@
},
});
+ website.editor.Media = openerp.Widget.extend({
+ init: function (parent, editor, media) {
+ this._super();
+ this.parent = parent;
+ this.editor = editor;
+ this.media = media;
+ },
+ start: function () {
+ this.$preview = this.$('.preview-container').detach();
+ return this._super();
+ },
+ search: function (needle) {
+ },
+ save: function () {
+ },
+ clear: function () {
+ },
+ cancel: function () {
+ },
+ close: function () {
+ },
+ });
+ website.editor.MediaDialog = website.editor.Dialog.extend({
+ template: 'website.editor.dialog.media',
+ events : _.extend({}, website.editor.Dialog.prototype.events, {
+ 'input input#icon-search': 'search',
+ }),
+
+ init: function (editor, media) {
+ this._super(editor);
+ this.editor = editor;
+ this.page = 0;
+ this.media = media;
+ },
+ start: function () {
+ var self = this;
+
+ this.imageDialog = new website.editor.RTEImageDialog(this, this.editor, this.media);
+ this.imageDialog.appendTo(this.$("#editor-media-image"));
+ this.iconDialog = new website.editor.FontIconsDialog(this, this.editor, this.media);
+ this.iconDialog.appendTo(this.$("#editor-media-icon"));
+ this.videoDialog = new website.editor.VideoDialog(this, this.editor, this.media);
+ this.videoDialog.appendTo(this.$("#editor-media-video"));
+
+ this.active = this.imageDialog;
+
+ $('a[data-toggle="tab"]').on('shown.bs.tab', function (event) {
+ if ($(event.target).is('[href="#editor-media-image"]')) {
+ self.active = self.imageDialog;
+ self.$('li.search, li.previous, li.next').removeClass("hidden");
+ } else if ($(event.target).is('[href="#editor-media-icon"]')) {
+ self.active = self.iconDialog;
+ self.$('li.search, li.previous, li.next').removeClass("hidden");
+ self.$('.nav-tabs li.previous, .nav-tabs li.next').addClass("hidden");
+ } else if ($(event.target).is('[href="#editor-media-video"]')) {
+ self.active = self.videoDialog;
+ self.$('.nav-tabs li.search').addClass("hidden");
+ }
+ });
+
+ if (this.media) {
+ if (this.media.$.nodeName === "IMG") {
+ this.$('[href="#editor-media-image"]').tab('show');
+ } else if (this.media.$.className.match(/(^|\s)media_iframe_video($|\s)/)) {
+ this.$('[href="#editor-media-video"]').tab('show');
+ } else if (this.media.$.className.match(/(^|\s)fa($|\s)/)) {
+ this.$('[href="#editor-media-icon"]').tab('show');
+ }
+
+ if ($(this.media.$).parent().data("oe-field") === "image") {
+ this.$('[href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden');
+ }
+ }
+
+ return this._super();
+ },
+ save: function () {
+ var self = this;
+ if (self.media) {
+ this.media.$.innerHTML = "";
+ if (this.active !== this.imageDialog) {
+ this.imageDialog.clear();
+ }
+ if (this.active !== this.iconDialog) {
+ this.iconDialog.clear();
+ }
+ if (this.active !== this.videoDialog) {
+ this.videoDialog.clear();
+ }
+ } else {
+ var selection = this.editor.getSelection();
+ var range = selection.getRanges(true)[0];
+ this.media = new CKEDITOR.dom.element("img");
+ range.insertNode(this.media);
+ range.selectNodeContents(this.media);
+ this.active.media = this.media;
+ }
+
+ var $el = $(self.active.media.$);
+
+ this.active.save();
+
+ this.media.$.className = this.media.$.className.replace(/\s+/g, ' ');
+
+ setTimeout(function () {
+ $el.trigger("saved", self.active.media.$);
+ $(document.body).trigger("media-saved", [$el[0], self.active.media.$]);
+ },0);
+
+ this._super();
+ },
+ searchTimer: null,
+ search: function () {
+ var self = this;
+ var needle = this.$("input#icon-search").val();
+ clearTimeout(this.searchTimer);
+ this.searchTimer = setTimeout(function () {
+ self.active.search(needle || "");
+ },250);
+ }
+ });
+
/**
* ImageDialog widget. Lets users change an image, including uploading a
* new image in OpenERP or selecting the image style (if supported by
@@ -1307,73 +1441,118 @@
* selected by the users (or possibly the ones
* originally passed in)
*/
- website.editor.ImageDialog = website.editor.Dialog.extend({
+ var IMAGES_PER_ROW = 6;
+ var IMAGES_ROWS = 2;
+ website.editor.ImageDialog = website.editor.Media.extend({
template: 'website.editor.dialog.image',
events: _.extend({}, website.editor.Dialog.prototype.events, {
- 'change .url-source': function (e) { this.changed($(e.target)); },
+ 'change .url-source': function (e) {
+ this.changed($(e.target));
+ },
'click button.filepicker': function () {
this.$('input[type=file]').click();
},
'change input[type=file]': 'file_selection',
- 'change input.url': 'preview_image',
- 'click a[href=#existing]': 'browse_existing',
- 'change select.image-style': 'preview_image',
+ 'submit form': 'form_submit',
+ 'change input.url': "change_input",
+ 'keyup input.url': "change_input",
+ //'change select.image-style': 'preview_image',
+ 'click .existing-attachments img': 'select_existing',
+ 'click .existing-attachment-remove': 'try_remove',
}),
+ init: function (parent, editor, media) {
+ this.page = 0;
+ this._super(parent, editor, media);
+ },
start: function () {
- this.$('button.wait').text("Uploading…");
- var $options = this.$('.image-style').children();
- this.image_styles = $options.map(function () { return this.value; }).get();
+ var self = this;
+ var res = this._super();
- var o = { url: null, style: null, };
+ var o = { url: null };
// avoid typos, prevent addition of new properties to the object
Object.preventExtensions(o);
this.trigger('start', o);
- if (o.url) {
- if (o.style) {
- this.$('.image-style').val(o.style);
+ this.parent.$(".pager > li").click(function (e) {
+ e.preventDefault();
+ var $target = $(e.currentTarget);
+ if ($target.hasClass('disabled')) {
+ return;
}
- this.set_image(o.url);
- }
+ self.page += $target.hasClass('previous') ? -1 : 1;
+ self.display_attachments();
+ });
- return this._super();
+ this.set_image(o.url);
+
+ return res;
},
save: function () {
+ if (!this.link) {
+ this.link = this.$(".existing-attachments img:first").attr('src');
+ }
this.trigger('save', {
- url: this.$('input.url').val(),
- style: this.$('.image-style').val(),
+ url: this.link
});
+ this.media.renameNode("img");
+ this.media.$.attributes.src = this.link;
return this._super();
},
+ clear: function () {
+ this.media.$.className = this.media.$.className.replace(/(^|\s)(img(\s|$)|img-[^\s]*)/g, ' ');
+ },
cancel: function () {
this.trigger('cancel');
},
- /**
- * Sets the provided image url as the dialog's value-to-save and
- * refreshes the preview element to use it.
- */
- set_image: function (url, error) {
- this.$('input.url').val(
- error ? '' : url);
- this.$('input.url').val(url);
- this.preview_image();
+ change_input: function (e) {
+ var $input = $(e.target);
+ var $button = $input.parent().find("button");
+ if ($input.val() === "") {
+ $button.addClass("btn-default").removeClass("btn-primary");
+ } else {
+ $button.removeClass("btn-default").addClass("btn-primary");
+ }
},
- file_selection: function () {
- this.$el.addClass('nosave');
- this.$('form').removeClass('has-error').find('.help-block').empty();
- this.$('button.filepicker').removeClass('btn-danger btn-success');
-
+ search: function (needle) {
var self = this;
+ this.fetch_existing(needle).then(function () {
+ self.selected_existing(self.$('input.url').val());
+ });
+ },
+
+ set_image: function (url, error) {
+ var self = this;
+ if (url) this.link = url;
+ this.$('input.url').val('');
+ this.fetch_existing().then(function () {
+ self.selected_existing(url);
+ });
+ },
+
+ form_submit: function (event) {
+ var self = this;
+ var $form = this.$('form[action="/website/attach"]');
+ if (!$form.find('input[name="upload"]').val().length) {
+ var url = $form.find('input[name="url"]').val();
+ if (this.selected_existing(url).size()) {
+ event.preventDefault();
+ return false;
+ }
+ }
var callback = _.uniqueId('func_');
this.$('input[name=func]').val(callback);
-
window[callback] = function (url, error) {
delete window[callback];
self.file_selected(url, error);
};
+ },
+ file_selection: function () {
+ this.$el.addClass('nosave');
+ this.$('form').removeClass('has-error').find('.help-block').empty();
+ this.$('button.filepicker').removeClass('btn-danger btn-success');
this.$('form').submit();
},
file_selected: function(url, error) {
@@ -1387,125 +1566,27 @@
$button.addClass('btn-danger');
}
this.set_image(url, error);
+ // auto save and close popup
+ this.parent.save();
},
- preview_image: function () {
- var loaded = function () {
- this.$el.removeClass('nosave');
- }.bind(this);
- var image = this.$('input.url').val();
- if (!image) { loaded(); return; }
- var $img = this.$('img.image-preview')
- .attr('src', image)
- .removeClass(this.image_styles.join(' '))
- .addClass(this.$('select.image-style').val());
-
- if ($img.prop('complete')) {
- loaded();
- } else {
- $img.load(loaded)
+ fetch_existing: function (needle) {
+ var domain = [['res_model', '=', 'ir.ui.view'], '|',
+ ['mimetype', '=', false], ['mimetype', '=like', 'image/%']];
+ if (needle && needle.length) {
+ domain.push('|', ['datas_fname', 'ilike', needle], ['name', 'ilike', needle]);
}
- },
- browse_existing: function (e) {
- e.preventDefault();
- this.$('form').removeClass('has-error').find('.help-block').empty();
- this.$('button.filepicker').removeClass('btn-danger btn-success');
- new website.editor.ExistingImageDialog(this).appendTo(document.body);
- },
- });
- website.editor.RTEImageDialog = website.editor.ImageDialog.extend({
- init: function (editor, image) {
- this._super(editor);
-
- this.element = image;
-
- this.on('start', this, this.proxy('started'));
- this.on('save', this, this.proxy('saved'));
- },
- started: function (holder) {
- if (!this.element) {
- var selection = this.editor.getSelection();
- this.element = selection && selection.getSelectedElement();
- }
-
- var el = this.element;
- if (!el || !el.is('img')) {
- return;
- }
- _(this.image_styles).each(function (style) {
- if (el.hasClass(style)) {
- holder.style = style;
- }
- });
- holder.url = el.getAttribute('src');
- },
- saved: function (data) {
- var element, editor = this.editor;
- if (!(element = this.element)) {
- element = editor.document.createElement('img');
- element.addClass('img');
- element.addClass('img-responsive');
- // focus event handler interactions between bootstrap (modal)
- // and ckeditor (RTE) lead to blowing the stack in Safari and
- // Chrome (but not FF) when this is done synchronously =>
- // defer insertion so modal has been hidden & destroyed before
- // it happens
- setTimeout(function () {
- editor.insertElement(element);
- }, 0);
- }
-
- var style = data.style;
- element.setAttribute('src', data.url);
- element.removeAttribute('data-cke-saved-src');
- $(element.$).removeClass(this.image_styles.join(' '));
- if (style) { element.addClass(style); }
- },
- });
-
- var IMAGES_PER_ROW = 6;
- var IMAGES_ROWS = 4;
- website.editor.ExistingImageDialog = website.editor.Dialog.extend({
- template: 'website.editor.dialog.image.existing',
- events: _.extend({}, website.editor.Dialog.prototype.events, {
- 'click .existing-attachments img': 'select_existing',
- 'click .pager > li': function (e) {
- e.preventDefault();
- var $target = $(e.currentTarget);
- if ($target.hasClass('disabled')) {
- return;
- }
- this.page += $target.hasClass('previous') ? -1 : 1;
- this.display_attachments();
- },
- 'click .existing-attachment-remove': 'try_remove',
- }),
- init: function (parent) {
- this.image = null;
- this.page = 0;
- this.parent = parent;
- this._super(parent.editor);
- },
-
- start: function () {
- return $.when(
- this._super(),
- this.fetch_existing().then(this.proxy('fetched_existing')));
- },
-
- fetch_existing: function () {
return openerp.jsonRpc('/web/dataset/call_kw', 'call', {
model: 'ir.attachment',
method: 'search_read',
args: [],
kwargs: {
fields: ['name', 'website_url'],
- domain: [['res_model', '=', 'ir.ui.view'], '|',
- ['mimetype', '=', false], ['mimetype', '=like', 'image/%']],
+ domain: domain,
order: 'id desc',
context: website.get_context(),
}
- });
+ }).then(this.proxy('fetched_existing'));
},
fetched_existing: function (records) {
this.records = records;
@@ -1528,24 +1609,33 @@
this.$('.existing-attachments').replaceWith(
openerp.qweb.render(
'website.editor.dialog.image.existing.content', {rows: rows}));
- this.$('.pager')
+ this.parent.$('.pager')
.find('li.previous').toggleClass('disabled', (from === 0)).end()
.find('li.next').toggleClass('disabled', (from + per_screen >= records.length));
-
},
select_existing: function (e) {
var link = $(e.currentTarget).attr('src');
- if (link) {
- this.parent.set_image(link);
- }
- this.close()
+ this.link = link;
+ this.selected_existing(link);
+ },
+ selected_existing: function (link) {
+ this.$('.existing-attachment-cell.media_selected').removeClass("media_selected");
+ var $select = this.$('.existing-attachment-cell img').filter(function () {
+ return $(this).attr("src") == link;
+ }).first();
+ $select.parent().addClass("media_selected");
+ return $select;
},
try_remove: function (e) {
var $help_block = this.$('.help-block').empty();
var self = this;
- var id = parseInt($(e.target).data('id'), 10);
+ var $a = $(e.target);
+ var id = parseInt($a.data('id'), 10);
var attachment = _.findWhere(this.records, {id: id});
+ var $both = $a.parent().children();
+
+ $both.css({borderWidth: "5px", borderColor: "#f00"});
return openerp.jsonRpc('/web/dataset/call_kw', 'call', {
model: 'ir.attachment',
@@ -1561,6 +1651,7 @@
self.display_attachments();
return;
}
+ $both.css({borderWidth: "", borderColor: ""});
$help_block.replaceWith(openerp.qweb.render(
'website.editor.dialog.image.existing.error', {
views: prevented[id]
@@ -1570,6 +1661,48 @@
},
});
+ website.editor.RTEImageDialog = website.editor.ImageDialog.extend({
+ init: function (parent, editor, media) {
+ this._super(parent, editor, media);
+
+ this.on('start', this, this.proxy('started'));
+ this.on('save', this, this.proxy('saved'));
+ },
+ started: function (holder) {
+ if (!this.media) {
+ var selection = this.editor.getSelection();
+ this.media = selection && selection.getSelectedElement();
+ }
+
+ var el = this.media;
+ if (!el || !el.is('img')) {
+ return;
+ }
+ holder.url = el.getAttribute('src');
+ },
+ saved: function (data) {
+ var element, editor = this.editor;
+ if (!(element = this.media)) {
+ element = editor.document.createElement('img');
+ element.addClass('img');
+ element.addClass('img-responsive');
+ // focus event handler interactions between bootstrap (modal)
+ // and ckeditor (RTE) lead to blowing the stack in Safari and
+ // Chrome (but not FF) when this is done synchronously =>
+ // defer insertion so modal has been hidden & destroyed before
+ // it happens
+ setTimeout(function () {
+ editor.insertElement(element);
+ }, 0);
+ }
+
+ var style = data.style;
+ element.setAttribute('src', data.url);
+ element.removeAttribute('data-cke-saved-src');
+ if (style) { element.addClass(style); }
+ },
+ });
+
function get_selected_link(editor) {
var sel = editor.getSelection(),
el = sel.getSelectedElement();
@@ -1581,7 +1714,7 @@
range.shrink(CKEDITOR.SHRINK_TEXT);
var commonAncestor = range.getCommonAncestor();
var viewRoot = editor.elementPath(commonAncestor).contains(function (element) {
- return element.data('oe-model') === 'ir.ui.view'
+ return element.data('oe-model') === 'ir.ui.view';
});
if (!viewRoot) { return null; }
// if viewRoot is the first link, don't edit it.
@@ -1589,7 +1722,7 @@
.contains('a', true);
}
- website.editor.FontIconsDialog = website.editor.Dialog.extend({
+ website.editor.FontIconsDialog = website.editor.Media.extend({
template: 'website.editor.dialog.font-icons',
events : _.extend({}, website.editor.Dialog.prototype.events, {
change: 'update_preview',
@@ -1607,20 +1740,6 @@
this.$('#fa-size').val(e.target.getAttribute('data-size'));
this.update_preview();
},
- 'input input#icon-search': function () {
- var needle = this.$('#icon-search').val();
- var icons = this.icons;
- if (needle) {
- icons = _(icons).filter(function (icon) {
- return icon.id.substring(3).indexOf(needle) !== -1;
- });
- }
-
- this.$('div.font-icons-icons').html(
- openerp.qweb.render(
- 'website.editor.dialog.font-icons.icons',
- {icons: icons}));
- },
}),
// List of FontAwesome icons in 4.0.3, extracted from the cheatsheet.
@@ -1630,10 +1749,6 @@
// where we still need to implement ``initSelection``)
// TODO: add id/name to the text in order to allow FAYT selection of icons?
icons: [{"text": "\uf000", "id": "fa-glass"}, {"text": "\uf001", "id": "fa-music"}, {"text": "\uf002", "id": "fa-search"}, {"text": "\uf003", "id": "fa-envelope-o"}, {"text": "\uf004", "id": "fa-heart"}, {"text": "\uf005", "id": "fa-star"}, {"text": "\uf006", "id": "fa-star-o"}, {"text": "\uf007", "id": "fa-user"}, {"text": "\uf008", "id": "fa-film"}, {"text": "\uf009", "id": "fa-th-large"}, {"text": "\uf00a", "id": "fa-th"}, {"text": "\uf00b", "id": "fa-th-list"}, {"text": "\uf00c", "id": "fa-check"}, {"text": "\uf00d", "id": "fa-times"}, {"text": "\uf00e", "id": "fa-search-plus"}, {"text": "\uf010", "id": "fa-search-minus"}, {"text": "\uf011", "id": "fa-power-off"}, {"text": "\uf012", "id": "fa-signal"}, {"text": "\uf013", "id": "fa-cog"}, {"text": "\uf014", "id": "fa-trash-o"}, {"text": "\uf015", "id": "fa-home"}, {"text": "\uf016", "id": "fa-file-o"}, {"text": "\uf017", "id": "fa-clock-o"}, {"text": "\uf018", "id": "fa-road"}, {"text": "\uf019", "id": "fa-download"}, {"text": "\uf01a", "id": "fa-arrow-circle-o-down"}, {"text": "\uf01b", "id": "fa-arrow-circle-o-up"}, {"text": "\uf01c", "id": "fa-inbox"}, {"text": "\uf01d", "id": "fa-play-circle-o"}, {"text": "\uf01e", "id": "fa-repeat"}, {"text": "\uf021", "id": "fa-refresh"}, {"text": "\uf022", "id": "fa-list-alt"}, {"text": "\uf023", "id": "fa-lock"}, {"text": "\uf024", "id": "fa-flag"}, {"text": "\uf025", "id": "fa-headphones"}, {"text": "\uf026", "id": "fa-volume-off"}, {"text": "\uf027", "id": "fa-volume-down"}, {"text": "\uf028", "id": "fa-volume-up"}, {"text": "\uf029", "id": "fa-qrcode"}, {"text": "\uf02a", "id": "fa-barcode"}, {"text": "\uf02b", "id": "fa-tag"}, {"text": "\uf02c", "id": "fa-tags"}, {"text": "\uf02d", "id": "fa-book"}, {"text": "\uf02e", "id": "fa-bookmark"}, {"text": "\uf02f", "id": "fa-print"}, {"text": "\uf030", "id": "fa-camera"}, {"text": "\uf031", "id": "fa-font"}, {"text": "\uf032", "id": "fa-bold"}, {"text": "\uf033", "id": "fa-italic"}, {"text": "\uf034", "id": "fa-text-height"}, {"text": "\uf035", "id": "fa-text-width"}, {"text": "\uf036", "id": "fa-align-left"}, {"text": "\uf037", "id": "fa-align-center"}, {"text": "\uf038", "id": "fa-align-right"}, {"text": "\uf039", "id": "fa-align-justify"}, {"text": "\uf03a", "id": "fa-list"}, {"text": "\uf03b", "id": "fa-outdent"}, {"text": "\uf03c", "id": "fa-indent"}, {"text": "\uf03d", "id": "fa-video-camera"}, {"text": "\uf03e", "id": "fa-picture-o"}, {"text": "\uf040", "id": "fa-pencil"}, {"text": "\uf041", "id": "fa-map-marker"}, {"text": "\uf042", "id": "fa-adjust"}, {"text": "\uf043", "id": "fa-tint"}, {"text": "\uf044", "id": "fa-pencil-square-o"}, {"text": "\uf045", "id": "fa-share-square-o"}, {"text": "\uf046", "id": "fa-check-square-o"}, {"text": "\uf047", "id": "fa-arrows"}, {"text": "\uf048", "id": "fa-step-backward"}, {"text": "\uf049", "id": "fa-fast-backward"}, {"text": "\uf04a", "id": "fa-backward"}, {"text": "\uf04b", "id": "fa-play"}, {"text": "\uf04c", "id": "fa-pause"}, {"text": "\uf04d", "id": "fa-stop"}, {"text": "\uf04e", "id": "fa-forward"}, {"text": "\uf050", "id": "fa-fast-forward"}, {"text": "\uf051", "id": "fa-step-forward"}, {"text": "\uf052", "id": "fa-eject"}, {"text": "\uf053", "id": "fa-chevron-left"}, {"text": "\uf054", "id": "fa-chevron-right"}, {"text": "\uf055", "id": "fa-plus-circle"}, {"text": "\uf056", "id": "fa-minus-circle"}, {"text": "\uf057", "id": "fa-times-circle"}, {"text": "\uf058", "id": "fa-check-circle"}, {"text": "\uf059", "id": "fa-question-circle"}, {"text": "\uf05a", "id": "fa-info-circle"}, {"text": "\uf05b", "id": "fa-crosshairs"}, {"text": "\uf05c", "id": "fa-times-circle-o"}, {"text": "\uf05d", "id": "fa-check-circle-o"}, {"text": "\uf05e", "id": "fa-ban"}, {"text": "\uf060", "id": "fa-arrow-left"}, {"text": "\uf061", "id": "fa-arrow-right"}, {"text": "\uf062", "id": "fa-arrow-up"}, {"text": "\uf063", "id": "fa-arrow-down"}, {"text": "\uf064", "id": "fa-share"}, {"text": "\uf065", "id": "fa-expand"}, {"text": "\uf066", "id": "fa-compress"}, {"text": "\uf067", "id": "fa-plus"}, {"text": "\uf068", "id": "fa-minus"}, {"text": "\uf069", "id": "fa-asterisk"}, {"text": "\uf06a", "id": "fa-exclamation-circle"}, {"text": "\uf06b", "id": "fa-gift"}, {"text": "\uf06c", "id": "fa-leaf"}, {"text": "\uf06d", "id": "fa-fire"}, {"text": "\uf06e", "id": "fa-eye"}, {"text": "\uf070", "id": "fa-eye-slash"}, {"text": "\uf071", "id": "fa-exclamation-triangle"}, {"text": "\uf072", "id": "fa-plane"}, {"text": "\uf073", "id": "fa-calendar"}, {"text": "\uf074", "id": "fa-random"}, {"text": "\uf075", "id": "fa-comment"}, {"text": "\uf076", "id": "fa-magnet"}, {"text": "\uf077", "id": "fa-chevron-up"}, {"text": "\uf078", "id": "fa-chevron-down"}, {"text": "\uf079", "id": "fa-retweet"}, {"text": "\uf07a", "id": "fa-shopping-cart"}, {"text": "\uf07b", "id": "fa-folder"}, {"text": "\uf07c", "id": "fa-folder-open"}, {"text": "\uf07d", "id": "fa-arrows-v"}, {"text": "\uf07e", "id": "fa-arrows-h"}, {"text": "\uf080", "id": "fa-bar-chart-o"}, {"text": "\uf081", "id": "fa-twitter-square"}, {"text": "\uf082", "id": "fa-facebook-square"}, {"text": "\uf083", "id": "fa-camera-retro"}, {"text": "\uf084", "id": "fa-key"}, {"text": "\uf085", "id": "fa-cogs"}, {"text": "\uf086", "id": "fa-comments"}, {"text": "\uf087", "id": "fa-thumbs-o-up"}, {"text": "\uf088", "id": "fa-thumbs-o-down"}, {"text": "\uf089", "id": "fa-star-half"}, {"text": "\uf08a", "id": "fa-heart-o"}, {"text": "\uf08b", "id": "fa-sign-out"}, {"text": "\uf08c", "id": "fa-linkedin-square"}, {"text": "\uf08d", "id": "fa-thumb-tack"}, {"text": "\uf08e", "id": "fa-external-link"}, {"text": "\uf090", "id": "fa-sign-in"}, {"text": "\uf091", "id": "fa-trophy"}, {"text": "\uf092", "id": "fa-github-square"}, {"text": "\uf093", "id": "fa-upload"}, {"text": "\uf094", "id": "fa-lemon-o"}, {"text": "\uf095", "id": "fa-phone"}, {"text": "\uf096", "id": "fa-square-o"}, {"text": "\uf097", "id": "fa-bookmark-o"}, {"text": "\uf098", "id": "fa-phone-square"}, {"text": "\uf099", "id": "fa-twitter"}, {"text": "\uf09a", "id": "fa-facebook"}, {"text": "\uf09b", "id": "fa-github"}, {"text": "\uf09c", "id": "fa-unlock"}, {"text": "\uf09d", "id": "fa-credit-card"}, {"text": "\uf09e", "id": "fa-rss"}, {"text": "\uf0a0", "id": "fa-hdd-o"}, {"text": "\uf0a1", "id": "fa-bullhorn"}, {"text": "\uf0f3", "id": "fa-bell"}, {"text": "\uf0a3", "id": "fa-certificate"}, {"text": "\uf0a4", "id": "fa-hand-o-right"}, {"text": "\uf0a5", "id": "fa-hand-o-left"}, {"text": "\uf0a6", "id": "fa-hand-o-up"}, {"text": "\uf0a7", "id": "fa-hand-o-down"}, {"text": "\uf0a8", "id": "fa-arrow-circle-left"}, {"text": "\uf0a9", "id": "fa-arrow-circle-right"}, {"text": "\uf0aa", "id": "fa-arrow-circle-up"}, {"text": "\uf0ab", "id": "fa-arrow-circle-down"}, {"text": "\uf0ac", "id": "fa-globe"}, {"text": "\uf0ad", "id": "fa-wrench"}, {"text": "\uf0ae", "id": "fa-tasks"}, {"text": "\uf0b0", "id": "fa-filter"}, {"text": "\uf0b1", "id": "fa-briefcase"}, {"text": "\uf0b2", "id": "fa-arrows-alt"}, {"text": "\uf0c0", "id": "fa-users"}, {"text": "\uf0c1", "id": "fa-link"}, {"text": "\uf0c2", "id": "fa-cloud"}, {"text": "\uf0c3", "id": "fa-flask"}, {"text": "\uf0c4", "id": "fa-scissors"}, {"text": "\uf0c5", "id": "fa-files-o"}, {"text": "\uf0c6", "id": "fa-paperclip"}, {"text": "\uf0c7", "id": "fa-floppy-o"}, {"text": "\uf0c8", "id": "fa-square"}, {"text": "\uf0c9", "id": "fa-bars"}, {"text": "\uf0ca", "id": "fa-list-ul"}, {"text": "\uf0cb", "id": "fa-list-ol"}, {"text": "\uf0cc", "id": "fa-strikethrough"}, {"text": "\uf0cd", "id": "fa-underline"}, {"text": "\uf0ce", "id": "fa-table"}, {"text": "\uf0d0", "id": "fa-magic"}, {"text": "\uf0d1", "id": "fa-truck"}, {"text": "\uf0d2", "id": "fa-pinterest"}, {"text": "\uf0d3", "id": "fa-pinterest-square"}, {"text": "\uf0d4", "id": "fa-google-plus-square"}, {"text": "\uf0d5", "id": "fa-google-plus"}, {"text": "\uf0d6", "id": "fa-money"}, {"text": "\uf0d7", "id": "fa-caret-down"}, {"text": "\uf0d8", "id": "fa-caret-up"}, {"text": "\uf0d9", "id": "fa-caret-left"}, {"text": "\uf0da", "id": "fa-caret-right"}, {"text": "\uf0db", "id": "fa-columns"}, {"text": "\uf0dc", "id": "fa-sort"}, {"text": "\uf0dd", "id": "fa-sort-asc"}, {"text": "\uf0de", "id": "fa-sort-desc"}, {"text": "\uf0e0", "id": "fa-envelope"}, {"text": "\uf0e1", "id": "fa-linkedin"}, {"text": "\uf0e2", "id": "fa-undo"}, {"text": "\uf0e3", "id": "fa-gavel"}, {"text": "\uf0e4", "id": "fa-tachometer"}, {"text": "\uf0e5", "id": "fa-comment-o"}, {"text": "\uf0e6", "id": "fa-comments-o"}, {"text": "\uf0e7", "id": "fa-bolt"}, {"text": "\uf0e8", "id": "fa-sitemap"}, {"text": "\uf0e9", "id": "fa-umbrella"}, {"text": "\uf0ea", "id": "fa-clipboard"}, {"text": "\uf0eb", "id": "fa-lightbulb-o"}, {"text": "\uf0ec", "id": "fa-exchange"}, {"text": "\uf0ed", "id": "fa-cloud-download"}, {"text": "\uf0ee", "id": "fa-cloud-upload"}, {"text": "\uf0f0", "id": "fa-user-md"}, {"text": "\uf0f1", "id": "fa-stethoscope"}, {"text": "\uf0f2", "id": "fa-suitcase"}, {"text": "\uf0a2", "id": "fa-bell-o"}, {"text": "\uf0f4", "id": "fa-coffee"}, {"text": "\uf0f5", "id": "fa-cutlery"}, {"text": "\uf0f6", "id": "fa-file-text-o"}, {"text": "\uf0f7", "id": "fa-building-o"}, {"text": "\uf0f8", "id": "fa-hospital-o"}, {"text": "\uf0f9", "id": "fa-ambulance"}, {"text": "\uf0fa", "id": "fa-medkit"}, {"text": "\uf0fb", "id": "fa-fighter-jet"}, {"text": "\uf0fc", "id": "fa-beer"}, {"text": "\uf0fd", "id": "fa-h-square"}, {"text": "\uf0fe", "id": "fa-plus-square"}, {"text": "\uf100", "id": "fa-angle-double-left"}, {"text": "\uf101", "id": "fa-angle-double-right"}, {"text": "\uf102", "id": "fa-angle-double-up"}, {"text": "\uf103", "id": "fa-angle-double-down"}, {"text": "\uf104", "id": "fa-angle-left"}, {"text": "\uf105", "id": "fa-angle-right"}, {"text": "\uf106", "id": "fa-angle-up"}, {"text": "\uf107", "id": "fa-angle-down"}, {"text": "\uf108", "id": "fa-desktop"}, {"text": "\uf109", "id": "fa-laptop"}, {"text": "\uf10a", "id": "fa-tablet"}, {"text": "\uf10b", "id": "fa-mobile"}, {"text": "\uf10c", "id": "fa-circle-o"}, {"text": "\uf10d", "id": "fa-quote-left"}, {"text": "\uf10e", "id": "fa-quote-right"}, {"text": "\uf110", "id": "fa-spinner"}, {"text": "\uf111", "id": "fa-circle"}, {"text": "\uf112", "id": "fa-reply"}, {"text": "\uf113", "id": "fa-github-alt"}, {"text": "\uf114", "id": "fa-folder-o"}, {"text": "\uf115", "id": "fa-folder-open-o"}, {"text": "\uf118", "id": "fa-smile-o"}, {"text": "\uf119", "id": "fa-frown-o"}, {"text": "\uf11a", "id": "fa-meh-o"}, {"text": "\uf11b", "id": "fa-gamepad"}, {"text": "\uf11c", "id": "fa-keyboard-o"}, {"text": "\uf11d", "id": "fa-flag-o"}, {"text": "\uf11e", "id": "fa-flag-checkered"}, {"text": "\uf120", "id": "fa-terminal"}, {"text": "\uf121", "id": "fa-code"}, {"text": "\uf122", "id": "fa-reply-all"}, {"text": "\uf122", "id": "fa-mail-reply-all"}, {"text": "\uf123", "id": "fa-star-half-o"}, {"text": "\uf124", "id": "fa-location-arrow"}, {"text": "\uf125", "id": "fa-crop"}, {"text": "\uf126", "id": "fa-code-fork"}, {"text": "\uf127", "id": "fa-chain-broken"}, {"text": "\uf128", "id": "fa-question"}, {"text": "\uf129", "id": "fa-info"}, {"text": "\uf12a", "id": "fa-exclamation"}, {"text": "\uf12b", "id": "fa-superscript"}, {"text": "\uf12c", "id": "fa-subscript"}, {"text": "\uf12d", "id": "fa-eraser"}, {"text": "\uf12e", "id": "fa-puzzle-piece"}, {"text": "\uf130", "id": "fa-microphone"}, {"text": "\uf131", "id": "fa-microphone-slash"}, {"text": "\uf132", "id": "fa-shield"}, {"text": "\uf133", "id": "fa-calendar-o"}, {"text": "\uf134", "id": "fa-fire-extinguisher"}, {"text": "\uf135", "id": "fa-rocket"}, {"text": "\uf136", "id": "fa-maxcdn"}, {"text": "\uf137", "id": "fa-chevron-circle-left"}, {"text": "\uf138", "id": "fa-chevron-circle-right"}, {"text": "\uf139", "id": "fa-chevron-circle-up"}, {"text": "\uf13a", "id": "fa-chevron-circle-down"}, {"text": "\uf13b", "id": "fa-html5"}, {"text": "\uf13c", "id": "fa-css3"}, {"text": "\uf13d", "id": "fa-anchor"}, {"text": "\uf13e", "id": "fa-unlock-alt"}, {"text": "\uf140", "id": "fa-bullseye"}, {"text": "\uf141", "id": "fa-ellipsis-h"}, {"text": "\uf142", "id": "fa-ellipsis-v"}, {"text": "\uf143", "id": "fa-rss-square"}, {"text": "\uf144", "id": "fa-play-circle"}, {"text": "\uf145", "id": "fa-ticket"}, {"text": "\uf146", "id": "fa-minus-square"}, {"text": "\uf147", "id": "fa-minus-square-o"}, {"text": "\uf148", "id": "fa-level-up"}, {"text": "\uf149", "id": "fa-level-down"}, {"text": "\uf14a", "id": "fa-check-square"}, {"text": "\uf14b", "id": "fa-pencil-square"}, {"text": "\uf14c", "id": "fa-external-link-square"}, {"text": "\uf14d", "id": "fa-share-square"}, {"text": "\uf14e", "id": "fa-compass"}, {"text": "\uf150", "id": "fa-caret-square-o-down"}, {"text": "\uf151", "id": "fa-caret-square-o-up"}, {"text": "\uf152", "id": "fa-caret-square-o-right"}, {"text": "\uf153", "id": "fa-eur"}, {"text": "\uf154", "id": "fa-gbp"}, {"text": "\uf155", "id": "fa-usd"}, {"text": "\uf156", "id": "fa-inr"}, {"text": "\uf157", "id": "fa-jpy"}, {"text": "\uf158", "id": "fa-rub"}, {"text": "\uf159", "id": "fa-krw"}, {"text": "\uf15a", "id": "fa-btc"}, {"text": "\uf15b", "id": "fa-file"}, {"text": "\uf15c", "id": "fa-file-text"}, {"text": "\uf15d", "id": "fa-sort-alpha-asc"}, {"text": "\uf15e", "id": "fa-sort-alpha-desc"}, {"text": "\uf160", "id": "fa-sort-amount-asc"}, {"text": "\uf161", "id": "fa-sort-amount-desc"}, {"text": "\uf162", "id": "fa-sort-numeric-asc"}, {"text": "\uf163", "id": "fa-sort-numeric-desc"}, {"text": "\uf164", "id": "fa-thumbs-up"}, {"text": "\uf165", "id": "fa-thumbs-down"}, {"text": "\uf166", "id": "fa-youtube-square"}, {"text": "\uf167", "id": "fa-youtube"}, {"text": "\uf168", "id": "fa-xing"}, {"text": "\uf169", "id": "fa-xing-square"}, {"text": "\uf16a", "id": "fa-youtube-play"}, {"text": "\uf16b", "id": "fa-dropbox"}, {"text": "\uf16c", "id": "fa-stack-overflow"}, {"text": "\uf16d", "id": "fa-instagram"}, {"text": "\uf16e", "id": "fa-flickr"}, {"text": "\uf170", "id": "fa-adn"}, {"text": "\uf171", "id": "fa-bitbucket"}, {"text": "\uf172", "id": "fa-bitbucket-square"}, {"text": "\uf173", "id": "fa-tumblr"}, {"text": "\uf174", "id": "fa-tumblr-square"}, {"text": "\uf175", "id": "fa-long-arrow-down"}, {"text": "\uf176", "id": "fa-long-arrow-up"}, {"text": "\uf177", "id": "fa-long-arrow-left"}, {"text": "\uf178", "id": "fa-long-arrow-right"}, {"text": "\uf179", "id": "fa-apple"}, {"text": "\uf17a", "id": "fa-windows"}, {"text": "\uf17b", "id": "fa-android"}, {"text": "\uf17c", "id": "fa-linux"}, {"text": "\uf17d", "id": "fa-dribbble"}, {"text": "\uf17e", "id": "fa-skype"}, {"text": "\uf180", "id": "fa-foursquare"}, {"text": "\uf181", "id": "fa-trello"}, {"text": "\uf182", "id": "fa-female"}, {"text": "\uf183", "id": "fa-male"}, {"text": "\uf184", "id": "fa-gittip"}, {"text": "\uf185", "id": "fa-sun-o"}, {"text": "\uf186", "id": "fa-moon-o"}, {"text": "\uf187", "id": "fa-archive"}, {"text": "\uf188", "id": "fa-bug"}, {"text": "\uf189", "id": "fa-vk"}, {"text": "\uf18a", "id": "fa-weibo"}, {"text": "\uf18b", "id": "fa-renren"}, {"text": "\uf18c", "id": "fa-pagelines"}, {"text": "\uf18d", "id": "fa-stack-exchange"}, {"text": "\uf18e", "id": "fa-arrow-circle-o-right"}, {"text": "\uf190", "id": "fa-arrow-circle-o-left"}, {"text": "\uf191", "id": "fa-caret-square-o-left"}, {"text": "\uf192", "id": "fa-dot-circle-o"}, {"text": "\uf193", "id": "fa-wheelchair"}, {"text": "\uf194", "id": "fa-vimeo-square"}, {"text": "\uf195", "id": "fa-try"}, {"text": "\uf196", "id": "fa-plus-square-o"}],
- init: function (editor, element) {
- this._super(editor);
- this.element = element;
- },
/**
* Initializes select2: in Chrome and Safari,
diff --git a/addons/website_blog/__openerp__.py b/addons/website_blog/__openerp__.py
index 97c163945af..831c180d51f 100644
--- a/addons/website_blog/__openerp__.py
+++ b/addons/website_blog/__openerp__.py
@@ -30,7 +30,7 @@ OpenERP Blog
""",
'author': 'OpenERP SA',
- 'depends': ['knowledge', 'website_mail'],
+ 'depends': ['knowledge', 'website_mail', 'website_partner'],
'data': [
'data/website_blog_data.xml',
'views/website_blog_views.xml',
diff --git a/addons/website_blog/controllers/main.py b/addons/website_blog/controllers/main.py
index 5cb678b5475..e2eac59678e 100644
--- a/addons/website_blog/controllers/main.py
+++ b/addons/website_blog/controllers/main.py
@@ -1,23 +1,4 @@
# -*- coding: utf-8 -*-
-##############################################################################
-#
-# OpenERP, Open Source Management Solution
-# Copyright (C) 2013-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 .
-#
-##############################################################################
import datetime
import werkzeug
@@ -29,6 +10,7 @@ from openerp.addons.website.models.website import slug
from openerp.osv.orm import browse_record
from openerp.tools.translate import _
from openerp import SUPERUSER_ID
+from openerp.tools import html2plaintext
class QueryURL(object):
@@ -62,12 +44,13 @@ class QueryURL(object):
class WebsiteBlog(http.Controller):
- _blog_post_per_page = 6
- _post_comment_per_page = 6
+ _blog_post_per_page = 20
+ _post_comment_per_page = 10
def nav_list(self):
blog_post_obj = request.registry['blog.post']
- groups = blog_post_obj.read_group(request.cr, request.uid, [], ['name', 'create_date'],
+ groups = blog_post_obj.read_group(
+ request.cr, request.uid, [], ['name', 'create_date'],
groupby="create_date", orderby="create_date asc", context=request.context)
for group in groups:
begin_date = datetime.datetime.strptime(group['__domain'][0][2], tools.DEFAULT_SERVER_DATETIME_FORMAT).date()
@@ -108,35 +91,27 @@ class WebsiteBlog(http.Controller):
def blog(self, blog=None, tag=None, page=1, **opt):
""" Prepare all values to display the blog.
- :param blog: blog currently browsed.
- :param tag: tag that is currently used to filter blog posts
- :param integer page: current page of the pager. Can be the blog or
- post pager.
- :param date: date currently used to filter blog posts (dateBegin_dateEnd)
-
:return dict values: values for the templates, containing
- - 'blog_posts': list of browse records that are the posts to display
- in a given blog, if not blog_post_id
- - 'blog': browse of the current blog, if blog_id
- - 'blogs': list of browse records of blogs
- - 'pager': the pager to display posts pager in a blog
- - 'tag': current tag, if tag_id
+ - 'blog': current blog
+ - 'blogs': all blogs for navigation
+ - 'pager': pager of posts
+ - 'tag': current tag
+ - 'tags': all tags, for navigation
- 'nav_list': a dict [year][month] for archives navigation
+ - 'date': date_begin optional parameter, used in archives navigation
+ - 'blog_url': help object to create URLs
"""
date_begin, date_end = opt.get('date_begin'), opt.get('date_end')
cr, uid, context = request.cr, request.uid, request.context
blog_post_obj = request.registry['blog.post']
- blog_posts = None
-
blog_obj = request.registry['blog.blog']
blog_ids = blog_obj.search(cr, uid, [], order="create_date asc", context=context)
blogs = blog_obj.browse(cr, uid, blog_ids, context=context)
domain = []
-
if blog:
domain += [('blog_id', '=', blog.id)]
if tag:
@@ -176,35 +151,29 @@ class WebsiteBlog(http.Controller):
'post_url': post_url,
'date': date_begin,
}
- return request.website.render("website_blog.blog_post_short", values)
+ response = request.website.render("website_blog.blog_post_short", values)
+ return response
@http.route([
- '/blogpost/',
+ '/blog//post/',
], type='http', auth="public", website=True, multilang=True)
- def blog_post(self, blog_post, tag_id=None, page=1, enable_editor=None, **post):
+ def blog_post(self, blog, blog_post, tag_id=None, page=1, enable_editor=None, **post):
""" Prepare all values to display the blog.
- :param blog_post: blog post currently browsed. If not set, the user is
- browsing the blog and a post pager is calculated.
- If set the user is reading the blog post and a
- comments pager is calculated.
- :param blog: blog currently browsed.
- :param tag: tag that is currently used to filter blog posts
- :param integer page: current page of the pager. Can be the blog or
- post pager.
- :param date: date currently used to filter blog posts (dateBegin_dateEnd)
-
- - 'enable_editor': editor control
-
:return dict values: values for the templates, containing
- - 'blog_post': browse of the current post, if blog_post_id
- - 'blog': browse of the current blog, if blog_id
+ - 'blog_post': browse of the current post
+ - 'blog': browse of the current blog
- 'blogs': list of browse records of blogs
- - 'pager': the pager to display comments pager in a blog post
- - 'tag': current tag, if tag_id
+ - 'tag': current tag, if tag_id in parameters
+ - 'tags': all tags, for tag-based navigation
+ - 'pager': a pager on the comments
- 'nav_list': a dict [year][month] for archives navigation
+ - 'next_post': next blog post, to direct the user towards the next interesting post
"""
+ cr, uid, context = request.cr, request.uid, request.context
+ tag_obj = request.registry['blog.tag']
+ blog_post_obj = request.registry['blog.post']
date_begin, date_end = post.get('date_begin'), post.get('date_end')
pager_url = "/blogpost/%s" % blog_post.id
@@ -226,62 +195,125 @@ class WebsiteBlog(http.Controller):
post_url = QueryURL('', ['blogpost'], blogpost=blog_post, tag_id=tag_id, date_begin=date_begin, date_end=date_end)
blog_url = QueryURL('', ['blog', 'tag'], blog=blog_post.blog_id, tag=tag, date_begin=date_begin, date_end=date_end)
- cr, uid, context = request.cr, request.uid, request.context
- blog_obj = request.registry['blog.blog']
- blog_ids = blog_obj.search(cr, uid, [], context=context)
- blogs = blog_obj.browse(cr, uid, blog_ids, context=context)
+ if not blog_post.blog_id.id == blog.id:
+ return request.redirect("/blog/%s/post/%s" % (slug(blog_post.blog_id), slug(blog_post)))
- tag_obj = request.registry['blog.tag']
- tag_ids = tag_obj.search(cr, uid, [], context=context)
- tags = tag_obj.browse(cr, uid, tag_ids, context=context)
+ tags = tag_obj.browse(cr, uid, tag_obj.search(cr, uid, [], context=context), context=context)
+
+ # Find next Post
+ visited_blogs = request.httprequest.cookies.get('visited_blogs') or ''
+ visited_ids = filter(None, visited_blogs.split(','))
+ visited_ids = map(lambda x: int(x), visited_ids)
+ if blog_post.id not in visited_ids:
+ visited_ids.append(blog_post.id)
+ next_post_id = blog_post_obj.search(cr, uid, [
+ ('id', 'not in', visited_ids),
+ ], order='ranking desc', limit=1, context=context)
+ if not next_post_id:
+ next_post_id = blog_post_obj.search(cr, uid, [('id', '!=', blog.id)], order='ranking desc', limit=1, context=context)
+ next_post = next_post_id and blog_post_obj.browse(cr, uid, next_post_id[0], context=context) or False
values = {
- 'blog': blog_post.blog_id,
- 'blogs': blogs,
'tags': tags,
'tag': tag,
+ 'blog': blog,
'blog_post': blog_post,
'main_object': blog_post,
- 'pager': pager,
'nav_list': self.nav_list(),
'enable_editor': enable_editor,
+ 'next_post': next_post,
'date': date_begin,
'post_url': post_url,
'blog_url': blog_url,
+ 'pager': pager,
}
- return request.website.render("website_blog.blog_post_complete", values)
+ response = request.website.render("website_blog.blog_post_complete", values)
+ response.set_cookie('visited_blogs', ','.join(map(str, visited_ids)))
+
+ request.session[request.session_id] = request.session.get(request.session_id, [])
+ if not (blog_post.id in request.session[request.session_id]):
+ request.session[request.session_id].append(blog_post.id)
+ # Increase counter
+ blog_post_obj.write(cr, SUPERUSER_ID, [blog_post.id], {
+ 'visits': blog_post.visits+1,
+ },context=context)
+ return response
+
+ def _blog_post_message(self, user, blog_post_id=0, **post):
+ cr, uid, context = request.cr, request.uid, request.context
+ blog_post = request.registry['blog.post']
+ partner_obj = request.registry['res.partner']
+ thread_obj = request.registry['mail.thread']
+ website = request.registry['website']
+
+ public_id = website.get_public_user(cr, uid, context)
+ if uid != public_id:
+ partner_ids = [user.partner_id.id]
+ else:
+ partner_ids = blog_post._find_partner_from_emails(
+ cr, SUPERUSER_ID, 0, [post.get('email')], context=context)
+ if not partner_ids or not partner_ids[0]:
+ partner_ids = [partner_obj.create(cr, SUPERUSER_ID, {'name': post.get('name'), 'email': post.get('email')}, context=context)]
+
+ message_id = blog_post.message_post(
+ cr, SUPERUSER_ID, int(blog_post_id),
+ body=post.get('comment'),
+ type='comment',
+ subtype='mt_comment',
+ author_id=partner_ids[0],
+ path=post.get('path', False),
+ context=dict(context, mail_create_nosubcribe=True))
+ return message_id
@http.route(['/blogpost/comment'], type='http', auth="public", methods=['POST'], website=True)
def blog_post_comment(self, blog_post_id=0, **post):
cr, uid, context = request.cr, request.uid, request.context
if post.get('comment'):
- user = request.registry['res.users'].browse(cr, SUPERUSER_ID, uid, context=context)
- group_ids = user.groups_id
- group_id = request.registry["ir.model.data"].get_object_reference(cr, uid, 'website_mail', 'group_comment')[1]
- if group_id in [group.id for group in group_ids]:
- blog_post = request.registry['blog.post']
- blog_post.check_access_rights(cr, uid, 'read')
- blog_post.message_post(
- cr, SUPERUSER_ID, int(blog_post_id),
- body=post.get('comment'),
- type='comment',
- subtype='mt_comment',
- author_id=user.partner_id.id,
- context=dict(context, mail_create_nosubcribe=True))
+ user = request.registry['res.users'].browse(cr, uid, uid, context=context)
+ blog_post = request.registry['blog.post']
+ blog_post.check_access_rights(cr, uid, 'read')
+ self._blog_post_message(user, blog_post_id, **post)
return werkzeug.utils.redirect(request.httprequest.referrer + "#comments")
+ def _get_discussion_detail(self, ids, publish=False, **post):
+ cr, uid, context = request.cr, request.uid, request.context
+ values = []
+ mail_obj = request.registry.get('mail.message')
+ for message in mail_obj.browse(cr, SUPERUSER_ID, ids, context=context):
+ values.append({
+ "id": message.id,
+ "author_name": message.author_id.name,
+ "author_image": message.author_id.image and \
+ ("data:image/png;base64,%s" % message.author_id.image) or \
+ '/website_blog/static/src/img/anonymous.png',
+ "date": message.date,
+ 'body': html2plaintext(message.body),
+ 'website_published' : message.website_published,
+ 'publish' : publish,
+ })
+ return values
+
+ @http.route(['/blogpost/post_discussion'], type='json', auth="public", website=True)
+ def post_discussion(self, blog_post_id=0, **post):
+ cr, uid, context = request.cr, request.uid, request.context
+ publish = request.registry['res.users'].has_group(cr, uid, 'base.group_website_publisher')
+ user = request.registry['res.users'].browse(cr, uid, uid, context=context)
+ id = self._blog_post_message(user, blog_post_id, **post)
+ return self._get_discussion_detail([id], publish, **post)
+
@http.route('/blogpost/new', type='http', auth="public", website=True, multilang=True)
def blog_post_create(self, blog_id, **post):
cr, uid, context = request.cr, request.uid, request.context
create_context = dict(context, mail_create_nosubscribe=True)
- new_blog_post_id = request.registry['blog.post'].create(
- request.cr, request.uid, {
- 'blog_id': blog_id,
- 'name': _("Blog Post Title"),
- 'content': '',
- 'website_published': False,
- }, context=create_context)
- return werkzeug.utils.redirect("/blogpost/%s?enable_editor=1" % new_blog_post_id)
+ new_blog_post_id = request.registry['blog.post'].create(cr, uid, {
+ 'blog_id': blog_id,
+ 'name': _("Blog Post Title"),
+ 'subtitle': _("Subtitle"),
+ 'content': '',
+ 'website_published': False,
+ }, context=create_context)
+ new_blog_post = request.registry['blog.post'].browse(cr, uid, new_blog_post_id, context=context)
+ return werkzeug.utils.redirect("/blog/%s/post/%s?enable_editor=1" % (slug(new_blog_post.blog_id), slug(new_blog_post)))
@http.route('/blogpost/duplicate', type='http', auth="public", website=True)
def blog_post_copy(self, blog_post_id, **post):
@@ -293,5 +325,31 @@ class WebsiteBlog(http.Controller):
"""
cr, uid, context = request.cr, request.uid, request.context
create_context = dict(context, mail_create_nosubscribe=True)
- new_blog_post_id = request.registry['blog.post'].copy(cr, uid, blog_post_id, {}, context=create_context)
- return werkzeug.utils.redirect("/blogpost/%s?enable_editor=1" % new_blog_post_id)
+ nid = request.registry['blog.post'].copy(cr, uid, blog_post_id, {}, context=create_context)
+ new_blog_post = request.registry['blog.post'].browse(cr, uid, nid, context=context)
+ post = request.registry['blog.post'].browse(cr, uid, nid, context)
+ return werkzeug.utils.redirect("/blog/%s/post/%s?enable_editor=1" % (slug(post.blog_id), slug(new_blog_post)))
+
+ @http.route('/blogpost/get_discussion/', type='json', auth="public", website=True)
+ def discussion(self, post_id=0, path=None, count=False, **post):
+ cr, uid, context = request.cr, request.uid, request.context
+ mail_obj = request.registry.get('mail.message')
+ domain = [('res_id', '=', int(post_id)), ('model', '=', 'blog.post'), ('path', '=', path)]
+ #check current user belongs to website publisher group
+ publish = request.registry['res.users'].has_group(cr, uid, 'base.group_website_publisher')
+ if not publish:
+ domain.append(('website_published', '=', True))
+ ids = mail_obj.search(cr, SUPERUSER_ID, domain, count=count)
+ if count:
+ return ids
+ return self._get_discussion_detail(ids, publish, **post)
+
+ @http.route('/blogpost/change_background', type='json', auth="public", website=True)
+ def change_bg(self, post_id=0, image=None, **post):
+ if not post_id:
+ return False
+ return request.registry['blog.post'].write(request.cr, request.uid, [int(post_id)], {'background_image': image}, request.context)
+
+ @http.route('/blog/get_user/', type='json', auth="public", website=True)
+ def get_user(self, **post):
+ return [False if request.session.uid else True]
diff --git a/addons/website_blog/data/website_blog_data.xml b/addons/website_blog/data/website_blog_data.xml
index a5a3aa3d8c9..efc25264633 100644
--- a/addons/website_blog/data/website_blog_data.xml
+++ b/addons/website_blog/data/website_blog_data.xml
@@ -2,7 +2,8 @@
- News
+ Our News
+ Sharing our evolution with passionPresentation of new OpenERP features
diff --git a/addons/website_blog/data/website_blog_demo.xml b/addons/website_blog/data/website_blog_demo.xml
index 913c2ddc9b1..d6827220414 100644
--- a/addons/website_blog/data/website_blog_demo.xml
+++ b/addons/website_blog/data/website_blog_demo.xml
@@ -8,329 +8,199 @@
functional
- technical
-
- website
-
- pos
-
- OpenERP v8 New Features
+ The Future of Emails
+ Ideas behing the OpenERP communication tools.
- OpenERP, Point of Sale, Hardware, Interface, Payment Terminal, Store
- Open source Point of Sale with no installation required that runs online and offline.
+ OpenERP, email
+ The Future of Emails
+ /website_blog/static/src/img/post1.jpg
-
-
-
-
-
-
-
- OpenERP's Point of Sale introduces a super clean
- interface with no installation required that runs
- online and offline on modern hardwares.
-
- It's full integration with the company inventory
- and accounting, gives you real time statistics
- without the hassle of integrating several applications.
-
-
-
-
+
+
+
+ Emails are broken.
+
+ Emails make me waste my time. But I need them.
+ Given the importance that emails have in our lives,
+ it's incredible it's still one of the only software
+ areas that did not evolve in the past 20 years!
+
+ Reading my inbox is the most unproductive task I do
+ on a daily basis. I have to spend one full hour a
+ day to process my emails. All the junk flows in the
+ same inbox; spams, information that doesn't matter,
+ quoted answers of quoted answers, etc. At the end
+ of the hour, only 10 emails actually requested an
+ answer from me. With a good tool, I could have done
+ my job in 10 minutes!
+
-
-
-
-
-
- Linked with Project Management
-
-
Infinitely flexible. Incredibly easy to use.
-
-
-
- OpenERP's collaborative and realtime project
- management helps your team get work done. Keep
- track of everything, from the big picture to the
- minute details, from the customer contract to the
- billing.
-
- Organize projects around your own processes. Work
- on tasks and issues using the kanban view, schedule
- tasks using the gantt chart and control deadlines
- in the calendar view. Every project may have it's
- own stages allowing teams to optimize their job.
-
-
-
-
+
+
+ At OpenERP, we build tools to bring productivity to
+ enterprises. As emails and information flows are one of
+ the biggest wastes of time in companies, we have to fix
+ this.
+
+ To disrupt emails, you need more than just another user
+ interface. We need to rethink the whole communication flow.
+
+
The Communication Mechanism of OpenERP
+
+ Here are the ideas behing the OpenERP communication tools:
+
+
+
+ Get Things Done: your inbox is a
+ todo list. You should be able to process (not only
+ read) the inbox and easily mark messages for future
+ actions. Every inbox should be empty after having
+ been processed; no more overload of information.
+
+
+ Keep control of what you want to receive or don't want
+ to receive. People should never receive spam. You
+ should follow/unfollow any kind of information in one
+ click.
+
+ Productivity is key: our smart user
+ interface does not require you to click on every mail
+ to read a thread. Reading a full thread, replying,
+ attaching documents is super fast.
+
+
+ A mix of push & pull: Today, people
+ are victims of what others decide to push to them.
+ OpenERP differentiates:
+
+
+ Messages "for information":
+ you can pull them when you need some specific
+ information; they are not required to be read
+ every day.You receive only what you decided
+ to follow.This accounts for 90% of your daily
+ emails.Use the "Inbox" menu for these.
+
+ Messages "for action": they
+ require your immediate attention and you need
+ to process them all. This accounts for 10%
+ of your daily emails. Use the "To: me" menu
+ for these.
+
+
+
+ Focus on the Content: Everything is
+ stripped to emphasize on the real message. No more
+ welcome introductions, greetings, signatures and legal
+ notes.We standardize the layout of each message.
+ (signatures are on the profile of a contact, not in
+ every message)
+
+ Folders and mailing lists are great tools but too
+ complex in traditional email clients. In OpenERP, a
+ group of contacts that share a discussion can be
+ created with one click. Every group should have it's
+ own email address.
+
+
-
-
-
-
-
Work with the hardware you already have...
-
-
-
-
-
-
- No installation required
-
-
- OpenERP's Point of Sale introduces a super clean
- interface with no installation required that runs
- online and offline on modern hardware. Laptops,
- tablets, industrial POS, it runs on everything.
-
-
-
]]>
- New Hardware Integration
+ Integrating your CMS and E-Commerce
+ Building your company's website and selling your products online easy.
-
-
-
-
-
-
-
-
-
-
- New Features Launched
-
-
- OpenERP's Point of Sale introduces a super clean
- interface with no installation required that runs
- online and offline on modern hardware. Laptops,
- tablets, industrial POS, it runs on everything.
-
-
-
-
-
-
-
-
-
-
Our Offers
-
-
-
-
-
-
-
Beginner
-
- Starter package
-
-
-
-
$450.00
-
per month
-
-
-
-
-
Battery: 8 hours
-
Screen: 2.5 inch
-
Weight: 1.1 ounces
-
No support
-
-
-
-
-
-
-
-
-
Professional
-
- Enterprise package
-
-
-
-
$590.00
-
per month
-
-
-
-
-
Battery: 12 hours
-
Screen: 2.8 inch
-
Weight: 1.2 ounces
-
Limited support
-
-
-
-
-
-
-
-
-
Expert
-
- The top of the top
-
-
-
-
$890.00
-
per month
-
-
-
-
-
Battery: 20 hours
-
Screen: 2.8 inch
-
Weight: 1.2 ounces
-
Unlimited support
-
-
-
-
-
-
-
-
-
-
-]]>
-
-
-
-
- Touchscreen Point of Sale for 6.1
-
-
- Point of Sale, Hardware, Interface, Payment Terminal, Store
- Point of Sale with no installation required that runs online and offline.
-
-The brand new OpenERP touchscreen point of sale is available with 6.1 which allows you
-to manage your shop sales very easily. It's fully web based so that you don't
-have to install or deploy any software and all the sales shops can be easily
-consolidated. It works in connected and disconnected modes so that you can
-continue to sell even if you lose your internet connection.
-
-
Here's a summary of its main features and benefits:
-
-
100% WEB based
-
available for any touchscreen device (ipod, ipad, any tablet)mobile (with portable devices)
-
no installation required
-
no synchronization needed, completely integrated
-
continue working even when your connection is down or if you close your browser, data won't be lost
-
fully web based with a clean interface smart interface
-
-
You have different options to select your products. You can do it through the
-barcode reader, just browse through the categories you have put in place (ie.
-drinks, snacks, meals, etc.), or text search in case neither of the other
-options work for you. For example, if you need to use the POS for your restaurant,
-your employees can record multiple tickets at the same time without having to wait
-to process one transaction at a time. In addition, you can facilitate payments,
-the application allows multiple payment methods.
-
The POS application is so simple and accessible to use that your shop or
-restaurant will never need any other tool to manage orders. Due to its smart
-and user-friendly interface you don't need any training to learn how to use it.
-Think of it as an out-of-the-box solution to boost your business' productivity.
-
-]]>
-
-
-
-
- Announcing a New Partnership
-
-
- OpenERP, Partnership, News, Accounting
- Our company partners with OpenERP to develop accounting best practices.
-
-
-
-
-
-
-
-
- We are proud to announce a new partnership with
- the company OpenERP. Their open source application suite
- will allow us to reach new markets, specifically in
- the accounting area.
-
- The full integration with the company inventory
- and accounting, will give our customers real time statistics
- without the hassle of integrating several applications.
-
-
-
+
+ /website_blog/static/src/img/post2.jpg
+
+
+
+
+
+
+
+ New Features Launched
+
+
+ To add to an already comprehensive set of OpenERP
+ features, a website content management system (CMS
+ or WMS) has been developed and a beta release is
+ available from today, 31st January 2014.
+
-
-
-
-
-
- OpenERP Project Management
-
-
Infinitely flexible. Incredibly easy to use.
-
-
-
- OpenERP's collaborative and realtime project
- management helps your team get work done. Keep
- track of everything, from the big picture to the
- minute details, from the customer contract to the
- billing.
-
- Organize projects around your own processes. Work
- on tasks and issues using the kanban view, schedule
- tasks using the gantt chart and control deadlines
- in the calendar view. Every project may have it's
- own stages allowing teams to optimize their job.
-
-
-
-
+
+
+ OpenERP claims to be 'the Open Source software that makes
+ building your company's website and selling your products
+ online easy'. So how true is this statement?
+
+ "OpenERP's latest launch will allow a business to go from
+ zero to trading online quicker than ever before,” Stuart
+ Mackintosh, MD of Open Source specialist and OpenERP
+ integration partner, OpusVL, explains. “The investment
+ required to have a fully automated business system is
+ dramatically reduced, enabling the small and medium
+ enterprise to compete at a level of functionality and
+ performance previously reserved for the big IT investors."
+
+
+
+ "Finally, the leading edge is being brought to the masses.
+ It will now be the turn of the big players to catch up to
+ the superior technologies of the SME."
+
+
+
+ "This is another clever and highly disruptive move by
+ OpenERP,which will force other technology providers to
+ take another look at the value they are providing to ensure
+ that their 'solutions' can still compete."
+
+ "OpenERP now competes on many fronts, with no real
+ competition out there to knock them off the top spot.
+ With the launch of their integrated CMS and Ecommerce
+ systems,it only elevates their position as one of the leading
+ lights in the open source revolution. It will be at least 5
+ years before another ERP or CMS provider will be able to
+ compete at this level due to the technology currently
+ employed by most industry providers."
+
+
Adding to industry leading technology
+
+ Like many modern website editors, with OpenERP you can edit
+ content in-line, enabling you to see exactly what you are
+ changing and ensure your changes suit the context.
+
+ However, unlike other web content management systems, it
+ fully integrates into the back-end database. This means
+ that when you edit a product description, image or price,
+ it updates the product database in real time, providing a
+ true self-service window into the business.
+
+ This provides a single source of data for your company and
+ removes the need to create offline synchronisation between
+ website and product database.
+
+ As it comes, there is a default website based on Bootstrap
+ 3, the latest industry standard for rapid development of
+ multi-device websites backed by Twitter, so can be directly
+ integrated with many web tools and works across all devices
+ by default.
+
]]>
-
-
diff --git a/addons/website_blog/doc/blog_blog.rst b/addons/website_blog/doc/blog_blog.rst
new file mode 100644
index 00000000000..8c5f80ab8e4
--- /dev/null
+++ b/addons/website_blog/doc/blog_blog.rst
@@ -0,0 +1,20 @@
+_blog_blog:
+
+blog.blog
+=========
+In ``blog.blog``, added field ``subtitle`` which Indicates the subtitle of blogs.
+ - ``subtitle``: fields.char('Blog Subtitle')
+
+mail.message
+============
+In ``mail.message``, added field ``discussion`` which Indicates the unique identification
+of paragraph on blog post.
+ - ``discussion``: fields.char('Discussion Unique Name')
+
+blog.post
+=========
+Fields
+++++++
+ - ``sub_title`` : contains the subtitle of every blog post.
+ - ``visits`` : Indicates the number of visits on evry blog post.
+ - ``ranking`` : Indicates the ranking on every blog post.
diff --git a/addons/website_blog/doc/changelog.rst b/addons/website_blog/doc/changelog.rst
index 8b6131b5806..42f2fd7e170 100644
--- a/addons/website_blog/doc/changelog.rst
+++ b/addons/website_blog/doc/changelog.rst
@@ -7,3 +7,29 @@ Changelog
----------------
- created ``website_blog`` menu, build on defunct document_page module.
+ - added new feature ``Inline Discussion`` , that will allow a user to comment
+ on every paragraph on blog post
+ - added new feature ``Select to Tweet``, that will alllow a user tweet a selected
+ text from blog to post , directly on twitter.
+
+
+
+WebsiteBlog(controller)
+=======================
+Methods
++++++++
+ - ``blog`` : remove routing related to date.
+ - ``blog_post`` : updated with , suggestion of next post to the user based on
+ cookie and number of views.
+ - ``discussion`` : added method , contains a detail of discussion on every paragraph,
+ if count is true it only return len of ids else return full detail.
+ def discussion(self, post_id=0, discussion=None, count=False, **post)
+ - ``post_discussion`` : added methodt, that allow to post discussion on any paragraph.
+ def post_discussion(self, blog_post_id=0, **post)
+ - ``change_bg`` : added method allow a user to change background image on blog
+ post from front-end.
+ def change_bg(self, post_id=0, image=None, **post)
+ - ``get_user`` : added method , that will return True if user is public else False.
+ def get_user(self, **post):
+ return [False if request.session.uid else True]
+
diff --git a/addons/website_blog/doc/controller.rst b/addons/website_blog/doc/controller.rst
new file mode 100644
index 00000000000..727368aab8c
--- /dev/null
+++ b/addons/website_blog/doc/controller.rst
@@ -0,0 +1,21 @@
+.. _controller:
+
+WebsiteBlog(controller)
+=======================
+Methods
++++++++
+ - ``blog`` : remove routing related to date.
+ - ``blog_post`` : updated with , suggestion of next post to the user based on
+ cookie and number of views.
+ - ``discussion`` : added method , contains a detail of discussion on every paragraph,
+ if count is true it only return len of ids else return full detail.
+ def discussion(self, post_id=0, discussion=None, count=False, **post)
+ - ``post_discussion`` : added methodt, that allow to post discussion on any paragraph.
+ def post_discussion(self, blog_post_id=0, **post)
+ - ``change_bg`` : added method allow a user to change background image on blog
+ post from front-end.
+ def change_bg(self, post_id=0, image=None, **post)
+ - ``get_user`` : added method , that will return True if user is public else False.
+ def get_user(self, **post):
+ return [False if request.session.uid else True]
+
diff --git a/addons/website_blog/models/__init__.py b/addons/website_blog/models/__init__.py
index fb5368a71c5..083e1ef6660 100644
--- a/addons/website_blog/models/__init__.py
+++ b/addons/website_blog/models/__init__.py
@@ -1 +1,2 @@
+import mail_message
import website_blog
diff --git a/addons/website_blog/models/mail_message.py b/addons/website_blog/models/mail_message.py
new file mode 100644
index 00000000000..e1b60f6ad20
--- /dev/null
+++ b/addons/website_blog/models/mail_message.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+
+from openerp.osv import osv, fields
+
+
+class MailMessage(osv.Model):
+ _inherit = 'mail.message'
+
+ _columns = {
+ 'path': fields.char(
+ 'Discussion Path', select=1,
+ help='Used to display messages in a paragraph-based chatter using a unique path;'),
+ }
diff --git a/addons/website_blog/models/website_blog.py b/addons/website_blog/models/website_blog.py
index e132e4c5e15..031856fff10 100644
--- a/addons/website_blog/models/website_blog.py
+++ b/addons/website_blog/models/website_blog.py
@@ -1,32 +1,15 @@
# -*- coding: utf-8 -*-
-##############################################################################
-#
-# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-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 datetime import datetime
+import difflib
+import lxml
+import random
from openerp import tools
from openerp import SUPERUSER_ID
from openerp.osv import osv, fields
from openerp.tools.translate import _
-import difflib
-
class Blog(osv.Model):
_name = 'blog.blog'
@@ -35,12 +18,9 @@ class Blog(osv.Model):
_order = 'name'
_columns = {
- 'name': fields.char('Name', required=True),
+ 'name': fields.char('Blog Name', required=True),
+ 'subtitle': fields.char('Blog Subtitle'),
'description': fields.text('Description'),
- 'blog_post_ids': fields.one2many(
- 'blog.post', 'blog_id',
- 'Blogs',
- ),
}
@@ -52,9 +32,6 @@ class BlogTag(osv.Model):
_columns = {
'name': fields.char('Name', required=True),
- 'blog_post_ids': fields.many2many(
- 'blog.post', string='Posts',
- ),
}
@@ -62,37 +39,20 @@ class BlogPost(osv.Model):
_name = "blog.post"
_description = "Blog Post"
_inherit = ['mail.thread', 'website.seo.metadata']
- _order = 'write_date DESC'
- # maximum number of characters to display in summary
- _shorten_max_char = 250
+ _order = 'id DESC'
- def get_shortened_content(self, cr, uid, ids, name, arg, context=None):
+ def _compute_ranking(self, cr, uid, ids, name, arg, context=None):
res = {}
- for page in self.browse(cr, uid, ids, context=context):
- try:
- body_short = tools.html_email_clean(
- page.content,
- remove=True,
- shorten=True,
- max_length=self._shorten_max_char,
- expand_options={
- 'oe_expand_container_tag': 'div',
- 'oe_expand_container_class': 'oe_mail_expand text-center',
- 'oe_expand_container_content': '',
- 'oe_expand_a_href': '/blogpost/%d' % page.id,
- 'oe_expand_a_class': 'oe_mail_expand btn btn-info',
- 'oe_expand_separator_node': 'br',
- },
- protect_sections=True,
- )
- except Exception:
- body_short = False
- res[page.id] = body_short
+ for blog_post in self.browse(cr, uid, ids, context=context):
+ age = datetime.now() - datetime.strptime(blog_post.create_date, tools.DEFAULT_SERVER_DATETIME_FORMAT)
+ res[blog_post.id] = blog_post.visits * (0.5+random.random()) / max(3, age.days)
return res
_columns = {
'name': fields.char('Title', required=True, translate=True),
- 'content_image': fields.binary('Background Image'),
+ 'subtitle': fields.char('Sub Title', translate=True),
+ 'author_id': fields.many2one('res.partner', 'Author'),
+ 'background_image': fields.binary('Background Image'),
'blog_id': fields.many2one(
'blog.blog', 'Blog',
required=True, ondelete='cascade',
@@ -101,32 +61,22 @@ class BlogPost(osv.Model):
'blog.tag', string='Tags',
),
'content': fields.html('Content', translate=True),
- 'shortened_content': fields.function(
- get_shortened_content,
- type='html',
- string='Shortened Content',
- help="Shortened content of the page that serves as a summary"
- ),
# website control
'website_published': fields.boolean(
'Publish', help="Publish on the website"
),
- 'website_published_datetime': fields.datetime(
- 'Publish Date'
- ),
- # TDE TODO FIXME: when website_mail/mail_thread.py inheritance work -> this field won't be necessary
'website_message_ids': fields.one2many(
'mail.message', 'res_id',
domain=lambda self: [
- '&', ('model', '=', self._name), ('type', '=', 'comment')
+ '&', '&', ('model', '=', self._name), ('type', '=', 'comment'), ('path', '=', False)
],
string='Website Messages',
help="Website communication history",
),
- # technical stuff: history, menu (to keep ?)
'history_ids': fields.one2many(
'blog.post.history', 'post_id',
- 'History', help='Last post modifications'
+ 'History', help='Last post modifications',
+ deprecated='This field will be removed for OpenERP v9.'
),
# creation / update stuff
'create_date': fields.datetime(
@@ -145,11 +95,71 @@ class BlogPost(osv.Model):
'res.users', 'Last Contributor',
select=True, readonly=True,
),
+ 'visits': fields.integer('No of Views'),
+ 'ranking': fields.function(_compute_ranking, string='Ranking', type='float'),
}
+
_defaults = {
- 'website_published': False
+ 'name': _('Blog Post Title'),
+ 'subtitle': _('Subtitle'),
+ 'author_id': lambda self, cr, uid, ctx=None: self.pool['res.users'].browse(cr, uid, uid, context=ctx).partner_id.id,
}
+ def html_tag_nodes(self, html, attribute=None, tags=None, context=None):
+ """ Processing of html content to tag paragraphs and set them an unique
+ ID.
+ :return result: (html, mappin), where html is the updated html with ID
+ and mapping is a list of (old_ID, new_ID), where old_ID
+ is None is the paragraph is a new one. """
+ mapping = []
+ if not html:
+ return html, mapping
+ if tags is None:
+ tags = ['p']
+ if attribute is None:
+ attribute = 'data-unique-id'
+ counter = 0
+
+ # form a tree
+ root = lxml.html.fragment_fromstring(html, create_parent='div')
+ if not len(root) and root.text is None and root.tail is None:
+ return html, mapping
+
+ # check all nodes, replace :
+ # - img src -> check URL
+ # - a href -> check URL
+ for node in root.iter():
+ if not node.tag in tags:
+ continue
+ ancestor_tags = [parent.tag for parent in node.iterancestors()]
+ if ancestor_tags:
+ ancestor_tags.pop()
+ ancestor_tags.append('counter_%s' % counter)
+ new_attribute = '/'.join(reversed(ancestor_tags))
+ old_attribute = node.get(attribute)
+ node.set(attribute, new_attribute)
+ mapping.append((old_attribute, counter))
+ counter += 1
+
+ html = lxml.html.tostring(root, pretty_print=False, method='html')
+ # this is ugly, but lxml/etree tostring want to put everything in a 'div' that breaks the editor -> remove that
+ if html.startswith('
+
+
+
+
+
+
@@ -155,5 +161,6 @@
name="Page History"
res_model="blog.post.history"
src_model="blog.post"/>
+
diff --git a/addons/website_event_track/__openerp__.py b/addons/website_event_track/__openerp__.py
index 338477eeb8e..418e049cec9 100644
--- a/addons/website_event_track/__openerp__.py
+++ b/addons/website_event_track/__openerp__.py
@@ -28,7 +28,8 @@ Adds support for:
],
'qweb': ['static/src/xml/*.xml'],
'demo': [
- 'data/event_demo.xml'
+ 'data/event_demo.xml',
+ 'data/website_event_track_demo.xml'
],
'installable': True,
}
diff --git a/addons/website_event_track/controllers/event.py b/addons/website_event_track/controllers/event.py
index 466e3d37a40..60d4bbd390d 100644
--- a/addons/website_event_track/controllers/event.py
+++ b/addons/website_event_track/controllers/event.py
@@ -23,11 +23,14 @@ import openerp
from openerp.addons.web import http
from openerp.addons.web.http import request
from openerp.addons.website.controllers.main import Website as controllers
+import datetime
import re
import werkzeug.utils
controllers = controllers()
+import pytz
+from pytz import timezone
class website_event(http.Controller):
@http.route(['/event//track/'], type='http', auth="public", website=True, multilang=True)
@@ -37,14 +40,65 @@ class website_event(http.Controller):
values = { 'track': track, 'event': track.event_id, 'main_object': track }
return request.website.render("website_event_track.track_view", values)
+ def _prepare_calendar(self, event, event_track_ids):
+ local_tz = pytz.timezone(event.timezone_of_event or 'UTC')
+ locations = {} # { location: [track, start_date, end_date, rowspan]}
+ dates = [] # [ (date, {}) ]
+ for track in event_track_ids:
+ locations.setdefault(track.location_id or False, [])
+
+ forcetr = True
+ for track in event_track_ids:
+ start_date = (datetime.datetime.strptime(track.date, '%Y-%m-%d %H:%M:%S')).replace(tzinfo=pytz.utc).astimezone(local_tz)
+ end_date = start_date + datetime.timedelta(hours = (track.duration or 30))
+ location = track.location_id or False
+ locations.setdefault(location, [])
+
+ # New TR, align all events
+ if forcetr or (start_date>dates[-1][0]) or not location:
+ dates.append((start_date, {}, bool(location)))
+ for loc in locations.keys():
+ if locations[loc] and (locations[loc][-1][2] > start_date):
+ locations[loc][-1][3] += 1
+ elif not locations[loc] or locations[loc][-1][2] < start_date:
+ locations[loc].append([False, locations[loc] and locations[loc][-1][2] or dates[0][0], start_date, 1])
+ dates[-1][1][loc] = locations[loc][-1]
+ forcetr = not bool(location)
+
+ # Add event
+ if locations[location] and locations[location][-1][1] > start_date:
+ locations[location][-1][3] -= 1
+ locations[location].append([track, start_date, end_date, 1])
+ dates[-1][1][location] = locations[location][-1]
+ return {
+ 'locations': locations,
+ 'dates': dates
+ }
+
+
# TODO: not implemented
@http.route(['/event//agenda'], type='http', auth="public", website=True, multilang=True)
def event_agenda(self, event, tag=None, **post):
- values = {
+ comp = lambda x: (x.date, bool(x.location_id))
+ event.track_ids.sort(lambda x,y: cmp(comp(x), comp(y)))
+
+ days = {}
+ days_nbr = {}
+ for track in event.track_ids:
+ if not track.date: continue
+ days.setdefault(track.date[:10], [])
+ days[track.date[:10]].append(track)
+
+ for d in days:
+ days_nbr[d] = len(days[d])
+ days[d] = self._prepare_calendar(event, days[d])
+
+ return request.website.render("website_event_track.agenda", {
'event': event,
- 'main_object': event,
- }
- return request.website.render("website_event_track.agenda", values)
+ 'days': days,
+ 'days_nbr': days_nbr,
+ 'tag': tag
+ })
@http.route([
'/event//track',
diff --git a/addons/website_event_track/data/event_demo.xml b/addons/website_event_track/data/event_demo.xml
index 5bdd302649b..784d962e471 100644
--- a/addons/website_event_track/data/event_demo.xml
+++ b/addons/website_event_track/data/event_demo.xml
@@ -24,20 +24,6 @@
Round Table
-
-
- Room 1
-
-
- Room 2
-
-
- Room 3
-
-
- Room 4
-
-
@@ -83,68 +69,6 @@
-
-
- A Better Future With OpenERP eCommerce
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Apart from being an enterprise management software, OpenERP is
- used nowadays for a great variety of enterprise frontends.
-
- The talk illustrates the impact of OpenERP in areas such as the
- company website, online events management, eCommerce, online
- recruitments, social media marketing, etc.
-
-
-
-
-
-
-
-
-
- How To Drive Sales With OpenERP CRM
-
-
-
- 1
-
-
-
-
-
-
-
-
-
-
-
- Apart from being an enterprise management software, OpenERP is
- used nowadays for a great variety of enterprise frontends.
-
- The talk illustrates the impact of OpenERP in areas such as the
- company website, online events management, eCommerce, online
- recruitments, social media marketing, etc.
-
-
-
-
-
-
-
-
http://facebook.com/openerp
@@ -160,35 +84,5 @@
-
-
- Social Marketing As a Source of Leads
-
-
-
- 0
-
-
-
-
-
-
-
-
-
-
- Apart from being an enterprise management software, OpenERP is
- used nowadays for a great variety of enterprise frontends.
-
- The talk illustrates the impact of OpenERP in areas such as the
- company website, online events management, eCommerce, online
- recruitments, social media marketing, etc.
-
-
-
-
-
-
-
diff --git a/addons/website_event_track/data/website_event_track_demo.xml b/addons/website_event_track/data/website_event_track_demo.xml
new file mode 100644
index 00000000000..b54d114e127
--- /dev/null
+++ b/addons/website_event_track/data/website_event_track_demo.xml
@@ -0,0 +1,325 @@
+
+
+
+
+ Le Foyer du lac
+
+
+ Theatre
+
+
+ Lauzelle
+
+
+ Foyer Royal
+
+
+ Biereau
+
+
+ Bruyère
+
+
+ How to develop a website module.
+
+
+
+
+
+
+ 3
+
+
+
+
+ How to integrate hardware materials with the OpenERP point of sale.
+
+
+
+
+
+
+
+
+
+ How to develop real time apps, the live chat module explained.
+
+
+
+
+
+
+
+
+
+ How to develop automated tests in the OpenERP web client.
+
+
+
+
+
+
+
+
+
+ The new way to promote your modules in the Apps platform and OpenERP website.
+
+
+
+
+
+
+
+
+
+ Detailed roadmap of accounting new modules and improvements for version 8.
+
+
+
+
+
+
+
+
+
+ A technical explanation of OpenERP as a CMS and a eCommerce platform for version 8.
+
+
+
+
+
+
+
+
+
+ Discover OpenERP CRM: How to optimize your sales, from leads to sales orders.
+
+
+
+
+
+ 2
+
+
+
+
+ How to use OpenERP for your HR process: recruitment, leaves management, appraisals, expenses, etc.
+
+
+
+
+
+
+ 2
+
+
+
+
+ Raising qualitive insights with the survey app
+
+
+
+
+
+ 5
+
+
+
+ Discover OpenERP Point-of-Sale: Your shop ready to use in 30 minutes.
+
+
+
+
+
+
+
+
+
+ Manage your events with OpenERP, the new training modules.
+
+
+
+
+
+
+
+
+
+ Advanced reporting with Google Spreadsheets integration.
+
+
+
+
+
+
+
+
+
+ New Paypal modules (portal, handling, installments).
+
+
+
+
+
+
+
+
+
+ OpenERP Mobile for Notes, Meetings and Messages.
+
+
+
+
+
+
+
+
+
+ OpenERP as your Enterprise Social Network.
+
+
+
+
+
+
+
+
+
+ The Art of Making an OpenERP Demo.
+
+
+
+
+
+
+
+
+
+ How to build your marketing strategy for the purpose of generating leads with OpenERP.
+
+
+
+
+
+ 5
+
+
+
+
+ Advanced lead management with OpenERP: tips and tricks from the fields
+
+
+
+
+
+ 5
+
+
+
+
+ New Certification Program (valid from Oct. 2013).
+
+
+
+
+
+
+
+
+
+ Recruiting high skilled talents with OpenERP HR apps
+
+
+
+
+
+ 7
+
+
+
+ Manage your KPIs (recomended to openERP partners).
+
+
+
+
+
+
+
+
+
+ Key Success factors selling OpenERP.
+
+
+
+
+
+
+
+
+
+ Merge proposals review, code sprint (entire day).
+
+
+
+
+
+
+
+
+
+ Merge proposals review, code sprint (entire afternoon)
+
+
+
+
+
+
+
+
+
+ OpenERP in 2014
+
+
+
+
+
+
+ 3
+
+
+ OpenERP Status & Strategy 2014
+
+
+
+
+
+
+
+
+ The new marketing strategy.
+
+
+
+
+
+
+ 6
+
+
+ Morning break
+
+
+
+
+
+
+
+ Lunch
+
+
+
+
+
+
+
+
diff --git a/addons/website_event_track/models/event.py b/addons/website_event_track/models/event.py
index 359b7df0067..c9d590d7068 100644
--- a/addons/website_event_track/models/event.py
+++ b/addons/website_event_track/models/event.py
@@ -23,6 +23,8 @@ from openerp.osv import fields, osv
from openerp.tools.translate import _
from openerp.addons.website.models.website import slug
+import pytz
+
class event_track_tag(osv.osv):
_name = "event.track.tag"
_order = 'name'
@@ -79,7 +81,7 @@ class event_track(osv.osv):
'stage_id': fields.many2one('event.track.stage', 'Stage'),
'description': fields.html('Track Description', translate=True),
'date': fields.datetime('Track Date'),
- 'duration': fields.integer('Duration'),
+ 'duration': fields.float('Duration', digits=(16,2)),
'location_id': fields.many2one('event.track.location', 'Location'),
'event_id': fields.many2one('event.event', 'Event', required=True),
'color': fields.integer('Color Index'),
@@ -99,10 +101,11 @@ class event_track(osv.osv):
_defaults = {
'user_id': lambda self, cr, uid, ctx: uid,
'website_published': lambda self, cr, uid, ctx: False,
- 'duration': lambda *args: 60,
+ 'duration': lambda *args: 1.5,
'stage_id': _default_stage_id,
'priority': '2'
}
+
def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
stage_obj = self.pool.get('event.track.stage')
result = stage_obj.name_search(cr, uid, '', context=context)
@@ -117,6 +120,10 @@ class event_track(osv.osv):
#
class event_event(osv.osv):
_inherit = "event.event"
+ def _tz_get(self,cr,uid, context=None):
+ # put POSIX 'Etc/*' entries at the end to avoid confusing users - see bug 1086728
+ return [(tz,tz) for tz in sorted(pytz.all_timezones, key=lambda tz: tz if not tz.startswith('Etc/') else '_')]
+
def _get_tracks_tag_ids(self, cr, uid, ids, field_names, arg=None, context=None):
res = dict.fromkeys(ids, [])
for event in self.browse(cr, uid, ids, context=context):
@@ -134,11 +141,13 @@ class event_event(osv.osv):
'show_blog': fields.boolean('News'),
'tracks_tag_ids': fields.function(_get_tracks_tag_ids, type='one2many', relation='event.track.tag', string='Tags of Tracks'),
'allowed_track_tag_ids': fields.many2many('event.track.tag', string='Accepted Tags', help="List of available tags for track proposals."),
+ 'timezone_of_event': fields.selection(_tz_get, 'Event Timezone', size=64),
}
_defaults = {
'show_track_proposal': False,
'show_tracks': False,
'show_blog': False,
+ 'timezone_of_event':lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).tz,
}
def _get_new_menu_pages(self, cr, uid, event, context=None):
context = context or {}
diff --git a/addons/website_event_track/static/src/css/website_event_track.css b/addons/website_event_track/static/src/css/website_event_track.css
index 7862175ec27..f6c01f8f19e 100644
--- a/addons/website_event_track/static/src/css/website_event_track.css
+++ b/addons/website_event_track/static/src/css/website_event_track.css
@@ -52,3 +52,43 @@
background-image: -ms-linear-gradient(top, #C2792A, #DB9141);
background-image: -o-linear-gradient(top, #C2792A, #DB9141);
}
+.event_color_0 {
+ background-color: white;
+ color: #5a5a5a;
+}
+.event_color_1 {
+ background-color: #cccccc;
+ color: #424242;
+}
+.event_color_2 {
+ background-color: #ffc7c7;
+ color: #7a3737;
+}
+.event_color_3 {
+ background-color: #fff1c7;
+ color: #756832;
+}
+.event_color_4 {
+ background-color: #e3ffc7;
+ color: #5d6937;
+}
+.event_color_5 {
+ background-color: #c7ffd5;
+ color: #1a7759;
+}
+.event_color_6 {
+ background-color: #c7ffff;
+ color: #1a5d83;
+}
+.event_color_7 {
+ background-color: #c7d5ff;
+ color: #3b3e75;
+}
+.event_color_8 {
+ background-color: #e3c7ff;
+ color: #4c3668;
+}
+.event_color_9 {
+ background-color: #ffc7f1;
+ color: #6d2c70;
+}
diff --git a/addons/website_event_track/static/src/js/website_event_track.js b/addons/website_event_track/static/src/js/website_event_track.js
new file mode 100644
index 00000000000..8093b9e7bca
--- /dev/null
+++ b/addons/website_event_track/static/src/js/website_event_track.js
@@ -0,0 +1,24 @@
+$(document).ready(function() {
+
+ jQuery.expr[":"].Contains = jQuery.expr.createPseudo(function(arg) {
+ return function( elem ) {
+ return jQuery(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0;
+ };
+ });
+
+ $("#event_track_search").bind('keyup', function(e){
+ var change_text = $(this).val();
+ $('.event_track').removeClass('invisible');
+
+ $("#search_summary").removeClass('invisible');
+ if (change_text) {
+ $("#search_number").text($(".event_track:Contains("+change_text+")").length);
+ $(".event_track:not(:Contains("+change_text+"))").addClass('invisible');
+ } else {
+ $("#search_number").text(30);
+ }
+
+ event.preventDefault();
+ });
+
+});
diff --git a/addons/website_event_track/views/event_backend.xml b/addons/website_event_track/views/event_backend.xml
index ad262b6122e..c028b8b47d4 100644
--- a/addons/website_event_track/views/event_backend.xml
+++ b/addons/website_event_track/views/event_backend.xml
@@ -162,6 +162,9 @@
+
+
+
@@ -226,7 +229,7 @@
Regular Talks. These are standard talks with slides,
- alocated in slots of 30 minutes.
+ alocated in slots of 60 minutes.
- Lightning Talks. These are 10 minutes talks on many
+ Lightning Talks. These are 30 minutes talks on many
different topics. Most topics are accepted in lightning talks.
diff --git a/addons/website_gengo/controllers/main.py b/addons/website_gengo/controllers/main.py
index b1776f9974a..f80c7cbb34b 100644
--- a/addons/website_gengo/controllers/main.py
+++ b/addons/website_gengo/controllers/main.py
@@ -4,9 +4,6 @@ import openerp
from openerp.addons.web import http
from openerp.addons.web.http import request
import time
-import json
-from openerp.tools.translate import _
-
GENGO_DEFAULT_LIMIT = 20
@@ -40,21 +37,3 @@ class website_gengo(http.Controller):
def post_gengo_jobs(self):
request.registry['base.gengo.translations']._sync_request(request.cr, request.uid, limit=GENGO_DEFAULT_LIMIT, context=request.context)
return True
-
- @http.route('/website/gengo_callback/', type='http', auth='none')
- def gengo_callback(self,term,**post):
- if post and post.get('job'):
- translation_pool = request.registry['ir.translation']
- base_gengo_pool = request.registry['base.gengo.translations']
- job, vals = json.loads(post['job']), {}
- if job.get('status') == 'approved':
- vals.update({'state': 'translated', 'value': job.get('body_tgt')})
- flag, gengo = base_gengo_pool.gengo_authentication(request.cr, openerp.SUPERUSER_ID, context=request.context)
- job_comment = gengo.getTranslationJobComments(id=job.get('job_id'))
- if job_comment['opstat']=='ok':
- gengo_comments=""
- for comment in job_comment['response']['thread']:
- gengo_comments += _('%s\n-- Commented on %s by %s.\n\n') % (comment['body'], time.ctime(comment['ctime']), comment['author'])
- vals.update({'gengo_comment': gengo_comments})
- if vals:
- translation_pool.write(request.cr, openerp.SUPERUSER_ID, term.id, vals)
diff --git a/addons/website_gengo/static/src/js/website_gengo.js b/addons/website_gengo/static/src/js/website_gengo.js
index 75c82f83a46..a1755ed1290 100644
--- a/addons/website_gengo/static/src/js/website_gengo.js
+++ b/addons/website_gengo/static/src/js/website_gengo.js
@@ -56,7 +56,7 @@
new_content:self.getInitialContent(this),
translation_id: data.oeTranslationId || null,
gengo_translation: gengo_service_level,
- gengo_comment:"Original page:" + document.URL
+ gengo_comment:"\nOriginal Page: " + document.URL
});
});
openerp.jsonRpc('/website/set_translations', 'call', {
diff --git a/addons/website_partner/website_partner_demo.xml b/addons/website_partner/website_partner_demo.xml
index bedd472cc2a..8909001b790 100644
--- a/addons/website_partner/website_partner_demo.xml
+++ b/addons/website_partner/website_partner_demo.xml
@@ -2,6 +2,13 @@
+
+ True
+
+
+ True
+
+
True
diff --git a/addons/website_quote/security/ir.model.access.csv b/addons/website_quote/security/ir.model.access.csv
index 1dbe5afbe34..88137036caa 100644
--- a/addons/website_quote/security/ir.model.access.csv
+++ b/addons/website_quote/security/ir.model.access.csv
@@ -6,3 +6,4 @@ access_sale_quote_line_manager,sale.quote.line,model_sale_quote_line,base.group_
access_sale_quote_option,sale.quote.option,model_sale_quote_option,base.group_sale_salesman,1,0,0,0
access_sale_quote_option_manager,sale.quote.option,model_sale_quote_option,base.group_sale_manager,1,1,1,1
access_sale_order_option,sale.order.option,model_sale_order_option,base.group_sale_salesman,1,1,1,1
+access_sale_order_option_all,sale.order.option,model_sale_order_option,,1,0,0,0
diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py
index bdf1ca7eaf2..374f17dcf06 100644
--- a/addons/website_sale/controllers/main.py
+++ b/addons/website_sale/controllers/main.py
@@ -43,7 +43,7 @@ class CheckoutInfo(object):
result = dict((prefix + field_name, getattr(partner, field_name)) for field_name in self.string_billing_fields if getattr(partner, field_name))
result[prefix + 'state_id'] = partner.state_id and partner.state_id.id or ''
result[prefix + 'country_id'] = partner.country_id and partner.country_id.id or ''
- result[prefix + 'company'] = partner.parent_id and partner.parent_id.name or ''
+ result[prefix + 'company'] = partner.commercial_partner_id and partner.commercial_partner_id.is_company and partner.commercial_partner_id.name or ''
return result
def from_post(self, post):
@@ -383,6 +383,8 @@ class Ecommerce(http.Controller):
def add_cart(self, product_id, remove=None, **kw):
request.registry['website']._ecommerce_add_product_to_cart(request.cr, request.uid,
product_id=int(product_id),
+ number=float(kw.get('number',1)),
+ set_number=float(kw.get('set_number',-1)),
context=request.context)
return request.redirect("/shop/mycart")
diff --git a/addons/website_sale/static/src/js/website.tour.shop.js b/addons/website_sale/static/src/js/website.tour.shop.js
index adb4a9e556c..6291f2fc54e 100644
--- a/addons/website_sale/static/src/js/website.tour.shop.js
+++ b/addons/website_sale/static/src/js/website.tour.shop.js
@@ -66,39 +66,23 @@
},
{
waitNot: '.product_price .oe_currency_value:containsExact(1.00)',
- element: '#wrap img.img:first',
+ element: '#wrap img.product_detail_img',
placement: 'top',
title: _t("Update image"),
content: _t("Click here to set an image describing your product."),
},
{
- element: 'button.hover-edition-button:visible',
+ element: 'img[alt=ipad]',
placement: 'top',
- title: _t("Update image"),
- content: _t("Click here to set an image describing your product."),
- },
- {
- wait: 500,
- element: '.well a.pull-right',
- placement: 'bottom',
title: _t("Select an Image"),
- content: _t("Let's select an existing image."),
- popover: { fixed: true },
+ content: _t("Let's select an ipad image."),
},
{
- element: 'img[alt=imac]',
- placement: 'bottom',
- title: _t("Select an Image"),
- content: _t("Let's select an imac image."),
- popover: { fixed: true },
- },
- {
- waitNot: 'img[alt=imac]',
+ waitFor: '.media_selected img[alt=ipad]',
element: '.modal-content button.save',
- placement: 'bottom',
- title: _t("Select this Image"),
- content: _t("Click to add the image to the product decsription."),
- popover: { fixed: true },
+ placement: 'top',
+ title: _t("Save this Image"),
+ content: _t("Click on save to add the image to the product decsription."),
},
{
waitNot: '.modal-content:visible',
diff --git a/addons/website_twitter/__init__.py b/addons/website_twitter/__init__.py
new file mode 100644
index 00000000000..396c76fe87a
--- /dev/null
+++ b/addons/website_twitter/__init__.py
@@ -0,0 +1,2 @@
+import models
+import controllers
diff --git a/addons/website_twitter/__openerp__.py b/addons/website_twitter/__openerp__.py
new file mode 100644
index 00000000000..96e17ff9356
--- /dev/null
+++ b/addons/website_twitter/__openerp__.py
@@ -0,0 +1,25 @@
+{
+ 'name': 'Twitter Roller',
+ 'category': 'Website',
+ 'summary': 'Add twitter scroller snippet in website builder',
+ 'version': '1.0',
+ 'description': """
+Display best tweets
+========================
+
+ """,
+ 'author': 'OpenERP SA',
+ 'depends': ['website'],
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'data/twitter_data.xml',
+ 'views/twitter_view.xml',
+ 'views/twitter_snippet.xml'
+ ],
+ 'demo': [],
+ 'qweb': [],
+ 'js': [],
+ 'css': [],
+ 'installable': True,
+ 'application': True,
+}
diff --git a/addons/website_twitter/controllers/__init__.py b/addons/website_twitter/controllers/__init__.py
new file mode 100644
index 00000000000..8ee9bae18d9
--- /dev/null
+++ b/addons/website_twitter/controllers/__init__.py
@@ -0,0 +1 @@
+import main
diff --git a/addons/website_twitter/controllers/main.py b/addons/website_twitter/controllers/main.py
new file mode 100644
index 00000000000..afd636a26c4
--- /dev/null
+++ b/addons/website_twitter/controllers/main.py
@@ -0,0 +1,32 @@
+from openerp.addons.web import http
+from openerp.addons.web.http import request
+from openerp.tools.translate import _
+
+import json
+
+class Twitter(http.Controller):
+ @http.route(['/twitter_reload'], type='json', auth="user", website=True)
+ def twitter_reload(self):
+ return request.website.fetch_favorite_tweets()
+
+ @http.route(['/get_favorites'], type='json', auth="public", website=True)
+ def get_tweets(self, limit=20):
+ key = request.website.twitter_api_key
+ secret = request.website.twitter_api_secret
+ screen_name = request.website.twitter_screen_name
+ if not key or not secret:
+ return {"error": _("Please set the Twitter API Key and Secret in the Website Settings.")}
+ if not screen_name:
+ return {"error": _("Please set a Twitter screen name to load favorites from, "
+ "in the Website Settings (it does not have to be yours)")}
+ twitter_tweets = request.registry['website.twitter.tweet']
+ tweets = twitter_tweets.search_read(
+ request.cr, request.uid,
+ [('website_id','=', request.website.id),
+ ('screen_name','=', screen_name)],
+ ['tweet'], limit=int(limit), order="tweet_id desc", context=request.context)
+ if len(tweets) < 12:
+ return {"error": _("Twitter user @%(username)s has less than 12 favorite tweets. "
+ "Please add more or choose a different screen name.") % \
+ {'username': screen_name}}
+ return [json.loads(tweet['tweet']) for tweet in tweets]
diff --git a/addons/website_twitter/data/twitter_data.xml b/addons/website_twitter/data/twitter_data.xml
new file mode 100644
index 00000000000..b7ca4281504
--- /dev/null
+++ b/addons/website_twitter/data/twitter_data.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ Fetch new Twitter favorites
+ 2
+ hours
+ -1
+
+ website
+ _refresh_favorite_tweets
+ ()
+
+
+
+
diff --git a/addons/website_twitter/models/__init__.py b/addons/website_twitter/models/__init__.py
new file mode 100644
index 00000000000..8c34d4f968e
--- /dev/null
+++ b/addons/website_twitter/models/__init__.py
@@ -0,0 +1,3 @@
+import twitter
+import twitter_config
+
diff --git a/addons/website_twitter/models/twitter.py b/addons/website_twitter/models/twitter.py
new file mode 100644
index 00000000000..7c1c417040b
--- /dev/null
+++ b/addons/website_twitter/models/twitter.py
@@ -0,0 +1,115 @@
+from urllib2 import urlopen, Request, HTTPError
+
+import base64
+import json
+import logging
+import werkzeug
+
+from openerp.osv import fields, osv
+
+API_ENDPOINT = 'https://api.twitter.com'
+API_VERSION = '1.1'
+REQUEST_TOKEN_URL = '%s/oauth2/token' % API_ENDPOINT
+REQUEST_FAVORITE_LIST_URL = '%s/%s/favorites/list.json' % (API_ENDPOINT, API_VERSION)
+URLOPEN_TIMEOUT = 10
+
+_logger = logging.getLogger(__name__)
+
+class TwitterClient(osv.osv):
+ _inherit = "website"
+
+ _columns = {
+ 'twitter_api_key': fields.char('Twitter API key', help="Twitter API Key"),
+ 'twitter_api_secret': fields.char('Twitter API secret', help="Twitter API Secret"),
+ 'twitter_screen_name': fields.char('Get favorites from this screen name'),
+ }
+
+ def _request(self, website, url, params=None):
+ """Send an authenticated request to the Twitter API."""
+ access_token = self._get_access_token(website)
+ if params:
+ params = werkzeug.url_encode(params)
+ url = url + '?' + params
+ try:
+ request = Request(url)
+ request.add_header('Authorization', 'Bearer %s' % access_token)
+ return json.load(urlopen(request, timeout=URLOPEN_TIMEOUT))
+ except HTTPError, e:
+ _logger.debug("Twitter API request failed with code: %r, msg: %r, content: %r",
+ e.code, e.msg, e.fp.read())
+ raise
+
+ def _refresh_favorite_tweets(self, cr, uid, context=None):
+ ''' called by cron job '''
+ website = self.pool['website']
+ ids = self.pool['website'].search(cr, uid, [('twitter_api_key', '!=', False),
+ ('twitter_api_secret', '!=', False),
+ ('twitter_screen_name', '!=', False)],
+ context=context)
+ _logger.debug("Refreshing tweets for website IDs: %r", ids)
+ website.fetch_favorite_tweets(cr, uid, ids, context=context)
+
+ def fetch_favorite_tweets(self, cr, uid, ids, context=None):
+ website_tweets = self.pool['website.twitter.tweet']
+ tweet_ids = []
+ for website in self.browse(cr, uid, ids, context=context):
+ if not all((website.twitter_api_key, website.twitter_api_secret,
+ website.twitter_screen_name)):
+ _logger.debug("Skip fetching favorite tweets for unconfigured website %s",
+ website)
+ continue
+ params = {'screen_name': website.twitter_screen_name}
+ last_tweet = website_tweets.search_read(
+ cr, uid, [('website_id', '=', website.id),
+ ('screen_name', '=', website.twitter_screen_name)],
+ ['tweet_id'],
+ limit=1, order='tweet_id desc', context=context)
+ if last_tweet:
+ params['since_id'] = int(last_tweet[0]['tweet_id'])
+ _logger.debug("Fetching favorite tweets using params %r", params)
+ response = self._request(website, REQUEST_FAVORITE_LIST_URL, params=params)
+ for tweet_dict in response:
+ tweet_id = tweet_dict['id'] # unsigned 64-bit snowflake ID
+ tweet_ids = website_tweets.search(cr, uid, [('tweet_id', '=', tweet_id)])
+ if not tweet_ids:
+ new_tweet = website_tweets.create(
+ cr, uid,
+ {
+ 'website_id': website.id,
+ 'tweet': json.dumps(tweet_dict),
+ 'tweet_id': tweet_id, # stored in NUMERIC PG field
+ 'screen_name': website.twitter_screen_name,
+ },
+ context=context)
+ _logger.debug("Found new favorite: %r, %r", tweet_id, tweet_dict)
+ tweet_ids.append(new_tweet)
+ return tweet_ids
+
+ def _get_access_token(self, website):
+ """Obtain a bearer token."""
+ bearer_token_cred = '%s:%s' % (website.twitter_api_key, website.twitter_api_secret)
+ encoded_cred = base64.b64encode(bearer_token_cred)
+ request = Request(REQUEST_TOKEN_URL)
+ request.add_header('Content-Type',
+ 'application/x-www-form-urlencoded;charset=UTF-8')
+ request.add_header('Authorization',
+ 'Basic %s' % encoded_cred)
+ request.add_data('grant_type=client_credentials')
+ data = json.load(urlopen(request, timeout=URLOPEN_TIMEOUT))
+ access_token = data['access_token']
+ return access_token
+
+class WebsiteTwitterTweet(osv.osv):
+ _name = "website.twitter.tweet"
+ _description = "Twitter Tweets"
+ _columns = {
+ 'website_id': fields.many2one('website', string="Website"),
+ 'screen_name': fields.char("Screen Name"),
+ 'tweet': fields.text('Tweets'),
+
+ # Twitter IDs are 64-bit unsigned ints, so we need to store them in
+ # unlimited precision NUMERIC columns, which can be done with a
+ # float field. Used digits=(0,0) to indicate unlimited.
+ # Using VARCHAR would work too but would have sorting problems.
+ 'tweet_id': fields.float("Tweet ID", digits=(0,0)), # Twitter
+ }
diff --git a/addons/website_twitter/models/twitter_config.py b/addons/website_twitter/models/twitter_config.py
new file mode 100644
index 00000000000..3330a5b0424
--- /dev/null
+++ b/addons/website_twitter/models/twitter_config.py
@@ -0,0 +1,42 @@
+import logging
+
+from openerp.osv import fields, osv
+from openerp.tools.translate import _
+
+_logger = logging.getLogger(__name__)
+
+class twitter_config_settings(osv.osv_memory):
+ _inherit = 'website.config.settings'
+
+ _columns = {
+ 'twitter_api_key': fields.related(
+ 'website_id', 'twitter_api_key', type="char",
+ string='Twitter API Key',
+ help="Twitter API key you can get it from https://apps.twitter.com/app/new"),
+ 'twitter_api_secret': fields.related(
+ 'website_id', 'twitter_api_secret', type="char",
+ string='Twitter API secret',
+ help="Twitter API secret you can get it from https://apps.twitter.com/app/new"),
+ 'twitter_tutorial': fields.dummy(
+ type="boolean", string="Show me how to obtain the Twitter API Key and Secret"),
+ 'twitter_screen_name': fields.related(
+ 'website_id', 'twitter_screen_name',
+ type="char", string='Get favorites from this screen name',
+ help="Screen Name of the Twitter Account from which you want to load favorites."
+ "It does not have to match the API Key/Secret."),
+ }
+
+ def _check_twitter_authorization(self, cr, uid, config_id, context=None):
+ website_obj = self.pool['website']
+ website_config = self.browse(cr, uid, config_id, context=context)
+ try:
+ website_obj.fetch_favorite_tweets(cr, uid, [website_config.website_id.id], context=context)
+ except Exception:
+ _logger.warning('Failed to verify twitter API authorization', exc_info=True)
+ raise osv.except_osv(_('Twitter authorization error!'), _('Please double-check your Twitter API Key and Secret'))
+
+ def create(self, cr, uid, vals, context=None):
+ res_id = super(twitter_config_settings, self).create(cr, uid, vals, context=context)
+ if vals.get('twitter_api_key') and vals.get('twitter_api_secret'):
+ self._check_twitter_authorization(cr, uid, res_id, context=context)
+ return res_id
\ No newline at end of file
diff --git a/addons/website_twitter/security/ir.model.access.csv b/addons/website_twitter/security/ir.model.access.csv
new file mode 100644
index 00000000000..eb88063e9c5
--- /dev/null
+++ b/addons/website_twitter/security/ir.model.access.csv
@@ -0,0 +1,2 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_website_twitter_tweet_public,access of twitter snippet,website_twitter.model_website_twitter_tweet,,1,0,0,0
diff --git a/addons/website_twitter/static/src/css/Makefile b/addons/website_twitter/static/src/css/Makefile
new file mode 100644
index 00000000000..21d0e6cdc39
--- /dev/null
+++ b/addons/website_twitter/static/src/css/Makefile
@@ -0,0 +1,3 @@
+sass:
+ sass -t expanded --compass --unix-newlines --watch website.twitter.sass:website.twitter.css
+
diff --git a/addons/website_twitter/static/src/css/website.twitter.css b/addons/website_twitter/static/src/css/website.twitter.css
new file mode 100644
index 00000000000..b44bf800bd7
--- /dev/null
+++ b/addons/website_twitter/static/src/css/website.twitter.css
@@ -0,0 +1,110 @@
+.wrap-row {
+ position: relative;
+ overflow: hidden;
+ height: 310px;
+}
+.wrap-row .twitter-row {
+ position: absolute;
+ width: 100%;
+ height: auto;
+}
+.wrap-row .twitter-row div.scrollWrapper {
+ position: relative;
+ overflow: hidden;
+ width: 100%;
+ height: 100%;
+}
+.wrap-row .twitter-row div.scrollableArea {
+ position: relative;
+ width: auto;
+ height: 100%;
+}
+.wrap-row .twitter-row div .tweet {
+ border: 1px solid #cccccc;
+ max-width: 500px;
+ width: 500px;
+ font-size: 0.8em;
+ padding-top: 12px;
+ padding-right: 10px;
+ padding-bottom: 12px;
+ padding-left: 10px;
+ float: left;
+ display: block;
+ margin: 6px;
+ max-height: 90px;
+ height: 90px;
+ opacity: 0.6;
+}
+.wrap-row .twitter-row div .tweet h4, .wrap-row .twitter-row div .tweet p {
+ padding: 0;
+ margin: 0;
+}
+.wrap-row .twitter-row div .tweet .left {
+ display: block;
+ float: left;
+ width: 80px;
+}
+.wrap-row .twitter-row div .tweet .left img {
+ width: 65px;
+ height: auto;
+ float: left;
+ display: block;
+ margin: 0px 5px 0px -5px;
+}
+.wrap-row .twitter-row div .tweet .right {
+ display: block;
+ float: left;
+ width: 470px;
+}
+.wrap-row .twitter-row div .tweet .right .top {
+ height: 20px;
+}
+.wrap-row .twitter-row div .tweet h4 {
+ font-size: 14px;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-weight: bold;
+ color: black;
+ float: left;
+ display: block;
+ position: relative;
+ margin-left: 70px;
+ margin-top: -65px;
+}
+.wrap-row .twitter-row div .tweet h4 span {
+ color: #cccccc;
+ font-weight: bold;
+ font-size: 14px;
+}
+.wrap-row .twitter-row div .tweet p {
+ line-height: 1.5em;
+ float: left;
+ position: relative;
+ display: block;
+}
+.wrap-row .twitter-row div .tweet .date {
+ float: right;
+ line-height: 0.5em;
+ margin-top: -60px;
+ margin-right: -10px;
+}
+.wrap-row .twitter-row div .tweet .right .bottom p {
+ margin-top: -65px;
+ margin-left: 70px;
+ font-size: 12px;
+ word-break: break-word;
+}
+.wrap-row .twitter-row div .tweet:hover {
+ -webkit-box-shadow: 0.5px 0.5px 0.5px 1px #428bca;
+ -moz-box-shadow: 0.5px 0.5px 0.5px 1px #428bca;
+ box-shadow: 0.5px 0.5px 0.5px 1px #428bca;
+ cursor: pointer;
+ opacity: 1;
+}
+
+@media screen and (max-width: 580px) {
+ .wrap-row {
+ position: relative;
+ overflow: hidden;
+ height: 100px;
+ }
+}
diff --git a/addons/website_twitter/static/src/css/website.twitter.sass b/addons/website_twitter/static/src/css/website.twitter.sass
new file mode 100644
index 00000000000..9eca59eb6ce
--- /dev/null
+++ b/addons/website_twitter/static/src/css/website.twitter.sass
@@ -0,0 +1,94 @@
+.wrap-row
+ position: relative
+ overflow: hidden
+ height: 310px
+ .twitter-row
+ position: absolute
+ width: 100%
+ height: auto
+ div
+ &.scrollWrapper
+ position: relative
+ overflow: hidden
+ width: 100%
+ height: 100%
+ &.scrollableArea
+ position: relative
+ width: auto
+ height: 100%
+ .tweet
+ border: 1px solid #ccc
+ max-width: 500px
+ width: 500px
+ font-size: 0.8em
+ padding-top: 12px
+ padding-right: 10px
+ padding-bottom: 12px
+ padding-left: 10px
+ float: left
+ display: block
+ margin: 6px
+ max-height: 90px
+ height: 90px
+ opacity: 0.6
+ h4, p
+ padding: 0
+ margin: 0
+ .left
+ display: block
+ float: left
+ width: 80px
+ img
+ width: 65px
+ height: auto
+ float: left
+ display: block
+ margin: 0px 5px 0px -5px
+ .right
+ display: block
+ float: left
+ width: 470px
+ .top
+ height: 20px
+ h4
+ font-size: 14px
+ font-family: "Helvetica Neue",Helvetica,Arial,sans-serif
+ font-weight: bold
+ color: #000
+ float: left
+ display: block
+ position: relative
+ margin-left: 70px
+ margin-top: -65px
+ span
+ color: #ccc
+ font-weight: bold
+ font-size: 14px
+ p
+ line-height: 1.5em
+ float: left
+ position: relative
+ display: block
+ &.date
+ float: right
+ line-height: 0.5em
+ margin-top: -60px
+ margin-right: -10px
+ .right .bottom p
+ margin-top: -65px
+ margin-left: 70px
+ font-size: 12px
+ word-break: break-word
+ &:hover
+ -webkit-box-shadow: 0.5px 0.5px 0.5px 1px #428BCA
+ -moz-box-shadow: 0.5px 0.5px 0.5px 1px #428BCA
+ box-shadow: 0.5px 0.5px 0.5px 1px #428BCA
+ cursor: pointer
+ opacity: 1
+
+
+@media screen and (max-width: 580px)
+ .wrap-row
+ position: relative
+ overflow: hidden
+ height: 100px
diff --git a/addons/website_twitter/static/src/img/api_key.png b/addons/website_twitter/static/src/img/api_key.png
new file mode 100644
index 00000000000..8f53f9649fa
Binary files /dev/null and b/addons/website_twitter/static/src/img/api_key.png differ
diff --git a/addons/website_twitter/static/src/img/loadtweet.gif b/addons/website_twitter/static/src/img/loadtweet.gif
new file mode 100644
index 00000000000..f8a0a6179bd
Binary files /dev/null and b/addons/website_twitter/static/src/img/loadtweet.gif differ
diff --git a/addons/website_twitter/static/src/img/twitter_scroll.png b/addons/website_twitter/static/src/img/twitter_scroll.png
new file mode 100644
index 00000000000..b8d602d519a
Binary files /dev/null and b/addons/website_twitter/static/src/img/twitter_scroll.png differ
diff --git a/addons/website_twitter/static/src/js/website.twitter.animation.js b/addons/website_twitter/static/src/js/website.twitter.animation.js
new file mode 100644
index 00000000000..bb19fd5e66a
--- /dev/null
+++ b/addons/website_twitter/static/src/js/website.twitter.animation.js
@@ -0,0 +1,144 @@
+(function () {
+ 'use strict';
+ var website = openerp.website,
+ qweb = openerp.qweb;
+
+ qweb.add_template('/website_twitter/static/src/xml/website.twitter.xml');
+ if (!website.snippet) website.snippet = {};
+ website.snippet.animationRegistry.twitter = website.snippet.Animation.extend({
+ selector: ".twitter",
+ start: function () {
+ var self = this;
+ var timeline = this.$target.find(".twitter_timeline");
+
+ this.$target.on('click', '.twitter_timeline .tweet', function($event) {
+ if ($event.target.tagName.toLowerCase() !== "a") {
+ var url = $($event.currentTarget).data('url');
+ if (url) {
+ window.open(url, '_blank');
+ }
+ else {
+ debugger;
+ }
+ }
+ });
+ $("
Set your Twitter API access below to be able to use the Twitter Scroller Website snippet.
+ You can get your API credentials from https://apps.twitter.com/app/new
+
- on -
- -Read Next
+- Apart from being an enterprise management software, OpenERP is - used nowadays for a great variety of enterprise frontends. -
- The talk illustrates the impact of OpenERP in areas such as the - company website, online events management, eCommerce, online - recruitments, social media marketing, etc. -
-- Apart from being an enterprise management software, OpenERP is - used nowadays for a great variety of enterprise frontends. -
- The talk illustrates the impact of OpenERP in areas such as the - company website, online events management, eCommerce, online - recruitments, social media marketing, etc. -
-- Apart from being an enterprise management software, OpenERP is - used nowadays for a great variety of enterprise frontends. -
- The talk illustrates the impact of OpenERP in areas such as the - company website, online events management, eCommerce, online - recruitments, social media marketing, etc. -
-You can click on cells to highlight your interests.
-- Monday, 3rd June - 23 talks -
-10:00
- Room 5 -
10:30
11:00
12:00
15:00
10:30
11:00
+
+ talks
+
++
Duration
- minutes
+ hours
Location
+
+
+
+
+
+Set your Twitter API access below to be able to use the Twitter Scroller Website snippet.
++ You can get your API credentials from https://apps.twitter.com/app/new +