blob: 042b198c6a2219f27e28fd8c9ec55a261d88bc54 [file] [log] [blame]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001# Copyright 2016 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
Copybara854996b2021-09-07 19:36:02 +00004
5"""Servlets for project administration main subtab."""
6from __future__ import print_function
7from __future__ import division
8from __future__ import absolute_import
9
Copybara854996b2021-09-07 19:36:02 +000010import time
11
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020012from google.cloud import exceptions
Copybara854996b2021-09-07 19:36:02 +000013from six import string_types
Copybara854996b2021-09-07 19:36:02 +000014import ezt
15
16from businesslogic import work_env
17from framework import emailfmt
Copybara854996b2021-09-07 19:36:02 +000018from framework import framework_helpers
19from framework import gcs_helpers
20from framework import permissions
21from framework import servlet
22from framework import urls
23from framework import validate
24from project import project_helpers
25from project import project_views
26from tracker import tracker_views
27
28
29_MSG_INVALID_EMAIL_ADDRESS = 'Invalid email address'
30_MSG_DESCRIPTION_MISSING = 'Description is missing'
31_MSG_SUMMARY_MISSING = 'Summary is missing'
32
33
34class ProjectAdmin(servlet.Servlet):
35 """A page with project configuration options for the Project Owner(s)."""
36
37 _PAGE_TEMPLATE = 'project/project-admin-page.ezt'
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010038 _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_ADMIN
Copybara854996b2021-09-07 19:36:02 +000039
40 def AssertBasePermission(self, mr):
41 super(ProjectAdmin, self).AssertBasePermission(mr)
42 if not self.CheckPerm(mr, permissions.EDIT_PROJECT):
43 raise permissions.PermissionException(
44 'User is not allowed to administer this project')
45
46 def GatherPageData(self, mr):
47 """Build up a dictionary of data values to use when rendering the page."""
48 available_access_levels = project_helpers.BuildProjectAccessOptions(
49 mr.project)
50 offer_access_level = len(available_access_levels) > 1
51 access_view = project_views.ProjectAccessView(mr.project.access)
52
53 return {
54 'admin_tab_mode':
55 self.ADMIN_TAB_META,
56 'initial_summary':
57 mr.project.summary,
58 'initial_project_home':
59 mr.project.home_page,
60 'initial_docs_url':
61 mr.project.docs_url,
62 'initial_source_url':
63 mr.project.source_url,
64 'initial_logo_gcs_id':
65 mr.project.logo_gcs_id,
66 'initial_logo_file_name':
67 mr.project.logo_file_name,
68 'logo_view':
69 tracker_views.LogoView(mr.project),
70 'initial_description':
71 mr.project.description,
72 'issue_notify':
73 mr.project.issue_notify_address,
74 'process_inbound_email':
75 ezt.boolean(mr.project.process_inbound_email),
76 'email_from_addr':
77 emailfmt.FormatFromAddr(mr.project),
78 'only_owners_remove_restrictions':
79 ezt.boolean(mr.project.only_owners_remove_restrictions),
80 'only_owners_see_contributors':
81 ezt.boolean(mr.project.only_owners_see_contributors),
82 'offer_access_level':
83 ezt.boolean(offer_access_level),
84 'initial_access':
85 access_view,
86 'available_access_levels':
87 available_access_levels,
88 'issue_notify_always_detailed':
89 ezt.boolean(mr.project.issue_notify_always_detailed),
90 }
91
92 def ProcessFormData(self, mr, post_data):
93 """Process the posted form."""
94 # 1. Parse and validate user input.
95 summary, description = self._ParseMeta(post_data, mr.errors)
96 access = project_helpers.ParseProjectAccess(
97 mr.project, post_data.get('access'))
98
99 only_owners_remove_restrictions = (
100 'only_owners_remove_restrictions' in post_data)
101 only_owners_see_contributors = 'only_owners_see_contributors' in post_data
102
103 issue_notify = post_data['issue_notify']
104 if issue_notify and not validate.IsValidEmail(issue_notify):
105 mr.errors.issue_notify = _MSG_INVALID_EMAIL_ADDRESS
106
107 process_inbound_email = 'process_inbound_email' in post_data
108 home_page = post_data.get('project_home')
109 if home_page and not (
110 home_page.startswith('http:') or home_page.startswith('https:')):
111 mr.errors.project_home = 'Home page link must start with http: or https:'
112 docs_url = post_data.get('docs_url')
113 if docs_url and not (
114 docs_url.startswith('http:') or docs_url.startswith('https:')):
115 mr.errors.docs_url = 'Documentation link must start with http: or https:'
116 source_url = post_data.get('source_url')
117 if source_url and not (
118 source_url.startswith('http:') or source_url.startswith('https:')):
119 mr.errors.source_url = 'Source link must start with http: or https:'
120
121 logo_gcs_id = ''
122 logo_file_name = ''
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100123 if 'logo' in post_data and post_data['logo'].filename != '':
Copybara854996b2021-09-07 19:36:02 +0000124 item = post_data['logo']
125 logo_file_name = item.filename
126 try:
127 logo_gcs_id = gcs_helpers.StoreLogoInGCS(
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100128 logo_file_name, item.read(), mr.project.project_id)
129 except gcs_helpers.UnsupportedMimeType as e:
130 mr.errors.logo = str(e)
Copybara854996b2021-09-07 19:36:02 +0000131 elif mr.project.logo_gcs_id and mr.project.logo_file_name:
132 logo_gcs_id = mr.project.logo_gcs_id
133 logo_file_name = mr.project.logo_file_name
134 if post_data.get('delete_logo'):
135 try:
136 gcs_helpers.DeleteObjectFromGCS(logo_gcs_id)
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200137 except exceptions.NotFound:
Copybara854996b2021-09-07 19:36:02 +0000138 pass
139 # Reset the GCS ID and file name.
140 logo_gcs_id = ''
141 logo_file_name = ''
142
143 issue_notify_always_detailed = 'issue_notify_always_detailed' in post_data
144
145 # 2. Call services layer to save changes.
146 if not mr.errors.AnyErrors():
147 with work_env.WorkEnv(mr, self.services) as we:
148 we.UpdateProject(
149 mr.project.project_id,
150 issue_notify_address=issue_notify,
151 summary=summary,
152 description=description,
153 only_owners_remove_restrictions=only_owners_remove_restrictions,
154 only_owners_see_contributors=only_owners_see_contributors,
155 process_inbound_email=process_inbound_email,
156 access=access,
157 home_page=home_page,
158 docs_url=docs_url,
159 source_url=source_url,
160 logo_gcs_id=logo_gcs_id,
161 logo_file_name=logo_file_name,
162 issue_notify_always_detailed=issue_notify_always_detailed)
163
164 # 3. Determine the next page in the UI flow.
165 if mr.errors.AnyErrors():
166 access_view = project_views.ProjectAccessView(access)
167 self.PleaseCorrect(
168 mr, initial_summary=summary, initial_description=description,
169 initial_access=access_view)
170 else:
171 return framework_helpers.FormatAbsoluteURL(
172 mr, urls.ADMIN_META, saved=1, ts=int(time.time()))
173
174 def _ParseMeta(self, post_data, errors):
175 """Process a POST on the project metadata section of the admin page."""
176 summary = None
177 description = None
178
179 if 'summary' in post_data:
180 summary = post_data['summary']
181 if not summary:
182 errors.summary = _MSG_SUMMARY_MISSING
183 if 'description' in post_data:
184 description = post_data['description']
185 if not description:
186 errors.description = _MSG_DESCRIPTION_MISSING
187
188 return summary, description
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200189
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100190 def GetProjectAdminPage(self, **kwargs):
191 return self.handler(**kwargs)
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200192
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100193 def PostProjectAdminPage(self, **kwargs):
194 return self.handler(**kwargs)