From 6c78d2c0aa8646665d19a10b66d1f1aeefffebe9 Mon Sep 17 00:00:00 2001 From: Jakub Kadlcik Date: Nov 06 2020 08:16:47 +0000 Subject: [PATCH 1/3] frontend: not access flask.g.user, user parameter instead IMHO this is a typo, why else we would access `flask.g.user` variable to find out Coprs permissible by an user, when we are already passing the user through method parameter. It forces us to create tests with application context which otherwise wouldn't be necessary. --- diff --git a/frontend/coprs_frontend/coprs/logic/complex_logic.py b/frontend/coprs_frontend/coprs/logic/complex_logic.py index 545be3e..ddc12bf 100644 --- a/frontend/coprs_frontend/coprs/logic/complex_logic.py +++ b/frontend/coprs_frontend/coprs/logic/complex_logic.py @@ -273,7 +273,7 @@ class ComplexLogic(object): def get_coprs_permissible_by_user(cls, user): coprs = CoprsLogic.filter_without_group_projects( CoprsLogic.get_multiple_owned_by_username( - flask.g.user.username, include_unlisted_on_hp=False)).all() + user.username, include_unlisted_on_hp=False)).all() for group in user.user_groups: coprs.extend(CoprsLogic.get_multiple_by_group_id(group.id).all()) From 0e42482cf0703b96cee821ac90e9961ec5fd758f Mon Sep 17 00:00:00 2001 From: Jakub Kadlcik Date: Nov 06 2020 08:16:47 +0000 Subject: [PATCH 2/3] frontend: add EOL repositories page for user (in opposite to project) Email notifications proved to be not reliable enough and we shouldn't rely on them. It can happen that emails are not being sent [1], that they are being delivered as spam [2], or that they are not being delivered to some users at all [3]. We don't want to create a notification system, that would duplicate all messages and show them directly at Copr site (at least for the time being), so we decided to create a page showing outdated repositories from all projects permissible by a user (in opposite to having them in project settings). This way, there is a single entry point for all of them. Users that have problems with receiving emails from Copr can then bookmark this page and take over the responsibility and schedule to prolong the duration of their EOL repositories. [1] https://pagure.io/fedora-infrastructure/issue/9233 [2] https://pagure.io/fedora-infrastructure/issue/9250 [3] https://bugzilla.redhat.com/show_bug.cgi?id=1868367 --- diff --git a/frontend/coprs_frontend/coprs/forms.py b/frontend/coprs_frontend/coprs/forms.py index bc87517..a46c9a6 100644 --- a/frontend/coprs_frontend/coprs/forms.py +++ b/frontend/coprs_frontend/coprs/forms.py @@ -1301,6 +1301,8 @@ class ChrootForm(FlaskForm): class CoprChrootExtend(FlaskForm): extend = wtforms.StringField("Chroot name") expire = wtforms.StringField("Chroot name") + ownername = wtforms.HiddenField("Owner name") + projectname = wtforms.HiddenField("Project name") class CoprLegalFlagForm(FlaskForm): diff --git a/frontend/coprs_frontend/coprs/templates/repositories.html b/frontend/coprs_frontend/coprs/templates/repositories.html new file mode 100644 index 0000000..63917bd --- /dev/null +++ b/frontend/coprs_frontend/coprs/templates/repositories.html @@ -0,0 +1,85 @@ +{% extends "coprs/show.html" %} +{% from "_helpers.html" import copr_url, render_form_errors %} +{% block title %}Outdated repositories{% endblock %} +{% block header %}Outdated repositories{% endblock %} +{% block breadcrumbs %} + +{% endblock %} + + +{% block content %} +

Outdated repositories

+

+ These projects have available repositories for at least some outdated chroots. + Unless you periodically take an action and extend the time for they should be preserved, + they are going to be removed in the future. Please see + Outdated repos removal policy + in the Copr Documentation. +

+ +{% if form %} +{{ render_form_errors(form=form) }} +{% endif %} + +{% for project in projects %} +{% if project.outdated_chroots %} +
+

{{ project.full_name }}

