class tip(models.Model): _name = 'web.tip' _description = 'Tips' @api.one @api.depends('user_ids') def _is_consumed(self): self.is_consumed = self.env.user in self.user_ids title = fields.Char('Tip title') description = fields.Html('Tip Description', required=True) action_id = fields.Many2one('ir.actions.act_window', string="Action", help="The action that will trigger the tip") model = fields.Char( "Model", help="Model name on which to trigger the tip, e.g. 'res.partner'.") type = fields.Char( "Type", help="Model type, e.g. lead or opportunity for crm.lead") mode = fields.Char("Mode", help="Mode, e.g. kanban, form") trigger_selector = fields.Char( 'Trigger selector', help= 'CSS selectors used to trigger the tip, separated by a comma (ANDed).') highlight_selector = fields.Char( 'Highlight selector', help='CSS selector for the element to highlight') end_selector = fields.Char('End selector', help='CSS selector used to end the tip') end_event = fields.Char('End event', help='Event to end the tip', default='click') placement = fields.Char( 'Placement', help='Popover placement, bottom, top, left or right', default='auto') user_ids = fields.Many2many('res.users', string='Consumed by') is_consumed = fields.Boolean(string='Tip consumed', compute='_is_consumed') @api.multi def consume(self): self.write({'user_ids': [(4, self.env.uid)]})
class ImLivechatChannel(models.Model): _name = 'im_livechat.channel' _inherit = ['im_livechat.channel', 'website.published.mixin'] @api.v7 # TODO : when mixin in new api.v8, change this ! def _website_url(self, cr, uid, ids, field_name, arg, context=None): res = super(ImLivechatChannel, self)._website_url(cr, uid, ids, field_name, arg, context=context) for channel in self.browse(cr, uid, ids, context=context): res[channel.id] = "/livechat/channel/%s" % (slug(channel), ) return res website_description = fields.Html( "Website description", default=False, help="Description of the channel displayed on the website page")
class Channel(models.Model): """ A channel is a container of slides. It has group-based access configuration allowing to configure slide upload and access. Slides can be promoted in channels. """ _name = 'slide.channel' _description = 'Channel for Slides' _inherit = [ 'mail.thread', 'website.seo.metadata', 'website.published.mixin' ] _order = 'sequence, id' _order_by_strategy = { 'most_viewed': 'total_views desc', 'most_voted': 'likes desc', 'latest': 'date_published desc', } name = fields.Char('Name', translate=True, required=True) description = fields.Html('Description', translate=True) sequence = fields.Integer(default=10, help='Display order') category_ids = fields.One2many('slide.category', 'channel_id', string="Categories") slide_ids = fields.One2many('slide.slide', 'channel_id', string="Slides") promote_strategy = fields.Selection([('none', 'No Featured Presentation'), ('latest', 'Latest Published'), ('most_voted', 'Most Voted'), ('most_viewed', 'Most Viewed'), ('custom', 'Featured Presentation')], string="Featuring Policy", default='most_voted', required=True) custom_slide_id = fields.Many2one('slide.slide', string='Slide to Promote') promoted_slide_id = fields.Many2one('slide.slide', string='Featured Slide', compute='_compute_promoted_slide_id', store=True) @api.depends('custom_slide_id', 'promote_strategy', 'slide_ids.likes', 'slide_ids.total_views', "slide_ids.date_published") def _compute_promoted_slide_id(self): for record in self: if record.promote_strategy == 'none': record.promoted_slide_id = False elif record.promote_strategy == 'custom': record.promoted_slide_id = record.custom_slide_id elif record.promote_strategy: slides = self.env['slide.slide'].search( [('website_published', '=', True), ('channel_id', '=', record.id)], limit=1, order=self._order_by_strategy[record.promote_strategy]) record.promoted_slide_id = slides and slides[0] or False nbr_presentations = fields.Integer('Number of Presentations', compute='_count_presentations', store=True) nbr_documents = fields.Integer('Number of Documents', compute='_count_presentations', store=True) nbr_videos = fields.Integer('Number of Videos', compute='_count_presentations', store=True) nbr_infographics = fields.Integer('Number of Infographics', compute='_count_presentations', store=True) total = fields.Integer(compute='_count_presentations', store=True) @api.depends('slide_ids.slide_type', 'slide_ids.website_published') def _count_presentations(self): result = dict.fromkeys(self.ids, dict()) res = self.env['slide.slide'].read_group( [('website_published', '=', True), ('channel_id', 'in', self.ids)], ['channel_id', 'slide_type'], ['channel_id', 'slide_type'], lazy=False) for res_group in res: result[res_group['channel_id'][0]][res_group[ 'slide_type']] = result[res_group['channel_id'][0]].get( res_group['slide_type'], 0) + res_group['__count'] for record in self: record.nbr_presentations = result[record.id].get('presentation', 0) record.nbr_documents = result[record.id].get('document', 0) record.nbr_videos = result[record.id].get('video', 0) record.nbr_infographics = result[record.id].get('infographic', 0) record.total = record.nbr_presentations + record.nbr_documents + record.nbr_videos + record.nbr_infographics publish_template_id = fields.Many2one( 'mail.template', string='Published Template', help="Email template to send slide publication through email", default=lambda self: self.env['ir.model.data'].xmlid_to_res_id( 'website_slides.slide_template_published')) share_template_id = fields.Many2one( 'mail.template', string='Shared Template', help="Email template used when sharing a slide", default=lambda self: self.env['ir.model.data'].xmlid_to_res_id( 'website_slides.slide_template_shared')) visibility = fields.Selection( [('public', 'Public'), ('private', 'Private'), ('partial', 'Show channel but restrict presentations')], default='public', required=True) group_ids = fields.Many2many( 'res.groups', 'rel_channel_groups', 'channel_id', 'group_id', string='Channel Groups', help="Groups allowed to see presentations in this channel") access_error_msg = fields.Html( 'Error Message', help="Message to display when not accessible due to access rights", default= "<p>This channel is private and its content is restricted to some users.</p>", translate=True) upload_group_ids = fields.Many2many( 'res.groups', 'rel_upload_groups', 'channel_id', 'group_id', string='Upload Groups', help= "Groups allowed to upload presentations in this channel. If void, every user can upload." ) # not stored access fields, depending on each user can_see = fields.Boolean('Can See', compute='_compute_access') can_see_full = fields.Boolean('Full Access', compute='_compute_access') can_upload = fields.Boolean('Can Upload', compute='_compute_access') @api.one @api.depends('visibility', 'group_ids', 'upload_group_ids') def _compute_access(self): self.can_see = self.visibility in [ 'public', 'private' ] or bool(self.group_ids & self.env.user.groups_id) self.can_see_full = self.visibility == 'public' or bool( self.group_ids & self.env.user.groups_id) self.can_upload = self.can_see and (not self.upload_group_ids or bool(self.upload_group_ids & self.env.user.groups_id)) @api.multi @api.depends('name') def _website_url(self, name, arg): res = super(Channel, self)._website_url(name, arg) base_url = self.env['ir.config_parameter'].get_param('web.base.url') res.update({(channel.id, '%s/slides/%s' % (base_url, slug(channel))) for channel in self}) return res @api.onchange('visibility') def change_visibility(self): if self.visibility == 'public': self.group_ids = False
class SaasPortalPlan(models.Model): _name = 'saas_portal.plan' name = fields.Char('Plan', required=True) summary = fields.Char('Summary') template_id = fields.Many2one('saas_portal.database', 'Template', ondelete='restrict') demo = fields.Boolean('Install Demo Data') maximum_allowed_dbs_per_partner = fields.Integer(help='maximum allowed non-trial databases per customer', require=True, default=0) maximum_allowed_trial_dbs_per_partner = fields.Integer(help='maximum allowed trial databases per customer', require=True, default=0) max_users = fields.Char('Initial Max users', default='0') total_storage_limit = fields.Integer('Total storage limit (MB)') block_on_expiration = fields.Boolean('Block clients on expiration', default=False) block_on_storage_exceed = fields.Boolean('Block clients on storage exceed', default=False) def _get_default_lang(self): return self.env.lang def _default_tz(self): return self.env.user.tz lang = fields.Selection(scan_languages(), 'Language', default=_get_default_lang) tz = fields.Selection(_tz_get, 'TimeZone', default=_default_tz) sequence = fields.Integer('Sequence') state = fields.Selection([('draft', 'Draft'), ('confirmed', 'Confirmed')], 'State', compute='_get_state', store=True) expiration = fields.Integer('Expiration (hours)', help='time to delete database. Use for demo') _order = 'sequence' dbname_template = fields.Char('DB Names', help='Used for generating client database domain name. Use %i for numbering. Ignore if you use manually created db names', placeholder='crm-%i.yuancloud.com') server_id = fields.Many2one('saas_portal.server', string='SaaS Server', ondelete='restrict', help='User this saas server or choose random') website_description = fields.Html('Website description') logo = fields.Binary('Logo') @api.one @api.depends('template_id.state') def _get_state(self): if self.template_id.state == 'template': self.state = 'confirmed' else: self.state = 'draft' @api.one def _new_database_vals(self, vals): vals['max_users'] = self.max_users vals['total_storage_limit'] = self.total_storage_limit vals['block_on_expiration'] = self.block_on_expiration vals['block_on_storage_exceed'] = self.block_on_storage_exceed return vals @api.multi def create_new_database(self, **kwargs): return self._create_new_database(**kwargs) @api.multi def _create_new_database(self, dbname=None, client_id=None, partner_id=None, user_id=None, notify_user=False, trial=False, support_team_id=None, async=None): self.ensure_one() server = self.server_id if not server: server = self.env['saas_portal.server'].get_saas_server() server.action_sync_server() if not partner_id and user_id: user = self.env['res.users'].browse(user_id) partner_id = user.partner_id.id if not trial and self.maximum_allowed_dbs_per_partner != 0: db_count = self.env['saas_portal.client'].search_count([('partner_id', '=', partner_id), ('state', '=', 'open'), ('plan_id', '=', self.id), ('trial', '=', False)]) if db_count >= self.maximum_allowed_dbs_per_partner: raise MaximumDBException("Limit of databases for this plan is %(maximum)s reached" % { 'maximum': self.maximum_allowed_dbs_per_partner}) if trial and self.maximum_allowed_trial_dbs_per_partner != 0: trial_db_count = self.env['saas_portal.client'].search_count([('partner_id', '=', partner_id), ('state', '=', 'open'), ('plan_id', '=', self.id), ('trial', '=', True)]) if trial_db_count >= self.maximum_allowed_trial_dbs_per_partner: raise MaximumTrialDBException("Limit of trial databases for this plan is %(maximum)s reached" % { 'maximum': self.maximum_allowed_trial_dbs_per_partner}) vals = {'name': dbname or self.generate_dbname()[0], 'server_id': server.id, 'plan_id': self.id, 'partner_id': partner_id, 'trial': trial, 'support_team_id': support_team_id, } client = None if client_id: vals['client_id'] = client_id client = self.env['saas_portal.client'].search([('client_id', '=', client_id)]) vals = self._new_database_vals(vals)[0] if client: client.write(vals) else: client = self.env['saas_portal.client'].create(vals) client_id = client.client_id scheme = server.request_scheme port = server.request_port if user_id: owner_user = self.env['res.users'].browse(user_id) else: owner_user = self.env.user owner_user_data = { 'user_id': owner_user.id, 'login': owner_user.login, 'name': owner_user.name, 'email': owner_user.email, } trial_expiration_datetime = (datetime.strptime(client.create_date, DEFAULT_SERVER_DATETIME_FORMAT) + timedelta( hours=self.expiration)).strftime(DEFAULT_SERVER_DATETIME_FORMAT) # for trial state = { 'd': client.name, 'e': trial and trial_expiration_datetime or client.create_date, 'r': '%s://%s:%s/web' % (scheme, client.name, port), 'owner_user': owner_user_data, 't': client.trial, 'addons': [addon.technical_name for addon in client.plan_id.app_store_module_ids] } if self.template_id: state.update({'db_template': self.template_id.name}) scope = ['userinfo', 'force_login', 'trial', 'skiptheuse'] url = server._request_server(path='/saas_server/new_database', scheme=scheme, port=port, state=state, client_id=client_id, scope=scope, )[0] res = requests.get(url, verify=(self.server_id.request_scheme == 'https' and self.server_id.verify_ssl)) if res.status_code != 200: # TODO /saas_server/new_database show more details here raise exceptions.Warning('Error %s' % res.status_code) data = simplejson.loads(res.text) params = { 'state': data.get('state'), 'access_token': client.oauth_application_id._get_access_token(user_id, create=True), } url = '{url}?{params}'.format(url=data.get('url'), params=werkzeug.url_encode(params)) # send email if notify_user: template = self.env.ref('saas_portal.email_template_create_saas') client.message_post_with_template(template.id, composition_mode='comment') if trial: client.expiration_datetime = trial_expiration_datetime client.send_params_to_client_db() client.server_id.action_sync_server() return {'url': url, 'id': client.id, 'client_id': client_id}
class Forum(models.Model): _name = 'forum.forum' _description = 'Forum' _inherit = ['mail.thread', 'website.seo.metadata'] def init(self, cr): """ Add forum uuid for user email validation. TDE TODO: move me somewhere else, auto_init ? """ forum_uuids = self.pool['ir.config_parameter'].search(cr, SUPERUSER_ID, [('key', '=', 'website_forum.uuid')]) if not forum_uuids: self.pool['ir.config_parameter'].set_param(cr, SUPERUSER_ID, 'website_forum.uuid', str(uuid.uuid4()), ['base.group_system']) @api.model def _get_default_faq(self): fname = modules.get_module_resource('website_forum', 'data', 'forum_default_faq.html') with open(fname, 'r') as f: return f.read() return False # description and use name = fields.Char('Forum Name', required=True, translate=True) faq = fields.Html('Guidelines', default=_get_default_faq, translate=True) description = fields.Text( 'Description', translate=True, default='This community is for professionals and enthusiasts of our products and services. ' 'Share and discuss the best content and new marketing ideas, ' 'build your professional profile and become a better marketer together.') welcome_message = fields.Html( 'Welcome Message', default = """<section class="bg-info" style="height: 168px;"><div class="container"> <div class="row"> <div class="col-md-12"> <h1 class="text-center" style="text-align: left;">Welcome!</h1> <p class="text-muted text-center" style="text-align: left;">This community is for professionals and enthusiasts of our products and services. Share and discuss the best content and new marketing ideas, build your professional profile and become a better marketer together.</p> </div> <div class="col-md-12"> <a href="#" class="js_close_intro">Hide Intro</a> <a class="btn btn-primary forum_register_url" href="/web/login">Register</a> </div> </div> </div> </section>""") default_order = fields.Selection([ ('create_date desc', 'Newest'), ('write_date desc', 'Last Updated'), ('vote_count desc', 'Most Voted'), ('relevancy desc', 'Relevance'), ('child_count desc', 'Answered')], string='Default Order', required=True, default='write_date desc') relevancy_post_vote = fields.Float('First Relevance Parameter', default=0.8, help="This formula is used in order to sort by relevance. The variable 'votes' represents number of votes for a post, and 'days' is number of days since the post creation") relevancy_time_decay = fields.Float('Second Relevance Parameter', default=1.8) default_post_type = fields.Selection([ ('question', 'Question'), ('discussion', 'Discussion'), ('link', 'Link')], string='Default Post', required=True, default='question') allow_question = fields.Boolean('Questions', help="Users can answer only once per question. Contributors can edit answers and mark the right ones.", default=True) allow_discussion = fields.Boolean('Discussions', default=True) allow_link = fields.Boolean('Links', help="When clicking on the post, it redirects to an external link", default=True) allow_bump = fields.Boolean('Allow Bump', default=True, help='Check this box to display a popup for posts older than 10 days ' 'without any given answer. The popup will offer to share it on social ' 'networks. When shared, a question is bumped at the top of the forum.') allow_share = fields.Boolean('Sharing Options', default=True, help='After posting the user will be proposed to share its question ' 'or answer on social networks, enabling social network propagation ' 'of the forum content.') count_posts_waiting_validation = fields.Integer(string="Number of posts waiting for validation", compute='_compute_count_posts_waiting_validation') count_flagged_posts = fields.Integer(string='Number of flagged posts', compute='_compute_count_flagged_posts') # karma generation karma_gen_question_new = fields.Integer(string='Asking a question', default=2) karma_gen_question_upvote = fields.Integer(string='Question upvoted', default=5) karma_gen_question_downvote = fields.Integer(string='Question downvoted', default=-2) karma_gen_answer_upvote = fields.Integer(string='Answer upvoted', default=10) karma_gen_answer_downvote = fields.Integer(string='Answer downvoted', default=-2) karma_gen_answer_accept = fields.Integer(string='Accepting an answer', default=2) karma_gen_answer_accepted = fields.Integer(string='Answer accepted', default=15) karma_gen_answer_flagged = fields.Integer(string='Answer flagged', default=-100) # karma-based actions karma_ask = fields.Integer(string='Ask questions', default=3) karma_answer = fields.Integer(string='Answer questions', default=3) karma_edit_own = fields.Integer(string='Edit own posts', default=1) karma_edit_all = fields.Integer(string='Edit all posts', default=300) karma_close_own = fields.Integer(string='Close own posts', default=100) karma_close_all = fields.Integer(string='Close all posts', default=500) karma_unlink_own = fields.Integer(string='Delete own posts', default=500) karma_unlink_all = fields.Integer(string='Delete all posts', default=1000) karma_upvote = fields.Integer(string='Upvote', default=5) karma_downvote = fields.Integer(string='Downvote', default=50) karma_answer_accept_own = fields.Integer(string='Accept an answer on own questions', default=20) karma_answer_accept_all = fields.Integer(string='Accept an answer to all questions', default=500) karma_comment_own = fields.Integer(string='Comment own posts', default=1) karma_comment_all = fields.Integer(string='Comment all posts', default=1) karma_comment_convert_own = fields.Integer(string='Convert own answers to comments and vice versa', default=50) karma_comment_convert_all = fields.Integer(string='Convert all answers to comments and vice versa', default=500) karma_comment_unlink_own = fields.Integer(string='Unlink own comments', default=50) karma_comment_unlink_all = fields.Integer(string='Unlink all comments', default=500) karma_retag = fields.Integer(string='Change question tags', default=75) karma_flag = fields.Integer(string='Flag a post as offensive', default=500) karma_dofollow = fields.Integer(string='Nofollow links', help='If the author has not enough karma, a nofollow attribute is added to links', default=500) karma_editor = fields.Integer(string='Editor Features: image and links', default=30, oldname='karma_editor_link_files') karma_user_bio = fields.Integer(string='Display detailed user biography', default=750) karma_post = fields.Integer(string='Ask questions without validation', default=100) karma_moderate = fields.Integer(string='Moderate posts', default=1000) @api.one @api.constrains('allow_question', 'allow_discussion', 'allow_link', 'default_post_type') def _check_default_post_type(self): if (self.default_post_type == 'question' and not self.allow_question) \ or (self.default_post_type == 'discussion' and not self.allow_discussion) \ or (self.default_post_type == 'link' and not self.allow_link): raise UserError(_('You cannot choose %s as default post since the forum does not allow it.' % self.default_post_type)) @api.one @api.constrains('allow_link', 'allow_question', 'allow_discussion', 'default_post_type') def _check_default_post_type(self): if self.default_post_type == 'link' and not self.allow_link or self.default_post_type == 'question' and not self.allow_question or self.default_post_type == 'discussion' and not self.allow_discussion: raise Warning(_('Post type in "Default post" must be activated')) @api.one def _compute_count_posts_waiting_validation(self): domain = [('forum_id', '=', self.id), ('state', '=', 'pending')] self.count_posts_waiting_validation = self.env['forum.post'].search_count(domain) @api.one def _compute_count_flagged_posts(self): domain = [('forum_id', '=', self.id), ('state', '=', 'flagged')] self.count_flagged_posts = self.env['forum.post'].search_count(domain) @api.model def create(self, values): return super(Forum, self.with_context(mail_create_nolog=True, mail_create_nosubscribe=True)).create(values) @api.model def _tag_to_write_vals(self, tags=''): User = self.env['res.users'] Tag = self.env['forum.tag'] post_tags = [] existing_keep = [] for tag in filter(None, tags.split(',')): if tag.startswith('_'): # it's a new tag # check that not arleady created meanwhile or maybe excluded by the limit on the search tag_ids = Tag.search([('name', '=', tag[1:])]) if tag_ids: existing_keep.append(int(tag_ids[0])) else: # check if user have Karma needed to create need tag user = User.sudo().browse(self._uid) if user.exists() and user.karma >= self.karma_retag: post_tags.append((0, 0, {'name': tag[1:], 'forum_id': self.id})) else: existing_keep.append(int(tag)) post_tags.insert(0, [6, 0, existing_keep]) return post_tags def get_tags_first_char(self): """ get set of first letter of forum tags """ tags = self.env['forum.tag'].search([('forum_id', '=', self.id), ('posts_count', '>', 0)]) return sorted(set([tag.name[0].upper() for tag in tags]))
class Post(models.Model): _name = 'forum.post' _description = 'Forum Post' _inherit = ['mail.thread', 'website.seo.metadata'] _order = "is_correct DESC, vote_count DESC, write_date DESC" name = fields.Char('Title') forum_id = fields.Many2one('forum.forum', string='Forum', required=True) content = fields.Html('Content', strip_style=True) plain_content = fields.Text('Plain Content', compute='_get_plain_content', store=True) content_link = fields.Char('URL', help="URL of Link Articles") tag_ids = fields.Many2many('forum.tag', 'forum_tag_rel', 'forum_id', 'forum_tag_id', string='Tags') state = fields.Selection([('active', 'Active'), ('pending', 'Waiting Validation'), ('close', 'Close'), ('offensive', 'Offensive'), ('flagged', 'Flagged')], string='Status', default='active') views = fields.Integer('Number of Views', default=0) active = fields.Boolean('Active', default=True) post_type = fields.Selection([ ('question', 'Question'), ('link', 'Article'), ('discussion', 'Discussion')], string='Type', default='question', required=True) website_message_ids = fields.One2many( 'mail.message', 'res_id', domain=lambda self: ['&', ('model', '=', self._name), ('message_type', 'in', ['email', 'comment'])], string='Post Messages', help="Comments on forum post", ) # history create_date = fields.Datetime('Asked on', select=True, readonly=True) create_uid = fields.Many2one('res.users', string='Created by', select=True, readonly=True) write_date = fields.Datetime('Update on', select=True, readonly=True) bump_date = fields.Datetime('Bumped on', readonly=True, help="Technical field allowing to bump a question. Writing on this field will trigger " "a write on write_date and therefore bump the post. Directly writing on write_date " "is currently not supported and this field is a workaround.") write_uid = fields.Many2one('res.users', string='Updated by', select=True, readonly=True) relevancy = fields.Float('Relevance', compute="_compute_relevancy", store=True) # vote vote_ids = fields.One2many('forum.post.vote', 'post_id', string='Votes') user_vote = fields.Integer('My Vote', compute='_get_user_vote') vote_count = fields.Integer('Votes', compute='_get_vote_count', store=True) # favorite favourite_ids = fields.Many2many('res.users', string='Favourite') user_favourite = fields.Boolean('Is Favourite', compute='_get_user_favourite') favourite_count = fields.Integer('Favorite Count', compute='_get_favorite_count', store=True) # hierarchy is_correct = fields.Boolean('Correct', help='Correct answer or answer accepted') parent_id = fields.Many2one('forum.post', string='Question', ondelete='cascade') self_reply = fields.Boolean('Reply to own question', compute='_is_self_reply', store=True) child_ids = fields.One2many('forum.post', 'parent_id', string='Answers') child_count = fields.Integer('Number of answers', compute='_get_child_count', store=True) uid_has_answered = fields.Boolean('Has Answered', compute='_get_uid_has_answered') has_validated_answer = fields.Boolean('Is answered', compute='_get_has_validated_answer', store=True) # offensive moderation tools flag_user_id = fields.Many2one('res.users', string='Flagged by') moderator_id = fields.Many2one('res.users', string='Reviewed by', readonly=True) # closing closed_reason_id = fields.Many2one('forum.post.reason', string='Reason') closed_uid = fields.Many2one('res.users', string='Closed by', select=1) closed_date = fields.Datetime('Closed on', readonly=True) # karma calculation and access karma_accept = fields.Integer('Convert comment to answer', compute='_get_post_karma_rights') karma_edit = fields.Integer('Karma to edit', compute='_get_post_karma_rights') karma_close = fields.Integer('Karma to close', compute='_get_post_karma_rights') karma_unlink = fields.Integer('Karma to unlink', compute='_get_post_karma_rights') karma_comment = fields.Integer('Karma to comment', compute='_get_post_karma_rights') karma_comment_convert = fields.Integer('Karma to convert comment to answer', compute='_get_post_karma_rights') karma_flag = fields.Integer('Flag a post as offensive', compute='_get_post_karma_rights') can_ask = fields.Boolean('Can Ask', compute='_get_post_karma_rights') can_answer = fields.Boolean('Can Answer', compute='_get_post_karma_rights') can_accept = fields.Boolean('Can Accept', compute='_get_post_karma_rights') can_edit = fields.Boolean('Can Edit', compute='_get_post_karma_rights') can_close = fields.Boolean('Can Close', compute='_get_post_karma_rights') can_unlink = fields.Boolean('Can Unlink', compute='_get_post_karma_rights') can_upvote = fields.Boolean('Can Upvote', compute='_get_post_karma_rights') can_downvote = fields.Boolean('Can Downvote', compute='_get_post_karma_rights') can_comment = fields.Boolean('Can Comment', compute='_get_post_karma_rights') can_comment_convert = fields.Boolean('Can Convert to Comment', compute='_get_post_karma_rights') can_view = fields.Boolean('Can View', compute='_get_post_karma_rights') can_display_biography = fields.Boolean("Is the author's biography visible from his post", compute='_get_post_karma_rights') can_post = fields.Boolean('Can Automatically be Validated', compute='_get_post_karma_rights') can_flag = fields.Boolean('Can Flag', compute='_get_post_karma_rights') can_moderate = fields.Boolean('Can Moderate', compute='_get_post_karma_rights') @api.one @api.depends('content') def _get_plain_content(self): self.plain_content = tools.html2plaintext(self.content)[0:500] if self.content else False @api.one @api.depends('vote_count', 'forum_id.relevancy_post_vote', 'forum_id.relevancy_time_decay') def _compute_relevancy(self): if self.create_date: days = (datetime.today() - datetime.strptime(self.create_date, tools.DEFAULT_SERVER_DATETIME_FORMAT)).days self.relevancy = math.copysign(1, self.vote_count) * (abs(self.vote_count - 1) ** self.forum_id.relevancy_post_vote / (days + 2) ** self.forum_id.relevancy_time_decay) else: self.relevancy = 0 @api.multi def _get_user_vote(self): votes = self.env['forum.post.vote'].search_read([('post_id', 'in', self._ids), ('user_id', '=', self._uid)], ['vote', 'post_id']) mapped_vote = dict([(v['post_id'][0], v['vote']) for v in votes]) for vote in self: vote.user_vote = mapped_vote.get(vote.id, 0) @api.multi @api.depends('vote_ids.vote') def _get_vote_count(self): read_group_res = self.env['forum.post.vote'].read_group([('post_id', 'in', self._ids)], ['post_id', 'vote'], ['post_id', 'vote'], lazy=False) result = dict.fromkeys(self._ids, 0) for data in read_group_res: result[data['post_id'][0]] += data['__count'] * int(data['vote']) for post in self: post.vote_count = result[post.id] @api.one def _get_user_favourite(self): self.user_favourite = self._uid in self.favourite_ids.ids @api.one @api.depends('favourite_ids') def _get_favorite_count(self): self.favourite_count = len(self.favourite_ids) @api.one @api.depends('create_uid', 'parent_id') def _is_self_reply(self): self.self_reply = self.parent_id.create_uid.id == self._uid @api.one @api.depends('child_ids.create_uid', 'website_message_ids') def _get_child_count(self): def process(node): total = len(node.website_message_ids) + len(node.child_ids) for child in node.child_ids: total += process(child) return total self.child_count = process(self) @api.one def _get_uid_has_answered(self): self.uid_has_answered = any(answer.create_uid.id == self._uid for answer in self.child_ids) @api.one @api.depends('child_ids.is_correct') def _get_has_validated_answer(self): self.has_validated_answer = any(answer.is_correct for answer in self.child_ids) @api.multi def _get_post_karma_rights(self): user = self.env.user is_admin = user.id == SUPERUSER_ID # sudoed recordset instead of individual posts so values can be # prefetched in bulk for post, post_sudo in itertools.izip(self, self.sudo()): is_creator = post.create_uid == user post.karma_accept = post.forum_id.karma_answer_accept_own if post.parent_id.create_uid == user else post.forum_id.karma_answer_accept_all post.karma_edit = post.forum_id.karma_edit_own if is_creator else post.forum_id.karma_edit_all post.karma_close = post.forum_id.karma_close_own if is_creator else post.forum_id.karma_close_all post.karma_unlink = post.forum_id.karma_unlink_own if is_creator else post.forum_id.karma_unlink_all post.karma_comment = post.forum_id.karma_comment_own if is_creator else post.forum_id.karma_comment_all post.karma_comment_convert = post.forum_id.karma_comment_convert_own if is_creator else post.forum_id.karma_comment_convert_all post.can_ask = is_admin or user.karma >= post.forum_id.karma_ask post.can_answer = is_admin or user.karma >= post.forum_id.karma_answer post.can_accept = is_admin or user.karma >= post.karma_accept post.can_edit = is_admin or user.karma >= post.karma_edit post.can_close = is_admin or user.karma >= post.karma_close post.can_unlink = is_admin or user.karma >= post.karma_unlink post.can_upvote = is_admin or user.karma >= post.forum_id.karma_upvote post.can_downvote = is_admin or user.karma >= post.forum_id.karma_downvote post.can_comment = is_admin or user.karma >= post.karma_comment post.can_comment_convert = is_admin or user.karma >= post.karma_comment_convert post.can_view = is_admin or user.karma >= post.karma_close or post_sudo.create_uid.karma > 0 post.can_display_biography = is_admin or post_sudo.create_uid.karma >= post.forum_id.karma_user_bio post.can_post = is_admin or user.karma >= post.forum_id.karma_post post.can_flag = is_admin or user.karma >= post.forum_id.karma_flag post.can_moderate = is_admin or user.karma >= post.forum_id.karma_moderate @api.one @api.constrains('post_type', 'forum_id') def _check_post_type(self): if (self.post_type == 'question' and not self.forum_id.allow_question) \ or (self.post_type == 'discussion' and not self.forum_id.allow_discussion) \ or (self.post_type == 'link' and not self.forum_id.allow_link): raise UserError(_('This forum does not allow %s' % self.post_type)) def _update_content(self, content, forum_id): forum = self.env['forum.forum'].browse(forum_id) if content and self.env.user.karma < forum.karma_dofollow: for match in re.findall(r'<a\s.*href=".*?">', content): content = re.sub(match, match[:3] + 'rel="nofollow" ' + match[3:], content) if self.env.user.karma <= forum.karma_editor: filter_regexp = r'(<img.*?>)|(<a[^>]*?href[^>]*?>)|(<[a-z|A-Z]+[^>]*style\s*=\s*[\'"][^\'"]*\s*background[^:]*:[^url;]*url)' content_match = re.search(filter_regexp, content, re.I) if content_match: raise KarmaError('User karma not sufficient to post an image or link.') return content @api.model def create(self, vals): if 'content' in vals and vals.get('forum_id'): vals['content'] = self._update_content(vals['content'], vals['forum_id']) post = super(Post, self.with_context(mail_create_nolog=True)).create(vals) # deleted or closed questions if post.parent_id and (post.parent_id.state == 'close' or post.parent_id.active is False): raise UserError(_('Posting answer on a [Deleted] or [Closed] question is not possible')) # karma-based access if not post.parent_id and not post.can_ask: raise KarmaError('Not enough karma to create a new question') elif post.parent_id and not post.can_answer: raise KarmaError('Not enough karma to answer to a question') if not post.parent_id and not post.can_post: post.state = 'pending' # add karma for posting new questions if not post.parent_id and post.state == 'active': self.env.user.sudo().add_karma(post.forum_id.karma_gen_question_new) post.post_notification() return post @api.model def check_mail_message_access(self, res_ids, operation, model_name=None): if operation in ('write', 'unlink') and (not model_name or model_name == 'forum.post'): # Make sure only author or moderator can edit/delete messages if any(not post.can_edit for post in self.browse(res_ids)): raise KarmaError('Not enough karma to edit a post.') return super(Post, self).check_mail_message_access(res_ids, operation, model_name=model_name) @api.multi @api.depends('name', 'post_type') def name_get(self): result = [] for post in self: if post.post_type == 'discussion' and post.parent_id and not post.name: result.append((post.id, '%s (%s)' % (post.parent_id.name, post.id))) else: result.append((post.id, '%s' % (post.name))) return result @api.multi def write(self, vals): if 'content' in vals: vals['content'] = self._update_content(vals['content'], self.forum_id.id) if 'state' in vals: if vals['state'] in ['active', 'close'] and any(not post.can_close for post in self): raise KarmaError('Not enough karma to close or reopen a post.') if 'active' in vals: if any(not post.can_unlink for post in self): raise KarmaError('Not enough karma to delete or reactivate a post') if 'is_correct' in vals: if any(not post.can_accept for post in self): raise KarmaError('Not enough karma to accept or refuse an answer') # update karma except for self-acceptance mult = 1 if vals['is_correct'] else -1 for post in self: if vals['is_correct'] != post.is_correct and post.create_uid.id != self._uid: post.create_uid.sudo().add_karma(post.forum_id.karma_gen_answer_accepted * mult) self.env.user.sudo().add_karma(post.forum_id.karma_gen_answer_accept * mult) if any(key not in ['state', 'active', 'is_correct', 'closed_uid', 'closed_date', 'closed_reason_id'] for key in vals.keys()) and any(not post.can_edit for post in self): raise KarmaError('Not enough karma to edit a post.') res = super(Post, self).write(vals) # if post content modify, notify followers if 'content' in vals or 'name' in vals: for post in self: if post.parent_id: body, subtype = _('Answer Edited'), 'website_forum.mt_answer_edit' obj_id = post.parent_id else: body, subtype = _('Question Edited'), 'website_forum.mt_question_edit' obj_id = post obj_id.message_post(body=body, subtype=subtype) return res @api.multi def post_notification(self): base_url = self.env['ir.config_parameter'].get_param('web.base.url') for post in self: if post.state == 'active' and post.parent_id: body = _( '<p>A new answer for <i>%s</i> has been posted. <a href="%s/forum/%s/question/%s">Click here to access the post.</a></p>' % (post.parent_id.name, base_url, slug(post.parent_id.forum_id), slug(post.parent_id)) ) post.parent_id.message_post(subject=_('Re: %s') % post.parent_id.name, body=body, subtype='website_forum.mt_answer_new') elif post.state == 'active' and not post.parent_id: body = _( '<p>A new question <i>%s</i> has been asked on %s. <a href="%s/forum/%s/question/%s">Click here to access the question.</a></p>' % (post.name, post.forum_id.name, base_url, slug(post.forum_id), slug(post)) ) post.message_post(subject=post.name, body=body, subtype='website_forum.mt_question_new') elif post.state == 'pending' and not post.parent_id: # TDE FIXME: in master, you should probably use a subtype; # however here we remove subtype but set partner_ids partners = post.sudo().message_partner_ids.filtered(lambda partner: partner.user_ids and partner.user_ids.karma >= post.forum_id.karma_moderate) note_subtype = self.sudo().env.ref('mail.mt_note') body = _( '<p>A new question <i>%s</i> has been asked on %s and require your validation. <a href="%s/forum/%s/question/%s">Click here to access the question.</a></p>' % (post.name, post.forum_id.name, base_url, slug(post.forum_id), slug(post)) ) post.message_post(subject=post.name, body=body, subtype_id=note_subtype.id, partner_ids=partners.ids) return True @api.multi def reopen(self): if any(post.parent_id or post.state != 'close' for post in self): return False reason_offensive = self.env.ref('website_forum.reason_7') reason_spam = self.env.ref('website_forum.reason_8') for post in self: if post.closed_reason_id in (reason_offensive, reason_spam): _logger.info('Upvoting user <%s>, reopening spam/offensive question', post.create_uid) post.create_uid.sudo().add_karma(post.forum_id.karma_gen_answer_flagged * -1) self.sudo().write({'state': 'active'}) @api.multi def close(self, reason_id): if any(post.parent_id for post in self): return False reason_offensive = self.env.ref('website_forum.reason_7').id reason_spam = self.env.ref('website_forum.reason_8').id if reason_id in (reason_offensive, reason_spam): for post in self: _logger.info('Downvoting user <%s> for posting spam/offensive contents', post.create_uid) post.create_uid.sudo().add_karma(post.forum_id.karma_gen_answer_flagged) self.write({ 'state': 'close', 'closed_uid': self._uid, 'closed_date': datetime.today().strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT), 'closed_reason_id': reason_id, }) return True @api.one def validate(self): if not self.can_moderate: raise KarmaError('Not enough karma to validate a post') # if state == pending, no karma previously added for the new question if self.state == 'pending': self.create_uid.sudo().add_karma(self.forum_id.karma_gen_question_new) self.write({ 'state': 'active', 'active': True, 'moderator_id': self.env.user.id, }) self.post_notification() return True @api.one def refuse(self): if not self.can_moderate: raise KarmaError('Not enough karma to refuse a post') self.moderator_id = self.env.user return True @api.one def flag(self): if not self.can_flag: raise KarmaError('Not enough karma to flag a post') if(self.state == 'flagged'): return {'error': 'post_already_flagged'} elif(self.state == 'active'): self.write({ 'state': 'flagged', 'flag_user_id': self.env.user.id, }) return self.can_moderate and {'success': 'post_flagged_moderator'} or {'success': 'post_flagged_non_moderator'} else: return {'error': 'post_non_flaggable'} @api.one def mark_as_offensive(self, reason_id): if not self.can_moderate: raise KarmaError('Not enough karma to mark a post as offensive') # remove some karma _logger.info('Downvoting user <%s> for posting spam/offensive contents', self.create_uid) self.create_uid.sudo().add_karma(self.forum_id.karma_gen_answer_flagged) self.write({ 'state': 'offensive', 'moderator_id': self.env.user.id, 'closed_date': datetime.today().strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT), 'closed_reason_id': reason_id, 'active': False, }) return True @api.multi def unlink(self): if any(not post.can_unlink for post in self): raise KarmaError('Not enough karma to unlink a post') # if unlinking an answer with accepted answer: remove provided karma for post in self: if post.is_correct: post.create_uid.sudo().add_karma(post.forum_id.karma_gen_answer_accepted * -1) self.env.user.sudo().add_karma(post.forum_id.karma_gen_answer_accepted * -1) return super(Post, self).unlink() @api.multi def bump(self): """ Bump a question: trigger a write_date by writing on a dummy bump_date field. One cannot bump a question more than once every 10 days. """ self.ensure_one() if self.forum_id.allow_bump and not self.child_ids and (datetime.today() - datetime.strptime(self.write_date, tools.DEFAULT_SERVER_DATETIME_FORMAT)).days > 9: # write through super to bypass karma; sudo to allow public user to bump any post return self.sudo().write({'bump_date': fields.Datetime.now()}) return False @api.multi def vote(self, upvote=True): Vote = self.env['forum.post.vote'] vote_ids = Vote.search([('post_id', 'in', self._ids), ('user_id', '=', self._uid)]) new_vote = '1' if upvote else '-1' voted_forum_ids = set() if vote_ids: for vote in vote_ids: if upvote: new_vote = '0' if vote.vote == '-1' else '1' else: new_vote = '0' if vote.vote == '1' else '-1' vote.vote = new_vote voted_forum_ids.add(vote.post_id.id) for post_id in set(self._ids) - voted_forum_ids: for post_id in self._ids: Vote.create({'post_id': post_id, 'vote': new_vote}) return {'vote_count': self.vote_count, 'user_vote': new_vote} @api.one def convert_answer_to_comment(self): """ Tools to convert an answer (forum.post) to a comment (mail.message). The original post is unlinked and a new comment is posted on the question using the post create_uid as the comment's author. """ if not self.parent_id: return False # karma-based action check: use the post field that computed own/all value if not self.can_comment_convert: raise KarmaError('Not enough karma to convert an answer to a comment') # post the message question = self.parent_id values = { 'author_id': self.sudo().create_uid.partner_id.id, # use sudo here because of access to res.users model 'body': tools.html_sanitize(self.content, strict=True, strip_style=True, strip_classes=True), 'message_type': 'comment', 'subtype': 'mail.mt_comment', 'date': self.create_date, } new_message = self.browse(question.id).with_context(mail_create_nosubscribe=True).message_post(**values) # unlink the original answer, using SUPERUSER_ID to avoid karma issues self.sudo().unlink() return new_message @api.model def convert_comment_to_answer(self, message_id, default=None): """ Tool to convert a comment (mail.message) into an answer (forum.post). The original comment is unlinked and a new answer from the comment's author is created. Nothing is done if the comment's author already answered the question. """ comment = self.env['mail.message'].sudo().browse(message_id) post = self.browse(comment.res_id) if not comment.author_id or not comment.author_id.user_ids: # only comment posted by users can be converted return False # karma-based action check: must check the message's author to know if own / all karma_convert = comment.author_id.id == self.env.user.partner_id.id and post.forum_id.karma_comment_convert_own or post.forum_id.karma_comment_convert_all can_convert = self.env.user.karma >= karma_convert if not can_convert: raise KarmaError('Not enough karma to convert a comment to an answer') # check the message's author has not already an answer question = post.parent_id if post.parent_id else post post_create_uid = comment.author_id.user_ids[0] if any(answer.create_uid.id == post_create_uid.id for answer in question.child_ids): return False # create the new post post_values = { 'forum_id': question.forum_id.id, 'content': comment.body, 'parent_id': question.id, } # done with the author user to have create_uid correctly set new_post = self.sudo(post_create_uid.id).create(post_values) # delete comment comment.unlink() return new_post @api.one def unlink_comment(self, message_id): user = self.env.user comment = self.env['mail.message'].sudo().browse(message_id) if not comment.model == 'forum.post' or not comment.res_id == self.id: return False # karma-based action check: must check the message's author to know if own or all karma_unlink = comment.author_id.id == user.partner_id.id and self.forum_id.karma_comment_unlink_own or self.forum_id.karma_comment_unlink_all can_unlink = user.karma >= karma_unlink if not can_unlink: raise KarmaError('Not enough karma to unlink a comment') return comment.unlink() @api.multi def set_viewed(self): self._cr.execute("""UPDATE forum_post SET views = views+1 WHERE id IN %s""", (self._ids,)) return True @api.multi def get_access_action(self): """ Override method that generated the link to access the document. Instead of the classic form view, redirect to the post on the website directly """ self.ensure_one() return { 'type': 'ir.actions.act_url', 'url': '/forum/%s/question/%s' % (self.forum_id.id, self.id), 'target': 'self', 'res_id': self.id, } @api.multi def _notification_get_recipient_groups(self, message, recipients): """ Override to set the access button: everyone can see an access button on their notification email. It will lead on the website view of the post. """ res = super(Post, self)._notification_get_recipient_groups(message, recipients) access_action = self._notification_link_helper('view', model=message.model, res_id=message.res_id) for category, data in res.iteritems(): res[category]['button_access'] = {'url': access_action, 'title': '%s %s' % (_('View'), self.post_type)} return res @api.cr_uid_ids_context def message_post(self, cr, uid, thread_id, message_type='notification', subtype=None, context=None, **kwargs): if thread_id and message_type == 'comment': # user comments have a restriction on karma if isinstance(thread_id, (list, tuple)): post_id = thread_id[0] else: post_id = thread_id post = self.browse(cr, uid, post_id, context=context) # TDE FIXME: trigger browse because otherwise the function field is not compted - check with RCO tmp1, tmp2 = post.karma_comment, post.can_comment user = self.pool['res.users'].browse(cr, uid, uid) tmp3 = user.karma # TDE END FIXME if not post.can_comment: raise KarmaError('Not enough karma to comment') return super(Post, self).message_post(cr, uid, thread_id, message_type=message_type, subtype=subtype, context=context, **kwargs)
class Planner(models.Model): """Planner Model. Each Planner has link to an ir.ui.view record that is a template used to display the planner pages. Each Planner has link to ir.ui.menu record that is a top menu used to display the planner launcher(progressbar) Method _prepare_<planner_application>_data(self, cr, uid, context) that generate the values used to display in specific planner pages """ _name = 'web.planner' _description = 'Planner' @api.model def _get_planner_application(self): return [] name = fields.Char(string='Name', required=True) menu_id = fields.Many2one('ir.ui.menu', string='Menu', required=True) view_id = fields.Many2one('ir.ui.view', string='Template', required=True) progress = fields.Integer(string="Progress Percentage", default=5) # data field is used to store the data filled by user in planner(JSON Data) data = fields.Text(string='Data') tooltip_planner = fields.Html(string='Planner Tooltips', translate=True) planner_application = fields.Selection('_get_planner_application', string='Planner Application', required=True) active = fields.Boolean(string="Active", default=True, help="If the active field is set to False, it will allow you to hide the planner. This change requires a refreshing a your page.") @api.model def render(self, template_id, planner_app): # prepare the planner data as per the planner application values = { 'prepare_backend_url': self.prepare_backend_url, 'is_module_installed': self.is_module_installed, } planner_find_method_name = '_prepare_%s_data' % planner_app if hasattr(self, planner_find_method_name): values.update(getattr(self, planner_find_method_name)()) # update the default value return self.env['ir.ui.view'].browse(template_id).render(values=values) @api.model def prepare_backend_url(self, action_xml_id, view_type='list', module_name=None): """ prepare the backend url to the given action, or to the given module view. :param action_xml_id : the xml id of the action to redirect to :param view_type : the view type to display when redirecting (form, kanban, list, ...) :param module_name : the name of the module to display (if action_xml_id is 'open_module_tree'), or to redirect to if the action is not found. :returns url : the url to the correct page """ params = dict(view_type=view_type) # setting the action action = self.env.ref(action_xml_id, False) if action: params['action'] = action.id else: params['model'] = 'ir.module.module' # setting the module if module_name: installed = self.env['ir.module.module']._installed() if module_name in installed: params['id'] = installed[module_name] return "/web#%s" % (urlencode(params),) @api.model def is_module_installed(self, module_name=None): return module_name in self.env['ir.module.module']._installed()
class MailTemplate(models.Model): "Templates for sending email" _name = "mail.template" _description = 'Email Templates' _order = 'name' @api.model def default_get(self, fields): res = super(MailTemplate, self).default_get(fields) if res.get('model'): res['model_id'] = self.env['ir.model'].search([ ('model', '=', res.pop('model')) ]).id return res name = fields.Char('Name') model_id = fields.Many2one( 'ir.model', 'Applies to', help="The kind of document with with this template can be used") model = fields.Char('Related Document Model', related='model_id.model', select=True, store=True, readonly=True) lang = fields.Char( 'Language', help= "Optional translation language (ISO code) to select when sending out an email. " "If not set, the english version will be used. " "This should usually be a placeholder expression " "that provides the appropriate language, e.g. " "${object.partner_id.lang}.", placeholder="${object.partner_id.lang}") user_signature = fields.Boolean( 'Add Signature', help= "If checked, the user's signature will be appended to the text version " "of the message") subject = fields.Char('Subject', translate=True, help="Subject (placeholders may be used here)") email_from = fields.Char( 'From', help= "Sender address (placeholders may be used here). If not set, the default " "value will be the author's email alias if configured, or email address." ) use_default_to = fields.Boolean( 'Default recipients', help="Default recipients of the record:\n" "- partner (using id on a partner or the partner_id field) OR\n" "- email (using email_from or email field)") email_to = fields.Char( 'To (Emails)', help= "Comma-separated recipient addresses (placeholders may be used here)") partner_to = fields.Char( 'To (Partners)', oldname='email_recipients', help= "Comma-separated ids of recipient partners (placeholders may be used here)" ) email_cc = fields.Char( 'Cc', help="Carbon copy recipients (placeholders may be used here)") reply_to = fields.Char( 'Reply-To', help="Preferred response address (placeholders may be used here)") mail_server_id = fields.Many2one( 'ir.mail_server', 'Outgoing Mail Server', readonly=False, help= "Optional preferred server for outgoing mails. If not set, the highest " "priority one will be used.") body_html = fields.Html( 'Body', translate=True, sanitize=False, help= "Rich-text/HTML version of the message (placeholders may be used here)" ) report_name = fields.Char( 'Report Filename', translate=True, help= "Name to use for the generated report file (may contain placeholders)\n" "The extension can be omitted and will then come from the report type." ) report_template = fields.Many2one('ir.actions.report.xml', 'Optional report to print and attach') ref_ir_act_window = fields.Many2one( 'ir.actions.act_window', 'Sidebar action', readonly=True, copy=False, help="Sidebar action to make this template available on records " "of the related document model") ref_ir_value = fields.Many2one( 'ir.values', 'Sidebar Button', readonly=True, copy=False, help="Sidebar button to open the sidebar action") attachment_ids = fields.Many2many( 'ir.attachment', 'email_template_attachment_rel', 'email_template_id', 'attachment_id', 'Attachments', help="You may attach files to this template, to be added to all " "emails created from this template") auto_delete = fields.Boolean( 'Auto Delete', default=True, help="Permanently delete this email after sending it, to save space") # Fake fields used to implement the placeholder assistant model_object_field = fields.Many2one( 'ir.model.fields', string="Field", help="Select target field from the related document model.\n" "If it is a relationship field you will be able to select " "a target field at the destination of the relationship.") sub_object = fields.Many2one( 'ir.model', 'Sub-model', readonly=True, help="When a relationship field is selected as first field, " "this field shows the document model the relationship goes to.") sub_model_object_field = fields.Many2one( 'ir.model.fields', 'Sub-field', help="When a relationship field is selected as first field, " "this field lets you select the target field within the " "destination document model (sub-model).") null_value = fields.Char( 'Default Value', help="Optional value to use if the target field is empty") copyvalue = fields.Char( 'Placeholder Expression', help= "Final placeholder expression, to be copy-pasted in the desired template field." ) @api.onchange('model_id') def onchange_model_id(self): # TDE CLEANME: should'nt it be a stored related ? if self.model_id: self.model = self.model_id.model else: self.model = False def build_expression(self, field_name, sub_field_name, null_value): """Returns a placeholder expression for use in a template field, based on the values provided in the placeholder assistant. :param field_name: main field name :param sub_field_name: sub field name (M2O) :param null_value: default value if the target value is empty :return: final placeholder expression """ expression = '' if field_name: expression = "${object." + field_name if sub_field_name: expression += "." + sub_field_name if null_value: expression += " or '''%s'''" % null_value expression += "}" return expression @api.onchange('model_object_field', 'sub_model_object_field', 'null_value') def onchange_sub_model_object_value_field(self): if self.model_object_field: if self.model_object_field.ttype in [ 'many2one', 'one2many', 'many2many' ]: models = self.env['ir.model'].search([ ('model', '=', self.model_object_field.relation) ]) if models: self.sub_object = models.id self.copyvalue = self.build_expression( self.model_object_field.name, self.sub_model_object_field and self.sub_model_object_field.name or False, self.null_value or False) else: self.sub_object = False self.sub_model_object_field = False self.copyvalue = self.build_expression( self.model_object_field.name, False, self.null_value or False) else: self.sub_object = False self.copyvalue = False self.sub_model_object_field = False self.null_value = False @api.multi def unlink(self): self.unlink_action() return super(MailTemplate, self).unlink() @api.multi def copy(self, default=None): default = dict(default or {}, name=_("%s (copy)") % self.name) return super(MailTemplate, self).copy(default=default) @api.multi def unlink_action(self): for template in self: if template.ref_ir_act_window: template.ref_ir_act_window.sudo().unlink() if template.ref_ir_value: template.ref_ir_value.sudo().unlink() return True @api.multi def create_action(self): ActWindowSudo = self.env['ir.actions.act_window'].sudo() IrValuesSudo = self.env['ir.values'].sudo() view = self.env.ref('mail.email_compose_message_wizard_form') for template in self: src_obj = template.model_id.model button_name = _('Send Mail (%s)') % template.name action = ActWindowSudo.create({ 'name': button_name, 'type': 'ir.actions.act_window', 'res_model': 'mail.compose.message', 'src_model': src_obj, 'view_type': 'form', 'context': "{'default_composition_mode': 'mass_mail', 'default_template_id' : %d, 'default_use_template': True}" % (template.id), 'view_mode': 'form,tree', 'view_id': view.id, 'target': 'new', 'auto_refresh': 1 }) ir_value = IrValuesSudo.create({ 'name': button_name, 'model': src_obj, 'key2': 'client_action_multi', 'value': "ir.actions.act_window,%s" % action.id }) template.write({ 'ref_ir_act_window': action.id, 'ref_ir_value': ir_value.id, }) return True # ---------------------------------------- # RENDERING # ---------------------------------------- @api.model def _replace_local_links(self, html): """ Post-processing of html content to replace local links to absolute links, using web.base.url as base url. """ if not html: return html # form a tree root = lxml.html.fromstring(html) if not len(root) and root.text is None and root.tail is None: html = '<div>%s</div>' % html root = lxml.html.fromstring(html) base_url = self.env['ir.config_parameter'].get_param('web.base.url') (base_scheme, base_netloc, bpath, bparams, bquery, bfragment) = urlparse.urlparse(base_url) def _process_link(url): new_url = url (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(url) if not scheme and not netloc: new_url = urlparse.urlunparse( (base_scheme, base_netloc, path, params, query, fragment)) return new_url # check all nodes, replace : # - img src -> check URL # - a href -> check URL for node in root.iter(): if node.tag == 'a' and node.get('href'): node.set('href', _process_link(node.get('href'))) elif node.tag == 'img' and not node.get('src', 'data').startswith('data'): node.set('src', _process_link(node.get('src'))) 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('<div>') and html.endswith('</div>'): html = html[5:-6] return html @api.model def render_post_process(self, html): html = self._replace_local_links(html) return html @api.model def render_template(self, template_txt, model, res_ids, post_process=False): """ Render the given template text, replace mako expressions ``${expr}`` with the result of evaluating these expressions with an evaluation context containing: - ``user``: browse_record of the current user - ``object``: record of the document record this mail is related to - ``context``: the context passed to the mail composition wizard :param str template_txt: the template text to render :param str model: model name of the document record this mail is related to. :param int res_ids: list of ids of document records those mails are related to. """ multi_mode = True if isinstance(res_ids, (int, long)): multi_mode = False res_ids = [res_ids] results = dict.fromkeys(res_ids, u"") # try to load the template try: mako_env = mako_safe_template_env if self.env.context.get( 'safe') else mako_template_env template = mako_env.from_string(tools.ustr(template_txt)) except Exception: _logger.info("Failed to load template %r", template_txt, exc_info=True) return multi_mode and results or results[res_ids[0]] # prepare template variables records = self.env[model].browse(filter( None, res_ids)) # filter to avoid browsing [None] res_to_rec = dict.fromkeys(res_ids, None) for record in records: res_to_rec[record.id] = record variables = { 'format_tz': lambda dt, tz=False, format=False, context=self._context: format_tz(self.pool, self._cr, self._uid, dt, tz, format, context), 'user': self.env.user, 'ctx': self._context, # context kw would clash with mako internals } for res_id, record in res_to_rec.iteritems(): variables['object'] = record try: render_result = template.render(variables) except Exception: _logger.info("Failed to render template %r using values %r" % (template, variables), exc_info=True) raise UserError( _("Failed to render template %r using values %r") % (template, variables)) render_result = u"" if render_result == u"False": render_result = u"" results[res_id] = render_result if post_process: for res_id, result in results.iteritems(): results[res_id] = self.render_post_process(result) return multi_mode and results or results[res_ids[0]] @api.multi def get_email_template(self, res_ids): multi_mode = True if isinstance(res_ids, (int, long)): res_ids = [res_ids] multi_mode = False if res_ids is None: res_ids = [None] results = dict.fromkeys(res_ids, False) if not self.ids: return results self.ensure_one() langs = self.render_template(self.lang, self.model, res_ids) for res_id, lang in langs.iteritems(): if lang: template = self.with_context(lang=lang) else: template = self results[res_id] = template return multi_mode and results or results[res_ids[0]] @api.multi def generate_recipients(self, results, res_ids): """Generates the recipients of the template. Default values can ben generated instead of the template values if requested by template or context. Emails (email_to, email_cc) can be transformed into partners if requested in the context. """ self.ensure_one() if self.use_default_to or self._context.get('tpl_force_default_to'): default_recipients = self.env[ 'mail.thread'].message_get_default_recipients( res_model=self.model, res_ids=res_ids) for res_id, recipients in default_recipients.iteritems(): results[res_id].pop('partner_to', None) results[res_id].update(recipients) for res_id, values in results.iteritems(): partner_ids = values.get('partner_ids', list()) if self._context.get('tpl_partners_only'): mails = tools.email_split(values.pop( 'email_to', '')) + tools.email_split( values.pop('email_cc', '')) for mail in mails: partner_id = self.env['res.partner'].find_or_create(mail) partner_ids.append(partner_id) partner_to = values.pop('partner_to', '') if partner_to: # placeholders could generate '', 3, 2 due to some empty field values tpl_partner_ids = [ int(pid) for pid in partner_to.split(',') if pid ] partner_ids += self.env['res.partner'].sudo().browse( tpl_partner_ids).exists().ids results[res_id]['partner_ids'] = partner_ids return results @api.multi def generate_email(self, res_ids, fields=None): """Generates an email from the template for given the given model based on records given by res_ids. :param template_id: id of the template to render. :param res_id: id of the record to use for rendering the template (model is taken from template definition) :returns: a dict containing all relevant fields for creating a new mail.mail entry, with one extra key ``attachments``, in the format [(report_name, data)] where data is base64 encoded. """ self.ensure_one() multi_mode = True if isinstance(res_ids, (int, long)): res_ids = [res_ids] multi_mode = False if fields is None: fields = [ 'subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to' ] res_ids_to_templates = self.get_email_template_batch(res_ids) # templates: res_id -> template; template -> res_ids templates_to_res_ids = {} for res_id, template in res_ids_to_templates.iteritems(): templates_to_res_ids.setdefault(template, []).append(res_id) results = dict() for template, template_res_ids in templates_to_res_ids.iteritems(): Template = self.env['mail.template'] # generate fields value for all res_ids linked to the current template if template.lang: Template = Template.with_context( lang=template._context.get('lang')) for field in fields: Template = Template.with_context(safe=field in {'subject'}) generated_field_values = Template.render_template( getattr(template, field), template.model, template_res_ids, post_process=(field == 'body_html')) for res_id, field_value in generated_field_values.iteritems(): results.setdefault(res_id, dict())[field] = field_value # compute recipients if any(field in fields for field in ['email_to', 'partner_to', 'email_cc']): results = template.generate_recipients(results, template_res_ids) # update values for all res_ids for res_id in template_res_ids: values = results[res_id] # body: add user signature, sanitize if 'body_html' in fields and template.user_signature: signature = self.env.user.signature if signature: values['body_html'] = tools.append_content_to_html( values['body_html'], signature, plaintext=False) if values.get('body_html'): values['body'] = tools.html_sanitize(values['body_html']) # technical settings values.update( mail_server_id=template.mail_server_id.id or False, auto_delete=template.auto_delete, model=template.model, res_id=res_id or False, attachment_ids=[ attach.id for attach in template.attachment_ids ], ) # Add report in attachments: generate once for all template_res_ids if template.report_template and not 'report_template_in_attachment' in self.env.context: for res_id in template_res_ids: attachments = [] report_name = self.render_template(template.report_name, template.model, res_id) report = template.report_template report_service = report.report_name if report.report_type in ['qweb-html', 'qweb-pdf']: result, format = self.pool['report'].get_pdf( self._cr, self._uid, [res_id], report_service, context=Template._context), 'pdf' else: result, format = yuancloud_report.render_report( self._cr, self._uid, [res_id], report_service, {'model': template.model}, Template._context) # TODO in trunk, change return format to binary to match message_post expected format result = base64.b64encode(result) if not report_name: report_name = 'report.' + report_service ext = "." + format if not report_name.endswith(ext): report_name += ext attachments.append((report_name, result)) results[res_id]['attachments'] = attachments return multi_mode and results or results[res_ids[0]] @api.multi def send_mail(self, res_id, force_send=False, raise_exception=False): """Generates a new mail message for the given template and record, and schedules it for delivery through the ``mail`` module's scheduler. :param int res_id: id of the record to render the template with (model is taken from the template) :param bool force_send: if True, the generated mail.message is immediately sent after being created, as if the scheduler was executed for this message only. :returns: id of the mail.message that was created """ self.ensure_one() Mail = self.env['mail.mail'] Attachment = self.env[ 'ir.attachment'] # TDE FIXME: should remove dfeault_type from context # create a mail_mail based on values, without attachments values = self.generate_email(res_id) values['recipient_ids'] = [ (4, pid) for pid in values.get('partner_ids', list()) ] attachment_ids = values.pop('attachment_ids', []) attachments = values.pop('attachments', []) # add a protection against void email_from if 'email_from' in values and not values.get('email_from'): values.pop('email_from') mail = Mail.create(values) # manage attachments for attachment in attachments: attachment_data = { 'name': attachment[0], 'datas_fname': attachment[0], 'datas': attachment[1], 'res_model': 'mail.message', 'res_id': mail.mail_message_id.id, } attachment_ids.append(Attachment.create(attachment_data).id) if attachment_ids: values['attachment_ids'] = [(6, 0, attachment_ids)] mail.write({'attachment_ids': [(6, 0, attachment_ids)]}) if force_send: mail.send(raise_exception=raise_exception) return mail.id # TDE CLEANME: return mail + api.returns ? # compatibility render_template_batch = render_template get_email_template_batch = get_email_template generate_email_batch = generate_email
class event_track(models.Model): _name = "event.track" _description = 'Event Track' _order = 'priority, date' _inherit = [ 'mail.thread', 'ir.needaction_mixin', 'website.seo.metadata', 'website.published.mixin' ] name = fields.Char('Title', required=True, translate=True) user_id = fields.Many2one('res.users', 'Responsible', track_visibility='onchange', default=lambda self: self.env.user) partner_id = fields.Many2one('res.partner', 'Proposed by') partner_name = fields.Char('Partner Name') partner_email = fields.Char('Partner Email') partner_phone = fields.Char('Partner Phone') partner_biography = fields.Html('Partner Biography') speaker_ids = fields.Many2many('res.partner', string='Speakers') tag_ids = fields.Many2many('event.track.tag', string='Tags') state = fields.Selection([('draft', 'Proposal'), ('confirmed', 'Confirmed'), ('announced', 'Announced'), ('published', 'Published'), ('refused', 'Refused'), ('cancel', 'Cancelled')], 'Status', default='draft', required=True, copy=False, track_visibility='onchange') description = fields.Html('Track Description', translate=True) date = fields.Datetime('Track Date') duration = fields.Float('Duration', digits=(16, 2), default=1.5) location_id = fields.Many2one('event.track.location', 'Room') event_id = fields.Many2one('event.event', 'Event', required=True) color = fields.Integer('Color Index') priority = fields.Selection([('0', 'Low'), ('1', 'Medium'), ('2', 'High'), ('3', 'Highest')], 'Priority', required=True, default='1') image = fields.Binary('Image', compute='_compute_image', store=True, attachment=True) @api.one @api.depends('speaker_ids.image') def _compute_image(self): if self.speaker_ids: self.image = self.speaker_ids[0].image else: self.image = False @api.model def create(self, vals): res = super(event_track, self).create(vals) res.message_subscribe(res.speaker_ids.ids) res.event_id.message_post(body="""<h3>%(header)s</h3> <ul> <li>%(proposed_by)s</li> <li>%(mail)s</li> <li>%(phone)s</li> <li>%(title)s</li> <li>%(speakers)s</li> <li>%(introduction)s</li> </ul>""" % { 'header': _('New Track Proposal'), 'proposed_by': '<b>%s</b>: %s' % (_('Proposed By'), (res.partner_id.name or res.partner_name or res.partner_email)), 'mail': '<b>%s</b>: %s' % (_('Mail'), '<a href="mailto:%s">%s</a>' % (res.partner_email, res.partner_email)), 'phone': '<b>%s</b>: %s' % (_('Phone'), res.partner_phone), 'title': '<b>%s</b>: %s' % (_('Title'), res.name), 'speakers': '<b>%s</b>: %s' % (_('Speakers Biography'), res.partner_biography), 'introduction': '<b>%s</b>: %s' % (_('Talk Introduction'), res.description), }, subtype='event.mt_event_track') return res @api.multi def write(self, vals): if vals.get('state') == 'published': vals.update({'website_published': True}) res = super(event_track, self).write(vals) if vals.get('speaker_ids'): self.message_subscribe([ speaker['id'] for speaker in self.resolve_2many_commands( 'speaker_ids', vals['speaker_ids'], ['id']) ]) return res @api.multi @api.depends('name') def _website_url(self, field_name, arg): res = super(event_track, self)._website_url(field_name, arg) res.update({ (track.id, '/event/%s/track/%s' % (slug(track.event_id), slug(track))) for track in self }) return res def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False, lazy=True): """ Override read_group to always display all states. """ if groupby and groupby[0] == "state": # Default result structure # states = self._get_state_list(cr, uid, context=context) states = [('draft', 'Proposal'), ('confirmed', 'Confirmed'), ('announced', 'Announced'), ('published', 'Published'), ('cancel', 'Cancelled')] read_group_all_states = [{ '__context': { 'group_by': groupby[1:] }, '__domain': domain + [('state', '=', state_value)], 'state': state_value, 'state_count': 0, } for state_value, state_name in states] # Get standard results read_group_res = super(event_track, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby) # Update standard results with default results result = [] for state_value, state_name in states: res = filter(lambda x: x['state'] == state_value, read_group_res) if not res: res = filter(lambda x: x['state'] == state_value, read_group_all_states) if state_value == 'cancel': res[0]['__fold'] = True res[0]['state'] = [state_value, state_name] result.append(res[0]) return result else: return super(event_track, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby) def open_track_speakers_list(self, cr, uid, track_id, context=None): track_id = self.browse(cr, uid, track_id, context=context) return { 'name': _('Speakers'), 'domain': [('id', 'in', [partner.id for partner in track_id.speaker_ids])], 'view_type': 'form', 'view_mode': 'kanban,form', 'res_model': 'res.partner', 'view_id': False, 'type': 'ir.actions.act_window', }
class event_event(models.Model): """Event""" _name = 'event.event' _description = 'Event' _inherit = ['mail.thread', 'ir.needaction_mixin'] _order = 'date_begin' name = fields.Char(string='Event Name', translate=True, required=True, readonly=False, states={'done': [('readonly', True)]}) active = fields.Boolean(default=True, track_visibility="onchange") user_id = fields.Many2one('res.users', string='Responsible', default=lambda self: self.env.user, readonly=False, states={'done': [('readonly', True)]}) company_id = fields.Many2one('res.company', string='Company', change_default=True, default=lambda self: self.env['res.company']. _company_default_get('event.event'), required=False, readonly=False, states={'done': [('readonly', True)]}) organizer_id = fields.Many2one( 'res.partner', string='Organizer', default=lambda self: self.env.user.company_id.partner_id) event_type_id = fields.Many2one('event.type', string='Category', readonly=False, states={'done': [('readonly', True)]}, oldname='type') color = fields.Integer('Kanban Color Index') event_mail_ids = fields.One2many( 'event.mail', 'event_id', string='Mail Schedule', default=lambda self: self._default_event_mail_ids()) @api.model def _default_event_mail_ids(self): return [(0, 0, { 'interval_unit': 'now', 'interval_type': 'after_sub', 'template_id': self.env.ref('event.event_subscription') })] # Seats and computation seats_max = fields.Integer( string='Maximum Attendees Number', oldname='register_max', readonly=True, states={ 'draft': [('readonly', False)], 'confirm': [('readonly', False)] }, help= "For each event you can define a maximum registration of seats(number of attendees), above this numbers the registrations are not accepted." ) seats_availability = fields.Selection([('limited', 'Limited'), ('unlimited', 'Unlimited')], 'Maximum Attendees', required=True, default='unlimited') seats_min = fields.Integer( string='Minimum Attendees', oldname='register_min', help= "For each event you can define a minimum reserved seats (number of attendees), if it does not reach the mentioned registrations the event can not be confirmed (keep 0 to ignore this rule)" ) seats_reserved = fields.Integer(oldname='register_current', string='Reserved Seats', store=True, readonly=True, compute='_compute_seats') seats_available = fields.Integer(oldname='register_avail', string='Maximum Attendees', store=True, readonly=True, compute='_compute_seats') seats_unconfirmed = fields.Integer(oldname='register_prospect', string='Unconfirmed Seat Reservations', store=True, readonly=True, compute='_compute_seats') seats_used = fields.Integer(oldname='register_attended', string='Number of Participants', store=True, readonly=True, compute='_compute_seats') seats_expected = fields.Integer(string='Number of Expected Attendees', readonly=True, compute='_compute_seats') @api.multi @api.depends('seats_max', 'registration_ids.state') def _compute_seats(self): """ Determine reserved, available, reserved but unconfirmed and used seats. """ # initialize fields to 0 for event in self: event.seats_unconfirmed = event.seats_reserved = event.seats_used = event.seats_available = 0 # aggregate registrations by event and by state if self.ids: state_field = { 'draft': 'seats_unconfirmed', 'open': 'seats_reserved', 'done': 'seats_used', } query = """ SELECT event_id, state, count(event_id) FROM event_registration WHERE event_id IN %s AND state IN ('draft', 'open', 'done') GROUP BY event_id, state """ self._cr.execute(query, (tuple(self.ids), )) for event_id, state, num in self._cr.fetchall(): event = self.browse(event_id) event[state_field[state]] += num # compute seats_available for event in self: if event.seats_max > 0: event.seats_available = event.seats_max - ( event.seats_reserved + event.seats_used) event.seats_expected = event.seats_unconfirmed + event.seats_reserved + event.seats_used # Registration fields registration_ids = fields.One2many('event.registration', 'event_id', string='Attendees', readonly=False, states={'done': [('readonly', True)]}) # Date fields date_tz = fields.Selection('_tz_get', string='Timezone', default=lambda self: self.env.user.tz) date_begin = fields.Datetime(string='Start Date', required=True, track_visibility='onchange', states={'done': [('readonly', True)]}) date_end = fields.Datetime(string='End Date', required=True, track_visibility='onchange', states={'done': [('readonly', True)]}) date_begin_located = fields.Datetime(string='Start Date Located', compute='_compute_date_begin_tz') date_end_located = fields.Datetime(string='End Date Located', compute='_compute_date_end_tz') @api.model def _tz_get(self): return [(x, x) for x in pytz.all_timezones] @api.one @api.depends('date_tz', 'date_begin') def _compute_date_begin_tz(self): if self.date_begin: self_in_tz = self.with_context(tz=(self.date_tz or 'UTC')) date_begin = fields.Datetime.from_string(self.date_begin) self.date_begin_located = fields.Datetime.to_string( fields.Datetime.context_timestamp(self_in_tz, date_begin)) else: self.date_begin_located = False @api.one @api.depends('date_tz', 'date_end') def _compute_date_end_tz(self): if self.date_end: self_in_tz = self.with_context(tz=(self.date_tz or 'UTC')) date_end = fields.Datetime.from_string(self.date_end) self.date_end_located = fields.Datetime.to_string( fields.Datetime.context_timestamp(self_in_tz, date_end)) else: self.date_end_located = False state = fields.Selection( [('draft', 'Unconfirmed'), ('cancel', 'Cancelled'), ('confirm', 'Confirmed'), ('done', 'Done')], string='Status', default='draft', readonly=True, required=True, copy=False, help= "If event is created, the status is 'Draft'. If event is confirmed for the particular dates the status is set to 'Confirmed'. If the event is over, the status is set to 'Done'. If event is cancelled the status is set to 'Cancelled'." ) auto_confirm = fields.Boolean(string='Confirmation not required', compute='_compute_auto_confirm') @api.one def _compute_auto_confirm(self): self.auto_confirm = self.env['ir.values'].get_default( 'event.config.settings', 'auto_confirmation') reply_to = fields.Char( 'Reply-To Email', readonly=False, states={'done': [('readonly', True)]}, help= "The email address of the organizer is likely to be put here, with the effect to be in the 'Reply-To' of the mails sent automatically at event or registrations confirmation. You can also put the email address of your mail gateway if you use one." ) address_id = fields.Many2one( 'res.partner', string='Location', default=lambda self: self.env.user.company_id.partner_id, readonly=False, states={'done': [('readonly', True)]}) country_id = fields.Many2one('res.country', 'Country', related='address_id.country_id', store=True) description = fields.Html(string='Description', oldname='note', translate=True, readonly=False, states={'done': [('readonly', True)]}) # badge fields badge_front = fields.Html(string='Badge Front') badge_back = fields.Html(string='Badge Back') badge_innerleft = fields.Html(string='Badge Innner Left') badge_innerright = fields.Html(string='Badge Inner Right') event_logo = fields.Html(string='Event Logo') @api.multi @api.depends('name', 'date_begin', 'date_end') def name_get(self): result = [] for event in self: dates = [ dt.split(' ')[0] for dt in [event.date_begin, event.date_end] if dt ] dates = sorted(set(dates)) result.append( (event.id, '%s (%s)' % (event.name, ' - '.join(dates)))) return result @api.one @api.constrains('seats_max', 'seats_available') def _check_seats_limit(self): if self.seats_availability == 'limited' and self.seats_max and self.seats_available < 0: raise UserError(_('No more available seats.')) @api.one @api.constrains('date_begin', 'date_end') def _check_closing_date(self): if self.date_end < self.date_begin: raise UserError( _('Closing Date cannot be set before Beginning Date.')) @api.model def create(self, vals): res = super(event_event, self).create(vals) if res.organizer_id: res.message_subscribe([res.organizer_id.id]) if res.auto_confirm: res.button_confirm() return res @api.multi def write(self, vals): res = super(event_event, self).write(vals) if vals.get('organizer_id'): self.message_subscribe([vals['organizer_id']]) return res @api.one def button_draft(self): self.state = 'draft' @api.one def button_cancel(self): for event_reg in self.registration_ids: if event_reg.state == 'done': raise UserError( _("You have already set a registration for this event as 'Attended'. Please reset it to draft if you want to cancel this event." )) self.registration_ids.write({'state': 'cancel'}) self.state = 'cancel' @api.one def button_done(self): self.state = 'done' @api.one def button_confirm(self): self.state = 'confirm' @api.onchange('event_type_id') def _onchange_type(self): if self.event_type_id: self.seats_min = self.event_type_id.default_registration_min self.seats_max = self.event_type_id.default_registration_max self.reply_to = self.event_type_id.default_reply_to @api.multi def action_event_registration_report(self): res = self.env['ir.actions.act_window'].for_xml_id( 'event', 'action_report_event_registration') res['context'] = { "search_default_event_id": self.id, "group_by": ['create_date:day'], } return res @api.one def mail_attendees(self, template_id, force_send=False, filter_func=lambda self: True): for attendee in self.registration_ids.filtered(filter_func): self.env['mail.template'].browse(template_id).send_mail( attendee.id, force_send=force_send)
class ServerActions(models.Model): """ Add email option in server actions. """ _name = 'ir.actions.server' _inherit = ['ir.actions.server'] @api.model def _get_states(self): res = super(ServerActions, self)._get_states() res.insert(0, ('email', 'Send Email')) return res email_from = fields.Char('From', related='template_id.email_from', readonly=True) email_to = fields.Char('To (Emails)', related='template_id.email_to', readonly=True) partner_to = fields.Char('To (Partners)', related='template_id.partner_to', readonly=True) subject = fields.Char('Subject', related='template_id.subject', readonly=True) body_html = fields.Html('Body', related='template_id.body_html', readonly=True) template_id = fields.Many2one( 'mail.template', 'Email Template', ondelete='set null', domain="[('model_id', '=', model_id)]", ) @api.onchange('template_id') def on_change_template_id(self): """ Render the raw template in the server action fields. """ if self.template_id and not self.template_id.email_from: raise UserError(_('Your template should define email_from')) @api.model def run_action_email(self, action, eval_context=None): # TDE CLEANME: when going to new api with server action, remove action if not action.template_id or not self._context.get('active_id'): return False action.template_id.send_mail(self._context.get('active_id'), force_send=False, raise_exception=False) return False @api.model def _get_eval_context(self, action=None): """ Override the method giving the evaluation context but also the context used in all subsequent calls. Add the mail_notify_force_send key set to False in the context. This way all notification emails linked to the currently executed action will be set in the queue instead of sent directly. This will avoid possible break in transactions. """ eval_context = super(ServerActions, self)._get_eval_context(action=action) # re-dictify, because eval_context['context'] is a frozendict ctx = dict(eval_context.get('context', {})) ctx['mail_notify_force_send'] = False eval_context['context'] = ctx return eval_context
class Invite(models.TransientModel): """ Wizard to invite partners (or channels) and make them followers. """ _name = 'mail.wizard.invite' _description = 'Invite wizard' @api.model def default_get(self, fields): result = super(Invite, self).default_get(fields) user_name = self.env.user.name_get()[0][1] model = result.get('res_model') res_id = result.get('res_id') if self._context.get('mail_invite_follower_channel_only'): result['send_mail'] = False if 'message' in fields and model and res_id: model_name = self.env['ir.model'].search([ ('model', '=', self.pool[model]._name) ]).name_get()[0][1] document_name = self.env[model].browse(res_id).name_get()[0][1] message = _( '<div><p>Hello,</p><p>%s invited you to follow %s document: %s.</p></div>' ) % (user_name, model_name, document_name) result['message'] = message elif 'message' in fields: result['message'] = _( '<div><p>Hello,</p><p>%s invited you to follow a new document.</p></div>' ) % user_name return result res_model = fields.Char('Related Document Model', required=True, select=1, help='Model of the followed resource') res_id = fields.Integer('Related Document ID', select=1, help='Id of the followed resource') partner_ids = fields.Many2many( 'res.partner', string='Recipients', help= "List of partners that will be added as follower of the current document." ) channel_ids = fields.Many2many( 'mail.channel', string='Channels', help= 'List of channels that will be added as listeners of the current document.', domain=[('channel_type', '=', 'channel')]) message = fields.Html('Message') send_mail = fields.Boolean( 'Send Email', default=True, help= "If checked, the partners will receive an email warning they have been added in the document's followers." ) @api.multi def add_followers(self): email_from = self.env['mail.message']._get_default_from() for wizard in self: Model = self.env[wizard.res_model] document = Model.browse(wizard.res_id) # filter partner_ids to get the new followers, to avoid sending email to already following partners new_partners = wizard.partner_ids - document.message_partner_ids new_channels = wizard.channel_ids - document.message_channel_ids document.message_subscribe(new_partners.ids, new_channels.ids) model_ids = self.env['ir.model'].search([('model', '=', wizard.res_model)]) model_name = model_ids.name_get()[0][1] # send an email if option checked and if a message exists (do not send void emails) if wizard.send_mail and wizard.message and not wizard.message == '<br>': # when deleting the message, cleditor keeps a <br> message = self.env['mail.message'].create({ 'subject': _('Invitation to follow %s: %s') % (model_name, document.name_get()[0][1]), 'body': wizard.message, 'record_name': document.name_get()[0][1], 'email_from': email_from, 'reply_to': email_from, 'model': wizard.res_model, 'res_id': wizard.res_id, 'no_auto_thread': True, }) new_partners.with_context(auto_delete=True)._notify( message, force_send=True, user_signature=True) message.unlink() return {'type': 'ir.actions.act_window_close'}