blob: 9c5fc1bfe26c6e8714ee8e0af4d8dac8481c69fb [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001# Copyright 2016 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style
3# license that can be found in the LICENSE file or at
4# https://developers.google.com/open-source/licenses/bsd
5
6"""Page and form handlers for project administration "advanced" subtab.
7
8The advanced subtab allows the project to be archived, unarchived, deleted, or
9marked as moved. Site admins can use this page to "doom" a project, which is
10basically archiving it in a way that cannot be reversed by the project owners.
11
12The page also shows project data storage quota and usage values, and
13site admins can edit those quotas.
14"""
15
16from __future__ import division
17from __future__ import print_function
18from __future__ import absolute_import
19
20import logging
21import time
22
23import ezt
24
25from businesslogic import work_env
26from framework import framework_constants
27from framework import framework_helpers
28from framework import permissions
29from framework import servlet
30from framework import template_helpers
31from framework import urls
32from proto import project_pb2
33from tracker import tracker_constants
34
35
36class ProjectAdminAdvanced(servlet.Servlet):
37 """A page with project state options for the Project Owner(s)."""
38
39 _PAGE_TEMPLATE = 'project/project-admin-advanced-page.ezt'
40 _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_ADMIN
41
42 def AssertBasePermission(self, mr):
43 """Make sure that the logged in user has permission to view this page.
44
45 Args:
46 mr: commonly used info parsed from the request.
47 """
48 super(ProjectAdminAdvanced, self).AssertBasePermission(mr)
49 if not self.CheckPerm(mr, permissions.EDIT_PROJECT):
50 raise permissions.PermissionException(
51 'User is not allowed to administer this project')
52
53 def GatherPageData(self, mr):
54 """Build up a dictionary of data values to use when rendering the page.
55
56 Args:
57 mr: commonly used info parsed from the request.
58
59 Returns:
60 Dict of values used by EZT for rendering the "Advanced" subtab.
61 """
62 page_data = {
63 'admin_tab_mode': self.ADMIN_TAB_ADVANCED,
64 }
65 page_data.update(self._GatherPublishingOptions(mr))
66 page_data.update(self._GatherQuotaData(mr))
67
68 return page_data
69
70 def _GatherPublishingOptions(self, mr):
71 """Gather booleans to control the publishing buttons to show in EZT."""
72 state = mr.project.state
73 offer_archive = state != project_pb2.ProjectState.ARCHIVED
74 offer_delete = state == project_pb2.ProjectState.ARCHIVED
75 offer_publish = (
76 state == project_pb2.ProjectState.ARCHIVED and
77 (self.CheckPerm(mr, permissions.PUBLISH_PROJECT) or
78 not mr.project.state_reason))
79 offer_move = state == project_pb2.ProjectState.LIVE
80 offer_doom = self.CheckPerm(mr, permissions.ADMINISTER_SITE)
81 moved_to = mr.project.moved_to or 'http://'
82
83 publishing_data = {
84 'offer_archive': ezt.boolean(offer_archive),
85 'offer_publish': ezt.boolean(offer_publish),
86 'offer_delete': ezt.boolean(offer_delete),
87 'offer_move': ezt.boolean(offer_move),
88 'moved_to': moved_to,
89 'offer_doom': ezt.boolean(offer_doom),
90 'default_doom_reason': framework_constants.DEFAULT_DOOM_REASON,
91 }
92
93 return publishing_data
94
95 def _GatherQuotaData(self, mr):
96 """Gather quota info from backends so that it can be passed to EZT."""
97 offer_quota_editing = self.CheckPerm(mr, permissions.EDIT_QUOTA)
98
99 quota_data = {
100 'offer_quota_editing': ezt.boolean(offer_quota_editing),
101 'attachment_quota': self._BuildAttachmentQuotaData(mr.project),
102 }
103
104 return quota_data
105
106 def _BuildComponentQuota(self, used_bytes, quota_bytes, field_name):
107 """Return an object to easily display quota info in EZT."""
108 if quota_bytes:
109 used_percent = 100 * used_bytes // quota_bytes
110 else:
111 used_percent = 0
112
113 quota_mb = quota_bytes // 1024 // 1024
114
115 return template_helpers.EZTItem(
116 used=template_helpers.BytesKbOrMb(used_bytes),
117 quota_mb=quota_mb,
118 used_percent=used_percent,
119 avail_percent=100 - used_percent,
120 field_name=field_name)
121
122 def _BuildAttachmentQuotaData(self, project):
123 return self._BuildComponentQuota(
124 project.attachment_bytes_used,
125 project.attachment_quota or
126 tracker_constants.ISSUE_ATTACHMENTS_QUOTA_HARD,
127 'attachment_quota_mb')
128
129 def ProcessFormData(self, mr, post_data):
130 """Process the posted form.
131
132 Args:
133 mr: commonly used info parsed from the request.
134 post_data: dictionary of HTML form data.
135
136 Returns:
137 String URL to redirect to after processing is completed.
138 """
139 if 'savechanges' in post_data:
140 self._ProcessQuota(mr, post_data)
141 else:
142 self._ProcessPublishingOptions(mr, post_data)
143
144 if 'deletebtn' in post_data:
145 url = framework_helpers.FormatAbsoluteURL(
146 mr, urls.HOSTING_HOME, include_project=False)
147 else:
148 url = framework_helpers.FormatAbsoluteURL(
149 mr, urls.ADMIN_ADVANCED, saved=1, ts=int(time.time()))
150
151 return url
152
153 def _ProcessQuota(self, mr, post_data):
154 """Process form data to update project quotas."""
155 if not self.CheckPerm(mr, permissions.EDIT_QUOTA):
156 raise permissions.PermissionException(
157 'User is not allowed to change project quotas')
158
159 try:
160 new_attachment_quota = int(post_data['attachment_quota_mb'])
161 new_attachment_quota *= 1024 * 1024
162 except ValueError:
163 mr.errors.attachment_quota = 'Invalid value'
164 self.PleaseCorrect(mr) # Don't echo back the bad input, just start over.
165 return
166
167 with work_env.WorkEnv(mr, self.services) as we:
168 we.UpdateProject(
169 mr.project.project_id, attachment_quota=new_attachment_quota)
170
171 def _ProcessPublishingOptions(self, mr, post_data):
172 """Process form data to update project state."""
173 # Note that EDIT_PROJECT is the base permission for this servlet, but
174 # dooming and undooming projects also requires PUBLISH_PROJECT.
175
176 state = mr.project.state
177
178 with work_env.WorkEnv(mr, self.services) as we:
179 if 'archivebtn' in post_data and not mr.project.delete_time:
180 we.UpdateProject(
181 mr.project.project_id, state=project_pb2.ProjectState.ARCHIVED)
182
183 elif 'deletebtn' in post_data: # Mark the project for immediate deletion.
184 if state != project_pb2.ProjectState.ARCHIVED:
185 raise permissions.PermissionException(
186 'Projects must be archived before being deleted')
187 we.DeleteProject(mr.project_id)
188
189 elif 'doombtn' in post_data: # Go from any state to forced ARCHIVED.
190 if not self.CheckPerm(mr, permissions.PUBLISH_PROJECT):
191 raise permissions.PermissionException(
192 'User is not allowed to doom projects')
193 reason = post_data.get('reason')
194 delete_time = time.time() + framework_constants.DEFAULT_DOOM_PERIOD
195 we.UpdateProject(
196 mr.project.project_id, state=project_pb2.ProjectState.ARCHIVED,
197 state_reason=reason, delete_time=delete_time)
198
199 elif 'publishbtn' in post_data: # Go from any state to LIVE
200 if (mr.project.delete_time and
201 not self.CheckPerm(mr, permissions.PUBLISH_PROJECT)):
202 raise permissions.PermissionException(
203 'User is not allowed to unarchive doomed projects')
204 we.UpdateProject(
205 mr.project.project_id, state=project_pb2.ProjectState.LIVE,
206 state_reason='', delete_time=0, read_only_reason='')
207
208 elif 'movedbtn' in post_data: # Record the moved_to location.
209 if state != project_pb2.ProjectState.LIVE:
210 raise permissions.PermissionException(
211 'This project is not live, no user can move it')
212 moved_to = post_data.get('moved_to', '')
213 we.UpdateProject(mr.project.project_id, moved_to=moved_to)