mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-05-27 10:44:33 +00:00
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:
parent
b33362cb7a
commit
eb526275c7
@ -46,6 +46,54 @@ def _get_repository_choices():
|
|||||||
return 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):
|
class CreateArchiveForm(forms.Form):
|
||||||
repository = forms.ChoiceField(label=_('Repository'))
|
repository = forms.ChoiceField(label=_('Repository'))
|
||||||
name = forms.RegexField(
|
name = forms.RegexField(
|
||||||
|
|||||||
@ -23,6 +23,12 @@
|
|||||||
{{ repository.name }}
|
{{ repository.name }}
|
||||||
|
|
||||||
<span class="pull-right">
|
<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.flags.mountable %}
|
||||||
|
|
||||||
{% if repository.mounted %}
|
{% if repository.mounted %}
|
||||||
|
|||||||
21
plinth/modules/backups/templates/backups_schedule.html
Normal file
21
plinth/modules/backups/templates/backups_schedule.html
Normal 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 %}
|
||||||
@ -24,3 +24,8 @@ Scenario: Download, upload and restore a backup
|
|||||||
And I download the app data backup with name test_backups
|
And I download the app data backup with name test_backups
|
||||||
And I restore the downloaded app data backup
|
And I restore the downloaded app data backup
|
||||||
Then bind forwarders should be 1.1.1.1
|
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
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import urllib.parse
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
from pytest import fixture
|
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
|
from plinth.tests import functional
|
||||||
|
|
||||||
@ -50,6 +50,42 @@ def backup_restore_from_upload(session_browser, app_name,
|
|||||||
os.remove(path)
|
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):
|
def _open_main_page(browser):
|
||||||
with functional.wait_for_page_update(browser):
|
with functional.wait_for_page_update(browser):
|
||||||
browser.find_link_by_href('/plinth/').first.click()
|
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,
|
with functional.wait_for_page_update(browser,
|
||||||
expected_url='/plinth/sys/backups/'):
|
expected_url='/plinth/sys/backups/'):
|
||||||
functional.submit(browser)
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -8,11 +8,13 @@ from django.conf.urls import url
|
|||||||
from .views import (AddRemoteRepositoryView, AddRepositoryView,
|
from .views import (AddRemoteRepositoryView, AddRepositoryView,
|
||||||
CreateArchiveView, DeleteArchiveView, DownloadArchiveView,
|
CreateArchiveView, DeleteArchiveView, DownloadArchiveView,
|
||||||
IndexView, RemoveRepositoryView, RestoreArchiveView,
|
IndexView, RemoveRepositoryView, RestoreArchiveView,
|
||||||
RestoreFromUploadView, UploadArchiveView,
|
RestoreFromUploadView, ScheduleView, UploadArchiveView,
|
||||||
VerifySshHostkeyView, mount_repository, umount_repository)
|
VerifySshHostkeyView, mount_repository, umount_repository)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^sys/backups/$', IndexView.as_view(), name='index'),
|
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/create/$', CreateArchiveView.as_view(), name='create'),
|
||||||
url(r'^sys/backups/(?P<uuid>[^/]+)/download/(?P<name>[^/]+)/$',
|
url(r'^sys/backups/(?P<uuid>[^/]+)/download/(?P<name>[^/]+)/$',
|
||||||
DownloadArchiveView.as_view(), name='download'),
|
DownloadArchiveView.as_view(), name='download'),
|
||||||
|
|||||||
@ -47,6 +47,55 @@ class IndexView(TemplateView):
|
|||||||
return context
|
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):
|
class CreateArchiveView(SuccessMessageMixin, FormView):
|
||||||
"""View to create a new archive."""
|
"""View to create a new archive."""
|
||||||
form_class = forms.CreateArchiveForm
|
form_class = forms.CreateArchiveForm
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user