|
1 | 1 | # Copyright 2025 Cetmix OÜ
|
2 | 2 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
3 | 3 |
|
| 4 | +import re |
| 5 | + |
4 | 6 | from lxml import html
|
5 | 7 |
|
6 | 8 | from odoo import Command, tools
|
@@ -84,6 +86,117 @@ def test_portal_task_search_link_format(self):
|
84 | 86 | self.url_task_code_pattern.format(self.task_1.code)[:-1] + query_params,
|
85 | 87 | )
|
86 | 88 |
|
| 89 | + def test_portal_task_report(self): |
| 90 | + """Test task report generation through portal.""" |
| 91 | + self.authenticate("portal", "portal") |
| 92 | + # Check if hr_timesheet module is installed |
| 93 | + hr_timesheet_installed = bool( |
| 94 | + self.env["ir.module.module"].search( |
| 95 | + [("name", "=", "hr_timesheet"), ("state", "=", "installed")] |
| 96 | + ) |
| 97 | + ) |
| 98 | + response = self.url_open(self.base_url + self.task_1.code + "?report_type=html") |
| 99 | + if hr_timesheet_installed: |
| 100 | + # If hr_timesheet is installed, expect successful response |
| 101 | + # _show_task_report is overridden by hr_timesheet to generate timesheet reports |
| 102 | + self.assertEqual(response.status_code, 200) |
| 103 | + self.assertIn("text/html", response.headers.get("Content-Type", "")) |
| 104 | + else: |
| 105 | + # If hr_timesheet is not installed, expect error response |
| 106 | + # _show_task_report raises MissingError("There is nothing to report.") |
| 107 | + # This method is to be overriden to report timesheets if the module is installed |
| 108 | + self.assertEqual(response.status_code, 400) |
| 109 | + content = response.content |
| 110 | + tree = html.fromstring(content) |
| 111 | + error_elements = tree.xpath( |
| 112 | + "//pre[contains(text(), 'There is nothing to report.')]" |
| 113 | + ) |
| 114 | + self.assertTrue( |
| 115 | + error_elements, |
| 116 | + "Error message 'There is nothing to report.' not found in response", |
| 117 | + ) |
| 118 | + |
| 119 | + def test_portal_task_project_sharing(self): |
| 120 | + """Test project sharing functionality.""" |
| 121 | + self.authenticate("portal", "portal") |
| 122 | + |
| 123 | + # First, access multiple tasks to build up history |
| 124 | + other_task = self.env["project.task"].create( |
| 125 | + { |
| 126 | + "name": "Other Task", |
| 127 | + "project_id": self.task_1.project_id.id, |
| 128 | + } |
| 129 | + ) |
| 130 | + self.url_open(f"{self.base_url}{other_task.code}") |
| 131 | + |
| 132 | + # Share the project with portal user |
| 133 | + project_share_wizard = self.env["project.share.wizard"].create( |
| 134 | + { |
| 135 | + "access_mode": "edit", |
| 136 | + "res_model": "project.project", |
| 137 | + "res_id": self.task_1.project_id.id, |
| 138 | + "partner_ids": [Command.link(self.partner_portal.id)], |
| 139 | + } |
| 140 | + ) |
| 141 | + project_share_wizard.action_send_mail() |
| 142 | + |
| 143 | + # Get the sharing link from the most recent mail message |
| 144 | + message = self.env["mail.message"].search( |
| 145 | + [ |
| 146 | + ("partner_ids", "in", self.partner_portal.id), |
| 147 | + ("model", "=", "project.project"), |
| 148 | + ("res_id", "=", self.task_1.project_id.id), |
| 149 | + ], |
| 150 | + order="id DESC", |
| 151 | + limit=1, |
| 152 | + ) |
| 153 | + |
| 154 | + share_link = str(message.body.split('href="')[1].split('">')[0]) |
| 155 | + match = re.search( |
| 156 | + r"access_token=([^&]+)&pid=([^&]+)&hash=([^&]*)", share_link |
| 157 | + ) |
| 158 | + access_token, pid, _hash = match.groups() |
| 159 | + |
| 160 | + # Access the task with project sharing context |
| 161 | + url = f"{self.base_url}{self.task_1.code}" |
| 162 | + |
| 163 | + # Get initial response to extract CSRF token |
| 164 | + initial_response = self.url_open(url) |
| 165 | + content = initial_response.text |
| 166 | + csrf_token = re.search(r'csrf_token: "([^"]+)"', content).group(1) |
| 167 | + |
| 168 | + # Make the POST request with CSRF token and project_sharing=True |
| 169 | + response = self.url_open( |
| 170 | + url=url, |
| 171 | + data={ |
| 172 | + "csrf_token": csrf_token, |
| 173 | + "access_token": access_token, |
| 174 | + "project_sharing": True, |
| 175 | + "pid": pid, |
| 176 | + "hash": _hash, |
| 177 | + }, |
| 178 | + ) |
| 179 | + |
| 180 | + self.assertEqual(response.status_code, 200) |
| 181 | + |
| 182 | + # Now check if the navigation links for previous/next task are not present |
| 183 | + # This would indicate that the history was reset to only contain the current task |
| 184 | + content = response.content |
| 185 | + tree = html.fromstring(content) |
| 186 | + |
| 187 | + # Check for absence of navigation links (prev/next) |
| 188 | + # which would be present if history had multiple tasks |
| 189 | + prev_links = tree.xpath("//a[contains(@class, 'o_portal_pager_previous')]") |
| 190 | + next_links = tree.xpath("//a[contains(@class, 'o_portal_pager_next')]") |
| 191 | + |
| 192 | + # If history was reset to only current task, there should be no prev/next links |
| 193 | + self.assertFalse( |
| 194 | + prev_links, "Previous task link should not be present if history was reset" |
| 195 | + ) |
| 196 | + self.assertFalse( |
| 197 | + next_links, "Next task link should not be present if history was reset" |
| 198 | + ) |
| 199 | + |
87 | 200 |
|
88 | 201 | @tagged("-at_install", "post_install")
|
89 | 202 | class TestPortalProjectTaskCode(TestProjectPortalCommon, HttpCaseWithUserPortal):
|
|
0 commit comments