backoffice: add popup to cleanup logged errors (#40821) #1024

Merged
fpeters merged 1 commits from wip/40821-logged-errors-cleanup into main 2024-02-09 15:22:11 +01:00
3 changed files with 170 additions and 5 deletions

View File

@ -284,3 +284,109 @@ def test_logged_error_trace(pub):
resp = app.get(f'/backoffice/studio/logged-errors/{logged_error.id}/')
assert 'pub.record_error(\'Exception' in resp.pyquery('.stack-trace--code')[0].text
assert '\n locals:' in resp.text
def test_logged_error_cleanup(pub):
create_superuser(pub)
FormDef.wipe()
CardDef.wipe()
Workflow.wipe()
pub.loggederror_class.wipe()
formdef = FormDef()
formdef.name = 'foo'
formdef.store()
carddef = CardDef()
carddef.name = 'bar'
carddef.store()
workflow = Workflow()
workflow.name = 'blah'
workflow.store()
# FormDef error
error1 = pub.loggederror_class()
error1.summary = 'LoggedError'
error1.formdef_class = 'FormDef'
error1.formdef_id = formdef.id
error1.workflow_id = workflow.id
error1.first_occurence_timestamp = error1.latest_occurence_timestamp = datetime.datetime.now()
error1.store()
# CardDef error
error2 = pub.loggederror_class()
error2.summary = 'LoggedError'
error2.formdef_class = 'CardDef'
error2.formdef_id = carddef.id
error2.workflow_id = workflow.id
error2.first_occurence_timestamp = error2.latest_occurence_timestamp = datetime.datetime.now()
error2.store()
# workflow-only error
error3 = pub.loggederror_class()
error3.summary = 'LoggedError'
error3.workflow_id = workflow.id
error3.first_occurence_timestamp = error3.latest_occurence_timestamp = datetime.datetime.now()
error3.store()
app = login(get_app(pub))
resp = app.get('/backoffice/studio/logged-errors/')
resp = resp.click('Cleanup')
resp = resp.form.submit('submit')
assert pub.loggederror_class().count() == 3 # nothing removed
# check there's a form error if nothing is checked
resp = app.get('/backoffice/studio/logged-errors/')
resp = resp.click('Cleanup')
resp.form['types$elementformdef'].checked = False
resp.form['types$elementcarddef'].checked = False
resp.form['types$elementothers'].checked = False
resp = resp.form.submit('submit')
assert resp.pyquery('[data-widget-name="types"].widget-with-error')
# check cleanup of only formdef errors
error1.first_occurence_timestamp = (
error1.latest_occurence_timestamp
) = datetime.datetime.now() - datetime.timedelta(days=280)
error1.store()
error2.first_occurence_timestamp = datetime.datetime.now() - datetime.timedelta(days=120)
error2.latest_occurence_timestamp = datetime.datetime.now() - datetime.timedelta(days=80)
error2.store()
error3.first_occurence_timestamp = (
error3.latest_occurence_timestamp
) = datetime.datetime.now() - datetime.timedelta(days=280)
error3.store()
resp = app.get('/backoffice/studio/logged-errors/')
resp = resp.click('Cleanup')
resp.form['types$elementcarddef'].checked = False
resp.form['types$elementothers'].checked = False
resp = resp.form.submit('submit')
assert {x.id for x in pub.loggederror_class().select()} == {error2.id, error3.id}
# check cleanup latest occurence value (error2 should not be cleaned)
resp = app.get('/backoffice/studio/logged-errors/')
resp = resp.click('Cleanup')
resp.form['latest_occurence'] = (datetime.datetime.now() - datetime.timedelta(days=100)).strftime(
'%Y-%m-%d'
)
resp = resp.form.submit('submit')
assert {x.id for x in pub.loggederror_class().select()} == {error2.id}
# check with a more recent date (error2 should be cleaned this time)
resp = app.get('/backoffice/studio/logged-errors/')
resp = resp.click('Cleanup')
resp.form['latest_occurence'] = (datetime.datetime.now() - datetime.timedelta(days=10)).strftime(
'%Y-%m-%d'
)
resp = resp.form.submit('submit')
assert {x.id for x in pub.loggederror_class().select()} == set()
# make formdefs not accessible to current user
pub.cfg['admin-permissions'] = {'forms': ['X']}
pub.write_cfg()
resp = app.get('/backoffice/studio/logged-errors/')
resp = resp.click('Cleanup')
assert [x.attrib['name'] for x in resp.pyquery('[type="checkbox"]')] == [
'types$elementcarddef',
'types$elementothers',
]

View File

@ -14,16 +14,18 @@
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.
import datetime
import re
from django.utils.text import Truncator
from quixote import get_publisher, get_request, get_response, redirect
from quixote.directory import Directory
from quixote.directory import AccessControlled, Directory
from quixote.html import TemplateIO, htmltext
from wcs.backoffice.pagination import pagination_links
from wcs.qommon import N_, _, errors, misc, ngettext, template
from wcs.sql_criterias import Equal, NotEqual, NotNull
from wcs.qommon.form import CheckboxesWidget, DateWidget, Form
from wcs.sql_criterias import Equal, Less, NotEqual, NotNull, Null, Or
class LoggedErrorDirectory(Directory):
@ -132,8 +134,8 @@ class LoggedErrorDirectory(Directory):
return redirect('..')
class LoggedErrorsDirectory(Directory):
_q_exports = ['']
class LoggedErrorsDirectory(AccessControlled, Directory):
_q_exports = ['', 'cleanup']
@classmethod
def get_errors(cls, offset, limit, formdef_class=None, formdef_id=None, workflow_id=None):
@ -208,7 +210,7 @@ class LoggedErrorsDirectory(Directory):
self.formdef_id = formdef_id
self.workflow_id = workflow_id
def _q_index(self):
def _q_access(self):

Ajout de la classe AccessControlled, le contrôle d'acccès qui était posé en haut de _q_index est mis dans la nouvelle méthode _q_access, pour être effectif également pour la nouvelle vue.

Ajout de la classe AccessControlled, le contrôle d'acccès qui était posé en haut de _q_index est mis dans la nouvelle méthode _q_access, pour être effectif également pour la nouvelle vue.
backoffice_root = get_publisher().get_backoffice_root()
if not (
backoffice_root.is_accessible('forms')
@ -217,6 +219,7 @@ class LoggedErrorsDirectory(Directory):
):
raise errors.AccessForbiddenError()
def _q_index(self):
get_response().breadcrumb.append(('logged-errors/', _('Logged Errors')))
get_response().set_title(_('Logged Errors'))
limit = misc.get_int_or_400(
@ -240,6 +243,58 @@ class LoggedErrorsDirectory(Directory):
},
)
def cleanup(self):
backoffice_root = get_publisher().get_backoffice_root()
form = Form(enctype='multipart/form-data')
options = []
if backoffice_root.is_accessible('forms'):
options.append(('formdef', _('Forms'), 'formdef'))
if backoffice_root.is_accessible('cards'):
options.append(('carddef', _('Card Models'), 'carddef'))
if backoffice_root.is_accessible('workflows'):
options.append(('others', _('Others'), 'others'))
form.add(
CheckboxesWidget,
'types',
title=_('Error types'),

Un champ pour choisir les types d'erreur.

Un champ pour choisir les types d'erreur.
value=[x[0] for x in options], # check all by default
options=options,
required=True,
)
form.add(
DateWidget,
'latest_occurence',
title=_('Latest occurence'),
value=datetime.date.today() - datetime.timedelta(days=180),

Un autre pour choisir la date max pour la dernière occurence, valeur par défaut à à peu près 6 mois, c'est assez arbitraire.

Un autre pour choisir la date max pour la dernière occurence, valeur par défaut à à peu près 6 mois, c'est assez arbitraire.
required=True,
)
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
if form.get_widget('cancel').parse():
return redirect('.')
if form.get_submit() == 'submit' and not form.has_errors():
type_criterias = []
if 'formdef' in form.get_widget('types').parse():
type_criterias.append(Equal('formdef_class', 'FormDef'))
if 'carddef' in form.get_widget('types').parse():
type_criterias.append(Equal('formdef_class', 'CardDef'))
if 'others' in form.get_widget('types').parse():
type_criterias.append(Null('formdef_class'))

