backups: Add UI to edit schedules

Closes: #1529.

Tests performed:

- Functional tests for backups app work.

- Functional tests for backup of several apps work.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Sunil Mohan Adapa 2021-01-05 16:30:36 -08:00 committed by James Valleroy
parent b33362cb7a
commit eb526275c7
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
7 changed files with 219 additions and 2 deletions

View File

@ -46,6 +46,54 @@ def _get_repository_choices():
return choices
class ScheduleForm(forms.Form):
"""Form to edit backups schedule."""
enabled = forms.BooleanField(
label=_('Enable scheduled backups'), required=False,
help_text=_('If enabled, a backup is taken every day, every week and '
'every month. Older backups are removed.'))
daily_to_keep = forms.IntegerField(
label=_('Number of daily backups to keep'), required=True, min_value=0,
help_text=_('This many latest backups are kept and the rest are '
'removed. A value of "0" disables backups of this type. '
'Triggered at specified hour every day.'))
weekly_to_keep = forms.IntegerField(
label=_('Number of weekly backups to keep'), required=True,
min_value=0,
help_text=_('This many latest backups are kept and the rest are '
'removed. A value of "0" disables backups of this type. '
'Triggered at specified hour every Sunday.'))
monthly_to_keep = forms.IntegerField(
label=_('Number of monthly backups to keep'), required=True,
min_value=0,
help_text=_('This many latest backups are kept and the rest are '
'removed. A value of "0" disables backups of this type. '
'Triggered at specified hour first day of every month.'))
run_at_hour = forms.IntegerField(
label=_('Hour of the day to trigger backup operation'), required=True,
min_value=0, max_value=23, help_text=_('In 24 hour format.'))
selected_apps = forms.MultipleChoiceField(
label=_('Included apps'), help_text=_('Apps to include in the backup'),
widget=forms.CheckboxSelectMultiple(attrs={'class': 'has-select-all'}))
def __init__(self, *args, **kwargs):
"""Initialize the form with selectable apps."""
super().__init__(*args, **kwargs)
components = api.get_all_components_for_backup()
choices = _get_app_choices(components)
self.fields['selected_apps'].choices = choices
self.fields['selected_apps'].initial = [
choice[0] for choice in choices
if choice[0] not in self.initial.get('unselected_apps', [])
]
class CreateArchiveForm(forms.Form):
repository = forms.ChoiceField(label=_('Repository'))
name = forms.RegexField(

View File

@ -23,6 +23,12 @@
{{ repository.name }}
<span class="pull-right">
<a class="repository-schedule btn btn-sm btn-primary"
href="{% url 'backups:schedule' uuid %}">
<span class="fa fa-clock-o" aria-hidden="true"></span>
{% trans "Schedule" %}
</a>
{% if repository.flags.mountable %}
{% if repository.mounted %}

View File

@ -0,0 +1,21 @@
{% extends "base.html" %}
{% comment %}
# SPDX-License-Identifier: AGPL-3.0-or-later
{% endcomment %}
{% load bootstrap %}
{% load i18n %}
{% load static %}
{% block content %}
<h2>{{ title }}</h2>
<form class="form" method="post">
{% csrf_token %}
{{ form|bootstrap }}
<input type="submit" class="btn btn-primary"
value="{% trans 'Update' %}" />
</form>
{% endblock %}

View File

@ -24,3 +24,8 @@ Scenario: Download, upload and restore a backup
And I download the app data backup with name test_backups
And I restore the downloaded app data backup
Then bind forwarders should be 1.1.1.1
Scenario: Set a schedule for a repository
Given the backup schedule is set to disable for 1 daily, 2 weekly and 3 monthly at 2:00 without app names
When I set the backup schedule to enable for 10 daily, 20 weekly and 30 monthly at 15:00 without app firewall
Then the schedule should be set to enable for 10 daily, 20 weekly and 30 monthly at 15:00 without app firewall

View File

@ -9,7 +9,7 @@ import urllib.parse
import requests
from pytest import fixture
from pytest_bdd import parsers, scenarios, then, when
from pytest_bdd import given, parsers, scenarios, then, when
from plinth.tests import functional
@ -50,6 +50,42 @@ def backup_restore_from_upload(session_browser, app_name,
os.remove(path)
@given(
parsers.parse('the backup schedule is set to {enable:w} for {daily:d} '
'daily, {weekly:d} weekly and {monthly:d} monthly at '
'{run_at:d}:00 without app {without_app:w}'))
def backup_schedule_set(session_browser, enable, daily, weekly, monthly,
run_at, without_app):
_backup_schedule_set(session_browser, enable == 'enable', daily, weekly,
monthly, run_at, without_app)
@when(
parsers.parse('I set the backup schedule to {enable:w} for {daily:d} '
'daily, {weekly:d} weekly and {monthly:d} monthly at '
'{run_at:d}:00 without app {without_app:w}'))
def backup_schedule_set2(session_browser, enable, daily, weekly, monthly,
run_at, without_app):
_backup_schedule_set(session_browser, enable == 'enable', daily, weekly,
monthly, run_at, without_app)
@then(
parsers.parse('the schedule should be set to {enable:w} for {daily:d} '
'daily, {weekly:d} weekly and {monthly:d} monthly at '
'{run_at:d}:00 without app {without_app:w}'))
def backup_schedule_assert(session_browser, enable, daily, weekly, monthly,
run_at, without_app):
schedule = _backup_schedule_get(session_browser)
assert schedule['enable'] == (enable == 'enable')
assert schedule['daily'] == daily
assert schedule['weekly'] == weekly
assert schedule['monthly'] == monthly
assert schedule['run_at'] == run_at
assert len(schedule['without_apps']) == 1
assert schedule['without_apps'][0] == without_app
def _open_main_page(browser):
with functional.wait_for_page_update(browser):
browser.find_link_by_href('/plinth/').first.click()
@ -88,3 +124,53 @@ def _upload_and_restore(browser, app_name, downloaded_file_path):
with functional.wait_for_page_update(browser,
expected_url='/plinth/sys/backups/'):
functional.submit(browser)
def _backup_schedule_set(browser, enable, daily, weekly, monthly, run_at,
without_app):
"""Set the schedule for root repository."""
functional.nav_to_module(browser, 'backups')
browser.find_link_by_href(
'/plinth/sys/backups/root/schedule/').first.click()
if enable:
browser.find_by_name('backups_schedule-enabled').check()
else:
browser.find_by_name('backups_schedule-enabled').uncheck()
browser.fill('backups_schedule-daily_to_keep', daily)
browser.fill('backups_schedule-weekly_to_keep', weekly)
browser.fill('backups_schedule-monthly_to_keep', monthly)
browser.fill('backups_schedule-run_at_hour', run_at)
functional.eventually(browser.find_by_css, args=['.select-all'])
browser.find_by_css('.select-all').first.check()
browser.find_by_css(f'input[value="{without_app}"]').first.uncheck()
functional.submit(browser)
def _backup_schedule_get(browser):
"""Return the current schedule set for the root repository."""
functional.nav_to_module(browser, 'backups')
browser.find_link_by_href(
'/plinth/sys/backups/root/schedule/').first.click()
without_apps = []
elements = browser.find_by_name('backups_schedule-selected_apps')
for element in elements:
if not element.checked:
without_apps.append(element.value)
return {
'enable':
browser.find_by_name('backups_schedule-enabled').checked,
'daily':
int(browser.find_by_name('backups_schedule-daily_to_keep').value),
'weekly':
int(browser.find_by_name('backups_schedule-weekly_to_keep').value),
'monthly':
int(
browser.find_by_name('backups_schedule-monthly_to_keep').value
),
'run_at':
int(browser.find_by_name('backups_schedule-run_at_hour').value),
'without_apps':
without_apps
}

View File

@ -8,11 +8,13 @@ from django.conf.urls import url
from .views import (AddRemoteRepositoryView, AddRepositoryView,
CreateArchiveView, DeleteArchiveView, DownloadArchiveView,
IndexView, RemoveRepositoryView, RestoreArchiveView,
RestoreFromUploadView, UploadArchiveView,
RestoreFromUploadView, ScheduleView, UploadArchiveView,
VerifySshHostkeyView, mount_repository, umount_repository)
urlpatterns = [
url(r'^sys/backups/$', IndexView.as_view(), name='index'),
url(r'^sys/backups/(?P<uuid>[^/]+)/schedule/$', ScheduleView.as_view(),
name='schedule'),
url(r'^sys/backups/create/$', CreateArchiveView.as_view(), name='create'),
url(r'^sys/backups/(?P<uuid>[^/]+)/download/(?P<name>[^/]+)/$',
DownloadArchiveView.as_view(), name='download'),

View File

@ -47,6 +47,55 @@ class IndexView(TemplateView):
return context
class ScheduleView(SuccessMessageMixin, FormView):
form_class = forms.ScheduleForm
prefix = 'backups_schedule'
template_name = 'backups_schedule.html'
success_url = reverse_lazy('backups:index')
success_message = ugettext_lazy('Backup schedule updated.')
def get_initial(self):
"""Return the values to fill in the form."""
initial = super().get_initial()
schedule = get_instance(self.kwargs['uuid']).schedule
initial.update({
'enabled': schedule.enabled,
'daily_to_keep': schedule.daily_to_keep,
'weekly_to_keep': schedule.weekly_to_keep,
'monthly_to_keep': schedule.monthly_to_keep,
'run_at_hour': schedule.run_at_hour,
'unselected_apps': schedule.unselected_apps,
})
return initial
def get_context_data(self, **kwargs):
"""Return additional context for rendering the template."""
context = super().get_context_data(**kwargs)
context['title'] = _('Schedule Backups')
return context
def form_valid(self, form):
"""Update backup schedule."""
repository = get_instance(self.kwargs['uuid'])
schedule = repository.schedule
data = form.cleaned_data
schedule.enabled = data['enabled']
schedule.daily_to_keep = data['daily_to_keep']
schedule.weekly_to_keep = data['weekly_to_keep']
schedule.monthly_to_keep = data['monthly_to_keep']
schedule.run_at_hour = data['run_at_hour']
components = api.get_all_components_for_backup()
unselected_apps = [
component.app_id for component in components
if component.app_id not in data['selected_apps']
]
schedule.unselected_apps = unselected_apps
repository.save()
return super().form_valid(form)
class CreateArchiveView(SuccessMessageMixin, FormView):
"""View to create a new archive."""
form_class = forms.CreateArchiveForm