+ + + + + + + + + + + {% for chroot in project.outdated_chroots %} + + + + + + + {% endfor %} + +
ReleaseArchitectureRemaining timeAction
{{ chroot.mock_chroot.os.capitalize() }}{{ chroot.mock_chroot.arch }} + {% if not chroot.delete_after_days %} + To be removed in next cleanup + {% else %} + {% set color = 'danger' if chroot.delete_after_days < 20 else 'secondary' %} + + {{ chroot.delete_after_days }} days + + {% endif %} + + + +
+ + + +
+{% endif %} +{% endfor %} + +{% endblock %} diff --git a/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py b/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py index a80440e..7901a6e 100644 --- a/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py +++ b/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py @@ -674,11 +674,21 @@ def render_copr_repositories(copr): @login_required @req_with_copr def copr_repositories_post(copr): + return process_copr_repositories(copr, render_copr_repositories) + + +def process_copr_repositories(copr, on_success): + form = forms.CoprChrootExtend() + if not copr and not (form.ownername.data or form.projectname.data): + raise ValidationError("Ambiguous to what project the chroot belongs") + + if not copr: + copr = ComplexLogic.get_copr_by_owner_safe(form.ownername.data, + form.projectname.data) if not flask.g.user.can_edit(copr): flask.flash("You don't have access to this page.", "error") return flask.redirect(url_for_copr_details(copr)) - form = forms.CoprChrootExtend() if form.extend.data: delete_after_days = app.config["DELETE_EOL_CHROOTS_AFTER"] + 1 chroot_name = form.extend.data @@ -698,7 +708,7 @@ def copr_repositories_post(copr): coprs_logic.CoprChrootsLogic.update_chroot(flask.g.user, copr_chroot, delete_after=delete_after_timestamp) db.session.commit() - return render_copr_repositories(copr) + return on_success(copr) @coprs_ns.route("/id//createrepo/", methods=["POST"]) diff --git a/frontend/coprs_frontend/coprs/views/user_ns/user_general.py b/frontend/coprs_frontend/coprs/views/user_ns/user_general.py index 8bf656a..aa73e6e 100644 --- a/frontend/coprs_frontend/coprs/views/user_ns/user_general.py +++ b/frontend/coprs_frontend/coprs/views/user_ns/user_general.py @@ -1,12 +1,13 @@ import flask -from . import user_ns from coprs import app, db, models, helpers from coprs.forms import PinnedCoprsForm from coprs.views.misc import login_required from coprs.logic.users_logic import UsersLogic, UserDataDumper from coprs.logic.builds_logic import BuildsLogic from coprs.logic.complex_logic import ComplexLogic -from coprs.logic.coprs_logic import CoprsLogic, PinnedCoprsLogic +from coprs.logic.coprs_logic import PinnedCoprsLogic +from coprs.views.coprs_ns.coprs_general import process_copr_repositories +from . import user_ns def render_user_info(user): @@ -93,3 +94,26 @@ def process_pinned_projects_post(owner, url_on_success): db.session.commit() return flask.redirect(url_on_success) + + +@user_ns.route("/repositories/") +@login_required +def repositories(): + return render_repositories() + + +def render_repositories(*_args, **_kwargs): + owner = flask.g.user + projects = ComplexLogic.get_coprs_permissible_by_user(owner) + projects = sorted(projects, key=lambda p: p.full_name) + return flask.render_template("repositories.html", + tasks_info=ComplexLogic.get_queue_sizes(), + graph=BuildsLogic.get_small_graph_data('30min'), + owner=owner, + projects=projects) + + +@user_ns.route("/repositories/", methods=["POST"]) +@login_required +def repositories_post(): + return process_copr_repositories(copr=None, on_success=render_repositories) From 9fef8d63f80459fd234a44b01210dc92613c0207 Mon Sep 17 00:00:00 2001 From: Jakub Kadlcik Date: Nov 06 2020 08:16:47 +0000 Subject: [PATCH 3/3] frontend: show a warning that user should visit their EOL repositories page This warning will be displayed indefinitely, on all `coprs_ns` pages, until a user visits the outdated repositories page. It will re-appear everytime some chroot is marked as EOL and affects any of the projects listed there. It will also re-appear everytime a chroot gets close to expiring (the last 14 days). --- diff --git a/frontend/coprs_frontend/alembic/versions/9b7211be5017_add_reviewed_outdated_chroot_table.py b/frontend/coprs_frontend/alembic/versions/9b7211be5017_add_reviewed_outdated_chroot_table.py new file mode 100644 index 0000000..83e2226 --- /dev/null +++ b/frontend/coprs_frontend/alembic/versions/9b7211be5017_add_reviewed_outdated_chroot_table.py @@ -0,0 +1,30 @@ +""" +Add reviewed_outdated_chroot table + +Revision ID: 9b7211be5017 +Revises: de903581465c +Create Date: 2020-10-08 11:27:27.588111 +""" + +import sqlalchemy as sa +from alembic import op + + +revision = '9b7211be5017' +down_revision = '63db6872060f' + +def upgrade(): + op.create_table('reviewed_outdated_chroot', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('copr_chroot_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['copr_chroot_id'], ['copr_chroot.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_reviewed_outdated_chroot_user_id'), 'reviewed_outdated_chroot', ['user_id'], unique=False) + + +def downgrade(): + op.drop_index(op.f('ix_reviewed_outdated_chroot_user_id'), table_name='reviewed_outdated_chroot') + op.drop_table('reviewed_outdated_chroot') diff --git a/frontend/coprs_frontend/coprs/logic/outdated_chroots_logic.py b/frontend/coprs_frontend/coprs/logic/outdated_chroots_logic.py new file mode 100644 index 0000000..0a186b0 --- /dev/null +++ b/frontend/coprs_frontend/coprs/logic/outdated_chroots_logic.py @@ -0,0 +1,90 @@ +import flask +from datetime import datetime, timedelta +from coprs import db +from coprs import app +from coprs import models +from coprs.logic.complex_logic import ComplexLogic +from coprs.logic.coprs_logic import CoprChrootsLogic + + +class OutdatedChrootsLogic: + @classmethod + def has_not_reviewed(cls, user): + """ + Does a user have some projects with newly outdated chroots that he + hasn't reviewed yet? + """ + projects = ComplexLogic.get_coprs_permissible_by_user(user) + projects_ids = [p.id for p in projects] + period = app.config["EOL_CHROOTS_NOTIFICATION_PERIOD"] + now = datetime.now() + soon = now + timedelta(days=period) + + reviewed = [x.copr_chroot_id for x in cls.get_all_reviews(user).all()] + return bool((models.CoprChroot.query + .filter(models.CoprChroot.copr_id.in_(projects_ids)) + .filter(models.CoprChroot.delete_after != None) + .filter(models.CoprChroot.delete_after <= soon) + .filter(models.CoprChroot.delete_after > now) + .filter(models.CoprChroot.id.notin_(reviewed)) + .first())) + + @classmethod + def get_all_reviews(cls, user): + """ + Query all outdated chroots that a user has already seen + """ + return (models.ReviewedOutdatedChroot.query + .filter(models.ReviewedOutdatedChroot.user_id == user.id)) + + @classmethod + def make_review(cls, user): + """ + A `user` declares that he has seen and reviewed all outdated chroots in + all of his projects (i.e. this method creates `ReviewedOutdatedChroot` + results for all of them) + """ + reviews = {x.copr_chroot_id for x in cls.get_all_reviews(user)} + for copr in ComplexLogic.get_coprs_permissible_by_user(user): + for chroot in copr.outdated_chroots: + if chroot.id in reviews: + continue + + period = app.config["EOL_CHROOTS_NOTIFICATION_PERIOD"] + if chroot.delete_after_days > period: + continue + + review = models.ReviewedOutdatedChroot( + user_id=user.id, + copr_chroot_id=chroot.id, + ) + db.session.add(review) + + @classmethod + def extend(cls, copr_chroot): + """ + A `user` decided to extend the preservation period for some EOL chroot + """ + delete_after_days = app.config["DELETE_EOL_CHROOTS_AFTER"] + 1 + cls._update_copr_chroot(copr_chroot, delete_after_days) + (models.ReviewedOutdatedChroot.query + .filter(models.ReviewedOutdatedChroot.copr_chroot_id + == copr_chroot.id)).delete() + + @classmethod + def expire(cls, copr_chroot): + """ + A `user` decided to expire some EOL chroot, + i.e. its data should be deleted ASAP + """ + delete_after_days = 0 + cls._update_copr_chroot(copr_chroot, delete_after_days) + + @classmethod + def _update_copr_chroot(cls, copr_chroot, delete_after_days): + delete_after_timestamp = ( + datetime.now() + + timedelta(days=delete_after_days) + ) + CoprChrootsLogic.update_chroot(flask.g.user, copr_chroot, + delete_after=delete_after_timestamp) diff --git a/frontend/coprs_frontend/coprs/models.py b/frontend/coprs_frontend/coprs/models.py index 031688d..43a3d1c 100644 --- a/frontend/coprs_frontend/coprs/models.py +++ b/frontend/coprs_frontend/coprs/models.py @@ -2047,6 +2047,31 @@ class CancelRequest(db.Model): what = db.Column(db.String(100), nullable=False, primary_key=True) +class ReviewedOutdatedChroot(db.Model): + id = db.Column(db.Integer, primary_key=True) + + user_id = db.Column( + db.Integer, + db.ForeignKey("user.id"), + nullable=False, + index=True, + ) + copr_chroot_id = db.Column( + db.Integer, + db.ForeignKey("copr_chroot.id", ondelete="CASCADE"), + nullable=False, + ) + + user = db.relationship( + "User", + backref=db.backref("reviewed_outdated_chroots"), + ) + copr_chroot = db.relationship( + "CoprChroot", + backref=db.backref("reviewed_outdated_chroots") + ) + + @listens_for(DistGitInstance.__table__, 'after_create') def insert_fedora_distgit(*args, **kwargs): db.session.add(DistGitInstance( diff --git a/frontend/coprs_frontend/coprs/templates/_helpers.html b/frontend/coprs_frontend/coprs/templates/_helpers.html index 1ad5a8f..b70e37a 100644 --- a/frontend/coprs_frontend/coprs/templates/_helpers.html +++ b/frontend/coprs_frontend/coprs/templates/_helpers.html @@ -214,7 +214,7 @@ - {{ message }} + {{ message |safe }} {% endmacro %} diff --git a/frontend/coprs_frontend/coprs/views/coprs_ns/__init__.py b/frontend/coprs_frontend/coprs/views/coprs_ns/__init__.py index 5a8ccd6..359cb7b 100644 --- a/frontend/coprs_frontend/coprs/views/coprs_ns/__init__.py +++ b/frontend/coprs_frontend/coprs/views/coprs_ns/__init__.py @@ -1,6 +1,22 @@ # coding: utf-8 import flask +from coprs.logic.outdated_chroots_logic import OutdatedChrootsLogic + + +def flash_outdated_chroots_warning(): + if not flask.g.user: + return + + if not OutdatedChrootsLogic.has_not_reviewed(flask.g.user): + return + + url = flask.url_for("user_ns.repositories", _external=True) + flask.flash("Some of the chroots you maintain are newly marked EOL, " + " and will be removed in the future. Please review " + "{0} to hide this warning." + .format(url), "warning") coprs_ns = flask.Blueprint("coprs_ns", __name__, url_prefix="/coprs") +coprs_ns.before_request(flash_outdated_chroots_warning) diff --git a/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py b/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py index 7901a6e..14c7d2c 100644 --- a/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py +++ b/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py @@ -35,6 +35,7 @@ from coprs.rmodels import TimedStatEvents from coprs.mail import send_mail, LegalFlagMessage, PermissionRequestMessage, PermissionChangeMessage from coprs.logic.complex_logic import ComplexLogic +from coprs.logic.outdated_chroots_logic import OutdatedChrootsLogic from coprs.views.misc import (login_required, page_not_found, req_with_copr, generic_error, req_with_copr_dir) @@ -690,12 +691,12 @@ def process_copr_repositories(copr, on_success): return flask.redirect(url_for_copr_details(copr)) if form.extend.data: - delete_after_days = app.config["DELETE_EOL_CHROOTS_AFTER"] + 1 + update_fun = OutdatedChrootsLogic.extend chroot_name = form.extend.data flask.flash("Repository for {} will be preserved for another {} days from now" .format(chroot_name, app.config["DELETE_EOL_CHROOTS_AFTER"])) elif form.expire.data: - delete_after_days = 0 + update_fun = OutdatedChrootsLogic.expire chroot_name = form.expire.data flask.flash("Repository for {} is scheduled to be removed." "If you changed your mind, click 'Extend` to revert your decision." @@ -704,9 +705,7 @@ def process_copr_repositories(copr, on_success): raise ValidationError("Copr chroot needs to be either extended or expired") copr_chroot = coprs_logic.CoprChrootsLogic.get_by_name(copr, chroot_name, active_only=False).one() - delete_after_timestamp = datetime.datetime.now() + datetime.timedelta(days=delete_after_days) - coprs_logic.CoprChrootsLogic.update_chroot(flask.g.user, copr_chroot, - delete_after=delete_after_timestamp) + update_fun(copr_chroot) db.session.commit() return on_success(copr) diff --git a/frontend/coprs_frontend/coprs/views/user_ns/user_general.py b/frontend/coprs_frontend/coprs/views/user_ns/user_general.py index aa73e6e..413d9fe 100644 --- a/frontend/coprs_frontend/coprs/views/user_ns/user_general.py +++ b/frontend/coprs_frontend/coprs/views/user_ns/user_general.py @@ -6,6 +6,7 @@ from coprs.logic.users_logic import UsersLogic, UserDataDumper from coprs.logic.builds_logic import BuildsLogic from coprs.logic.complex_logic import ComplexLogic from coprs.logic.coprs_logic import PinnedCoprsLogic +from coprs.logic.outdated_chroots_logic import OutdatedChrootsLogic from coprs.views.coprs_ns.coprs_general import process_copr_repositories from . import user_ns @@ -106,6 +107,8 @@ def render_repositories(*_args, **_kwargs): owner = flask.g.user projects = ComplexLogic.get_coprs_permissible_by_user(owner) projects = sorted(projects, key=lambda p: p.full_name) + OutdatedChrootsLogic.make_review(owner) + db.session.commit() return flask.render_template("repositories.html", tasks_info=ComplexLogic.get_queue_sizes(), graph=BuildsLogic.get_small_graph_data('30min'), diff --git a/frontend/coprs_frontend/tests/test_logic/test_outdated_chroots_logic.py b/frontend/coprs_frontend/tests/test_logic/test_outdated_chroots_logic.py new file mode 100644 index 0000000..93b81af --- /dev/null +++ b/frontend/coprs_frontend/tests/test_logic/test_outdated_chroots_logic.py @@ -0,0 +1,131 @@ +import flask +import pytest +from datetime import datetime, timedelta +from tests.coprs_test_case import CoprsTestCase, new_app_context +from coprs.logic.outdated_chroots_logic import OutdatedChrootsLogic +from coprs.logic.complex_logic import ComplexLogic + + +class TestOutdatedChrootsLogic(CoprsTestCase): + + @new_app_context + @pytest.mark.usefixtures("f_users", "f_coprs", "f_mock_chroots", "f_db") + def test_outdated_chroots_simple(self): + # Make sure, that there are no unreviewed outdated chroots yet + assert not OutdatedChrootsLogic.has_not_reviewed(self.u2) + + # Once a chroot is EOLed, we should see that a user has something unreviewed + self.c2.copr_chroots[0].delete_after = datetime.now() + timedelta(days=10) + assert OutdatedChrootsLogic.has_not_reviewed(self.u2) + + # User just reviewed his outdated chroots + # (e.g. by visiting the /repositories page) + OutdatedChrootsLogic.make_review(self.u2) + assert not OutdatedChrootsLogic.has_not_reviewed(self.u2) + + @new_app_context + @pytest.mark.usefixtures("f_users", "f_groups", "f_group_copr", "f_db") + def test_outdated_chroots_group(self): + # Make sure that a user is a part of a group + self.u3.openid_groups = {"fas_groups": [self.g1.fas_name]} + assert self.u3.can_build_in_group(self.g1) + + # Make sure a project is owned by a group but not by our user himself + permissible = ComplexLogic.get_coprs_permissible_by_user(self.u3) + assert self.gc2 in permissible + assert self.gc2.user != self.u3 + + # Make sure, that there are no unreviewed outdated chroots yet + assert not OutdatedChrootsLogic.has_not_reviewed(self.u3) + + # Once a chroot is EOLed, we should see that a user has something unreviewed + self.gc2.copr_chroots[0].delete_after = datetime.now() + timedelta(days=10) + assert OutdatedChrootsLogic.has_not_reviewed(self.u3) + + # User just reviewed his outdated chroots + # (e.g. by visiting the /repositories page) + OutdatedChrootsLogic.make_review(self.u3) + assert not OutdatedChrootsLogic.has_not_reviewed(self.u3) + + # Only a `self.u3` did the review, other group members still has + # unreviewed chroots + self.u2.openid_groups = {"fas_groups": [self.g1.fas_name]} + assert OutdatedChrootsLogic.has_not_reviewed(self.u2) + + @new_app_context + @pytest.mark.usefixtures("f_users", "f_coprs", "f_mock_chroots", "f_db") + def test_outdated_chroots_flash_not_immediately(self): + # Make sure, that there are no unreviewed outdated chroots yet + assert not OutdatedChrootsLogic.has_not_reviewed(self.u2) + + # A chroot was just marked as EOL, we don't want to see any warning just yet + self.c2.copr_chroots[0].delete_after = datetime.now() + timedelta(days=180) + assert not OutdatedChrootsLogic.has_not_reviewed(self.u2) + + # Once around half of the preservation period for an EOLed chroot + # runned out, we want to start showing some notification + self.c2.copr_chroots[0].delete_after = datetime.now() + timedelta(days=80) + assert OutdatedChrootsLogic.has_not_reviewed(self.u2) + + @new_app_context + @pytest.mark.usefixtures("f_users", "f_coprs", "f_mock_chroots", "f_db") + def test_outdated_chroots_flash_not_expired(self): + # A preservation period is gone and the chroot is scheduled to be + # deleted ASAP. At this point, user has no chance to extend it anymore, + # so make sure we don't notify him about such chroots + self.c2.copr_chroots[0].delete_after = datetime.now() - timedelta(days=1) + assert not OutdatedChrootsLogic.has_not_reviewed(self.u2) + + @new_app_context + @pytest.mark.usefixtures("f_users", "f_coprs", "f_mock_chroots", "f_db") + def test_outdated_chroots_review_only_after_some_time(self): + # Make sure that `self.u2` hasn't reviewed anything yet + assert not OutdatedChrootsLogic.get_all_reviews(self.u2).all() + + # Some chroots are going to be deleted, with various times remaining + self.c2.copr_chroots[0].delete_after = datetime.now() + timedelta(days=35) + self.c2.copr_chroots[1].delete_after = datetime.now() + timedelta(days=160) + self.c3.copr_chroots[0].delete_after = datetime.now() + timedelta(days=80) + self.c3.copr_chroots[1].delete_after = datetime.now() + timedelta(days=50) + + # User just reviewed his outdated chroots + # (e.g. by visiting the /repositories page) + OutdatedChrootsLogic.make_review(self.u2) + reviews = OutdatedChrootsLogic.get_all_reviews(self.u2).all() + + # Make sure that not all EOL chroots have been reviewed. We want to + # review only those with a significant portion of the preservation + # period already run out. + assert len(reviews) == 3 + assert self.c2.copr_chroots[1] not in [x.copr_chroot for x in reviews] + assert {x.copr_chroot for x in reviews} == { + self.c2.copr_chroots[0], + self.c3.copr_chroots[0], + self.c3.copr_chroots[1], + } + + @new_app_context + @pytest.mark.usefixtures("f_users", "f_coprs", "f_mock_chroots", "f_db") + def test_outdated_chroots_extend_or_expire(self): + # Make sure that `self.u2` hasn't reviewed anything yet + assert len(OutdatedChrootsLogic.get_all_reviews(self.u2).all()) == 0 + + # Let's have some outdated chroot + self.c2.copr_chroots[0].delete_after = datetime.now() + timedelta(days=35) + + # Make sure a user reviewed it + OutdatedChrootsLogic.make_review(self.u2) + assert len(OutdatedChrootsLogic.get_all_reviews(self.u2).all()) == 1 + + # Extend should properly extend the preservation period + # and drop the review + flask.g.user = self.u2 + OutdatedChrootsLogic.extend(self.c2.copr_chroots[0]) + assert (self.c2.copr_chroots[0].delete_after + > datetime.now() + timedelta(days=35)) + assert len(OutdatedChrootsLogic.get_all_reviews(self.u2).all()) == 0 + + # User changed his mind and expired the chroot instead + OutdatedChrootsLogic.expire(self.c2.copr_chroots[0]) + assert (self.c2.copr_chroots[0].delete_after.date() + == datetime.now().date())