From bc8332808faf1aa186b1bf038f4b594678bc3f92 Mon Sep 17 00:00:00 2001 From: "Hugo Carlier (Huca)" Date: Wed, 15 Mar 2023 16:35:20 +0100 Subject: [PATCH] [ADD] todo, note, *: replace Notes with To-do - Impacted modules: - todo (new module) - note (deprecated app) - project - test_discuss_full (adding a dependency) - Main changes: - The Notes app is deprecated - A To-do app is created to replace it - The To-do app uses the model project.task (from module project) for to-dos. To-dos are equivalent to private tasks (tasks without project) with no parent task set. - Private tasks/to-dos can either be displayed in the My Tasks menu of the Project app or in the To-do app that proposes simplified views to edit them. - Details: - depracation of Notes: Before the module note can be fully removed, some changes still need to be done in other modules that use its models (mainly note.note model). This commit deprecate the Notes app in the meantime by removing it from the main menu/dashboard and by replacing the creation of notes directly from the systray by the creation of private tasks (to-dos). - activity: In the systray, it is currently not possible to segregate records using the same model. Therefore to-dos will appear in the task section. This will change in the future when this possibility will be implemented in mail. - name generation: When edited from To-do, a name is auto-genrated for private tasks. This behavior is similar to the one existing in notes with a few adaptation to Todo UX. When a to-do is untitled and saved (through the save button or when leaving the form view), a name is generated based on the first line of the description of this to-do. If the generated name is longer than 100 characters it is truncated. - security: - project.task model need to be accessible in all crud modes to all internal users as it is used in the to-do module. This is a big change compared to the previous behavior where only project users could edit tasks. - project.tags model access is way more permisive now. All users have access to edition/delition of tags as they are use in the to-do app. - Record rules: A similar behavior as the current one is implemented, i.e: - project users can access all tasks (with edition rights) verifying one of the following conditions: - they follow the task - they follow the project the task belongs to - they are assigned to the task - the project the task belongs to is accessible for all internal users. - internal users have only read-access to the non-private tasks shared with them. - Those changes related to security only occurs when the app To-do is installed. When it is note, the current behavior of Project security-wise is kept unchanged. Note on this last point: As the write access of internal users to project.task is now managed with record rules, the form of this model won't be displayed in read-only anymore when To-do is installed. This will results in access error message for the tasks for which the write access is restricted. Unfortunatelly, there is no better solution at the moment as neither restricted read access, nor extendent write access for internal users are desired. An improvement of the framework to deal with such situation could be benificial. task-3085077 --- addons/note/__init__.py | 3 +- addons/note/__manifest__.py | 4 - addons/note/controllers/note.py | 20 -- addons/note/models/__init__.py | 2 - addons/note/models/mail_activity.py | 10 - addons/note/models/res_users.py | 27 -- .../helpers/mock_server/models/res_users.js | 37 --- addons/note/views/note_views.xml | 26 +- addons/project/models/project_task.py | 3 +- addons/test_discuss_full/__manifest__.py | 1 + addons/todo/__init__.py | 11 + addons/todo/__manifest__.py | 35 +++ addons/{note => todo}/controllers/__init__.py | 2 +- addons/todo/controllers/todo.py | 18 ++ .../data/mail_activity_type_data.xml | 0 addons/todo/models/__init__.py | 2 + .../models/mail_activity_type.py | 0 addons/todo/models/project_task.py | 21 ++ addons/todo/security/ir.model.access.csv | 4 + addons/todo/security/todo_security.xml | 37 +++ addons/todo/static/description/icon.png | Bin 0 -> 1127 bytes addons/todo/static/description/icon.svg | 1 + addons/todo/static/src/scss/todo.scss | 40 +++ .../views/todo_form/todo_form_controller.scss | 11 + .../src/web/activity/activity_menu_patch.js | 49 ++-- .../src/web/activity/activity_menu_patch.scss | 0 .../src/web/activity/activity_menu_patch.xml | 18 +- .../static/tests/activity_menu_tests.js | 39 +-- .../mock_server/controller/project_todo.js} | 17 +- .../tests/helpers/model_definitions_setup.js | 2 +- addons/todo/tests/__init__.py | 3 + addons/todo/tests/test_access_rights.py | 20 ++ addons/todo/views/project_task_views.xml | 244 ++++++++++++++++++ addons/todo/views/todo_menus.xml | 9 + 34 files changed, 534 insertions(+), 182 deletions(-) delete mode 100644 addons/note/controllers/note.py delete mode 100644 addons/note/models/mail_activity.py delete mode 100644 addons/note/static/tests/helpers/mock_server/models/res_users.js create mode 100644 addons/todo/__init__.py create mode 100644 addons/todo/__manifest__.py rename addons/{note => todo}/controllers/__init__.py (84%) create mode 100644 addons/todo/controllers/todo.py rename addons/{note => todo}/data/mail_activity_type_data.xml (100%) create mode 100644 addons/todo/models/__init__.py rename addons/{note => todo}/models/mail_activity_type.py (100%) create mode 100644 addons/todo/models/project_task.py create mode 100644 addons/todo/security/ir.model.access.csv create mode 100644 addons/todo/security/todo_security.xml create mode 100644 addons/todo/static/description/icon.png create mode 100644 addons/todo/static/description/icon.svg create mode 100644 addons/todo/static/src/scss/todo.scss create mode 100644 addons/todo/static/src/views/todo_form/todo_form_controller.scss rename addons/{note => todo}/static/src/web/activity/activity_menu_patch.js (52%) rename addons/{note => todo}/static/src/web/activity/activity_menu_patch.scss (100%) rename addons/{note => todo}/static/src/web/activity/activity_menu_patch.xml (64%) rename addons/{note => todo}/static/tests/activity_menu_tests.js (63%) rename addons/{note/static/tests/helpers/mock_server/controller/note.js => todo/static/tests/helpers/mock_server/controller/project_todo.js} (66%) rename addons/{note => todo}/static/tests/helpers/model_definitions_setup.js (73%) create mode 100644 addons/todo/tests/__init__.py create mode 100644 addons/todo/tests/test_access_rights.py create mode 100644 addons/todo/views/project_task_views.xml create mode 100644 addons/todo/views/todo_menus.xml diff --git a/addons/note/__init__.py b/addons/note/__init__.py index 3ea0f0cac45..dc5e6b693d1 100644 --- a/addons/note/__init__.py +++ b/addons/note/__init__.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import controllers -from . import models \ No newline at end of file +from . import models diff --git a/addons/note/__manifest__.py b/addons/note/__manifest__.py index 9036a7a9869..45d535c2c9b 100644 --- a/addons/note/__manifest__.py +++ b/addons/note/__manifest__.py @@ -14,7 +14,6 @@ 'data': [ 'security/note_security.xml', 'security/ir.model.access.csv', - 'data/mail_activity_type_data.xml', 'data/note_data.xml', 'data/res_users_data.xml', 'views/note_views.xml', @@ -28,9 +27,6 @@ 'web.assets_backend': [ 'note/static/src/**/*', ], - 'web.qunit_suite_tests': [ - 'note/static/tests/**/*', - ], }, 'license': 'LGPL-3', } diff --git a/addons/note/controllers/note.py b/addons/note/controllers/note.py deleted file mode 100644 index 24db9d49e96..00000000000 --- a/addons/note/controllers/note.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import http -from odoo.http import request - - -class NoteController(http.Controller): - - @http.route('/note/new', type='json', auth='user') - def note_new_from_systray(self, note, activity_type_id=None, date_deadline=None): - """ Route to create note and their activity directly from the systray """ - note = request.env['note.note'].create({'memo': note}) - if date_deadline: - note.activity_schedule( - activity_type_id=activity_type_id or request.env['mail.activity.type'].search([('category', '=', 'reminder')], limit=1).id, - note=note.memo, - date_deadline=date_deadline - ) - return note.id diff --git a/addons/note/models/__init__.py b/addons/note/models/__init__.py index 11615bf4301..5c651fca9a0 100644 --- a/addons/note/models/__init__.py +++ b/addons/note/models/__init__.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import mail_activity -from . import mail_activity_type from . import note from . import res_users diff --git a/addons/note/models/mail_activity.py b/addons/note/models/mail_activity.py deleted file mode 100644 index 0b03a60ff5f..00000000000 --- a/addons/note/models/mail_activity.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import models, fields - - -class MailActivity(models.Model): - _inherit = "mail.activity" - - note_id = fields.Many2one('note.note', string="Related Note", ondelete='cascade') diff --git a/addons/note/models/res_users.py b/addons/note/models/res_users.py index 5e1e9a772fc..62be55b4a33 100644 --- a/addons/note/models/res_users.py +++ b/addons/note/models/res_users.py @@ -45,30 +45,3 @@ GROUP BY id""" stage.sudo().copy(default={'user_id': user.id}) else: _logger.debug("Created note columns for %s", self) - - @api.model - def systray_get_activities(self): - """ If user have not scheduled any note, it will not appear in activity menu. - Making note activity always visible with number of notes on label. If there is no notes, - activity menu not visible for note. - """ - activities = super(Users, self).systray_get_activities() - notes_count = self.env['note.note'].search_count([('user_id', '=', self.env.uid)]) - if notes_count: - note_index = next((index for (index, a) in enumerate(activities) if a["model"] == "note.note"), None) - note_label = _('Notes') - if note_index is not None: - activities[note_index]['name'] = note_label - else: - activities.append({ - 'id': self.env['ir.model']._get('note.note').id, - 'type': 'activity', - 'name': note_label, - 'model': 'note.note', - 'icon': modules.module.get_module_icon(self.env['note.note']._original_module), - 'total_count': 0, - 'today_count': 0, - 'overdue_count': 0, - 'planned_count': 0 - }) - return activities diff --git a/addons/note/static/tests/helpers/mock_server/models/res_users.js b/addons/note/static/tests/helpers/mock_server/models/res_users.js deleted file mode 100644 index 03192e1d044..00000000000 --- a/addons/note/static/tests/helpers/mock_server/models/res_users.js +++ /dev/null @@ -1,37 +0,0 @@ -/** @odoo-module **/ - -// ensure mail override is applied first. -import '@mail/../tests/helpers/mock_server/models/res_users'; - -import { patch } from '@web/core/utils/patch'; -import { MockServer } from '@web/../tests/helpers/mock_server'; - -patch(MockServer.prototype, 'note/models/res_users', { - /** - * Simulates `systray_get_activities` on `res.users`. - * - * @override - */ - _mockResUsersSystrayGetActivities() { - const activities = this._super(...arguments); - const noteCount = this.pyEnv['note.note'].searchCount([['user_id', '=', this.currentUserId]]); - if (noteCount) { - const noteIndex = activities.findIndex(act => act['model'] === 'note.note'); - if (noteIndex) { - activities[noteIndex]['name'] = 'Notes'; - } else { - activities.push({ - id: 'note.note', // for simplicity - type: 'activity', - name: 'Notes', - model: 'note.note', - planned_count: 0, - today_count: 0, - overdue_count: 0, - total_count: 0, - }); - } - } - return activities; - }, -}); diff --git a/addons/note/views/note_views.xml b/addons/note/views/note_views.xml index 582cdb628a5..0527d5db5d4 100644 --- a/addons/note/views/note_views.xml +++ b/addons/note/views/note_views.xml @@ -171,6 +171,9 @@ +
+

