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.py b/addons/event/event.py index 92b237687d7..7ee99402158 100644 --- a/addons/event/event.py +++ b/addons/event/event.py @@ -158,16 +158,16 @@ class event_event(osv.osv): 'seats_min': fields.integer('Minimum Reserved Seats', oldname='register_min', help="You can for each event define a minimum registration level. If you do not enough registrations you are not able to confirm your event. (put 0 to ignore this rule )", readonly=True, states={'draft': [('readonly', False)]}), 'seats_reserved': fields.function(_get_seats, oldname='register_current', string='Reserved Seats', type='integer', multi='seats_reserved', store={'event.registration': (_get_events_from_registrations, ['state'], 10), - 'event.event': (lambda self, cr, uid, ids, c = {}: ids, ['seats_max'], 20)}), + 'event.event': (lambda self, cr, uid, ids, c = {}: ids, ['seats_max', 'registration_ids'], 20)}), 'seats_available': fields.function(_get_seats, oldname='register_avail', string='Available Seats', type='integer', multi='seats_reserved', store={'event.registration': (_get_events_from_registrations, ['state'], 10), - 'event.event': (lambda self, cr, uid, ids, c = {}: ids, ['seats_max'], 20)}), + 'event.event': (lambda self, cr, uid, ids, c = {}: ids, ['seats_max', 'registration_ids'], 20)}), 'seats_unconfirmed': fields.function(_get_seats, oldname='register_prospect', string='Unconfirmed Seat Reservations', type='integer', multi='seats_reserved', store={'event.registration': (_get_events_from_registrations, ['state'], 10), - 'event.event': (lambda self, cr, uid, ids, c = {}: ids, ['seats_max'], 20)}), + 'event.event': (lambda self, cr, uid, ids, c = {}: ids, ['seats_max', 'registration_ids'], 20)}), 'seats_used': fields.function(_get_seats, oldname='register_attended', string='Number of Participations', type='integer', multi='seats_reserved', store={'event.registration': (_get_events_from_registrations, ['state'], 10), - 'event.event': (lambda self, cr, uid, ids, c = {}: ids, ['seats_max'], 20)}), + 'event.event': (lambda self, cr, uid, ids, c = {}: ids, ['seats_max', 'registration_ids'], 20)}), 'registration_ids': fields.one2many('event.registration', 'event_id', 'Registrations', readonly=False, states={'done': [('readonly', True)]}), 'date_begin': fields.datetime('Start Date', required=True, readonly=True, states={'draft': [('readonly', False)]}), 'date_end': fields.datetime('End Date', required=True, readonly=True, states={'draft': [('readonly', False)]}), @@ -201,7 +201,6 @@ class event_event(osv.osv): } def _check_seats_limit(self, cr, uid, ids, context=None): - print "event _check_seats_limit" for event in self.browse(cr, uid, ids, context=context): if event.seats_max and event.seats_available < 0: return False 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 @@ - +
+ + + +
+
-

Followers

