Example #1
0
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)]})
Example #2
0
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")
Example #3
0
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
Example #4
0
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}
Example #5
0
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]))
Example #6
0
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)
Example #7
0
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()
Example #8
0
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
Example #9
0
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',
        }
Example #10
0
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)
Example #11
0
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
Example #12
0
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'}