Possible d'ajouter un test avec aucune des cases présente cochée ? Juste pour vérifier que le form sera en erreur (parce que champ required), et qu'on ne passera pas dans un cas où il n'y a pas de critère sur formdef_class, ce qui supprimerait tout.

Possible d'ajouter un test avec aucune des cases présente cochée ? Juste pour vérifier que le form sera en erreur (parce que champ required), et qu'on ne passera pas dans un cas où il n'y a pas de critère sur formdef_class, ce qui supprimerait tout.

Voilà j'ai ajouté ce test, ainsi que quelques lignes de commentaire dans le test pour expliciter les différentes intentions.

Voilà j'ai ajouté ce test, ainsi que quelques lignes de commentaire dans le test pour expliciter les différentes intentions.
criterias = [
Less('latest_occurence_timestamp', form.get_widget('latest_occurence').parse()),
Or(type_criterias),
]
get_publisher().loggederror_class.wipe(clause=criterias)
return redirect('.')
get_response().set_title(_('Cleanup'))
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('Cleanup')
r += form.render()
return r.getvalue()
def _q_lookup(self, component):
try:
error = get_publisher().loggederror_class.get(component)

View File

@ -3,6 +3,10 @@
{% block appbar-title %}{% trans "Logged Errors" %}{% endblock %}
{% block appbar-actions %}
<a rel="popup" href="cleanup">{% trans "Cleanup" %}</a>
{% endblock %}
{% block content %}
<ul class="objects-list single-links">
{% for error in errors %}