diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..35b1102 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,17 @@ +[run] +branch = True +source = . +concurrency = multiprocessing +parallel = True +omit = + */migrations/* + venv/* + */tests/* + +[report] +fail_under = 90 +precision = 2 +sort = Cover +show_missing = False +skip_covered = True +skip_empty = True diff --git a/install_requirements.py b/install_requirements.py new file mode 100644 index 0000000..3e0f25c --- /dev/null +++ b/install_requirements.py @@ -0,0 +1,71 @@ +# +# OSIS stands for Open Student Information System. It's an application +# designed to manage the core business of higher education institutions, +# such as universities, faculties, institutes and professional schools. +# The core business involves the administration of students, teachers, +# courses, programs and so on. +# +# Copyright (C) 2015-2021 Université catholique de Louvain (http://www.uclouvain.be) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# A copy of this license - GNU General Public License - is available +# at the root of the source code of this program. If not, +# see http://www.gnu.org/licenses/. +# +############################################################################## +import glob +import subprocess +import sys +from typing import List +import argparse + + +DEFAULT_CORE_REQUIREMENTS_FILE = './dev-requirements.txt' + +parser = argparse.ArgumentParser(description="A utility to install OSIS project requirements.") +parser.add_argument( + "-c", + "--core", + help="core requirements file (default: dev-requirements.txt)", + default=DEFAULT_CORE_REQUIREMENTS_FILE, + type=str, + metavar="FILE" +) + +args = parser.parse_args() + + +def install(core_file: str) -> None: + install_requirement(core_file) + install_app_requirements() + + +def install_app_requirements(): + app_requirements = find_app_requirements() + for requirement in app_requirements: + install_requirement(requirement) + + +def find_app_requirements() -> List[str]: + return [file for file in glob.glob("./*/requirements.txt")] + + +def install_requirement(path: str) -> None: + subprocess.run( + ["pip", "install", "-r", path], + stdout=sys.stdout, + stderr=sys.stderr, + universal_newlines=True, + ) + + +install(args.core) diff --git a/management/commands/check_all_app_messages.py b/management/commands/check_all_app_messages.py new file mode 100644 index 0000000..0cefd55 --- /dev/null +++ b/management/commands/check_all_app_messages.py @@ -0,0 +1,67 @@ +# ############################################################################ +# OSIS stands for Open Student Information System. It's an application +# designed to manage the core business of higher education institutions, +# such as universities, faculties, institutes and professional schools. +# The core business involves the administration of students, teachers, +# courses, programs and so on. +# +# Copyright (C) 2015-2020 Université catholique de Louvain (http://www.uclouvain.be) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# A copy of this license - GNU General Public License - is available +# at the root of the source code of this program. If not, +# see http://www.gnu.org/licenses/. +# ############################################################################ +import pathlib +import subprocess +from typing import Tuple + +from django.conf import settings +from django.core.management import BaseCommand + +OUTPUT_MESSAGE = str +RETURN_CODE = int +SUCCESS_RETURN_CODE = 0 + + +class Command(BaseCommand): + def handle(self, *args, **options): + apps = settings.INSTALLED_APPS + apps_with_locale_directory = [app for app in apps if has_locale_directory(app)] + + errors = [] + for app in apps_with_locale_directory: + output_message, return_code = check_messages_for_app(app) + if return_code != SUCCESS_RETURN_CODE: + error_message = "{app}\n{output}".format(app=app, output=output_message) + errors.append(error_message) + + if errors: + self.stderr.write("\n".join(errors)) + raise SystemExit(1) + else: + self.stdout.write("All good") + + +def check_messages_for_app(app_name: str) -> Tuple[OUTPUT_MESSAGE, RETURN_CODE]: + completed_process = subprocess.run( + ["./manage.py", "check_app_messages", app_name], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + return completed_process.stderr, completed_process.returncode + + +def has_locale_directory(app_name: str) -> bool: + path = pathlib.Path(app_name) / "locale" + return path.exists() diff --git a/management/commands/check_app_messages.py b/management/commands/check_app_messages.py new file mode 100644 index 0000000..3bba261 --- /dev/null +++ b/management/commands/check_app_messages.py @@ -0,0 +1,71 @@ +# ############################################################################ +# OSIS stands for Open Student Information System. It's an application +# designed to manage the core business of higher education institutions, +# such as universities, faculties, institutes and professional schools. +# The core business involves the administration of students, teachers, +# courses, programs and so on. +# +# Copyright (C) 2015-2020 Université catholique de Louvain (http://www.uclouvain.be) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# A copy of this license - GNU General Public License - is available +# at the root of the source code of this program. If not, +# see http://www.gnu.org/licenses/. +# ############################################################################ +import os +import subprocess + +from osis_common.management.commands import makemessages + + +class MessagesNotTranslatedException(Exception): + pass + + +class Command(makemessages.Command): + def add_arguments(self, parser): + super().add_arguments(parser) + parser.add_argument('directory') + + def handle(self, *args, **options): + options['keep_pot'] = True + options['verbosity'] = 0 + + # Change dir for makemessage execution + previous_dir = os.getcwd() + os.chdir(options['directory']) + super().handle(*args, **options) + + self.check_all_messages_are_translated() + os.chdir(previous_dir) + + def check_all_messages_are_translated(self): + fr_po_file_location = "locale/fr_BE/LC_MESSAGES/django.po" + pot_file_location = "locale/django.pot" + + try: + subprocess.run( + ["msgcmp", fr_po_file_location, pot_file_location], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + check=True + ) + except subprocess.CalledProcessError as e: + self.stderr.write(e.stderr) + raise SystemExit(1) + finally: + self.remove_potfiles() + + def write_po_file(self, potfile, locale): + # don't need to overwrite existing po file + pass diff --git a/management/commands/check_coverage.py b/management/commands/check_coverage.py new file mode 100644 index 0000000..b7b7d1f --- /dev/null +++ b/management/commands/check_coverage.py @@ -0,0 +1,108 @@ +# ############################################################################ +# OSIS stands for Open Student Information System. It's an application +# designed to manage the core business of higher education institutions, +# such as universities, faculties, institutes and professional schools. +# The core business involves the administration of students, teachers, +# courses, programs and so on. +# +# Copyright (C) 2015-2020 Université catholique de Louvain (http://www.uclouvain.be) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# A copy of this license - GNU General Public License - is available +# at the root of the source code of this program. If not, +# see http://www.gnu.org/licenses/. +# ############################################################################ + +from django.core.management import BaseCommand + +from osis_common.management import utils +from osis_common.management.utils import RunCommandMixin + + +ALL_APPS = 'ALL' +DEFAULT_TARGET_BRANCH = "origin/dev" + + +class Command(BaseCommand, RunCommandMixin): + help = "Launch tests and show diff coverage." + + def add_arguments(self, parser): + super().add_arguments(parser) + + parser.add_argument( + "-t", + "--target_branch", + help="target branch to compare with", + default=DEFAULT_TARGET_BRANCH, + type=str, + metavar="BRANCH" + ) + + parser.add_argument( + "-a", + "--app", + help="limit to app", + default=ALL_APPS, + type=str, + metavar="APP" + ) + + def handle(self, *args, **options): + target_branch = options['target_branch'] + target_app = options['app'] + + success = self.run_tests(target_app) + self.show_diff_coverage(target_branch, target_app) + + if not success: + self.stdout.write( + self.style.ERROR("TESTS FAILED") + ) + raise SystemExit(1) + + self.stdout.write( + self.style.SUCCESS("TESTS PASSED") + ) + + def run_tests(self, target_app: str) -> utils.SUCCESS_RESULT: + return self.run_command( + [ + "coverage", + "run", + "--rcfile=osis_common/.coveragerc", + "--source={}/".format(target_app) if target_app != ALL_APPS else "", + "manage.py", + "test", + target_app if target_app != ALL_APPS else "", + "--parallel", + "--no-logs", + "--noinput" + ] + ) + + def show_diff_coverage(self, target_branch: str, target_app): + self.run_command( + ["coverage", "combine"], + ) + + self.run_command( + ["coverage", "xml"], + ) + + self.run_command( + [ + "diff-cover", + "../coverage.xml" if target_app != ALL_APPS else "coverage.xml", + "--compare-branch={}".format(target_branch) + ], + cwd=self.compute_app_path(target_app) if target_app != ALL_APPS else None + ) diff --git a/management/commands/check_quality.py b/management/commands/check_quality.py new file mode 100644 index 0000000..4e84cd9 --- /dev/null +++ b/management/commands/check_quality.py @@ -0,0 +1,103 @@ +# ############################################################################ +# OSIS stands for Open Student Information System. It's an application +# designed to manage the core business of higher education institutions, +# such as universities, faculties, institutes and professional schools. +# The core business involves the administration of students, teachers, +# courses, programs and so on. +# +# Copyright (C) 2015-2020 Université catholique de Louvain (http://www.uclouvain.be) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# A copy of this license - GNU General Public License - is available +# at the root of the source code of this program. If not, +# see http://www.gnu.org/licenses/. +# ############################################################################ + +from django.core.management import BaseCommand + +from osis_common.management import utils +from osis_common.management.utils import RunCommandMixin + + +ALL_APPS = 'ALL' +DEFAULT_TARGET_BRANCH = "origin/dev" + + +class Command(BaseCommand, RunCommandMixin): + help = "Check the quality of code (pycodestyle, pylint, ...)" + + def add_arguments(self, parser): + super().add_arguments(parser) + + parser.add_argument( + "-t", + "--target_branch", + help="target branch to compare with", + default=DEFAULT_TARGET_BRANCH, + type=str, + metavar="BRANCH" + ) + + parser.add_argument( + "-a", + "--app", + help="limit to app", + default=ALL_APPS, + type=str, + metavar="APP" + ) + + def handle(self, *args, **options): + target_branch = options['target_branch'] + target_app = options['app'] + + failed_checks = [] + + if not self.check_pycodestyle(target_branch, target_app): + failed_checks.append('pycodestyle') + + if not self.check_pylint(target_branch, target_app): + failed_checks.append('pylint') + + if failed_checks: + self.stdout.write( + self.style.ERROR( + "QUALITY CHECKS FAILED : {}".format(', '.join(failed_checks)) + ) + ) + raise SystemExit(1) + + self.stdout.write( + self.style.SUCCESS("QUALITY CHECKS PASSED") + ) + + def check_pycodestyle(self, target_branch: str, target_app: str) -> utils.SUCCESS_RESULT: + return self.run_command( + [ + "diff-quality", + "--violations=pycodestyle", + "--compare-branch={}".format(target_branch), + "--fail-under=100" + ], + cwd=self.compute_app_path(target_app) if target_app != ALL_APPS else None + ) + + def check_pylint(self, target_branch: str, target_app: str) -> utils.SUCCESS_RESULT: + return self.run_command( + [ + "diff-quality", + "--violations=pylint", + "--compare-branch={}".format(target_branch), + "--fail-under=90" + ], + cwd=self.compute_app_path(target_app) if target_app != ALL_APPS else None + ) diff --git a/management/commands/makemessages.py b/management/commands/makemessages.py new file mode 100644 index 0000000..15305df --- /dev/null +++ b/management/commands/makemessages.py @@ -0,0 +1,30 @@ +############################################################################## +# +# OSIS stands for Open Student Information System. It's an application +# designed to manage the core business of higher education institutions, +# such as universities, faculties, institutes and professional schools. +# The core business involves the administration of students, teachers, +# courses, programs and so on. +# +# Copyright (C) 2015-2018 Université catholique de Louvain (http://www.uclouvain.be) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# A copy of this license - GNU General Public License - is available +# at the root of the source code of this program. If not, +# see http://www.gnu.org/licenses/. +# +############################################################################## +from django.core.management.commands import makemessages + + +class Command(makemessages.Command): + xgettext_options = makemessages.Command.xgettext_options + ['--sort-output', '--no-location'] diff --git a/management/utils.py b/management/utils.py new file mode 100644 index 0000000..8e29230 --- /dev/null +++ b/management/utils.py @@ -0,0 +1,80 @@ +# +# OSIS stands for Open Student Information System. It's an application +# designed to manage the core business of higher education institutions, +# such as universities, faculties, institutes and professional schools. +# The core business involves the administration of students, teachers, +# courses, programs and so on. +# +# Copyright (C) 2015-2021 Université catholique de Louvain (http://www.uclouvain.be) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# A copy of this license - GNU General Public License - is available +# at the root of the source code of this program. If not, +# see http://www.gnu.org/licenses/. +# +############################################################################## +import subprocess +import sys +from typing import List + +from django.core.management.base import OutputWrapper + +SUCCESS_RESULT = bool + + +class RunCommandMixin: + """ + Mixin for running shell commands for django manage.py commands. + """ + + stdout = None # type: OutputWrapper + + def run_command( + self, + cmd_args: List[str], + stdout=sys.stdout, + stderr=sys.stdout, + **kwargs + ) -> SUCCESS_RESULT: + """ + Run a command via subprocess.run(). + Remove empty string and non value from cmd_args before executing command. + + :param cmd_args: subprocess.run args value + :param stdout: standard output + :param stderr: standard error + :param kwargs: subprocess.run parameters + :return: a success result that is true if the command was successful + """ + success = True + cleaned_cmd_args = [argument for argument in cmd_args if argument] + + self.stdout.write( + self.style.MIGRATE_HEADING(" ".join(cleaned_cmd_args)) + ) + + try: + completed_process = subprocess.run( + cleaned_cmd_args, + stdout=stdout, + stderr=stderr, + **kwargs + ) + completed_process.check_returncode() + except subprocess.CalledProcessError: + success = False + + self.stdout.write("\n\n") + return success + + def compute_app_path(self, app_name: str) -> str: + return "./{}".format(app_name) diff --git a/requirements.txt b/requirements.txt index 3b741c5..09197e7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,10 @@ django-ckeditor==5.7.1 # Version 5.2.2 to support PasteFromWord into CKEditor XlsxWriter==0.9.3 voluptuous==0.9.3 weasyprint==0.42.3 -attrs==19.3.0 \ No newline at end of file +attrs==19.3.0 + +coverage==6.1.1 +pycodestyle==2.4.0 +pylint==2.4.4 +diff-cover==6.4.2 +tblib==1.7.0 \ No newline at end of file