From 893a6e4f8ee4f1d3a9ef02b76d2f13f7bffa5a60 Mon Sep 17 00:00:00 2001 From: "Lopes Marc (loma)" Date: Wed, 16 Aug 2023 12:59:42 +0200 Subject: [PATCH] [IMP] website_forum,*: populate with forums, posts and comments * website_slides_forum Populate a database with different quantities of forums, posts and comments.The number of replies per question and comments per post (question or reply) are defined by weights. We use SQL queries in the '_populate' method to update certain fields automatically managed by the ORM, as well as the 'last_activity_date' of posts (questions). The aim is to give posts, replies and comments varied and consistent create_dates, as well as a wide variety of authors (create_uid). Task-3349374 Task-3463533 closes odoo/odoo#132042 Signed-off-by: Florian Charlier (flch) --- addons/website_forum/__init__.py | 1 + addons/website_forum/populate/__init__.py | 6 + addons/website_forum/populate/forum_forum.py | 15 +++ addons/website_forum/populate/forum_post.py | 84 ++++++++++++++ addons/website_forum/populate/mail_message.py | 106 ++++++++++++++++++ addons/website_slides_forum/__init__.py | 3 +- .../website_slides_forum/populate/__init__.py | 3 + .../populate/forum_forum.py | 30 +++++ 8 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 addons/website_forum/populate/__init__.py create mode 100644 addons/website_forum/populate/forum_forum.py create mode 100644 addons/website_forum/populate/forum_post.py create mode 100644 addons/website_forum/populate/mail_message.py create mode 100644 addons/website_slides_forum/populate/__init__.py create mode 100644 addons/website_slides_forum/populate/forum_forum.py diff --git a/addons/website_forum/__init__.py b/addons/website_forum/__init__.py index aa4d0fd63a9..4bf600d50ff 100644 --- a/addons/website_forum/__init__.py +++ b/addons/website_forum/__init__.py @@ -2,3 +2,4 @@ from . import controllers from . import models +from . import populate diff --git a/addons/website_forum/populate/__init__.py b/addons/website_forum/populate/__init__.py new file mode 100644 index 00000000000..ccf52d64ebd --- /dev/null +++ b/addons/website_forum/populate/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import forum_forum +from . import forum_post +from . import mail_message diff --git a/addons/website_forum/populate/forum_forum.py b/addons/website_forum/populate/forum_forum.py new file mode 100644 index 00000000000..65ebe65f9ab --- /dev/null +++ b/addons/website_forum/populate/forum_forum.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models +from odoo.tools import populate + +class Forum(models.Model): + _inherit = 'forum.forum' + _populate_sizes = {'small': 1, 'medium': 3, 'large': 10} + + def _populate_factories(self): + return [ + ('name', populate.constant('Forum_{counter}')), + ('description', populate.constant('This is forum number {counter}')) + ] diff --git a/addons/website_forum/populate/forum_post.py b/addons/website_forum/populate/forum_post.py new file mode 100644 index 00000000000..18d090ccc1a --- /dev/null +++ b/addons/website_forum/populate/forum_post.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import logging +from odoo import fields, models +from odoo.tools import populate + +QA_WEIGHTS = {0: 25, 1: 35, 2: 20, 3: 10, 4: 4, 5: 3, 6: 2, 7: 1} +_logger = logging.getLogger(__name__) + +class Post(models.Model): + _inherit = 'forum.post' + # Include an additional average of 2 post answers for each given size + # e.g.: 100 posts as populated_model_records = ~300 actual forum.posts records + _populate_sizes = {'small': 100, 'medium': 1000, 'large': 90000} + _populate_dependencies = ['forum.forum', 'res.users'] + + def _populate_factories(self): + forum_ids = self.env.registry.populated_models['forum.forum'] + hours = [_ for _ in range(1, 49)] + random = populate.Random('forum_posts') + + def create_answers(values=None, **kwargs): + """Create random number of answers + We set the `last_activity_date` to convert it to `create_date` in `_populate` + as the ORM prevents setting these. + """ + return [ + fields.Command.create({ + 'name': f"reply to {values['name']}", + 'forum_id': values['forum_id'], + 'content': 'Answer content', + 'last_activity_date': fields.Datetime.add(values['last_activity_date'], hours=random.choice(hours)), + }) + for _ in range(random.choices(*zip(*QA_WEIGHTS.items()))[0]) + ] + + def get_last_activity_date(iterator, *args): + days = [_ for _ in range(3, 93)] + now = fields.Datetime.now() + + for values in iterator: + values.update(last_activity_date=fields.Datetime.subtract(now, days=random.choice(days))) + yield values + + return [ + ('forum_id', populate.randomize(forum_ids)), + ('name', populate.constant('post_{counter}')), + ('last_activity_date', get_last_activity_date), # Must be before call to 'create_answers' + ('child_ids', populate.compute(create_answers)), + ] + + def _populate(self, size): + records = super()._populate(size) + user_ids = self.env.registry.populated_models['res.users'] + + # Overwrite auto-fields: use last_activity_date to update create date + _logger.info('forum.post: update create date and uid') + question_ids = tuple(records.ids) + query = """ + SELECT setseed(0.5); + UPDATE forum_post + SET create_date = last_activity_date, + create_uid = floor(random() * (%(max_value)s - %(min_value)s + 1) + %(min_value)s) + WHERE id in %(question_ids)s or parent_id in %(question_ids)s + """ + self.env.cr.execute(query, {'question_ids': question_ids, 'min_value': user_ids[0], 'max_value': user_ids[-1]}) + + _logger.info('forum.post: update last_activity_date of questions with answers') + query = """ + WITH latest_answer AS( + SELECT parent_id, max(last_activity_date) as answer_date + FROM forum_post + WHERE parent_id in %(question_ids)s + GROUP BY parent_id + ) + UPDATE forum_post fp + SET last_activity_date = latest_answer.answer_date + FROM latest_answer + WHERE fp.id = latest_answer.parent_id + """ + self.env.cr.execute(query, {'question_ids': question_ids}) + + return records diff --git a/addons/website_forum/populate/mail_message.py b/addons/website_forum/populate/mail_message.py new file mode 100644 index 00000000000..4d03ebe80bf --- /dev/null +++ b/addons/website_forum/populate/mail_message.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import logging +from odoo import fields, models +from odoo.tools import populate + +CP_WEIGHTS = {1: 35, 2: 30, 3: 25, 4: 10} +_logger = logging.getLogger(__name__) + +class Message(models.Model): + _inherit = 'mail.message' + _populate_dependencies = ['forum.post'] + _populate_sizes = {'small': 100, 'medium': 1000, 'large': 90000} + + def _populate_factories(self): + comment_subtype = self.env.ref('mail.mt_comment') + random = populate.Random('comments_on_forum_posts') + + def get_author_id(iterator, *args): + user_ids = self.env.registry.populated_models['res.users'] + author_ids = self.env['res.users'].search_read([('id', 'in', user_ids)], ['partner_id']) + + for values in iterator: + author_id = random.choice(author_ids) + values.update(author_id=author_id['partner_id'][0]) + yield values + + def get_res_id_and_date(iterator, *args): + """Randomly assign messages to some populated posts and answers. + This makes sure these questions and answers get one to four comments. + Also define a date that is more recent than the post/answer's create_date + """ + question_ids = self.env.registry.populated_models['forum.post'] + posts = self.env['forum.post'].search_read(['|', ('id', 'in', question_ids), ('parent_id', 'in', question_ids)], ['id', 'create_date']) + sample_size = int(len(question_ids) * 0.7) + res_ids = random.sample(posts, sample_size) + nb_comments = 0 + idx = 0 + post_to_comment_id = None + hours = [_ for _ in range(1, 24)] + + for values in iterator: + if nb_comments == 0: + nb_comments = random.choices(*zip(*CP_WEIGHTS.items()))[0] + idx += 1 + post_to_comment_id = res_ids[idx]['id'] + nb_comments -= 1 + values.update({ + 'date': fields.Datetime.add(res_ids[idx]['create_date'], hours=random.choice(hours)), + 'res_id': post_to_comment_id + }) + yield values + + return [ + ('author_id', get_author_id), + ('body', populate.constant('message_body_{counter}')), + ('message_type', populate.constant('comment')), + ('model', populate.constant('forum.post')), + ('_res_id_and_date', get_res_id_and_date), + ('subtype_id', populate.constant(comment_subtype.id)), + ] + + def _populate(self, size): + records = super()._populate(size) + + _logger.info('mail.message: update comments create date and uid') + _logger.info('forum.post: update last_activity_date for posts with comments and/or commented answers') + query = """ + WITH comment_author AS( + SELECT mm.id, mm.author_id, ru.id as user_id, ru.partner_id + FROM mail_message mm + JOIN res_users ru + ON mm.author_id = ru.partner_id + WHERE mm.id in %(comment_ids)s + ), + updated_comments as ( + UPDATE mail_message mm + SET create_date = date, + create_uid = ca.user_id + FROM comment_author ca + WHERE mm.id = ca.id + RETURNING res_id as post_id, create_date as comment_date + ), + max_comment_dates AS ( + SELECT post_id, max(comment_date) as last_comment_date + FROM updated_comments + GROUP BY post_id + ), + updated_posts AS ( + UPDATE forum_post fp + SET last_activity_date = CASE --on questions, answer could be more recent + WHEN fp.parent_id IS NOT NULL THEN greatest(last_activity_date, last_comment_date) + ELSE last_comment_date END + FROM max_comment_dates + WHERE max_comment_dates.post_id = fp.id + RETURNING fp.id as post_id, fp.last_activity_date as last_activity_date, fp.parent_id as parent_id + ) + UPDATE forum_post fp + SET last_activity_date = greatest(fp.last_activity_date, up.last_activity_date) + FROM updated_posts up + WHERE up.parent_id = fp.id + """ + self.env.cr.execute(query, {'comment_ids': tuple(records.ids)}) + + return records diff --git a/addons/website_slides_forum/__init__.py b/addons/website_slides_forum/__init__.py index 8c67f893b37..ca9d16e5409 100644 --- a/addons/website_slides_forum/__init__.py +++ b/addons/website_slides_forum/__init__.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import models from . import controllers +from . import models +from . import populate diff --git a/addons/website_slides_forum/populate/__init__.py b/addons/website_slides_forum/populate/__init__.py new file mode 100644 index 00000000000..2efe347cd8d --- /dev/null +++ b/addons/website_slides_forum/populate/__init__.py @@ -0,0 +1,3 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import forum_forum diff --git a/addons/website_slides_forum/populate/forum_forum.py b/addons/website_slides_forum/populate/forum_forum.py new file mode 100644 index 00000000000..58e329d5919 --- /dev/null +++ b/addons/website_slides_forum/populate/forum_forum.py @@ -0,0 +1,30 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from itertools import zip_longest + +from odoo import models +from odoo.addons.website_slides.populate.slide_channel import SlideChannel + + +class SlidesForum(models.Model): + _inherit = 'forum.forum' + + @property + def _populate_sizes(self): + return {size: count + SlideChannel._populate_sizes[size] for size, count in super()._populate_sizes.items()} + + @property + def _populate_dependencies(self): + return super()._populate_dependencies + ['slide.channel'] + + def _populate_factories(self): + def link_course(iterator, *args, **kwargs): + courses = self.env['slide.channel'].browse(self.env.registry.populated_models['slide.channel']) + for values, course in zip_longest(iterator, courses): + if course: + values.update(slide_channel_ids=course, name=f"{course.name}'s Forum") + yield values + + return super()._populate_factories() + [ + ('_name_and_course', link_course), + ]