@@ -42,8 +48,12 @@ - - + +
+ + + +
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 @@
+ days
- diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index 1711bddcfce..19df0de92a2 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -247,37 +247,47 @@ class Website(openerp.addons.web.controllers.main.Home): return True @http.route('/website/attach', type='http', auth='user', methods=['POST'], website=True) - def attach(self, func, upload): + def attach(self, func, upload=None, url=None): + Attachments = request.registry['ir.attachment'] - url = message = None - try: - image_data = upload.read() - image = Image.open(cStringIO.StringIO(image_data)) - w, h = image.size - if w*h > 42e6: # Nokia Lumia 1020 photo resolution - raise ValueError( - u"Image size excessive, uploaded images must be smaller " - u"than 42 million pixel") - - Attachments = request.registry['ir.attachment'] + website_url = message = None + if not upload: + website_url = url + name = url.split("/").pop() attachment_id = Attachments.create(request.cr, request.uid, { - 'name': upload.filename, - 'datas': image_data.encode('base64'), - 'datas_fname': upload.filename, + 'name':name, + 'type': 'url', + 'url': url, 'res_model': 'ir.ui.view', }, request.context) + else: + try: + image_data = upload.read() + image = Image.open(cStringIO.StringIO(image_data)) + w, h = image.size + if w*h > 42e6: # Nokia Lumia 1020 photo resolution + raise ValueError( + u"Image size excessive, uploaded images must be smaller " + u"than 42 million pixel") - [attachment] = Attachments.read( - request.cr, request.uid, [attachment_id], ['website_url'], - context=request.context) - url = attachment['website_url'] - except Exception, e: - logger.exception("Failed to upload image to attachment") - message = unicode(e) + attachment_id = Attachments.create(request.cr, request.uid, { + 'name': upload.filename, + 'datas': image_data.encode('base64'), + 'datas_fname': upload.filename, + 'res_model': 'ir.ui.view', + }, request.context) + + [attachment] = Attachments.read( + request.cr, request.uid, [attachment_id], ['website_url'], + context=request.context) + website_url = attachment['website_url'] + except Exception, e: + logger.exception("Failed to upload image to attachment") + message = unicode(e) return """""" % (func, json.dumps(url), json.dumps(message)) + """ % (func, json.dumps(website_url), json.dumps(message)) @http.route(['/website/publish'], type='json', auth="public", website=True) def publish(self, id, object): @@ -288,8 +298,6 @@ class Website(openerp.addons.web.controllers.main.Home): values = {} if 'website_published' in _object._all_columns: values['website_published'] = not obj.website_published - if 'website_published_datetime' in _object._all_columns and values.get('website_published'): - values['website_published_datetime'] = fields.datetime.now() _object.write(request.cr, request.uid, [_id], values, context=request.context) @@ -341,9 +349,10 @@ class Website(openerp.addons.web.controllers.main.Home): id = int(id) ids = Model.search(request.cr, request.uid, - [('id', '=', id)], context=request.context) \ - or Model.search(request.cr, openerp.SUPERUSER_ID, - [('id', '=', id), ('website_published', '=', True)], context=request.context) + [('id', '=', id)], context=request.context) + if not ids and 'website_published' in Model._all_columns: + ids = Model.search(request.cr, openerp.SUPERUSER_ID, + [('id', '=', id), ('website_published', '=', True)], context=request.context) if not ids: return self.placeholder(response) diff --git a/addons/website/models/website.py b/addons/website/models/website.py index a516f5cb96f..a0a6d1fb28b 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -638,7 +638,7 @@ class ir_attachment(osv.osv): # in-document URLs are html-escaped, a straight search will not # find them url = werkzeug.utils.escape(attachment.website_url) - ids = Views.search(cr, uid, [('arch', 'like', url)], context=context) + ids = Views.search(cr, uid, ["|", ('arch', 'like', '"%s"' % url), ('arch', 'like', "'%s'" % url)], context=context) if ids: removal_blocked_by[attachment.id] = Views.read( diff --git a/addons/website/static/src/css/editor.css b/addons/website/static/src/css/editor.css index 0bd20bdcca9..9b708057852 100644 --- a/addons/website/static/src/css/editor.css +++ b/addons/website/static/src/css/editor.css @@ -94,6 +94,10 @@ display: none; } +.cke_editable .css_editable_mode_display { + display: block !important; +} + .oe_structure.oe_empty:empty, [data-oe-type=html]:empty, .oe_structure.oe_empty > .oe_drop_zone.oe_insert:only-child, [data-oe-type=html] > .oe_drop_zone.oe_insert:only-child { background-image: url("/website/static/src/img/edit_here.png") !important; } @@ -180,7 +184,7 @@ ul.oe_menu_editor .disclose { cursor: text !important; } -.modal-dialog.select-image { +.modal-dialog.select-media { width: 80%; } @@ -212,7 +216,7 @@ ul.oe_menu_editor .disclose { .modal .font-icons-icons { font-size: 2em; - max-height: 6em; + max-height: 9em; overflow: auto; } .modal .font-icons-icons .font-icons-icon { @@ -271,6 +275,10 @@ ul.oe_menu_editor .disclose { -webkit-border-bottom-right-radius: 8px; border-bottom-right-radius: 8px; } +.existing-attachments .existing-attachment-cell.media_selected > i, .existing-attachments .existing-attachment-cell.media_selected > img { + border-width: 5px; + border-color: #00f8f8; +} .cke_widget_wrapper { position: static !important; @@ -296,17 +304,34 @@ ul.oe_menu_editor .disclose { background-color: #ffd9dd !important; } -.hover-edition-button { +.hover-edition { + display: inline-block; position: absolute; top: 0; left: 0; - z-index: 1000; + z-index: 1001; +} + +.preview-container { + text-align: center; + line-height: 100px; + height: 100px; +} +.preview-container > * { + max-height: 100px; + line-height: 100px; + margin: 0 auto; + display: inline-block; } .cke_editable .fa { cursor: pointer; } +.img-responsive { + text-align: center; +} + /* ---- MOBILE PREVIEW ---- {{{ */ .oe_mobile_preview.modal .modal-content { height: 660px; diff --git a/addons/website/static/src/css/editor.sass b/addons/website/static/src/css/editor.sass index bd0f4628009..b9db2bc5bbb 100644 --- a/addons/website/static/src/css/editor.sass +++ b/addons/website/static/src/css/editor.sass @@ -77,6 +77,9 @@ .cke_editable .css_editable_mode_hidden display: none +.cke_editable .css_editable_mode_display + display: block !important + .oe_structure.oe_empty:empty, [data-oe-type=html]:empty, .oe_structure.oe_empty > .oe_drop_zone.oe_insert:only-child, [data-oe-type=html] > .oe_drop_zone.oe_insert:only-child background-image: url('/website/static/src/img/edit_here.png') !important @@ -150,7 +153,7 @@ ul.oe_menu_editor +user-select(auto) cursor: text !important -.modal-dialog.select-image +.modal-dialog.select-media width: 80% .modal .existing-attachments @@ -179,7 +182,7 @@ ul.oe_menu_editor .modal .font-icons-icons font-size: 2em - max-height: 6em + max-height: 9em overflow: auto .font-icons-icon @@ -232,6 +235,10 @@ $attachment-border-color: #848490 border-top: none border-left: none +border-bottom-right-radius(8px) + &.media_selected + > i, > img + border-width: 5px + border-color: rgb(0, 248, 248) // wrapper positioned relatively for drag&drop widget which is disabled below. // Breaks completely horribly crazy products listing page, so take it out. @@ -259,17 +266,30 @@ $attachment-border-color: #848490 outline: 1px solid red !important background-color: #ffd9dd !important -.hover-edition-button +.hover-edition + display: inline-block position: absolute top: 0 left: 0 - // This z-index is due to .navbar of bootstrap - z-index: 1000 + // This z-index is due to .navbar of bootstrap & jQuery-transfo + z-index: 1001 +.preview-container + text-align: center + line-height: 100px + height: 100px + > * + max-height: 100px + line-height: 100px + margin: 0 auto + display: inline-block // fontawesome .cke_editable .fa cursor: pointer +.img-responsive + text-align: center + // }}} /* ---- MOBILE PREVIEW ---- {{{ */ diff --git a/addons/website/static/src/css/snippets.css b/addons/website/static/src/css/snippets.css index 726855f311b..cbfca8be131 100644 --- a/addons/website/static/src/css/snippets.css +++ b/addons/website/static/src/css/snippets.css @@ -201,6 +201,7 @@ height: 0; position: absolute; background: transparent; + z-index: 1001; -webkit-border-radius: 3px; -moz-border-radius: 3px; -ms-border-radius: 3px; @@ -325,11 +326,13 @@ top: auto; left: 50%; bottom: -6px; + margin-left: -50px; } .oe_overlay .oe_handle.size .oe_handle_button { + position: relative; z-index: 3; text-align: center; - margin-left: -32px; + margin-left: -52px; margin-top: -10px; left: 0px; } @@ -341,15 +344,17 @@ box-shadow: 0 0 5px 3px rgba(255, 255, 255, 0.7); } .oe_overlay .oe_handle.size .size { + position: absolute; width: 64px; cursor: row-resize; top: 9px; + margin-left: 52px; + padding: 0 5px; } .oe_overlay .oe_handle.size .auto_size { - width: 20px; - padding: 0 5px; - top: 1px; - margin-left: 36px; + position: absolute; + width: 100px; + top: 9px; cursor: pointer; } .oe_overlay .oe_handle.readonly { diff --git a/addons/website/static/src/css/snippets.sass b/addons/website/static/src/css/snippets.sass index 9b12e9df246..40467897e26 100644 --- a/addons/website/static/src/css/snippets.sass +++ b/addons/website/static/src/css/snippets.sass @@ -151,6 +151,7 @@ height: 0 position: absolute background: transparent + z-index: 1001 //@include background-image( repeating-linear-gradient(45deg, rgba(255,255,255,.02) ,rgba(255,255,255,.02) 35px, rgba(0,0,0,.02) 35px, rgba(0,0,0,.02) 75px)) +border-radius(3px) @include transition(opacity 100ms linear) @@ -238,10 +239,12 @@ top: auto left: 50% bottom: -6px + margin-left: -50px .oe_handle_button + position: relative z-index: 3 text-align: center - margin-left: -32px + margin-left: -52px margin-top: -10px left: 0px &:hover @@ -249,14 +252,16 @@ color: #fff +box-shadow(0 0 5px 3px rgba(255,255,255,.7)) .size + position: absolute width: 64px cursor: row-resize top: 9px - .auto_size - width: 20px + margin-left: 52px padding: 0 5px - top: 1px - margin-left: 36px + .auto_size + position: absolute + width: 100px + top: 9px cursor: pointer &.readonly cursor: auto !important diff --git a/addons/website/static/src/css/website.css b/addons/website/static/src/css/website.css index 354796a053e..9339b5e03b9 100644 --- a/addons/website/static/src/css/website.css +++ b/addons/website/static/src/css/website.css @@ -1,3 +1,4 @@ +@charset "utf-8"; /* THIS CSS FILE IS FOR WEBSITE THEMING CUSTOMIZATION ONLY * * css for editor buttons, openerp widget included in the website and other @@ -284,10 +285,6 @@ ul.nav-stacked > li > a { background-size: 100%; } -section, .carousel, .parallax, .row, .hr, .blockquote { - min-height: 30px; -} - .carousel, .parallax, .blockquote { overflow: hidden; } @@ -376,17 +373,33 @@ section, .carousel, .parallax, .row, .hr, .blockquote { position: absolute; margin-top: -8px; } -.carousel .carousel-control.left span { - left: 10px; +.carousel .carousel-control.left { + left: -10px; } -.carousel .carousel-control.right span { - right: 10px; +.carousel .carousel-control.left * { + position: absolute; + top: 50%; + z-index: 5; + right: 50%; +} +.carousel .carousel-control.right { + right: -10px; +} +.carousel .carousel-control.right * { + position: absolute; + top: 50%; + z-index: 5; + left: 50%; } .quotecarousel { padding-bottom: 16px; } +.hr { + padding: 4px 0; +} + /* Parallax Theme */ div.carousel .carousel-indicators li { border: 1px solid grey; @@ -527,3 +540,33 @@ span[data-oe-type="monetary"] { width: 400px; margin: 40px auto; } + +div.media_iframe_video { + height: 0; + margin: 0 auto; + text-align: center; + position: relative; + overflow: hidden; + padding-bottom: 66.5%; +} +div.media_iframe_video iframe { + width: 100%; + height: 100%; +} +div.media_iframe_video div { + position: absolute; + width: 100%; + height: 100%; + display: none; +} + +/* Mobile view */ +@media (max-width: 768px) { + img:not(.cke_iframe), .media_iframe_video, span.fa, i.fa { + -webkit-transform: none !important; + -moz-transform: none !important; + -ms-transform: none !important; + -o-transform: none !important; + transform: none !important; + } +} diff --git a/addons/website/static/src/css/website.sass b/addons/website/static/src/css/website.sass index 77da53c2958..e8a7d870623 100644 --- a/addons/website/static/src/css/website.sass +++ b/addons/website/static/src/css/website.sass @@ -234,8 +234,6 @@ ul.nav-stacked > li > a .oe_img_bg background-size: 100% -section, .carousel, .parallax, .row, .hr, .blockquote - min-height: 30px .carousel, .parallax, .blockquote overflow: hidden @@ -308,14 +306,27 @@ section, .carousel, .parallax, .row, .hr, .blockquote top: 50% position: absolute margin-top: -8px - &.left span - left: 10px - &.right span - right: 10px + &.left + left: -10px + * + position: absolute + top: 50% + z-index: 5 + right: 50% + &.right + right: -10px + * + position: absolute + top: 50% + z-index: 5 + left: 50% .quotecarousel padding-bottom: 16px +.hr + padding: 4px 0 + /* Parallax Theme */ div.carousel @@ -434,3 +445,28 @@ span[data-oe-type="monetary"] overflow:hidden text-overflow:ellipsis +div.media_iframe_video + height: 0 + margin: 0 auto + text-align: center + position: relative + overflow: hidden + padding-bottom: 66.5% + iframe + width: 100% + height: 100% + div + position: absolute + width: 100% + height: 100% + display: none + +/* Mobile view */ + +@media (max-width: 768px) + img:not(.cke_iframe), .media_iframe_video, span.fa, i.fa + -webkit-transform: none !important + -moz-transform: none !important + -ms-transform: none !important + -o-transform: none !important + transform: none !important diff --git a/addons/website/static/src/js/jQuery.transfo.js b/addons/website/static/src/js/jQuery.transfo.js new file mode 100644 index 00000000000..725e5a02216 --- /dev/null +++ b/addons/website/static/src/js/jQuery.transfo.js @@ -0,0 +1,433 @@ +/* +Copyright (c) 2014 Christophe Matthieu, + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. +*/ + +(function($){ + 'use strict'; + var rad = Math.PI/180; + + // public methods + var methods = { + init : function(settings) { + return this.each(function() { + var $this = $(this), transfo = $this.data('transfo'); + if (!transfo) { + _init($this, settings); + } else { + _overwriteOptions($this, transfo, settings); + _targetCss($this, transfo); + } + }); + }, + + destroy : function() { + return this.each(function() { + var $this = $(this); + if ($this.data('transfo')) { + _destroy($this); + } + }); + }, + + reset : function() { + return this.each(function() { + var $this = $(this); + if ($this.data('transfo')) { + _reset($this); + } + }); + }, + + toggle : function() { + return this.each(function() { + var $this = $(this); + var transfo = $this.data('transfo'); + if (transfo) { + transfo.settings.hide = !transfo.settings.hide; + _showHide($this, transfo); + } + }); + }, + + hide : function() { + return this.each(function() { + var $this = $(this); + var transfo = $this.data('transfo'); + if (transfo) { + transfo.settings.hide = true; + _showHide($this, transfo); + } + }); + }, + + show : function() { + return this.each(function() { + var $this = $(this); + var transfo = $this.data('transfo'); + if (transfo) { + transfo.settings.hide = false; + _showHide($this, transfo); + } + }); + }, + + settings : function() { + if(this.length > 1) { + this.map(function () { + var $this = $(this); + return $this.data('transfo') && $this.data('transfo').settings; + }); + } + return this.data('transfo') && $this.data('transfo').settings; + }, + center : function() { + if(this.length > 1) { + this.map(function () { + var $this = $(this); + return $this.data('transfo') && $this.data('transfo').$center.offset(); + }); + } + return this.data('transfo') && this.data('transfo').$center.offset(); + } + }; + + $.fn.transfo = function( method ) { + if ( methods[method] ) { + return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 )); + } else if ( typeof method === 'object' || ! method ) { + return methods.init.apply( this, arguments ); + } else { + $.error( 'Method ' + method + ' does not exist on jQuery.transfo' ); + } + return false; + }; + + function _init ($this, settings) { + var transfo = {}; + $this.data('transfo', transfo); + transfo.settings = settings; + + // generate all the controls markup + var css = "box-sizing: border-box; position: absolute; background-color: #fff; border: 1px solid #ccc; width: 8px; height: 8px; margin-left: -4px; margin-top: -4px;"; + transfo.$markup = $('' + + '
' + + '
' + + '' + + '' + + '' + + '' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
'); + 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, - - +
-
- Browse existing images -

