Skip to content
Open
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
7 changes: 7 additions & 0 deletions frontend/coprs_frontend/coprs/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from coprs import app
from coprs.constants import BANNER_LOCATION
from coprs.helpers import current_url
from coprs.helpers import copr_url
from coprs.oidc import oidc_enabled


Expand Down Expand Up @@ -89,3 +90,9 @@ def counter(name):
def current_url_processor():
""" Provide 'current_url()' method in templates """
return dict(current_url=current_url)


@app.context_processor
def copr_url_processor():
""" Provide `copr_url()` helper in templates. """
return {"copr_url": copr_url}
53 changes: 53 additions & 0 deletions frontend/coprs_frontend/coprs/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -1750,6 +1750,59 @@ class AdminPlaygroundSearchForm(BaseForm):
project = wtforms.StringField("Project")


class BuildChrootResultSearchForm(BaseForm):
""" Search built packages across all build chroot results. """
name = wtforms.StringField(
"Name",
validators=[wtforms.validators.Optional()],
filters=[EmptyStringToNone()])
epoch = wtforms.IntegerField(
"Epoch",
validators=[wtforms.validators.Optional()])
version = wtforms.StringField(
"Version",
validators=[wtforms.validators.Optional()],
filters=[EmptyStringToNone()])
release = wtforms.StringField(
"Release",
validators=[wtforms.validators.Optional()],
filters=[EmptyStringToNone()])
arch = wtforms.StringField(
"Arch",
validators=[wtforms.validators.Optional()],
filters=[EmptyStringToNone()])

def validate(self, extra_validators=None):
""" Validate that at least one search field is provided. """
# pylint: disable=unused-argument
result = super().validate()
if not result:
return False

has_value = any([
self.name.data,
self.version.data,
self.release.data,
self.arch.data,
self.epoch.data is not None,
])
if not has_value:
self.name.errors.append("At least one search field must be provided")
return False
Comment on lines +1782 to +1791

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic to check if at least one field has a value is duplicated here and in the search_params method. You can simplify this by calling self.search_params() and checking if the resulting dictionary is empty. This avoids code duplication and makes the validation more maintainable.

        if not self.search_params():
            self.name.errors.append("At least one search field must be provided")
            return False

return True

def search_params(self):
""" Return validated query params to preserve in pagination links. """
params = {
"name": self.name.data,
"epoch": self.epoch.data,
"version": self.version.data,
"release": self.release.data,
"arch": self.arch.data,
}
return {key: value for key, value in params.items() if value is not None}


class GroupUniqueNameValidator(object):

def __init__(self, message=None):
Expand Down
38 changes: 38 additions & 0 deletions frontend/coprs_frontend/coprs/logic/builds_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -1608,6 +1608,19 @@ class BuildChrootResultsLogic:
High-level interface for working with `models.BuildChrootResult` objects
"""

@classmethod
def get_multiply(cls):
""" Return a base query for searching built packages. """
return (
models.BuildChrootResult.query
.join(models.BuildChrootResult.build_chroot)
.join(models.BuildChroot.build)
.join(models.BuildChroot.mock_chroot)
.join(models.Build.copr)
.join(models.Copr.user)
.outerjoin(models.Group)
)

@classmethod
def create(cls, build_chroot, name, epoch, version, release, arch):
"""
Expand Down Expand Up @@ -1639,6 +1652,31 @@ def create_from_dict(cls, build_chroot, results):
return [cls.create(build_chroot, **result)
for result in results["packages"]]

@classmethod
def filter_by_name(cls, query, name):
""" Filter search query by package name. """
return query.filter(models.BuildChrootResult.name == name)

@classmethod
def filter_by_epoch(cls, query, epoch):
""" Filter search query by package epoch. """
return query.filter(models.BuildChrootResult.epoch == epoch)

@classmethod
def filter_by_version(cls, query, version):
""" Filter search query by package version. """
return query.filter(models.BuildChrootResult.version == version)

@classmethod
def filter_by_release(cls, query, release):
""" Filter search query by package release. """
return query.filter(models.BuildChrootResult.release == release)

@classmethod
def filter_by_arch(cls, query, arch):
""" Filter search query by package architecture. """
return query.filter(models.BuildChrootResult.arch == arch)


class BuildsMonitorLogic(object):
@classmethod
Expand Down
5 changes: 5 additions & 0 deletions frontend/coprs_frontend/coprs/templates/_helpers.html
Original file line number Diff line number Diff line change
Expand Up @@ -986,6 +986,11 @@ <h3 class="list-group-item-heading" style="display: inline;">
Search by package name
</a>
</li>
<li>
<a href="{{ url_for('coprs_ns.build_chroot_results_search') }}">
Search built packages
</a>
</li>
<li role="separator" class="divider"></li>

<li>
Expand Down
85 changes: 85 additions & 0 deletions frontend/coprs_frontend/coprs/templates/coprs/search_packages.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
{% extends "layout.html" %}
{% from "_helpers.html" import render_field, render_pagination, build_href %}

