configuration: Option to set a default app for FreedomBox

Closes #1315

Signed-off-by: Joseph Nuthalapati <njoseph@thoughtworks.com>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Joseph Nuthalapati 2018-07-24 21:45:53 +05:30 committed by James Valleroy
parent c733339719
commit 9a3af288fa
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
8 changed files with 166 additions and 68 deletions

View File

@ -9,3 +9,6 @@ Header set Strict-Transport-Security "max-age=31536000; includeSubDomains" env=H
## other services. ## other services.
## ##
RedirectMatch "^/$" "/plinth" RedirectMatch "^/$" "/plinth"
RedirectMatch "^/freedombox" "/plinth"
RedirectMatch "^/home" "/plinth"

View File

@ -29,3 +29,10 @@ Scenario: Change hostname
Scenario: Change domain name Scenario: Change domain name
When I change the domain name to mydomain When I change the domain name to mydomain
Then the domain name should be mydomain Then the domain name should be mydomain
Scenario: Change default app
Given the syncthing application is installed
And the default app is syncthing
When I change the default app to plinth
Then the default app should be plinth

View File

@ -36,6 +36,11 @@ language_codes = {
} }
@given(parsers.parse('the default app is {app_name:w}'))
def set_default_app(browser, app_name):
system.set_default_app(browser, app_name)
@given(parsers.parse('the domain name is set to {domain:w}')) @given(parsers.parse('the domain name is set to {domain:w}'))
def set_domain_name(browser, domain): def set_domain_name(browser, domain):
system.set_domain_name(browser, domain) system.set_domain_name(browser, domain)
@ -51,6 +56,11 @@ def change_domain_name_to(browser, domain):
system.set_domain_name(browser, domain) system.set_domain_name(browser, domain)
@when(parsers.parse('I change the default app to {app_name:w}'))
def change_default_app_to(browser, app_name):
system.set_default_app(browser, app_name)
@when('I change the language to <language>') @when('I change the language to <language>')
def change_language(browser, language): def change_language(browser, language):
system.set_language(browser, language_codes[language]) system.set_language(browser, language_codes[language])
@ -85,3 +95,8 @@ def create_snapshot(browser):
def verify_snapshot_count(browser, count): def verify_snapshot_count(browser, count):
num_snapshots = system.get_snapshot_count(browser) num_snapshots = system.get_snapshot_count(browser)
assert num_snapshots == count assert num_snapshots == count
@then(parsers.parse('the default app should be {app_name:w}'))
def default_app_should_be(browser, app_name):
assert system.check_home_page_redirect(browser, app_name)

View File