The Notes app is deprecated and has been replaced by Todo. All your existing notes have been converted to to-dos that you will find in this new app. The Notes app will be removed in the future. It is therefore adviced to not use it anymore as all the notes that would be created/modified in it will be lost during future upgrade.

+
@@ -225,27 +228,4 @@
- - - - - - - diff --git a/addons/project/models/project_task.py b/addons/project/models/project_task.py index fc54bc051bb..4a8b9d12a1e 100644 --- a/addons/project/models/project_task.py +++ b/addons/project/models/project_task.py @@ -91,7 +91,8 @@ class Task(models.Model): @api.model def _default_personal_stage_type_id(self): - return self.env['project.task.type'].search([('user_id', '=', self.env.user.id)], limit=1).id + default_id = self.env.context.get('default_personal_stage_type_ids') + return (default_id or self.env['project.task.type'].search([('user_id', '=', self.env.user.id)], limit=1).ids or [False])[0] @api.model def _default_company_id(self): diff --git a/addons/test_discuss_full/__manifest__.py b/addons/test_discuss_full/__manifest__.py index 7e9acd93eeb..eb69c9871a1 100644 --- a/addons/test_discuss_full/__manifest__.py +++ b/addons/test_discuss_full/__manifest__.py @@ -17,6 +17,7 @@ 'mail', 'mail_bot', 'note', + 'project_todo', 'website_livechat', ], 'installable': True, diff --git a/addons/todo/__init__.py b/addons/todo/__init__.py new file mode 100644 index 00000000000..c50aeb7438d --- /dev/null +++ b/addons/todo/__init__.py @@ -0,0 +1,11 @@ +from . import controllers +from . import models + +def _todo_uninstall(env): + # The record rule project.task_visibility_rule needs to apply to all rights and not just read after uninstallation + project_task_visibility_rule_rec = env.ref("project.task_visibility_rule") + project_task_visibility_rule_rec.write({ + 'perm_create': True, + 'perm_unlink': True, + 'perm_write': True, + }) diff --git a/addons/todo/__manifest__.py b/addons/todo/__manifest__.py new file mode 100644 index 00000000000..ba50ea0aaf5 --- /dev/null +++ b/addons/todo/__manifest__.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'To-Do', + 'version': '1.0', + 'category': 'Productivity/To-Do', + 'summary': 'Organize your work with memos and to-do lists', + 'sequence': 260, + 'depends': [ + 'project', + ], + 'auto_install': True, + 'data': [ + 'security/ir.model.access.csv', + 'security/todo_security.xml', + 'data/mail_activity_type_data.xml', + 'views/project_task_views.xml', + 'views/todo_menus.xml', + ], + 'installable': True, + 'application': True, + 'uninstall_hook': '_todo_uninstall', + 'assets': { + 'web.assets_backend': [ + 'todo/static/src/scss/todo.scss', + 'todo/static/src/views/**/*', + 'todo/static/src/web/**/*', + ], + 'web.qunit_suite_tests': [ + 'todo/static/tests/**/*', + ], + }, + 'license': 'LGPL-3', +} diff --git a/addons/note/controllers/__init__.py b/addons/todo/controllers/__init__.py similarity index 84% rename from addons/note/controllers/__init__.py rename to addons/todo/controllers/__init__.py index 4d2e8b5ad27..d6a5545a3da 100644 --- a/addons/note/controllers/__init__.py +++ b/addons/todo/controllers/__init__.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import note \ No newline at end of file +from . import todo diff --git a/addons/todo/controllers/todo.py b/addons/todo/controllers/todo.py new file mode 100644 index 00000000000..8d69d4cec70 --- /dev/null +++ b/addons/todo/controllers/todo.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.http import Controller, request, route + +class TodoController(Controller): + + @route('/project_todo/new', type='json', auth='user') + def todo_new_from_systray(self, todo_description, activity_type_id=None, date_deadline=None): + """ Route to create todo and their activity directly from the systray """ + todo = request.env['project.task'].create({'description': todo_description}) + if date_deadline: + todo.activity_schedule( + activity_type_id=activity_type_id or request.env['mail.activity.type'].search([('category', '=', 'reminder')], limit=1).id, + note=todo.description, + date_deadline=date_deadline + ) + return todo.id diff --git a/addons/note/data/mail_activity_type_data.xml b/addons/todo/data/mail_activity_type_data.xml similarity index 100% rename from addons/note/data/mail_activity_type_data.xml rename to addons/todo/data/mail_activity_type_data.xml diff --git a/addons/todo/models/__init__.py b/addons/todo/models/__init__.py new file mode 100644 index 00000000000..9df39df10f8 --- /dev/null +++ b/addons/todo/models/__init__.py @@ -0,0 +1,2 @@ +from . import mail_activity_type +from . import project_task diff --git a/addons/note/models/mail_activity_type.py b/addons/todo/models/mail_activity_type.py similarity index 100% rename from addons/note/models/mail_activity_type.py rename to addons/todo/models/mail_activity_type.py diff --git a/addons/todo/models/project_task.py b/addons/todo/models/project_task.py new file mode 100644 index 00000000000..00f014b4c58 --- /dev/null +++ b/addons/todo/models/project_task.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models, _ +from odoo.tools import html2plaintext + +class Task(models.Model): + _inherit = 'project.task' + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if not vals.get('name') and not vals.get('project_id') and not vals.get('parent_id'): + if vals.get('description'): + # Generating name from first line of the description + text = html2plaintext(vals['description']) + name = text.strip().replace('*', '').partition("\n")[0] + vals['name'] = (name[:97] + '...') if len(name) > 100 else name + else: + vals['name'] = _('Untitled to-do') + return super().create(vals_list) diff --git a/addons/todo/security/ir.model.access.csv b/addons/todo/security/ir.model.access.csv new file mode 100644 index 00000000000..452e7f160e7 --- /dev/null +++ b/addons/todo/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_project_task_type_user,project.task.type.user,project.model_project_task_type,base.group_user,1,1,1,1 +access_task_on_partner,project.task on partners,project.model_project_task,base.group_user,1,1,1,1 +access_project_tags_user,project.project_tags_user,project.model_project_tags,base.group_user,1,1,1,1 diff --git a/addons/todo/security/todo_security.xml b/addons/todo/security/todo_security.xml new file mode 100644 index 00000000000..ec80ae8bea4 --- /dev/null +++ b/addons/todo/security/todo_security.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + Project/Task: employees: Full access to own private task only + + [('project_id', '=', False), ('user_ids', 'in', user.id), ('parent_id', '=', False)] + + + + + Project/Task: project users: follow required for follower-only projects + + [ + '|', + '&', + ('project_id', '!=', False), + '|', + ('project_id.privacy_visibility', '!=', 'followers'), + ('project_id.message_partner_ids', 'in', [user.partner_id.id]), + '|', + ('message_partner_ids', 'in', [user.partner_id.id]), + # to subscribe check access to the record, follower is not enough at creation + ('user_ids', 'in', user.id) + ] + + + + + diff --git a/addons/todo/static/description/icon.png b/addons/todo/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..916b9717cb72077b2556f51ea48b45b8190c71cc GIT binary patch literal 1127 zcmV-t1ep7YP)@l zDphkOQg9kbX)s!NE?9OGLu4mYau`QvGF*8!UwkZAbrnTsAx~}}Ol%!XYa>u^AWm&K zVSO7)Y7o)5G+lZW(z!Zfel=cuK4pMBV}B4qV+_!?52k?&q<;y}whljG9jk~Ms)icV zyBE{C4bitFt&A3^g$K{ICa;YXr-LA@iU*^81kbcAkcJY$qzbN#3ap4GP84AP001O( zQchCh)xIG{3!fU{CMrbF7s+BB`D)>EKSFniC+s8oz0 z5mquV3eJ`6A&pZqB+@Y}VZ%cjgs_sD)&s1en>1M=BS0IEgkhU>0$BuV)K4kN2h2z& zM#Y}HK){AUSTP9HDAk3Mx{^!`eeP{pB)}S_l0@t&l7ckuuu;0ccf3(FMbx;&v|3<; zkyI0`@vulS)3WD&A8FKzdRq2z;SGv2ttQr}aT9$d3IQ9v9yd`B*fzl%vPqVPD3U^; zM#M$bO6>0y*6^c<5T^;&@FO)YB6Jn3u|tMNbP>GKwMFbsutv8lHj*k*K^wmBBKNRH zSFFSsb-dB<6XL7{Y;X~7QZooo3urvuiTgr{{K$TR!N#+g5H3Q7MZm^@47CzrNCj;O z#82%VX}lN=xQTthc*7G$Ool{+QlcOYj|{UCUr&n-I1;nLNUg^R+VG@7h}pX}@JQF} zT^e{Kx%MI%c%&a+o7OuIkLXj>KUmLyQs(pda=H9UnNELDE-t=M#^dqoo6c-D`>gUw z3i9{;Yfx*8GSOo?KI_4??8i^Q>34i9PPhH#DX t)g=jT2R0ULtSN8ae)RI>$&=^5&L6r3k*uz!lL!C+002ovPDHLkV1i;1?5zL* literal 0 HcmV?d00001 diff --git a/addons/todo/static/description/icon.svg b/addons/todo/static/description/icon.svg new file mode 100644 index 00000000000..6b7a2a1ec8a --- /dev/null +++ b/addons/todo/static/description/icon.svg @@ -0,0 +1 @@ + diff --git a/addons/todo/static/src/scss/todo.scss b/addons/todo/static/src/scss/todo.scss new file mode 100644 index 00000000000..469a67aa3d4 --- /dev/null +++ b/addons/todo/static/src/scss/todo.scss @@ -0,0 +1,40 @@ +.o_todo_form_view { + .o_form_sheet_bg { + display: flex; + flex-direction: column; + .o_form_statusbar { + margin: 0; + .o_field_widget { + align-self: center !important; + } + } + .o_form_sheet { + flex: 1; + max-width: 100%; + .oe_pad { + flex: 1; + margin-bottom: 0; + display: flex; + flex-direction: column; + .oe_pad_content { + border: none; + flex: 1; + } + } + } + .o_field_html .note-editable { + border: none !important; + padding: $o-sheet-vpadding $o-horizontal-padding 10px !important; + min-height: 300px; + } + } + + &.o_form_readonly { + .oe_description { + padding: $o-sheet-vpadding $o-horizontal-padding; + } + } +} +.o_todo_kanban_card_body { + padding-left: 9px; +} diff --git a/addons/todo/static/src/views/todo_form/todo_form_controller.scss b/addons/todo/static/src/views/todo_form/todo_form_controller.scss new file mode 100644 index 00000000000..c7fd3eaf592 --- /dev/null +++ b/addons/todo/static/src/views/todo_form/todo_form_controller.scss @@ -0,0 +1,11 @@ +.o_form_view.o_xxl_form_view { + + .o_todo_form_sheet_bg { + + > .o_form_statusbar, > .alert { + margin-left: 0; + margin-right: 0; + } + + } +} diff --git a/addons/note/static/src/web/activity/activity_menu_patch.js b/addons/todo/static/src/web/activity/activity_menu_patch.js similarity index 52% rename from addons/note/static/src/web/activity/activity_menu_patch.js rename to addons/todo/static/src/web/activity/activity_menu_patch.js index 474dadf10e7..aa8b1832495 100644 --- a/addons/note/static/src/web/activity/activity_menu_patch.js +++ b/addons/todo/static/src/web/activity/activity_menu_patch.js @@ -7,63 +7,68 @@ import { DateTimeInput } from "@web/core/datetime/datetime_input"; import { useService } from "@web/core/utils/hooks"; import { patch } from "@web/core/utils/patch"; -patch(ActivityMenu.prototype, "note", { +patch(ActivityMenu.prototype, "todo", { setup() { this._super(...arguments); this.rpc = useService("rpc"); - this.state = useState({ addingNote: false }); - this.noteInputRef = useRef("noteInput"); - this.addingNoteDate = false; + this.state = useState({ addingTodo: false }); + this.todoInputRef = useRef("todoInput"); + this.addingTodoDate = false; + this.notification = useService("notification"); useEffect( - (addingNote) => { - if (addingNote) { - this.noteInputRef.el.focus(); + (addingTodo) => { + if (addingTodo) { + this.todoInputRef.el.focus(); } }, - () => [this.state.addingNote] + () => [this.state.addingTodo] ); - onExternalClick("noteInput", (ev) => { + onExternalClick("todoInput", (ev) => { if ( ev.target.closest(".o-mail-ActivityMenu-show") || ev.target.closest(".o_datetime_picker") ) { return; } - this.state.addingNote = false; + this.state.addingTodo = false; }); }, sortActivityGroups() { this._super(); this.store.activityGroups.sort((g1, g2) => { - if (g1.model === "note.note" ? true : false) { + if (g1.model === "project.task" ? true : false) { return -1; } - if (g2.model === "note.note" ? true : false) { + if (g2.model === "project.task" ? true : false) { return 1; } }); }, - onKeydownNoteInput(ev) { + onKeydownTodoInput(ev) { if (ev.key === "Enter") { - this.saveNote(); + this.saveTodo(); } }, - async saveNote() { + async saveTodo() { const { DateTime } = luxon; const urlRegExp = /http(s)?:\/\/(www\.)?[a-zA-Z0-9@:%_+~#=~#?&/=\-;!.]{3,2000}/g; - const note = this.noteInputRef.el.value.replace(urlRegExp, '$&').trim(); - if (!note) { + const todo = this.todoInputRef.el.value.replace(urlRegExp, '$&').trim(); + if (!todo) { return; } - await this.rpc("/note/new", { - note: note, - date_deadline: this.addingNoteDate ? this.addingNoteDate : DateTime.local(), + await this.rpc("/project_todo/new", { + todo_description: todo, + date_deadline: this.addingTodoDate ? this.addingTodoDate : DateTime.local(), }); - this.state.addingNote = false; - this.fetchSystrayActivities(); + document.body.click(); // hack to close dropdown + this.notification.add( + this.env._t("Your to-do has been successfully added to your tasks and scheduled for completion."), + { type: "success" }, + ); + this.state.addingTodo = false; }, }); diff --git a/addons/note/static/src/web/activity/activity_menu_patch.scss b/addons/todo/static/src/web/activity/activity_menu_patch.scss similarity index 100% rename from addons/note/static/src/web/activity/activity_menu_patch.scss rename to addons/todo/static/src/web/activity/activity_menu_patch.scss diff --git a/addons/note/static/src/web/activity/activity_menu_patch.xml b/addons/todo/static/src/web/activity/activity_menu_patch.xml similarity index 64% rename from addons/note/static/src/web/activity/activity_menu_patch.xml rename to addons/todo/static/src/web/activity/activity_menu_patch.xml index f23e8c63e94..339a444a284 100644 --- a/addons/note/static/src/web/activity/activity_menu_patch.xml +++ b/addons/todo/static/src/web/activity/activity_menu_patch.xml @@ -2,26 +2,26 @@ -
- +
+
-
- Addnote +
+ AddTodo
- Add a note + Add a To-do
- + - +
diff --git a/addons/note/static/tests/activity_menu_tests.js b/addons/todo/static/tests/activity_menu_tests.js similarity index 63% rename from addons/note/static/tests/activity_menu_tests.js rename to addons/todo/static/tests/activity_menu_tests.js index 7f76c4d67fe..cf0ef07b5bf 100644 --- a/addons/note/static/tests/activity_menu_tests.js +++ b/addons/todo/static/tests/activity_menu_tests.js @@ -5,13 +5,13 @@ import { getFixture, editInput, patchDate } from "@web/../tests/helpers/utils"; let target; -QUnit.module("note activity menu", { +QUnit.module("todo activity menu", { async beforeEach() { target = getFixture(); }, }); -QUnit.test("create note from activity menu without date", async function (assert) { +QUnit.test("create todo from activity menu without date", async function (assert) { await start(); assert.containsOnce(target, ".o_menu_systray i[aria-label='Activities']"); assert.containsNone(target, ".o-mail-ActivityMenu-counter"); @@ -20,28 +20,34 @@ QUnit.test("create note from activity menu without date", async function (assert target, ".o-mail-ActivityMenu:contains(Congratulations, you're done with your activities.)" ); - assert.containsOnce(target, "button:contains(Add new note)"); - await click("button:contains(Add new note)"); - assert.containsNone(target, "button:contains(Add new note)"); + assert.containsOnce(target, "button:contains(Add a To-do)"); + await click("button:contains(Add a To-do)"); + assert.containsNone(target, "button:contains(Add a To-do)"); assert.containsOnce(target, ".o-mail-ActivityMenu-show"); - await editInput(target, "input.o-mail-ActivityMenu-input", "New Note"); + await editInput(target, "input.o-mail-ActivityMenu-input", "New To-do"); await click("button:contains(SAVE)"); + // Need to reopen systray as it automatically closes when a todo is created + assert.containsOnce(target, ".o_menu_systray i[aria-label='Activities']"); + await click(".o_menu_systray i[aria-label='Activities']"); assert.containsOnce(target, ".o-mail-ActivityMenu-counter:contains(1)"); assert.containsOnce(target, ".o-mail-ActivityGroup"); - assert.containsOnce(target, ".o-mail-ActivityGroup:contains(note.note)"); + assert.containsOnce(target, ".o-mail-ActivityGroup:contains(project.task)"); assert.containsOnce(target, "button:contains(1 Today)"); - assert.containsOnce(target, "button:contains(Add new note)"); + assert.containsOnce(target, "button:contains(Add a To-do)"); assert.containsNone(target, ".o-mail-ActivityMenu-show"); assert.containsNone(target, ".o-mail-ActivityMenu-input"); - await click("button:contains(Add new note)"); - assert.containsNone(target, "button:contains(Add new note)"); - await editInput(target, "input.o-mail-ActivityMenu-input", "New Note"); + await click("button:contains(Add a To-do)"); + assert.containsNone(target, "button:contains(Add a To-do)"); + await editInput(target, "input.o-mail-ActivityMenu-input", "New To-do"); await click("button:contains(SAVE)"); + // Need to reopen systray as it automatically closes when a todo is created + assert.containsOnce(target, ".o_menu_systray i[aria-label='Activities']"); + await click(".o_menu_systray i[aria-label='Activities']"); assert.containsOnce(target, ".o-mail-ActivityMenu-counter:contains(2)"); assert.containsOnce(target, "button:contains(2 Today)"); }); -QUnit.test("create note from activity menu with date", async function (assert) { +QUnit.test("create to-do from activity menu with date", async function (assert) { patchDate(2023, 1, 1, 6, 0, 0); const { DateTime } = luxon; const today = DateTime.utc(); @@ -51,16 +57,19 @@ QUnit.test("create note from activity menu with date", async function (assert) { assert.containsNone(target, ".o-mail-ActivityMenu-counter"); await click(".o_menu_systray i[aria-label='Activities']"); - await click("button:contains(Add new note)"); - await editInput(target, "input.o-mail-ActivityMenu-input", "New Note"); + await click("button:contains(Add a To-do)"); + await editInput(target, "input.o-mail-ActivityMenu-input", "New To-do"); await editInput( target, "input.o_datetime_input", futureDay.toString(luxon.DateTime.DATE_SHORT) ); await click("button:contains(SAVE)"); + // Need to reopen systray as it automatically closes when a todo is created + assert.containsOnce(target, ".o_menu_systray i[aria-label='Activities']"); + await click(".o_menu_systray i[aria-label='Activities']"); assert.containsOnce(target, ".o-mail-ActivityMenu-counter:contains(1)"); assert.containsOnce(target, ".o-mail-ActivityGroup"); - assert.containsOnce(target, ".o-mail-ActivityGroup:contains(note.note)"); + assert.containsOnce(target, ".o-mail-ActivityGroup:contains(project.task)"); assert.containsOnce(target, "button:contains(1 Future)"); }); diff --git a/addons/note/static/tests/helpers/mock_server/controller/note.js b/addons/todo/static/tests/helpers/mock_server/controller/project_todo.js similarity index 66% rename from addons/note/static/tests/helpers/mock_server/controller/note.js rename to addons/todo/static/tests/helpers/mock_server/controller/project_todo.js index 2e92a160a9b..8d622de50c6 100644 --- a/addons/note/static/tests/helpers/mock_server/controller/note.js +++ b/addons/todo/static/tests/helpers/mock_server/controller/project_todo.js @@ -5,7 +5,7 @@ import { MockServer } from '@web/../tests/helpers/mock_server'; import { date_to_str } from 'web.time'; -patch(MockServer.prototype, 'note/controller/note', { +patch(MockServer.prototype, 'project_todo/controller/project_todo', { //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- @@ -14,8 +14,8 @@ patch(MockServer.prototype, 'note/controller/note', { * @override */ async _performRPC(route, args) { - if (route === '/note/new') { - return this._mockRouteNoteNew(args); + if (route === '/project_todo/new') { + return this._mockRouteTodoNew(args); } return this._super(...arguments); }, @@ -25,17 +25,18 @@ patch(MockServer.prototype, 'note/controller/note', { //-------------------------------------------------------------------------- /** - * Simulates the `/note/new` route. + * Simulates the `/project_todo/new` route. * * @private */ - _mockRouteNoteNew(values) { - const noteId = this.pyEnv['note.note'].create({ memo: values['note'] }); + _mockRouteTodoNew(values) { + const taskId = this.pyEnv['project.task'].create({ description: values['todo_description'] }); if (values['date_deadline']) { this.pyEnv['mail.activity'].create({ date_deadline: date_to_str(new Date(values['date_deadline'])), - note_id: noteId, - res_model: 'note.note', + note: values['todo_description'], + res_model: 'project.task', + res_id: taskId, }); } }, diff --git a/addons/note/static/tests/helpers/model_definitions_setup.js b/addons/todo/static/tests/helpers/model_definitions_setup.js similarity index 73% rename from addons/note/static/tests/helpers/model_definitions_setup.js rename to addons/todo/static/tests/helpers/model_definitions_setup.js index 9411e568f60..d0fb9c5300a 100644 --- a/addons/note/static/tests/helpers/model_definitions_setup.js +++ b/addons/todo/static/tests/helpers/model_definitions_setup.js @@ -2,4 +2,4 @@ import { addModelNamesToFetch } from '@bus/../tests/helpers/model_definitions_helpers'; -addModelNamesToFetch(['note.note']); +addModelNamesToFetch(['project.task']); diff --git a/addons/todo/tests/__init__.py b/addons/todo/tests/__init__.py new file mode 100644 index 00000000000..af79c315e2d --- /dev/null +++ b/addons/todo/tests/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import test_access_rights diff --git a/addons/todo/tests/test_access_rights.py b/addons/todo/tests/test_access_rights.py new file mode 100644 index 00000000000..81f1a8d4ca9 --- /dev/null +++ b/addons/todo/tests/test_access_rights.py @@ -0,0 +1,20 @@ +from odoo.tests.common import users +from odoo.exceptions import AccessError +from odoo.addons.project.tests.test_access_rights import TestAccessRights + +class TestAccessRightsTodo(TestAccessRights): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.private_task = cls.env['project.task'].create({'name': 'OdooBot Private Task'}) + + @users('Internal user') + def test_internal_cannot_rud_private_task(self): + with self.assertRaises(AccessError): + self.private_task.with_user(self.env.user).write({'name': 'Test write'}) + + with self.assertRaises(AccessError): + self.private_task.with_user(self.env.user).unlink() + + with self.assertRaises(AccessError): + self.private_task.with_user(self.env.user).read(['name']) diff --git a/addons/todo/views/project_task_views.xml b/addons/todo/views/project_task_views.xml new file mode 100644 index 00000000000..b562fe527df --- /dev/null +++ b/addons/todo/views/project_task_views.xml @@ -0,0 +1,244 @@ + + + + + + project.task.kanban + project.task + 800 + + + + + + + + + + + + + + + Delete +
    + + + +
    +
    +
    +
    +
    +
    + +
    +
    + + + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    + + + + + + + + + project.task.todo.tree + project.task + + + + + + + + + + + + + + project.task.view.todo.form + project.task + +
    +
    +
    + +
    +
    + +
    + + +
    + + + + +
    + + + +
    +
    +
    +
    + + + + project.task.view.todo.quick.create.todo + project.task + 1000 + +
    + + + +
    +
    +
    + + + + project.task.view.todo.activity + project.task + 1000 + + + + +
    +
    + + + +
    + +
    +
    +
    +
    +
    + + + + project.task.view.todo.search + project.task + 1000 + + + + + + + + + + + + + + + + + + + + + + + + + + To-dos + project.task + [('user_ids', 'in', [uid]), ('project_id', '=', False), ('parent_id', '=', False)] + kanban,form,tree,activity + + {'search_default_open_tasks': 1, 'tree_view_ref': 'todo.project_task_view_todo_tree'} + +

    + No to-do found. Let's create one! +

    + Keep your work organized by using memos and to-do lists. + Your to-do items are private by default, but you can choose to share them with others by adding them as assignees. +

    +
    +
    + + + kanban + + + + + + form + + + + + + tree + + + + + + activity + + + + + diff --git a/addons/todo/views/todo_menus.xml b/addons/todo/views/todo_menus.xml new file mode 100644 index 00000000000..9362e720e4f --- /dev/null +++ b/addons/todo/views/todo_menus.xml @@ -0,0 +1,9 @@ + + + + +