Image URL

- + + - -
- -
- - -
- +
+ + + + + +
+
+ +
+
+ + + diff --git a/addons/website/static/src/xml/website.snippets.xml b/addons/website/static/src/xml/website.snippets.xml index c5cf94d4791..b1853f366b0 100644 --- a/addons/website/static/src/xml/website.snippets.xml +++ b/addons/website/static/src/xml/website.snippets.xml @@ -39,8 +39,8 @@
+
Auto Resize
Resize
-
x
diff --git a/addons/website/tests/test_requests.py b/addons/website/tests/test_requests.py index 2a22ae04445..127aea274d5 100644 --- a/addons/website/tests/test_requests.py +++ b/addons/website/tests/test_requests.py @@ -6,6 +6,7 @@ import werkzeug.urls import lxml.html +import openerp from openerp import tools import cases @@ -43,15 +44,23 @@ class CrawlSuite(unittest2.TestSuite): def __init__(self, user=None, password=None): super(CrawlSuite, self).__init__() - self.opener = urllib2.OpenerDirector() - self.opener.add_handler(urllib2.UnknownHandler()) - self.opener.add_handler(urllib2.HTTPHandler()) - self.opener.add_handler(urllib2.HTTPSHandler()) - self.opener.add_handler(urllib2.HTTPCookieProcessor()) - self.opener.add_handler(RedirectHandler()) + registry = openerp.registry(tools.config['db_name']) + try: + # switch registry to test mode, so that requests can be made + registry.enter_test_mode() - self._authenticate(user, password) - self.user = user + self.opener = urllib2.OpenerDirector() + self.opener.add_handler(urllib2.UnknownHandler()) + self.opener.add_handler(urllib2.HTTPHandler()) + self.opener.add_handler(urllib2.HTTPSHandler()) + self.opener.add_handler(urllib2.HTTPCookieProcessor()) + self.opener.add_handler(RedirectHandler()) + + self._authenticate(user, password) + self.user = user + + finally: + registry.leave_test_mode() def _request(self, path): return self.opener.open(urlparse.urlunsplit([ @@ -79,37 +88,45 @@ class CrawlSuite(unittest2.TestSuite): assert auth.getcode() < 400, "Auth failure %d" % auth.getcode() def _wrapped_run(self, result, debug=False): - paths = [URL('/'), URL('/sitemap')] - seen = set(paths) + registry = openerp.registry(tools.config['db_name']) + try: + # switch registry to test mode, so that requests can be made + registry.enter_test_mode() - while paths: - url = paths.pop(0) - r = self._request(url.url) - url.to_case(self.user, r).run(result) + paths = [URL('/'), URL('/sitemap')] + seen = set(paths) - if r.info().gettype() != 'text/html': - continue + while paths: + url = paths.pop(0) + r = self._request(url.url) + url.to_case(self.user, r).run(result) - doc = lxml.html.fromstring(r.read()) - for link in doc.xpath('//a[@href]'): - href = link.get('href') - - # avoid repeats, even for links we won't crawl no need to - # bother splitting them if we've already ignored them - # previously - if href in seen: continue - seen.add(href) - - parts = urlparse.urlsplit(href) - - if parts.netloc or \ - not parts.path.startswith('/') or \ - parts.path == '/web' or\ - parts.path.startswith('/web/') or \ - (parts.scheme and parts.scheme not in ('http', 'https')): + if r.info().gettype() != 'text/html': continue - paths.append(URL(href, url.url)) + doc = lxml.html.fromstring(r.read()) + for link in doc.xpath('//a[@href]'): + href = link.get('href') + + # avoid repeats, even for links we won't crawl no need to + # bother splitting them if we've already ignored them + # previously + if href in seen: continue + seen.add(href) + + parts = urlparse.urlsplit(href) + + if parts.netloc or \ + not parts.path.startswith('/') or \ + parts.path == '/web' or\ + parts.path.startswith('/web/') or \ + (parts.scheme and parts.scheme not in ('http', 'https')): + continue + + paths.append(URL(href, url.url)) + + finally: + registry.leave_test_mode() class URL(object): def __init__(self, url, source=None): diff --git a/addons/website/views/snippets.xml b/addons/website/views/snippets.xml index 3e27ffb227f..8ade1725e4e 100644 --- a/addons/website/views/snippets.xml +++ b/addons/website/views/snippets.xml @@ -39,8 +39,8 @@ - - + + @@ -522,7 +522,7 @@