{% block title %}Package search{% endblock %}
{% block header %}Package search{% endblock %}

{% block breadcrumbs %}
<ol class="breadcrumb">
<li>
<a href="{{ url_for('coprs_ns.coprs_show') }}">Home</a>
</li>
<li class="active">
Package search
</li>
</ol>
{% endblock %}

{% block body %}
<h1>Search built packages</h1>
<p>Search built packages by NEVRA fields across all build chroot results.</p>

<form class="form-horizontal" method="get" action="{{ url_for('coprs_ns.build_chroot_results_search') }}">
{{ form.csrf_token }}
{{ render_field(form.name, placeholder="git") }}
{{ render_field(form.epoch, placeholder="0") }}
{{ render_field(form.version, placeholder="2.32.0") }}
{{ render_field(form.release, placeholder="1.fc36") }}
{{ render_field(form.arch, placeholder="x86_64") }}

<div class="form-group">
<div class="col-sm-10 col-sm-offset-2">
<button class="btn btn-primary" type="submit">
<span class="glyphicon glyphicon-search"></span> Search
</button>
</div>
</div>
</form>

{% if search_performed %}
{% if results %}
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>Project</th>
<th>Build</th>
<th>Chroot</th>
<th>Name</th>
<th>Epoch</th>
<th>Version</th>
<th>Release</th>
<th>Arch</th>
</tr>
</thead>
<tbody>
{% for result in results %}
{% set build = result.build_chroot.build %}
{% set copr = build.copr %}
<tr>
<td>
<a href="{{ copr_url('coprs_ns.copr_detail', copr) }}">
{{ copr.full_name }}
</a>
</td>
<td>
<a href="{{ build_href(build) }}">{{ build.id }}</a>
</td>
<td>{{ result.build_chroot.name }}</td>
<td>{{ result.name }}</td>
<td>{{ result.epoch if result.epoch is not none else '-' }}</td>
<td>{{ result.version }}</td>
<td>{{ result.release }}</td>
<td>{{ result.arch }}</td>
</tr>
{% endfor %}
</tbody>
</table>

{% if paginator %}
{{ render_pagination(request, paginator) }}
{% endif %}
{% else %}
<p>No matching packages found.</p>
{% endif %}
{% endif %}
{% endblock %}
42 changes: 42 additions & 0 deletions frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,48 @@ def coprs_fulltext_search(page=1):
graph=data)


@coprs_ns.route("/packages/search/", defaults={"page": 1})
@coprs_ns.route("/packages/search/<int:page>/")
def build_chroot_results_search(page=1):
""" Search built packages across all build chroot results. """
form = forms.BuildChrootResultSearchForm(formdata=flask.request.args, meta={"csrf": False})
results = []
paginator = None
search_performed = False
search_params = {}

if flask.request.args:
search_performed = True
if form.validate():
search_params = form.search_params()
display_query = builds_logic.BuildChrootResultsLogic.get_multiply()
count_query = models.BuildChrootResult.query

filter_mapping = {
"name": builds_logic.BuildChrootResultsLogic.filter_by_name,
"epoch": builds_logic.BuildChrootResultsLogic.filter_by_epoch,
"version": builds_logic.BuildChrootResultsLogic.filter_by_version,
"release": builds_logic.BuildChrootResultsLogic.filter_by_release,
"arch": builds_logic.BuildChrootResultsLogic.filter_by_arch,
}

for param, value in search_params.items():
display_query = filter_mapping[param](display_query, value)
count_query = filter_mapping[param](count_query, value)

total_count = count_query.count()
display_query = display_query.order_by(models.BuildChrootResult.id.desc())
paginator = helpers.Paginator(display_query, total_count, page,
additional_params=search_params)
results = paginator.sliced_query

return render_template("coprs/search_packages.html",
form=form,
results=results,
paginator=paginator,
search_performed=search_performed)


@coprs_ns.route("/<username>/new-fedora-review/", methods=["GET", "POST"])
@login_required
def copr_add_fedora_review(username): # pylint: disable=unused-argument
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Tests for built package search."""

import pytest

from coprs.logic.builds_logic import BuildChrootResultsLogic
from tests.coprs_test_case import CoprsTestCase, TransactionDecorator


class TestPackageSearch(CoprsTestCase):

@TransactionDecorator("u1")
@pytest.mark.usefixtures("f_users", "f_coprs", "f_mock_chroots", "f_builds", "f_db")
def test_search_by_name_and_arch(self):
self.db.session.add(self.b1)

built_packages = {
"packages": [
{
"name": "git",
"epoch": 0,
"version": "2.32.0",
"release": "1.fc36",
"arch": "x86_64",
},
]
}
BuildChrootResultsLogic.create_from_dict(
self.b1.build_chroots[0],
built_packages,
)
self.db.session.commit()

response = self.test_client.get(
"/coprs/packages/search/?name=git&arch=x86_64"
)
assert response.status_code == 200
assert b"git" in response.data
assert str(self.b1.id).encode("utf-8") in response.data