mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-21 07:55:00 +00:00
- Add tags to Info component of apps. Use only English tags for all operations. Localized tags are used for presentation to the user only. Add tags to all the apps. Conventions (English): 1. Tags describing use cases should be in kebab case. 2. Protocols in tag names should be in their canonical format. 3. Tags needn't be 100% technically correct. This can get in the way of comparing apps using a tag. Words that describe use cases that users can easily understand should be preferred over being pedantic. 4. Tags should be short, ideally not more than 2 words. Avoid conjunctions like "and", "or" in tags. 5. Avoid redundant words like "server", or "web-clients". Most apps on FreedomBox are either servers or web clients. 6. Keep your nouns singular in tags. - Use query params to filter the Apps page by tags. When all tags are removed, redirect to /apps. - Add UI elements to add and remove tag filters in the Apps page. Make the UI similar to GitLab issue tags. Since there are 40 apps, there will be at least 40 tags. Selecting a tag from a dropdown will be difficult on mobile devices. A fuzzy search is useful to find tags to add to the filter. Allow user to find the best match for the search term and highlight it visually. The user can then press Enter to select the highlighted tag. Make tag search case-insensitive. Make the dropdown menu scrollable with a fixed size. User input is debounced by 300 ms during search. - tests: Add missing mock in test_module_loader.py - Add functional tests [sunil] - 'list' can be used instead of 'List' for typing in recent Python versions. - Reserve tripe-quoted strings for docstrings. - Undo some changes in module initialization, use module_name for logging errors. - isort and yapf changes. - Encode parameters before adding them to the URL. Tests: - Tested the functionality of filtering by tag with one tag and two tags. Signed-off-by: Joseph Nuthalapati <njoseph@riseup.net> Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: Sunil Mohan Adapa <sunil@medhas.org>
153 lines
4.7 KiB
Python
153 lines
4.7 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
"""
|
|
Discover, load and manage FreedomBox applications.
|
|
"""
|
|
|
|
import importlib
|
|
import logging
|
|
import pathlib
|
|
import re
|
|
|
|
import django
|
|
|
|
from plinth import cfg
|
|
from plinth.signals import pre_module_loading
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
loaded_modules = dict()
|
|
_modules_to_load = None
|
|
|
|
|
|
def include_urls():
|
|
"""Include the URLs of the modules into main Django project."""
|
|
for module_import_path in get_modules_to_load():
|
|
module_name = module_import_path.split('.')[-1]
|
|
_include_module_urls(module_import_path, module_name)
|
|
|
|
|
|
def load_modules():
|
|
"""
|
|
Read names of enabled modules in modules/enabled directory and
|
|
import them from modules directory.
|
|
"""
|
|
pre_module_loading.send_robust(sender="module_loader")
|
|
for module_import_path in get_modules_to_load():
|
|
module_name = module_import_path.split('.')[-1]
|
|
try:
|
|
module = importlib.import_module(module_import_path)
|
|
loaded_modules[module_name] = module
|
|
except Exception as exception:
|
|
logger.exception('Could not import %s: %s', module_import_path,
|
|
exception)
|
|
if cfg.develop:
|
|
raise
|
|
|
|
|
|
def _include_module_urls(module_import_path, module_name):
|
|
"""Include the module's URLs in global project URLs list"""
|
|
from plinth import urls
|
|
url_module = module_import_path + '.urls'
|
|
try:
|
|
urls.urlpatterns += [
|
|
django.urls.re_path(r'',
|
|
django.urls.include((url_module, module_name)))
|
|
]
|
|
except ImportError:
|
|
logger.debug('No URLs for %s', module_name)
|
|
if cfg.develop:
|
|
raise
|
|
|
|
|
|
def _get_modules_enabled_paths():
|
|
"""Return list of paths from which enabled modules list must be read."""
|
|
return [
|
|
pathlib.Path('/usr/share/freedombox/modules-enabled/'),
|
|
pathlib.Path('/etc/plinth/modules-enabled/'), # For compatibility
|
|
pathlib.Path('/etc/freedombox/modules-enabled/'),
|
|
]
|
|
|
|
|
|
def _get_modules_enabled_files_to_read():
|
|
"""Return the list of files containing modules import paths."""
|
|
module_files = {}
|
|
for path in _get_modules_enabled_paths():
|
|
directory = pathlib.Path(path)
|
|
files = list(directory.glob('*'))
|
|
for file_ in files:
|
|
# Omit hidden or backup files
|
|
if file_.name.startswith('.') or '.dpkg' in file_.name:
|
|
continue
|
|
|
|
if file_.is_symlink() and str(file_.resolve()) == '/dev/null':
|
|
module_files.pop(file_.name, None)
|
|
continue
|
|
|
|
module_files[file_.name] = file_
|
|
|
|
if module_files:
|
|
return module_files.values()
|
|
|
|
# 'make build install' has not been executed yet. Pickup files to load from
|
|
# local module directories.
|
|
directory = pathlib.Path(__file__).parent
|
|
glob_pattern = 'modules/*/data/usr/share/freedombox/modules-enabled/*'
|
|
return list(directory.glob(glob_pattern))
|
|
|
|
|
|
def get_modules_to_load():
|
|
"""Get the list of modules to be loaded"""
|
|
global _modules_to_load
|
|
if _modules_to_load is not None:
|
|
return _modules_to_load
|
|
|
|
modules = []
|
|
for file_ in _get_modules_enabled_files_to_read():
|
|
module = _read_module_import_paths_from_file(file_)
|
|
if module:
|
|
modules.append(module)
|
|
|
|
_modules_to_load = modules
|
|
return modules
|
|
|
|
|
|
def get_module_import_path(module_name: str) -> str:
|
|
"""Return the import path for a module."""
|
|
import_path_file = None
|
|
for path in _get_modules_enabled_paths():
|
|
file_ = path / module_name
|
|
if file_.exists():
|
|
import_path_file = file_
|
|
|
|
if file_.is_symlink() and str(file_.resolve()) == '/dev/null':
|
|
import_path_file = None
|
|
|
|
if not import_path_file:
|
|
# 'make build install' has not been executed yet. Pickup files to load
|
|
# from local module directories.
|
|
directory = pathlib.Path(__file__).parent
|
|
import_path_file = (directory /
|
|
f'modules/{module_name}/data/usr/share/'
|
|
f'freedombox/modules-enabled/{module_name}')
|
|
|
|
if not import_path_file:
|
|
raise ValueError('Unknown module')
|
|
|
|
import_path = _read_module_import_paths_from_file(import_path_file)
|
|
if not import_path:
|
|
raise ValueError('Module disabled')
|
|
|
|
return import_path
|
|
|
|
|
|
def _read_module_import_paths_from_file(file_path: pathlib.Path) -> str | None:
|
|
"""Read a module's import path from a file."""
|
|
with file_path.open() as file_handle:
|
|
for line in file_handle:
|
|
line = re.sub('#.*', '', line)
|
|
line = line.strip()
|
|
if line:
|
|
return line
|
|
|
|
return None
|