- + Contact Us Now @@ -942,6 +942,11 @@ data-selector-children=".content">
+
+
+
+
+
  • Change...
  • +
    + + + diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml index 83910f0f825..22544ffe75b 100644 --- a/addons/website/views/website_templates.xml +++ b/addons/website/views/website_templates.xml @@ -282,6 +282,8 @@ + + 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 passion Presentation 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. -

    -

    - Get more information » -

    -
    -
    -
    -
    - ]]> - 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('
    ') and html.endswith('
    '): + html = html[5:-6] + return html, mapping + + def _postproces_content(self, cr, uid, id, content=None, context=None): + if content is None: + content = self.browse(cr, uid, id, context=context).content + if content is False: + return content + content, mapping = self.html_tag_nodes(content, attribute='data-chatter-id', tags=['p'], context=context) + for old_attribute, new_attribute in mapping: + if not old_attribute: + continue + msg_ids = self.pool['mail.message'].search(cr, SUPERUSER_ID, [('path', '=', old_attribute)], context=context) + self.pool['mail.message'].write(cr, SUPERUSER_ID, msg_ids, {'path': new_attribute}, context=context) + return content + def create_history(self, cr, uid, ids, vals, context=None): for i in ids: history = self.pool.get('blog.post.history') @@ -163,12 +173,16 @@ class BlogPost(osv.Model): def create(self, cr, uid, vals, context=None): if context is None: context = {} + if 'content' in vals: + vals['content'] = self._postproces_content(cr, uid, None, vals['content'], context=context) create_context = dict(context, mail_create_nolog=True) post_id = super(BlogPost, self).create(cr, uid, vals, context=create_context) self.create_history(cr, uid, [post_id], vals, context) return post_id def write(self, cr, uid, ids, vals, context=None): + if 'content' in vals: + vals['content'] = self._postproces_content(cr, uid, None, vals['content'], context=context) result = super(BlogPost, self).write(cr, uid, ids, vals, context) self.create_history(cr, uid, ids, vals, context) return result @@ -183,10 +197,6 @@ class BlogPost(osv.Model): }) return super(BlogPost, self).copy(cr, uid, id, default=default, context=context) - def img(self, cr, uid, ids, field='image_small', context=None): - post = self.browse(cr, SUPERUSER_ID, ids[0], context=context) - return "/website/image?model=%s&field=%s&id=%s" % ('res.users', field, post.create_uid.id) - class BlogPostHistory(osv.Model): _name = "blog.post.history" @@ -215,5 +225,3 @@ class BlogPostHistory(osv.Model): raise osv.except_osv(_('Warning!'), _('There are no changes in revisions.')) diff = difflib.HtmlDiff() return diff.make_table(line1, line2, "Revision-%s" % (v1), "Revision-%s" % (v2), context=True) - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/website_blog/static/lib/contentshare.js b/addons/website_blog/static/lib/contentshare.js new file mode 100644 index 00000000000..9bfda158703 --- /dev/null +++ b/addons/website_blog/static/lib/contentshare.js @@ -0,0 +1,63 @@ +(function(){ + $.fn.share = function(options) { + var option = $.extend($.fn.share.defaults,options); + $.extend($.fn.share,{ + init : function(shareable) { + var self = this; + $.fn.share.defaults.shareable = shareable; + $.fn.share.defaults.shareable.on('mouseup',function(){ + self.popOver(); + }); + $.fn.share.defaults.shareable.on('mousedown',function(){ + self.destroy(); + }); + }, + getContent : function() { + var current_url = window.location.href + var selected_text = this.getSelection('string').substring(0,option.maxLength-(current_url.length+option.author_name.length+7)); + var text = encodeURIComponent('\"'+selected_text+'\" '+'--@'+option.author_name+' '+current_url) + return ''; + }, + getSelection : function(share) { + if(window.getSelection){ + return (share=='string')?String(window.getSelection().getRangeAt(0)).replace(/\s{2,}/g, ' '):window.getSelection().getRangeAt(0); + } + else if(document.selection){ + return (share=='string')?document.selection.createRange().text.replace(/\s{2,}/g, ' '):document.selection.createRange(); + } + }, + popOver : function() { + this.destroy(); + if(this.getSelection('string').length < option.minLength) + return; + var data = this.getContent(); + var range = this.getSelection(); + var newNode = document.createElement("mark"); + range.surroundContents(newNode); + $('mark').addClass(option.className); + $('.'+option.className).popover({trigger:'manual', placement: option.placement, html: true + , content:function(){ + return data; + } + }); + $('.'+option.className).popover('show'); + }, + destroy : function(){ + $('.'+option.className).popover('hide'); + $('mark').contents().unwrap(); + $('mark').remove(); + } + }); + $.fn.share.init(this); + }; + + $.fn.share.defaults = { + shareLink : "http://twitter.com/intent/tweet?text=", + minLength : 5, + maxLength : 140, + target : "blank", + className : "share", + placement : "top", + }; + +}()); diff --git a/addons/website_blog/static/src/css/website_blog.css b/addons/website_blog/static/src/css/website_blog.css index f7355ce783d..9097e3af31f 100644 --- a/addons/website_blog/static/src/css/website_blog.css +++ b/addons/website_blog/static/src/css/website_blog.css @@ -9,6 +9,12 @@ display: block; } +.read_width { + max-width: 700px; + margin-left: auto; + margin-right: auto; +} + .blog_content a.oe_mail_expand:after { content: " →"; } @@ -20,3 +26,127 @@ p.post-meta { position: relative; top: -5px; } + +div#blog_angle_down a:hover { + text-decoration: none; +} + +.cover { + -webkit-background-size: cover; + -moz-background-size: cover; + -o-background-size: cover; + background-size: cover; + background-position: center; + background-repeat: no-repeat; + background-color: black; + color: white; + position: relative; +} +.cover .blog_title { + position: absolute; + text-align: center; + top: 20%; + left: 0; + right: 0; +} +.cover .blog_title h1 { + font-weight: bold; +} + +.cover_footer { + min-height: 350px; + height: 65vh; + cursor: pointer; +} + +/*Inline Discussion */ +.discussion { + padding: 5px 10px 10px; + position: absolute; + top: 0; + left: 0; + line-height: 16px; + font-size: 13px; + font-weight: bold; + font-family: sans-serif; + text-align: center; + z-index: 7; +} +.discussion > a { + opacity: 0; + display: block; + overflow: hidden; + width: 20px; + height: 17px; + color: white; + text-decoration: none; + cursor: pointer; + background: #bbbbbb; + -webkit-border-radius: 2px; + -moz-border-radius: 2px; + -ms-border-radius: 2px; + -o-border-radius: 2px; + border-radius: 2px; + -webkit-transition: all 0.5s; + -moz-transition: all 0.5s; + -o-transition: all 0.5s; + transition: all 0.5s; +} +.discussion > a.has-comments { + opacity: 0.6; +} + +.discussion-contain:hover .discussion > a { + opacity: 1; +} + +.discussion > a:after { + border-right: 7px solid transparent; + border-top: 7px solid #bbbbbb; + right: 19px; + top: 22px; + height: 0; + width: 0; + display: block; + content: " "; + position: absolute; + -webkit-transition: all 0.5s; + -moz-transition: all 0.5s; + -o-transition: all 0.5s; + transition: all 0.5s; +} +.discussion:hover > a, .discussion.hovered > a { + opacity: 1; + background: #57ad68; +} +.discussion:hover > a:after, .discussion.hovered > a:after { + border-top-color: #57ad68; +} + +#discussions_wrapper { + position: absolute; + top: 0; + left: 0; +} + +#discussions_overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 8; + display: none; +} + +.discussion .popover-content { + max-height: 250px; + width: 250px; + overflow: auto; + font-weight: normal; +} + +mark + .popover { + cursor: pointer; +} diff --git a/addons/website_blog/static/src/css/website_blog.sass b/addons/website_blog/static/src/css/website_blog.sass index 4691dde83c2..6342c6b99a2 100644 --- a/addons/website_blog/static/src/css/website_blog.sass +++ b/addons/website_blog/static/src/css/website_blog.sass @@ -1,5 +1,4 @@ @charset "utf-8" - @import "compass/css3" .css_website_mail @@ -10,6 +9,11 @@ &:first-of-type display: block +.read_width + max-width: 700px + margin-left: auto + margin-right: auto + .blog_content a.oe_mail_expand:after content: " →" @@ -20,3 +24,113 @@ p.post-meta position: relative top: -5px +div#blog_angle_down + a:hover + text-decoration: none + +.cover + -webkit-background-size: cover + -moz-background-size: cover + -o-background-size: cover + background-size: cover + background-position: center + background-repeat: no-repeat + background-color: #000 + color: #fff + position: relative + .blog_title + position: absolute + text-align: center + top: 20% + left: 0 + right: 0 + h1 + font-weight: bold + +.cover_footer + min-height : 350px + height: 65vh + cursor: pointer + +/*Inline Discussion + +.discussion + padding: 5px 10px 10px + position: absolute + top: 0 + left: 0 + line-height: 16px + font-size: 13px + font-weight: bold + font-family: sans-serif + text-align: center + z-index: 7 + > a + opacity: 0 + display: block + overflow: hidden + width: 20px + height: 17px + color: white + text-decoration: none + cursor: pointer + background: #bbbbbb + -webkit-border-radius: 2px + -moz-border-radius: 2px + -ms-border-radius: 2px + -o-border-radius: 2px + border-radius: 2px + -webkit-transition: all 0.5s + -moz-transition: all 0.5s + -o-transition: all 0.5s + transition: all 0.5s + &.has-comments + opacity: .6 + +.discussion-contain:hover .discussion > a + opacity: 1 + +.discussion + > a:after + border-right: 7px solid transparent + border-top: 7px solid #bbbbbb + right: 19px + top: 22px + height: 0 + width: 0 + display: block + content: " " + position: absolute + -webkit-transition: all 0.5s + -moz-transition: all 0.5s + -o-transition: all 0.5s + transition: all 0.5s + &:hover > a, &.hovered > a + opacity: 1 + background: #57AD68 + &:hover > a:after, &.hovered > a:after + border-top-color: #57AD68 + +#discussions_wrapper + position: absolute + top: 0 + left: 0 + +#discussions_overlay + position: fixed + top: 0 + left: 0 + right: 0 + bottom: 0 + background: rgba(0, 0, 0, 0.5) + z-index: 8 + display: none + +.discussion .popover-content + max-height: 250px + width: 250px + overflow: auto + font-weight: normal + +mark + .popover + cursor: pointer diff --git a/addons/website_blog/static/src/img/CMS_WMS_screens.jpg b/addons/website_blog/static/src/img/CMS_WMS_screens.jpg new file mode 100644 index 00000000000..08fc4aafc41 Binary files /dev/null and b/addons/website_blog/static/src/img/CMS_WMS_screens.jpg differ diff --git a/addons/website_blog/static/src/img/anonymous.png b/addons/website_blog/static/src/img/anonymous.png new file mode 100644 index 00000000000..6461bf735f1 Binary files /dev/null and b/addons/website_blog/static/src/img/anonymous.png differ diff --git a/addons/website_blog/static/src/img/mail-sc-00.png b/addons/website_blog/static/src/img/mail-sc-00.png new file mode 100644 index 00000000000..dbb14b4da36 Binary files /dev/null and b/addons/website_blog/static/src/img/mail-sc-00.png differ diff --git a/addons/website_blog/static/src/img/mail-sc-03.png b/addons/website_blog/static/src/img/mail-sc-03.png new file mode 100644 index 00000000000..3011ef129b5 Binary files /dev/null and b/addons/website_blog/static/src/img/mail-sc-03.png differ diff --git a/addons/website_blog/static/src/img/post1.jpg b/addons/website_blog/static/src/img/post1.jpg new file mode 100644 index 00000000000..f3e3aa852a6 Binary files /dev/null and b/addons/website_blog/static/src/img/post1.jpg differ diff --git a/addons/website_blog/static/src/img/post2.jpg b/addons/website_blog/static/src/img/post2.jpg new file mode 100644 index 00000000000..8f6002a40db Binary files /dev/null and b/addons/website_blog/static/src/img/post2.jpg differ diff --git a/addons/website_blog/static/src/js/website_blog.editor.js b/addons/website_blog/static/src/js/website_blog.editor.js index 1c5deef56f7..103b2bee1e7 100644 --- a/addons/website_blog/static/src/js/website_blog.editor.js +++ b/addons/website_blog/static/src/js/website_blog.editor.js @@ -27,7 +27,38 @@ }).then(function (cat_id) { document.location = '/blogpost/new?blog_id=' + cat_id; }); - } + }, }), + edit: function () { + $('.popover').remove(); + this._super(); + var vHeight = $(window).height(); + $('body').on('click','#change_cover',_.bind(this.change_bg,{},vHeight)); + $('body').on('click', '#clear_cover',_.bind(this.clean_bg,{},vHeight)); + }, + save : function() { + var res = this._super(); + if ($('.cover').length) { + openerp.jsonRpc("/blogpost/change_background", 'call', { + 'post_id' : $('#blog_post_name').attr('data-oe-id'), + 'image' : $('.cover').css('background-image').replace(/url\(|\)|"|'/g,''), + }); + } + return res; + }, + clean_bg : function(vHeight) { + $('.js_fullheight').css({"background-image":'none', 'min-height': vHeight}); + }, + change_bg : function(vHeight) { + var self = this; + var editor = new website.editor.ImageDialog(); + editor.on('start', self, function (o) { + o.url = $('.js_fullheight').length ? $('.js_fullheight').css('background-image').replace(/url\(|\)|"|'/g,'') : ''; + }); + editor.on('save', self, function (o) { + $('.js_fullheight').css({"background-image": o.url && o.url !== "" ? 'url(' + o.url + ')' : "", 'min-height': vHeight}) + }); + editor.appendTo('body'); + }, }); })(); diff --git a/addons/website_blog/static/src/js/website_blog.inline.discussion.js b/addons/website_blog/static/src/js/website_blog.inline.discussion.js new file mode 100644 index 00000000000..56851c3939c --- /dev/null +++ b/addons/website_blog/static/src/js/website_blog.inline.discussion.js @@ -0,0 +1,199 @@ +// Inspired from https://github.com/tsi/inlineDisqussions +(function () { + + 'use strict'; + + var website = openerp.website, + qweb = openerp.qweb; + website.add_template_file('/website_blog/static/src/xml/website_blog.inline.discussion.xml'); + website.blog_discussion = openerp.Class.extend({ + init: function(options) { + var self = this ; + self.discus_identifier; + var defaults = { + position: 'right', + post_id: $('#blog_post_name').attr('data-blog-id'), + content : false, + public_user: false, + }; + self.settings = $.extend({}, defaults, options); + self.do_render(self); + }, + do_render: function(data) { + var self = this; + if ($('#discussions_wrapper').length === 0 && self.settings.content.length > 0) { + $('
    ').insertAfter($('#blog_content')); + } + // Attach a discussion to each paragraph. + $(self.settings.content).each(function(i) { + self.discussion_handler(i, $(this)); + }); + // Hide the discussion. + $('html').click(function(event) { + if($(event.target).parents('#discussions_wrapper, .main-discussion-link-wrp').length === 0) { + self.hide_discussion(); + } + if(!$(event.target).hasClass('discussion-link') && !$(event.target).parents('.popover').length){ + if($('.move_discuss').length){ + $('[enable_chatter_discuss=True]').removeClass('move_discuss'); + $('[enable_chatter_discuss=True]').animate({ + 'marginLeft': "+=40%" + }); + $('#discussions_wrapper').animate({ + 'marginLeft': "+=250px" + }); + } + } + }); + }, + prepare_data : function(identifier, comment_count) { + var self = this; + return openerp.jsonRpc("/blogpost/get_discussion/", 'call', { + 'post_id': self.settings.post_id, + 'path': identifier, + 'count': comment_count, //if true only get length of total comment, display on discussion thread. + }) + }, + discussion_handler : function(i, node) { + var self = this; + var identifier = node.attr('data-chatter-id'); + if (identifier) { + self.prepare_data(identifier, true).then( function (data) { + self.prepare_discuss_link(data, identifier, node); + }); + } + }, + prepare_discuss_link : function(data, identifier, node) { + var self = this; + var cls = data > 0 ? 'discussion-link has-comments' : 'discussion-link'; + var a = $('') + .attr('data-discus-identifier', identifier) + .attr('data-discus-position', self.settings.position) + .text(data > 0 ? data : '+') + .attr('data-contentwrapper', '.mycontent') + .wrap('
    ') + .parent() + .appendTo('#discussions_wrapper'); + a.css({ + 'top': node.offset().top, + 'left': self.settings.position == 'right' ? node.outerWidth() + node.offset().left: node.offset().left - a.outerWidth() + }); + // node.attr('data-discus-identifier', identifier) + node.mouseover(function() { + a.addClass("hovered"); + }).mouseout(function() { + a.removeClass("hovered"); + }); + + a.delegate('a.discussion-link', "click", function(e) { + e.preventDefault(); + if(!$('.move_discuss').length){ + $('[enable_chatter_discuss=True]').addClass('move_discuss'); + $('[enable_chatter_discuss=True]').animate({ + 'marginLeft': "-=40%" + }); + $('#discussions_wrapper').animate({ + 'marginLeft': "-=250px" + }); + } + if ($(this).is('.active')) { + e.stopPropagation(); + self.hide_discussion(); + } + else { + self.get_discussion($(this), function(source) {}); + } + }); + }, + get_discussion : function(source, callback) { + var self = this; + var identifier = source.attr('data-discus-identifier'); + self.hide_discussion(); + self.discus_identifier = identifier; + var elt = $('a[data-discus-identifier="'+identifier+'"]'); + elt.append(qweb.render("website.blog_discussion.popover", {'identifier': identifier , 'options': self.settings})); + var comment = ''; + self.prepare_data(identifier,false).then(function(data){ + _.each(data, function(res){ + comment += qweb.render("website.blog_discussion.comment", {'res': res}); + }); + $('.discussion_history').html('
      '+comment+'
    '); + self.create_popover(elt, identifier); + // Add 'active' class. + $('a.discussion-link, a.main-discussion-link').removeClass('active').filter(source).addClass('active'); + elt.popover('hide').filter(source).popover('show'); + callback(source); + }); + }, + create_popover : function(elt, identifier) { + var self = this; + elt.popover({ + placement:'right', + trigger:'manual', + html:true, content:function(){ + return $($(this).data('contentwrapper')).html(); + } + }).parent().delegate(self).on('click','button#comment_post',function(e) { + e.stopImmediatePropagation(); + self.post_discussion(identifier); + }); + }, + validate : function(public_user){ + var comment = $(".popover textarea#inline_comment").val(); + if (public_user){ + var author_name = $('.popover input#author_name').val(); + var author_email = $('.popover input#author_email').val(); + if(!comment || !author_name || !author_email){ + if (!author_name) + $('div#author_name').addClass('has-error'); + else + $('div#author_name').removeClass('has-error'); + if (!author_email) + $('div#author_email').addClass('has-error'); + else + $('div#author_email').removeClass('has-error'); + if(!comment) + $('div#inline_comment').addClass('has-error'); + else + $('div#inline_comment').removeClass('has-error'); + return false + } + } + else if(!comment) { + $('div#inline_comment').addClass('has-error'); + return false + } + $("div#inline_comment").removeClass('has-error'); + $('div#author_name').removeClass('has-error'); + $('div#author_email').removeClass('has-error'); + $(".popover textarea#inline_comment").val(''); + $('.popover input#author_name').val(''); + $('.popover input#author_email').val(''); + return [comment, author_name, author_email] + }, + post_discussion : function(identifier) { + var self = this; + var val = self.validate(self.settings.public_user) + if(!val) return + openerp.jsonRpc("/blogpost/post_discussion", 'call', { + 'blog_post_id': self.settings.post_id, + 'path': self.discus_identifier, + 'comment': val[0], + 'name' : val[1], + 'email': val[2], + }).then(function(res){ + $(".popover ul.media-list").prepend(qweb.render("website.blog_discussion.comment", {'res': res[0]})) + var ele = $('a[data-discus-identifier="'+ self.discus_identifier +'"]'); + ele.text(_.isNaN(parseInt(ele.text())) ? 1 : parseInt(ele.text())+1) + ele.addClass('has-comments'); + }); + }, + hide_discussion : function() { + var self = this; + $('a[data-discus-identifier="'+ self.discus_identifier+'"]').popover('destroy'); + $('a.discussion-link').removeClass('active'); + } + + }); + +})(); diff --git a/addons/website_blog/static/src/js/website_blog.js b/addons/website_blog/static/src/js/website_blog.js new file mode 100644 index 00000000000..3217bf1a04d --- /dev/null +++ b/addons/website_blog/static/src/js/website_blog.js @@ -0,0 +1,40 @@ +$(document).ready(function() { + + function page_transist(event) { + event.preventDefault(); + newLocation = $('.js_next')[0].href; + var top = $('.cover_footer').offset().top; + $('.cover_footer').animate({ + height: $(window).height()+'px' + }, 300); + $('html, body').animate({ + scrollTop: top + }, 300, 'swing', function() { + window.location.href = newLocation; + }); + } + function animate(event) { + event.preventDefault(); + event.stopImmediatePropagation(); + var target = $(this.hash); + $('html, body').stop().animate({ + 'scrollTop': target.offset().top - 32 + }, 500, 'swing', function () { + window.location.hash = 'blog_content'; + }); + } + + var content = $("div[enable_chatter_discuss='True']").find('p[data-chatter-id]'); + if (content) { + openerp.jsonRpc("/blog/get_user/", 'call', {}).then(function(data){ + $('#discussions_wrapper').empty(); + new openerp.website.blog_discussion({'content' : content, 'public_user':data[0]}); + }); + } + + $('.js_fullheight').css('min-height', $(window).height()); + $(".js_tweet").share({'author_name':$('#blog_author').text()}); + $('.cover_footer').on('click',page_transist); + $('a[href^="#blog_content"]').on('click', animate); + +}); diff --git a/addons/website_blog/static/src/xml/website_blog.inline.discussion.xml b/addons/website_blog/static/src/xml/website_blog.inline.discussion.xml new file mode 100644 index 00000000000..8c1cda26265 --- /dev/null +++ b/addons/website_blog/static/src/xml/website_blog.inline.discussion.xml @@ -0,0 +1,38 @@ + + + +
  • +
    + +
    +
    + + +
    +
    +
    + + by + + +
    +
  • +
    + +
    -
    - -