Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

frontend, python: allow ignoring errors that a project already exists #3085

Merged
merged 1 commit into from
Jan 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions frontend/coprs_frontend/coprs/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ def __call__(self, form, field):

class CoprUniqueNameValidator(object):

def __init__(self, message=None, user=None, group=None):
def __init__(self, message=None, user=None, group=None, exist_ok=False):
if not message:
if group is None:
message = "You already have a project named '{}'."
Expand All @@ -273,6 +273,8 @@ def __init__(self, message=None, user=None, group=None):
user = flask.g.user
self.user = user
self.group = group
self.exist_ok = exist_ok
self.copr = None

def __call__(self, form, field):
if self.group:
Expand All @@ -282,6 +284,11 @@ def __call__(self, form, field):
existing = CoprsLogic.exists_for_user(
self.user, field.data).first()

# Save the existing copr instance, so we can later return it without
# querying the database again
if existing and self.exist_ok:
self.copr = existing

if existing and str(existing.id) != form.id.data:
raise wtforms.ValidationError(self.message.format(field.data))

Expand Down Expand Up @@ -704,7 +711,7 @@ def errors(self):
class CoprFormFactory(object):

@staticmethod
def create_form_cls(user=None, group=None, copr=None):
def create_form_cls(user=None, group=None, copr=None, exist_ok=False):
class F(CoprForm):
# also use id here, to be able to find out whether user
# is updating a copr if so, we don't want to shout
Expand All @@ -717,7 +724,8 @@ class F(CoprForm):
validators=[
wtforms.validators.DataRequired(),
NameCharactersValidator(),
CoprUniqueNameValidator(user=user, group=group),
CoprUniqueNameValidator(user=user, group=group,
exist_ok=exist_ok),
NameNotNumberValidator()
])

Expand Down
24 changes: 19 additions & 5 deletions frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
project_delete_input_model,
fullname_params,
pagination_project_model,
ownername_params,
project_params,
pagination_params,
)
from coprs.views.apiv3_ns.schema.docs import query_docs
Expand Down Expand Up @@ -139,7 +139,7 @@ def get(self, ownername, projectname):
class ProjectList(Resource):
@restx_pagination
@query_to_parameters
@apiv3_projects_ns.doc(params=ownername_params | pagination_params)
@apiv3_projects_ns.doc(params=project_params | pagination_params)
@apiv3_projects_ns.marshal_list_with(pagination_project_model)
@apiv3_projects_ns.response(
HTTPStatus.PARTIAL_CONTENT.value, HTTPStatus.PARTIAL_CONTENT.description
Expand Down Expand Up @@ -184,25 +184,39 @@ def get(self, query, **kwargs):
@apiv3_projects_ns.route("/add/<ownername>")
class ProjectAdd(Resource):
@restx_api_login_required
@apiv3_projects_ns.doc(params=ownername_params)
@query_to_parameters
@apiv3_projects_ns.doc(params=project_params)
@apiv3_projects_ns.marshal_with(project_model)
@apiv3_projects_ns.expect(project_add_input_model)
@apiv3_projects_ns.response(HTTPStatus.OK.value, "Copr project created")
@apiv3_projects_ns.response(
HTTPStatus.BAD_REQUEST.value, HTTPStatus.BAD_REQUEST.description
)
def post(self, ownername):
def post(self, ownername, exist_ok=False):
"""
Create new Copr project
Create new Copr project for ownername with specified data inserted in form.
"""
exist_ok = flask.request.args.get("exist_ok") == "True"
user, group = owner2tuple(ownername)
data = rename_fields(get_form_compatible_data(preserve=["chroots"]))
form_class = forms.CoprFormFactory.create_form_cls(user=user, group=group)
form_class = forms.CoprFormFactory.create_form_cls(user=user, group=group,
exist_ok=exist_ok)
set_defaults(data, form_class)
form = form_class(data, meta={"csrf": False})

if not form.validate_on_submit():
if exist_ok:
# This is an ugly hack to avoid additional database query.
# If a project with this owner and name already exists, the
# `CoprUniqueNameValidator` saved its instance. Let's find the
# validator and return the existing copr instance.
for validator in form.name.validators:
if not isinstance(validator, forms.CoprUniqueNameValidator):
continue
if not validator.copr:
continue
return to_dict(validator.copr)
raise InvalidForm(form)
validate_chroots(get_input_dict(), MockChrootsLogic.get_multiple())

Expand Down
8 changes: 8 additions & 0 deletions frontend/coprs_frontend/coprs/views/apiv3_ns/schema/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,14 @@
)
)

exist_ok = Boolean(
description=(
"Don't fail if a project with this owner and name already exist, "
"return the existing instance instead. Please be aware that the "
"project attributes are not updated in such case."
)
)

# TODO: these needs description

chroot_repos = Raw()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -481,8 +481,9 @@ class FullnameSchema(ParamsSchema):


@dataclass
class OwnernameSchema(ParamsSchema):
class ProjectParamsSchema(ParamsSchema):
ownername: String
exist_ok: Boolean


# OUTPUT MODELS
Expand Down Expand Up @@ -515,5 +516,5 @@ class OwnernameSchema(ParamsSchema):
package_get_params = PackageGet.get_cls().params_schema()
project_chroot_get_params = ProjectChrootGet.get_cls().params_schema()
fullname_params = FullnameSchema.get_cls().params_schema()
ownername_params = OwnernameSchema.get_cls().params_schema()
project_params = ProjectParamsSchema.get_cls().params_schema()
pagination_params = PaginationMeta.get_cls().params_schema()
22 changes: 22 additions & 0 deletions frontend/coprs_frontend/tests/test_apiv3/test_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,3 +426,25 @@ def test_perms_set_sends_emails_2(self, send_mail):
r = self.auth_post('/request/user2/barcopr', permissions, u)
assert r.status_code == 200
assert len(calls) == 2

@TransactionDecorator("u1")
@pytest.mark.usefixtures("f_users", "f_users_api", "f_mock_chroots", "f_db")
def test_add_exist_ok(self):
route = "/api_3/project/add/{}".format(self.transaction_username)
data = {"name": "foo", "chroots": ["fedora-rawhide-i386"]}

# There is no conflict, we can obviously create the project
response = self.api3.post(route, data)
assert response.status_code == 200

# The project already exists
response = self.api3.post(route, data)
assert response.status_code == 400
assert "already have a project" in json.loads(response.data)["error"]

# When using exist_ok, the request is successful, and existing project
# is returned
route += "?exist_ok=True"
response = self.api3.post(route, data)
assert response.status_code == 200
assert json.loads(response.data)["full_name"] == "user1/foo"
3 changes: 2 additions & 1 deletion python/copr/v3/proxies/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def add(self, ownername, projectname, chroots, description=None, instructions=No
delete_after_days=None, multilib=False, module_hotfixes=False,
bootstrap=None, bootstrap_image=None, isolation=None, follow_fedora_branching=True,
fedora_review=None, appstream=False, runtime_dependencies=None, packit_forge_projects_allowed=None,
repo_priority=None):
repo_priority=None, exist_ok=False):
"""
Create a project

Expand Down Expand Up @@ -115,6 +115,7 @@ def add(self, ownername, projectname, chroots, description=None, instructions=No
endpoint = "/project/add/{ownername}"
params = {
"ownername": ownername,
"exist_ok": exist_ok,
}
data = {
"name": projectname,
Expand Down