@ -58,6 +58,13 @@ def set_domain_name(browser, domain_name):
submit(browser) submit(browser)
def set_default_app(browser, app_name):
nav_to_module(browser, 'config')
drop_down = browser.find_by_id('id_configuration-defaultapp')
drop_down.select(app_name)
submit(browser)
def set_language(browser, language_code): def set_language(browser, language_code):
username = config['DEFAULT']['username'] username = config['DEFAULT']['username']
browser.visit(config['DEFAULT']['url'] + browser.visit(config['DEFAULT']['url'] +
@ -86,3 +93,9 @@ def get_snapshot_count(browser):
browser.visit(config['DEFAULT']['url'] + '/plinth/sys/snapshot/manage/') browser.visit(config['DEFAULT']['url'] + '/plinth/sys/snapshot/manage/')
# Subtract 1 for table header # Subtract 1 for table header
return len(browser.find_by_xpath('//tr')) - 1 return len(browser.find_by_xpath('//tr')) - 1
def check_home_page_redirect(browser, app_name):
browser.visit(config['DEFAULT']['url'])
return browser.find_by_xpath(
"//a[contains(@href, '/plinth/') and @title='FreedomBox']")

View File

@ -22,28 +22,33 @@ from . import actions
shortcuts = {} shortcuts = {}
def get_shortcuts(username): def get_shortcuts(username=None, web_apps_only=False, sort_by='label'):
"""Return menu items in sorted order according to current locale.""" """Return menu items in sorted order according to current locale."""
shortcuts_to_return = {}
if username: if username:
shortcuts_to_return = {}
output = actions.superuser_run('users', ['get-user-groups', username]) output = actions.superuser_run('users', ['get-user-groups', username])
user_groups = set(output.strip().split('\n')) user_groups = set(output.strip().split('\n'))
if 'admin' in user_groups: if 'admin' in user_groups: # Admin has access to all services
# Admin has access to all services shortcuts_to_return = shortcuts
return sorted(shortcuts.values(), key=lambda item: item['label']) else:
for shortcut_id, shortcut in shortcuts.items():
for shortcut_id, shortcut in shortcuts.items(): if shortcut['allowed_groups']:
if shortcut['allowed_groups']: if not user_groups.isdisjoint(shortcut['allowed_groups']):
if not user_groups.isdisjoint(shortcut['allowed_groups']): shortcuts_to_return[shortcut_id] = shortcut
else:
shortcuts_to_return[shortcut_id] = shortcut shortcuts_to_return[shortcut_id] = shortcut
else:
shortcuts_to_return[shortcut_id] = shortcut
return sorted(shortcuts_to_return.values(),
key=lambda item: item['label'])
else: else:
return sorted(shortcuts.values(), key=lambda item: item['label']) shortcuts_to_return = shortcuts
if web_apps_only:
shortcuts_to_return = {
_id: shortcut
for _id, shortcut in shortcuts_to_return.items()
if not shortcut['url'].startswith('?selected=')
}
return sorted(shortcuts_to_return.values(), key=lambda item: item[sort_by])
def add_shortcut(shortcut_id, name, short_description="", login_required=False, def add_shortcut(shortcut_id, name, short_description="", login_required=False,

View File

@ -18,6 +18,7 @@
FreedomBox app for basic system configuration. FreedomBox app for basic system configuration.
""" """
import re
import socket import socket
from django.utils.translation import ugettext_lazy from django.utils.translation import ugettext_lazy
@ -48,6 +49,16 @@ def get_hostname():
return socket.gethostname() return socket.gethostname()
def get_default_app():
"""Get the default application for the domain."""
with open('/etc/apache2/conf-available/freedombox.conf') as conf_file:
for line in conf_file:
if re.findall(r'\^\/\$', line):
app_path = line.split()[-1].strip('"')
break
return app_path.strip("/")
def init(): def init():
"""Initialize the module""" """Initialize the module"""
menu = main_menu.get('system') menu = main_menu.get('system')

View File

@ -14,38 +14,27 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
""" """
Forms for basic system configuration Forms for basic system configuration
""" """
from django import forms
from django.utils.translation import ugettext as _, ugettext_lazy
from django.core import validators
from django.core.exceptions import ValidationError
from plinth import cfg
from plinth.utils import format_lazy
import logging import logging
import re import re
from django import forms
from django.core import validators
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy
from plinth import cfg, frontpage
from plinth.utils import format_lazy
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
HOSTNAME_REGEX = r'^[a-zA-Z0-9]([-a-zA-Z0-9]{,61}[a-zA-Z0-9])?$' HOSTNAME_REGEX = r'^[a-zA-Z0-9]([-a-zA-Z0-9]{,61}[a-zA-Z0-9])?$'
class TrimmedCharField(forms.CharField):
"""Trim the contents of a CharField"""
def clean(self, value):
"""Clean and validate the field value"""
if value:
value = value.strip()
return super(TrimmedCharField, self).clean(value)
def domain_label_validator(domainname): def domain_label_validator(domainname):
"""Validate domain name labels.""" """Validate domain name labels."""
for label in domainname.split('.'): for label in domainname.split('.'):
@ -53,6 +42,12 @@ def domain_label_validator(domainname):
raise ValidationError(_('Invalid domain name')) raise ValidationError(_('Invalid domain name'))
def get_default_app_choices():
shortcuts = frontpage.get_shortcuts(web_apps_only=True, sort_by='name')
apps = [(shortcut['id'], shortcut['name']) for shortcut in shortcuts]
return [('plinth', 'FreedomBox Service (Plinth)')] + apps
class ConfigurationForm(forms.Form): class ConfigurationForm(forms.Form):
"""Main system configuration form""" """Main system configuration form"""
# See: # See:
@ -60,32 +55,44 @@ class ConfigurationForm(forms.Form):
# https://tools.ietf.org/html/rfc1035#section-2.3.1 # https://tools.ietf.org/html/rfc1035#section-2.3.1
# https://tools.ietf.org/html/rfc1123#section-2 # https://tools.ietf.org/html/rfc1123#section-2
# https://tools.ietf.org/html/rfc2181#section-11 # https://tools.ietf.org/html/rfc2181#section-11
hostname = TrimmedCharField( hostname = forms.CharField(
label=ugettext_lazy('Hostname'), label=ugettext_lazy('Hostname'), help_text=format_lazy(
help_text=format_lazy(ugettext_lazy( ugettext_lazy(
'Hostname is the local name by which other devices on the local ' 'Hostname is the local name by which other devices on the local '
'network can reach your {box_name}. It must start and end with ' 'network can reach your {box_name}. It must start and end with '
'an alphabet or a digit and have as interior characters only ' 'an alphabet or a digit and have as interior characters only '
'alphabets, digits and hyphens. Total length must be 63 ' 'alphabets, digits and hyphens. Total length must be 63 '
'characters or less.'), box_name=ugettext_lazy(cfg.box_name)), 'characters or less.'), box_name=ugettext_lazy(cfg.box_name)),
validators=[ validators=[
validators.RegexValidator( validators.RegexValidator(HOSTNAME_REGEX,
HOSTNAME_REGEX, ugettext_lazy('Invalid hostname'))
ugettext_lazy('Invalid hostname'))]) ], strip=True)
domainname = TrimmedCharField( domainname = forms.CharField(
label=ugettext_lazy('Domain Name'), label=ugettext_lazy('Domain Name'), help_text=format_lazy(
help_text=format_lazy(ugettext_lazy( ugettext_lazy(
'Domain name is the global name by which other devices on the ' 'Domain name is the global name by which other devices on the '
'Internet can reach your {box_name}. It must consist of labels ' 'Internet can reach your {box_name}. It must consist of labels '
'separated by dots. Each label must start and end with an ' 'separated by dots. Each label must start and end with an '
'alphabet or a digit and have as interior characters only ' 'alphabet or a digit and have as interior characters only '
'alphabets, digits and hyphens. Length of each label must be 63 ' 'alphabets, digits and hyphens. Length of each label must be 63 '
'characters or less. Total length of domain name must be 253 ' 'characters or less. Total length of domain name must be 253 '
'characters or less.'), box_name=ugettext_lazy(cfg.box_name)), 'characters or less.'), box_name=ugettext_lazy(cfg.box_name)),
required=False, required=False, validators=[
validators=[
validators.RegexValidator( validators.RegexValidator(
r'^[a-zA-Z0-9]([-a-zA-Z0-9.]{,251}[a-zA-Z0-9])?$', r'^[a-zA-Z0-9]([-a-zA-Z0-9.]{,251}[a-zA-Z0-9])?$',
ugettext_lazy('Invalid domain name')), ugettext_lazy('Invalid domain name')), domain_label_validator
domain_label_validator]) ], strip=True)
defaultapp = forms.ChoiceField(
label=ugettext_lazy('Default App'), help_text=format_lazy(
ugettext_lazy(
'Choose the default web application that must be served when '
'someone visits your {box_name} on the web. A typical use '
'case is to set your blog or wiki as the landing page when '
'someone visits the domain name. Note that once the default '
'app is set to something other than {box_name} Service '
'(Plinth), your users must explicitly type /plinth or '
'/freedombox to reach {box_name} Service (Plinth).'),
box_name=ugettext_lazy(cfg.box_name)), required=False,
choices=get_default_app_choices)

View File

@ -19,12 +19,13 @@ FreedomBox views for basic system configuration.
""" """
import logging import logging
import re
from django.contrib import messages from django.contrib import messages
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from plinth import actions from plinth import action_utils, actions, frontpage
from plinth.modules import config, firewall from plinth.modules import config, firewall
from plinth.modules.names import SERVICES from plinth.modules.names import SERVICES
from plinth.signals import (domain_added, domain_removed, domainname_change, from plinth.signals import (domain_added, domain_removed, domainname_change,
@ -63,6 +64,7 @@ def get_status(request):
return { return {
'hostname': config.get_hostname(), 'hostname': config.get_hostname(),
'domainname': config.get_domainname(), 'domainname': config.get_domainname(),
'defaultapp': config.get_default_app(),
} }
@ -72,9 +74,10 @@ def _apply_changes(request, old_status, new_status):
try: try:
set_hostname(new_status['hostname']) set_hostname(new_status['hostname'])
except Exception as exception: except Exception as exception:
messages.error(request, messages.error(
_('Error setting hostname: {exception}') request,
.format(exception=exception)) _('Error setting hostname: {exception}')
.format(exception=exception))
else: else:
messages.success(request, _('Hostname set')) messages.success(request, _('Hostname set'))
@ -82,12 +85,46 @@ def _apply_changes(request, old_status, new_status):
try: try:
set_domainname(new_status['domainname']) set_domainname(new_status['domainname'])
except Exception as exception: except Exception as exception:
messages.error(request, messages.error(
_('Error setting domain name: {exception}') request,
.format(exception=exception)) _('Error setting domain name: {exception}')
.format(exception=exception))
else: else:
messages.success(request, _('Domain name set')) messages.success(request, _('Domain name set'))
if old_status['defaultapp'] != new_status['defaultapp']:
try:
change_default_app(new_status['defaultapp'])
except Exception as exception:
messages.error(
request,
_('Error setting default app: {exception}')
.format(exception=exception))
else:
messages.success(request, _('Default app set'))
def change_default_app(app_id):
"""Changes the FreedomBox's default app to the app specified by app_id."""
if app_id == 'plinth':
url = '/plinth'
else:
shortcuts = frontpage.get_shortcuts()
url = [
shortcut['url'] for shortcut in shortcuts
if shortcut['id'] == app_id
][0]
lines = []
freedombox_apache_conf = '/etc/apache2/conf-available/freedombox.conf'
with open(freedombox_apache_conf, 'r') as conf_file:
for line in conf_file:
if re.findall(r'\^\/\$', line):
line = 'RedirectMatch "^/$" ' + '"{}"'.format(url)
lines.append(line)
with open(freedombox_apache_conf, 'w') as conf_file:
conf_file.write("\n".join(lines))
action_utils.service_reload('apache2')
def set_hostname(hostname): def set_hostname(hostname):
"""Sets machine hostname to hostname""" """Sets machine hostname to hostname"""
@ -95,7 +132,7 @@ def set_hostname(hostname):
domainname = config.get_domainname() domainname = config.get_domainname()
# Hostname should be ASCII. If it's unicode but passed our # Hostname should be ASCII. If it's unicode but passed our
# valid_hostname check, convert to ASCII. # valid_hostname check, convert
hostname = str(hostname) hostname = str(hostname)
pre_hostname_change.send_robust(sender='config', old_hostname=old_hostname, pre_hostname_change.send_robust(sender='config', old_hostname=old_hostname,