diff --git a/.gitignore b/.gitignore index d55d7fd62..5b7614c2e 100644 --- a/.gitignore +++ b/.gitignore @@ -273,3 +273,6 @@ pip-selfcheck.json ### Project template codewof/media/ + +### Tem files +codewof/temp/ diff --git a/CHANGELOG.md b/CHANGELOG.md index a9ae6891f..12fe99a37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 3.0.0 + +Add style checker for beginners. + +- Style checker for beginners is a freely accessible style checker. + - Currently only Python 3 is supported. + - Code is anonymously stored on the website for analysis and then instantly deleted. + - Count of style issues triggered by submitted code are stored, but the code itself is not permanently stored. + - Statistics of style issue occurence counts are publically visible. +- Dependency updates: + - Add django-bootstrap-breadcrumbs 0.9.2. + - Set flake8 to custom version that allows isolated configurations, to be updated to official release in next update. + - Add flake8-docstrings 1.5.0. + - Add flake8-quotes 2.1.1. + - Add pep8-naming 0.9.1. + ## 2.0.0 Adds gamification elements (points and achievements) to the website, including for all previous submissions for each user. diff --git a/codewof/config/__init__.py b/codewof/config/__init__.py index bcfe02ac3..d7e1daa08 100644 --- a/codewof/config/__init__.py +++ b/codewof/config/__init__.py @@ -1,6 +1,6 @@ """Configuration for Django system.""" -__version__ = "2.0.0" +__version__ = "3.0.0" __version_info__ = tuple( [ int(num) if num.isdigit() else num diff --git a/codewof/config/settings/base.py b/codewof/config/settings/base.py index 0c5551e0b..8e9d398fe 100644 --- a/codewof/config/settings/base.py +++ b/codewof/config/settings/base.py @@ -117,12 +117,14 @@ 'ckeditor', 'ckeditor_uploader', 'captcha', + 'django_bootstrap_breadcrumbs', ] LOCAL_APPS = [ 'general.apps.GeneralAppConfig', 'users.apps.UsersAppConfig', 'programming.apps.ProgrammingConfig', 'research.apps.ResearchConfig', + 'style.apps.StyleAppConfig', ] # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS @@ -249,6 +251,7 @@ ], 'libraries': { 'query_replace': 'config.templatetags.query_replace', + 'simplify_error_template': 'config.templatetags.simplify_error_template', }, }, }, @@ -382,6 +385,7 @@ # Other # ------------------------------------------------------------------------------ +BREADCRUMBS_TEMPLATE = "django_bootstrap_breadcrumbs/bootstrap4.html" DEPLOYMENT_TYPE = env("DEPLOYMENT", default='local') QUESTIONS_BASE_PATH = os.path.join(str(ROOT_DIR.path("programming")), "content") CUSTOM_VERTO_TEMPLATES = os.path.join(str(ROOT_DIR.path("utils")), "custom_converter_templates", "") @@ -390,6 +394,37 @@ SVG_DIRS = [ os.path.join(str(STATIC_ROOT), 'svg') ] +# Key 'example_code' uses underscore to be accessible in templates +STYLE_CHECKER_LANGUAGES = { + 'python3': { + 'name': 'Python 3', + 'svg-icon': 'devicon-python.svg', + 'checker-config': os.path.join( + str(ROOT_DIR), + 'style', + 'style_checkers', + 'flake8.ini' + ), + 'example_code': """\"\"\"a simple fizzbuzz program.\"\"\" + +def fizzbuzz(): + for i in range(1 ,100): + if i % 3 == 0 and i % 5 == 0 : + print("FizzBuzz") + elif i%3 == 0: + print( "Fizz") + elif i % 5==0: + print("Buzz") + else: + print(i) +""" + }, +} +# Add slug key to values for each language +for slug, data in STYLE_CHECKER_LANGUAGES.items(): + data['slug'] = slug +STYLE_CHECKER_TEMP_FILES_ROOT = os.path.join(str(ROOT_DIR), 'temp', 'style') +STYLE_CHECKER_MAX_CHARACTER_COUNT = 10000 # reCAPTCHA # ------------------------------------------------------------------------------ diff --git a/codewof/config/templatetags/simplify_error_template.py b/codewof/config/templatetags/simplify_error_template.py new file mode 100644 index 000000000..94a4e1978 --- /dev/null +++ b/codewof/config/templatetags/simplify_error_template.py @@ -0,0 +1,23 @@ +"""Module for the custom simplify_error_template template tag.""" + +from django import template +from django.utils.safestring import mark_safe + +register = template.Library() + +SEARCH_TEXT = '{article} {character_description}' +REPLACE_TEXT = 'a {character}' + + +@register.simple_tag +def simplify_error_template(template): + """Simplify template for rendering to user. + + Args: + template (str): String of template. + + Returns: + Updated string. + """ + new_text = template.replace(SEARCH_TEXT, REPLACE_TEXT) + return mark_safe(new_text) diff --git a/codewof/config/urls.py b/codewof/config/urls.py index 85eb2a8d4..75e69dd4f 100644 --- a/codewof/config/urls.py +++ b/codewof/config/urls.py @@ -13,6 +13,7 @@ path('', include('general.urls', namespace='general')), path(settings.ADMIN_URL, admin.site.urls), path('research/', include('research.urls', namespace='research')), + path('style/', include('style.urls', namespace='style')), path('users/', include('users.urls', namespace='users'),), path('accounts/', include('allauth.urls')), path('', include('programming.urls', namespace='programming'),), diff --git a/codewof/general/management/commands/sampledata.py b/codewof/general/management/commands/sampledata.py index 31877f7ba..b022715c5 100644 --- a/codewof/general/management/commands/sampledata.py +++ b/codewof/general/management/commands/sampledata.py @@ -44,7 +44,7 @@ def handle(self, *args, **options): management.call_command('load_user_types') print(LOG_HEADER.format('Create sample users')) - User = get_user_model() + User = get_user_model() # noqa N806 # Create admin account admin = User.objects.create_superuser( 'admin', @@ -88,6 +88,9 @@ def handle(self, *args, **options): management.call_command('load_achievements') print('Achievements loaded.\n') + management.call_command('load_style_errors') + print('Style errors loaded.\n') + # Research StudyFactory.create_batch(size=5) StudyGroupFactory.create_batch(size=15) diff --git a/codewof/package.json b/codewof/package.json index 43b4e4cf3..d43edead3 100644 --- a/codewof/package.json +++ b/codewof/package.json @@ -6,6 +6,7 @@ "dependencies": { "bootstrap": "4.3.1", "codemirror": "5.47.0", + "clipboard": "2.0.6", "details-element-polyfill": "2.3.1", "fuse.js": "3.4.4", "jquery": "3.4.1", diff --git a/codewof/programming/codewof_utils.py b/codewof/programming/codewof_utils.py index cc867d0bf..15a7bad5a 100644 --- a/codewof/programming/codewof_utils.py +++ b/codewof/programming/codewof_utils.py @@ -236,13 +236,13 @@ def backdate_user(profile): profile.save() -def backdate_points_and_achievements(n=-1, ignoreFlags=True): +def backdate_points_and_achievements(n=-1, ignore_flags=True): """Perform batch backdate of all points and achievements for n profiles in the system.""" backdate_achievements_times = [] backdate_points_times = [] time_before = time.perf_counter() profiles = Profile.objects.all() - if not ignoreFlags: + if not ignore_flags: profiles = profiles.filter(has_backdated=False) if (n > 0): profiles = profiles[:n] @@ -252,7 +252,7 @@ def backdate_points_and_achievements(n=-1, ignoreFlags=True): # The commented out part below seems to break travis somehow print("Backdating user: {}/{}".format(str(i + 1), str(num_profiles))) # , end="\r") profile = profiles[i] - if not profile.has_backdated or ignoreFlags: + if not profile.has_backdated or ignore_flags: attempts = all_attempts.filter(profile=profile) achievements_time_before = time.perf_counter() diff --git a/codewof/programming/content/en/celsius-to-fahrenheit/solution.py b/codewof/programming/content/en/celsius-to-fahrenheit/solution.py index 24cad2509..932d6563c 100644 --- a/codewof/programming/content/en/celsius-to-fahrenheit/solution.py +++ b/codewof/programming/content/en/celsius-to-fahrenheit/solution.py @@ -1,2 +1,2 @@ def celsius_to_fahrenheit(temperature): - return (temperature * (9/5) + 32) + return (temperature * (9 / 5) + 32) diff --git a/codewof/programming/content/en/fahrenheit-to-celsius/solution.py b/codewof/programming/content/en/fahrenheit-to-celsius/solution.py index be3cc28ab..422f1c74c 100644 --- a/codewof/programming/content/en/fahrenheit-to-celsius/solution.py +++ b/codewof/programming/content/en/fahrenheit-to-celsius/solution.py @@ -1,2 +1,2 @@ def fahrenheit_to_celsius(temperature): - return ((temperature - 32) * 5/9) + return ((temperature - 32) * 5 / 9) diff --git a/codewof/programming/content/en/sum-multiples-3-5/solution.py b/codewof/programming/content/en/sum-multiples-3-5/solution.py index 110de0b59..8b2812578 100644 --- a/codewof/programming/content/en/sum-multiples-3-5/solution.py +++ b/codewof/programming/content/en/sum-multiples-3-5/solution.py @@ -1,6 +1,6 @@ number = int(input("Enter a positive integer: ")) sum = 0 -for i in range(1, number+1): +for i in range(1, number + 1): if (i % 3 == 0) or (i % 5 == 0): sum += i diff --git a/codewof/programming/content/en/triangle-pattern/solution.py b/codewof/programming/content/en/triangle-pattern/solution.py index 52f104e7b..2ef0700d9 100644 --- a/codewof/programming/content/en/triangle-pattern/solution.py +++ b/codewof/programming/content/en/triangle-pattern/solution.py @@ -2,5 +2,5 @@ def triangle(x): if x <= 1: print("That isn't a triangle!") else: - for i in range(1, x+1): + for i in range(1, x + 1): print(i * '*') diff --git a/codewof/programming/management/commands/backdate_points_and_achievements.py b/codewof/programming/management/commands/backdate_points_and_achievements.py index 265f96df8..457b2d18d 100644 --- a/codewof/programming/management/commands/backdate_points_and_achievements.py +++ b/codewof/programming/management/commands/backdate_points_and_achievements.py @@ -25,8 +25,8 @@ def add_arguments(self, parser): def handle(self, *args, **options): """Automatically called when the backdate command is given.""" print("Backdating points and achievements\n") - ignoreFlags = options['ignore_flags'] + ignore_flags = options['ignore_flags'] number = int(options['profiles']) - if ignoreFlags and number > 0: + if ignore_flags and number > 0: raise ValueError("If ignoring backdate flags you must backdate all profiles.") - backdate_points_and_achievements(number, ignoreFlags) + backdate_points_and_achievements(number, ignore_flags) diff --git a/codewof/programming/management/commands/load_achievements.py b/codewof/programming/management/commands/load_achievements.py index 83ddcbd51..b36b98473 100644 --- a/codewof/programming/management/commands/load_achievements.py +++ b/codewof/programming/management/commands/load_achievements.py @@ -6,116 +6,116 @@ # TODO: Consider relocating to a yaml file like the questions ACHIEVEMENTS = [ { - 'id_name': 'create-account', + 'id_name': 'create-account', 'display_name': 'Created an account!', - 'description': 'Created your very own account', - 'icon_name': 'img/icons/achievements/icons8-achievement-create-account-48.png', - 'achievement_tier': 0, - 'parent': None + 'description': 'Created your very own account', + 'icon_name': 'img/icons/achievements/icons8-achievement-create-account-48.png', + 'achievement_tier': 0, + 'parent': None }, { - 'id_name': 'questions-solved-100', + 'id_name': 'questions-solved-100', 'display_name': 'Solved one hundred questions!', - 'description': 'Solved one hundred questions', - 'icon_name': 'img/icons/achievements/icons8-question-solved-gold-50.png', - 'achievement_tier': 4, - 'parent': None + 'description': 'Solved one hundred questions', + 'icon_name': 'img/icons/achievements/icons8-question-solved-gold-50.png', + 'achievement_tier': 4, + 'parent': None }, { - 'id_name': 'questions-solved-10', + 'id_name': 'questions-solved-10', 'display_name': 'Solved ten questions!', - 'description': 'Solved ten questions', - 'icon_name': 'img/icons/achievements/icons8-question-solved-silver-50.png', - 'achievement_tier': 3, - 'parent': 'questions-solved-100' + 'description': 'Solved ten questions', + 'icon_name': 'img/icons/achievements/icons8-question-solved-silver-50.png', + 'achievement_tier': 3, + 'parent': 'questions-solved-100' }, { - 'id_name': 'questions-solved-5', + 'id_name': 'questions-solved-5', 'display_name': 'Solved five questions!', - 'description': 'Solved five questions', - 'icon_name': 'img/icons/achievements/icons8-question-solved-bronze-50.png', - 'achievement_tier': 2, - 'parent': 'questions-solved-10' + 'description': 'Solved five questions', + 'icon_name': 'img/icons/achievements/icons8-question-solved-bronze-50.png', + 'achievement_tier': 2, + 'parent': 'questions-solved-10' }, { - 'id_name': 'questions-solved-1', + 'id_name': 'questions-solved-1', 'display_name': 'Solved one question!', - 'description': 'Solved your very first question', - 'icon_name': 'img/icons/achievements/icons8-question-solved-black-50.png', - 'achievement_tier': 1, - 'parent': 'questions-solved-5' + 'description': 'Solved your very first question', + 'icon_name': 'img/icons/achievements/icons8-question-solved-black-50.png', + 'achievement_tier': 1, + 'parent': 'questions-solved-5' }, { - 'id_name': 'attempts-made-100', + 'id_name': 'attempts-made-100', 'display_name': 'Made one hundred question attempts!', - 'description': 'Attempted one hundred questions', - 'icon_name': 'img/icons/achievements/icons8-attempt-made-gold-50.png', - 'achievement_tier': 4, - 'parent': None + 'description': 'Attempted one hundred questions', + 'icon_name': 'img/icons/achievements/icons8-attempt-made-gold-50.png', + 'achievement_tier': 4, + 'parent': None }, { - 'id_name': 'attempts-made-10', + 'id_name': 'attempts-made-10', 'display_name': 'Made ten question attempts!', - 'description': 'Attempted ten questions', - 'icon_name': 'img/icons/achievements/icons8-attempt-made-silver-50.png', - 'achievement_tier': 3, - 'parent': 'attempts-made-100' + 'description': 'Attempted ten questions', + 'icon_name': 'img/icons/achievements/icons8-attempt-made-silver-50.png', + 'achievement_tier': 3, + 'parent': 'attempts-made-100' }, { - 'id_name': 'attempts-made-5', + 'id_name': 'attempts-made-5', 'display_name': 'Made five question attempts!', - 'description': 'Attempted five questions', - 'icon_name': 'img/icons/achievements/icons8-attempt-made-bronze-50.png', - 'achievement_tier': 2, - 'parent': 'attempts-made-10' + 'description': 'Attempted five questions', + 'icon_name': 'img/icons/achievements/icons8-attempt-made-bronze-50.png', + 'achievement_tier': 2, + 'parent': 'attempts-made-10' }, { - 'id_name': 'attempts-made-1', + 'id_name': 'attempts-made-1', 'display_name': 'Made your first question attempt!', - 'description': 'Attempted one question', - 'icon_name': 'img/icons/achievements/icons8-attempt-made-black-50.png', - 'achievement_tier': 1, - 'parent': 'attempts-made-5' + 'description': 'Attempted one question', + 'icon_name': 'img/icons/achievements/icons8-attempt-made-black-50.png', + 'achievement_tier': 1, + 'parent': 'attempts-made-5' }, { - 'id_name': 'consecutive-days-28', + 'id_name': 'consecutive-days-28', 'display_name': 'Worked on coding every day for four weeks!', - 'description': 'Attempted at least one question every day for four weeks', - 'icon_name': 'img/icons/achievements/icons8-calendar-28-50.png', - 'achievement_tier': 5, - 'parent': None + 'description': 'Attempted at least one question every day for four weeks', + 'icon_name': 'img/icons/achievements/icons8-calendar-28-50.png', + 'achievement_tier': 5, + 'parent': None }, { - 'id_name': 'consecutive-days-21', + 'id_name': 'consecutive-days-21', 'display_name': 'Worked on coding every day for three weeks!', - 'description': 'Attempted at least one question every day for three weeks', - 'icon_name': 'img/icons/achievements/icons8-calendar-21-50.png', - 'achievement_tier': 4, - 'parent': 'consecutive-days-28' + 'description': 'Attempted at least one question every day for three weeks', + 'icon_name': 'img/icons/achievements/icons8-calendar-21-50.png', + 'achievement_tier': 4, + 'parent': 'consecutive-days-28' }, { - 'id_name': 'consecutive-days-14', + 'id_name': 'consecutive-days-14', 'display_name': 'Worked on coding every day for two weeks!', - 'description': 'Attempted at least one question every day for two weeks', - 'icon_name': 'img/icons/achievements/icons8-calendar-14-50.png', - 'achievement_tier': 3, - 'parent': 'consecutive-days-21' + 'description': 'Attempted at least one question every day for two weeks', + 'icon_name': 'img/icons/achievements/icons8-calendar-14-50.png', + 'achievement_tier': 3, + 'parent': 'consecutive-days-21' }, { - 'id_name': 'consecutive-days-7', + 'id_name': 'consecutive-days-7', 'display_name': 'Worked on coding every day for one week!', - 'description': 'Attempted at least one question every day for one week', - 'icon_name': 'img/icons/achievements/icons8-calendar-7-50.png', - 'achievement_tier': 2, - 'parent': 'consecutive-days-14' + 'description': 'Attempted at least one question every day for one week', + 'icon_name': 'img/icons/achievements/icons8-calendar-7-50.png', + 'achievement_tier': 2, + 'parent': 'consecutive-days-14' }, { - 'id_name': 'consecutive-days-2', + 'id_name': 'consecutive-days-2', 'display_name': 'Worked on coding for two days in a row!', - 'description': 'Attempted at least one question two days in a row', - 'icon_name': 'img/icons/achievements/icons8-calendar-2-50.png', - 'achievement_tier': 1, - 'parent': 'consecutive-days-7' + 'description': 'Attempted at least one question two days in a row', + 'icon_name': 'img/icons/achievements/icons8-calendar-2-50.png', + 'achievement_tier': 1, + 'parent': 'consecutive-days-7' }, ] diff --git a/codewof/static/js/style_checkers/python3.js b/codewof/static/js/style_checkers/python3.js new file mode 100644 index 000000000..22d414155 --- /dev/null +++ b/codewof/static/js/style_checkers/python3.js @@ -0,0 +1,176 @@ +var editor; +var CodeMirror = require('codemirror'); +require('codemirror/mode/python/python.js'); +var ClipboardJS = require('clipboard'); +var HIGHLIGHT_CLASS = 'style-highlight'; +var result_text = ''; + + +$(document).ready(function () { + editor = CodeMirror.fromTextArea(document.getElementById('code'), { + mode: { + name: 'python', + version: 3, + singleLineStringErrors: false + }, + lineNumbers: true, + textWrapping: false, + styleActiveLine: true, + autofocus: true, + indentUnit: 4, + viewportMargin: Infinity + }); + var CSRF_TOKEN = jQuery('[name=csrfmiddlewaretoken]').val(); + + $('#load_example_btn').click(function () { + reset(); + editor.setValue(EXAMPLE_CODE); + }); + + $('#reset-btn').click(function () { + reset(); + }); + + $('#check_btn').click(function () { + $('#run-checker-error').hide(); + var user_code = editor.getValue(); + if (user_code.length == 0) { + $('#run-checker-result').text('No code submitted!'); + } else if (user_code.length > MAX_CHARACTER_COUNT) { + var message = 'Your file is too long! We accept a maximum of ' + MAX_CHARACTER_COUNT + ' characters, and your code is ' + user_code.length + ' characters.'; + $('#run-checker-result').text(message); + } else { + // TODO: Add message how to reset text + editor.setOption('readOnly', 'nocursor'); + $('.CodeMirror').addClass('read-only'); + $('#run-checker-result').text('Loading...'); + $.ajax({ + url: '/style/ajax/check/', + type: 'POST', + method: 'POST', + data: JSON.stringify({ + user_code: user_code, + language: 'python3', + }), + contentType: 'application/json; charset=utf-8', + headers: { 'X-CSRFToken': CSRF_TOKEN }, + dataType: 'json', + success: display_style_checker_results, + error: display_style_checker_error, + }); + } + }); + + $('#run-checker-result').on('click', 'div[data-line-number]', function () { + toggle_highlight($(this), true); + }); + + // Clipboard button and event methods + + $('#copy-text-btn').tooltip({ + trigger: 'click', + animation: true + }); + + function setTooltip(btn, message) { + $(btn).tooltip('hide') + .attr('data-original-title', message) + .tooltip('show'); + } + + function hideTooltip(btn) { + setTimeout(function () { + $(btn).tooltip('hide'); + }, 2000); + } + + var clipboard_button = new ClipboardJS('#copy-text-btn', { + text: function(trigger) { + return result_text; + } + }); + + clipboard_button.on('success', function (e) { + setTooltip(e.trigger, 'Copied!'); + hideTooltip(e.trigger); + }); + + clipboard_button.on('error', function (e) { + setTooltip(e.trigger, 'Failed!'); + hideTooltip(e.trigger); + }); + +}); + + +function display_style_checker_results(data, textStatus, jqXHR) { + if (data['success']) { + $('#run-checker-result').html(data['result_html']); + result_text = data['result_text']; + $('#check_btn').hide(); + $('#reset-btn').show(); + $('#copy-text-btn').show(); + // Render all code examples + $('.issue-card pre').each(function () { + var pre_block = this; + CodeMirror( + function (element) { + pre_block.parentNode.replaceChild(element, pre_block); + }, { + mode: { + name: 'python', + version: 3, + singleLineStringErrors: false + }, + value: pre_block.innerText.trim(), + readOnly: true, + cursorBlinkRate: -1, + lineNumbers: true, + textWrapping: false, + styleActiveLine: false, + autofocus: false, + indentUnit: 4, + viewportMargin: 0 + }); + }); + } else { + display_style_checker_error(); + } +} + + +function display_style_checker_error(jqXHR, textStatus, errorThrown) { + $('#run-checker-result').html(''); + $('#run-checker-error').show(); +} + + +function toggle_highlight(issue_button, remove_existing) { + var line_number = issue_button.data('line-number') - 1; + if (issue_button.hasClass(HIGHLIGHT_CLASS)) { + editor.removeLineClass(line_number, 'background', HIGHLIGHT_CLASS); + issue_button.removeClass(HIGHLIGHT_CLASS); + } else { + // Remove existing highlights + if (remove_existing) { + $('div[data-line-number].' + HIGHLIGHT_CLASS).each(function () { + toggle_highlight($(this), false); + }); + } + issue_button.addClass(HIGHLIGHT_CLASS); + editor.addLineClass(line_number, 'background', HIGHLIGHT_CLASS); + } +} + + +function reset() { + editor.setValue(''); + editor.setOption('readOnly', false); + result_text = ''; + $('#reset-btn').hide(); + $('#run-checker-error').hide(); + $('#copy-text-btn').hide(); + $('.CodeMirror').removeClass('read-only'); + $('#run-checker-result').empty(); + $('#check_btn').show(); +} diff --git a/codewof/static/js/website.js b/codewof/static/js/website.js index 7056334e5..f511a6c2b 100644 --- a/codewof/static/js/website.js +++ b/codewof/static/js/website.js @@ -1,3 +1,4 @@ $ = jQuery = require('jquery'); require('bootstrap'); +require('popper.js'); require('details-element-polyfill'); diff --git a/codewof/static/scss/style_checker.scss b/codewof/static/scss/style_checker.scss new file mode 100644 index 000000000..db210ca0c --- /dev/null +++ b/codewof/static/scss/style_checker.scss @@ -0,0 +1,54 @@ +@import "node_modules/bootstrap/scss/functions"; +@import "node_modules/bootstrap/scss/mixins"; +@import "core-variables"; +@import "functions"; +@import "node_modules/bootstrap/scss/variables"; + +$highlight_colour: #ffeb3b; + +.style-highlight { + background-color: $highlight_colour; +} + +.issue-card { + .CodeMirror { + height: auto; + } + p { + margin-bottom: 0.25rem; + } + .card-body > div:last-child > p:last-child { + margin-bottom: 0; + } + #run-checker-result & { + user-select: none; + cursor: pointer !important; + } +} + + +#code-container { + position: sticky; + top: 0; + .CodeMirror { + height: 500px; + } +} + +.read-only { + background-color: #e6e6e6; +} + +#run-checker-error, +#reset-btn, +#copy-text-btn { + display: none; +} + +.feedback-title { + p { + margin-bottom: 0; + color: $red; + font-weight: bold;; + } +} diff --git a/codewof/static/scss/website.scss b/codewof/static/scss/website.scss index 376c7e2fe..18491adda 100644 --- a/codewof/static/scss/website.scss +++ b/codewof/static/scss/website.scss @@ -217,6 +217,21 @@ strong { .img-y5 { max-height: 5rem; } +.img-x1 { + max-width: 1rem; +} +.img-x2 { + max-width: 2rem; +} +.img-x3 { + max-width: 3rem; +} +.img-x4 { + max-width: 4rem; +} +.img-x5 { + max-width: 5rem; +} //////////////////////////////// //Variables// //////////////////////////////// diff --git a/codewof/static/svg/devicon-python.svg b/codewof/static/svg/devicon-python.svg new file mode 100644 index 000000000..9d6cfe612 --- /dev/null +++ b/codewof/static/svg/devicon-python.svg @@ -0,0 +1,2 @@ + + diff --git a/codewof/style/__init__.py b/codewof/style/__init__.py new file mode 100644 index 000000000..da0d7c0d0 --- /dev/null +++ b/codewof/style/__init__.py @@ -0,0 +1 @@ +"""Module for style checking application.""" diff --git a/codewof/style/apps.py b/codewof/style/apps.py new file mode 100644 index 000000000..5c8920ff7 --- /dev/null +++ b/codewof/style/apps.py @@ -0,0 +1,9 @@ +"""Application configuration for style application.""" + +from django.apps import AppConfig + + +class StyleAppConfig(AppConfig): + """Application configuration for style application.""" + + name = 'style' diff --git a/codewof/style/management/__init__.py b/codewof/style/management/__init__.py new file mode 100644 index 000000000..6d63bc8c4 --- /dev/null +++ b/codewof/style/management/__init__.py @@ -0,0 +1 @@ +"""Module for the management of the style application.""" diff --git a/codewof/style/management/commands/__init__.py b/codewof/style/management/commands/__init__.py new file mode 100644 index 000000000..04ce04d5a --- /dev/null +++ b/codewof/style/management/commands/__init__.py @@ -0,0 +1 @@ +"""Module for the custom commands for the style appliation.""" diff --git a/codewof/style/management/commands/load_style_errors.py b/codewof/style/management/commands/load_style_errors.py new file mode 100644 index 000000000..be4560c42 --- /dev/null +++ b/codewof/style/management/commands/load_style_errors.py @@ -0,0 +1,89 @@ +"""Module for the custom Django load_style_errors command.""" + +import importlib +from django.core import management +from verto import Verto +from verto.errors.Error import Error as VertoError +from style.utils import get_language_slugs +from style.models import Error +from utils.errors.VertoConversionError import VertoConversionError + +BASE_DATA_MODULE_PATH = 'style.style_checkers.{}_data' +MARKDOWN_CONVERTER = Verto( + extensions=[ + "markdown.extensions.fenced_code", + ], +) + + +class Command(management.base.BaseCommand): + """Required command class for the custom Django load_style_errors command.""" + + help = "Load progress outcomes to database." + + def convert_markdown(self, module_path, code, field, markdown): + """Render Markdown string to HTML. + + Args: + module_path (str): Path to Python 3 module. + code (str): Code of style error that text belongs too. + field (str): Field of style error that text belongs too. + markdown (str): Markdown text to convert. + + Returns: + HTML of converted text. + """ + MARKDOWN_CONVERTER.clear_saved_data() + try: + result = MARKDOWN_CONVERTER.convert(markdown) + except VertoError as e: + location = '{} - {} - {}'.format(module_path, code, field) + raise VertoConversionError(location, e) from e + return result.html_string + + def handle(self, *args, **options): + """Automatically called when the load_style_errors command is given.""" + created_count = 0 + updated_count = 0 + + for language_code in get_language_slugs(): + # Import langauge data + module_path = BASE_DATA_MODULE_PATH.format(language_code) + module = importlib.import_module(module_path) + language_data = getattr(module, 'DATA') + + # Load errors into database + for code, code_data in language_data.items(): + code_data['title'] = self.convert_markdown( + module_path, + code, + 'title', + code_data['title'], + ) + if code_data.get('solution'): + code_data['solution'] = self.convert_markdown( + module_path, + code, + 'solution', + code_data['solution'], + ) + if code_data.get('explanation'): + code_data['explanation'] = self.convert_markdown( + module_path, + code, + 'explanation', + code_data['explanation'], + ) + + obj, created = Error.objects.update_or_create( + language=language_code, + code=code, + defaults=code_data, + ) + if created: + created_count += 1 + print('Created {}'.format(obj)) + else: + updated_count += 1 + print('Updated {}'.format(obj)) + print('Style errors loaded ({} created, {} updated).'.format(created_count, updated_count)) diff --git a/codewof/style/migrations/0001_initial.py b/codewof/style/migrations/0001_initial.py new file mode 100644 index 000000000..9e9837873 --- /dev/null +++ b/codewof/style/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# Generated by Django 2.1.5 on 2020-04-27 01:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Error', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('language', models.CharField(max_length=20)), + ('code', models.CharField(max_length=20)), + ('count', models.PositiveIntegerField(default=0)), + ('original_message', models.TextField(blank=True)), + ('title', models.TextField()), + ('title_templated', models.TextField(blank=True)), + ('solution', models.TextField()), + ('explanation', models.TextField()), + ], + ), + ] diff --git a/codewof/style/migrations/0002_auto_20200607_1425.py b/codewof/style/migrations/0002_auto_20200607_1425.py new file mode 100644 index 000000000..d63790ec6 --- /dev/null +++ b/codewof/style/migrations/0002_auto_20200607_1425.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.5 on 2020-06-07 02:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('style', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='error', + name='title_templated', + field=models.BooleanField(default=False), + ), + ] diff --git a/codewof/style/migrations/__init__.py b/codewof/style/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/codewof/style/models.py b/codewof/style/models.py new file mode 100644 index 000000000..4e373c470 --- /dev/null +++ b/codewof/style/models.py @@ -0,0 +1,21 @@ +"""Models for style checker application.""" + +from django.db import models + + +class Error(models.Model): + """Model to track style checker error code counts.""" + + language = models.CharField(max_length=20) + code = models.CharField(max_length=20) + count = models.PositiveIntegerField(default=0) + original_message = models.TextField(blank=True) + title_templated = models.BooleanField(default=False) + # The following fields are stored as HTML + title = models.TextField() + solution = models.TextField() + explanation = models.TextField() + + def __str__(self): + """Text representation of an error.""" + return '{} - {}'.format(self.language, self.code) diff --git a/codewof/style/style_checkers/flake8.ini b/codewof/style/style_checkers/flake8.ini new file mode 100644 index 000000000..14ca0186d --- /dev/null +++ b/codewof/style/style_checkers/flake8.ini @@ -0,0 +1,8 @@ +[flake8] +show_source = False +statistics = False +count = False +ignore = Q000, Q001, Q002, E121, E123, E126, E133, E226, E241, E242, E704, W503, W504 + +[darglint] +strictness = long diff --git a/codewof/style/style_checkers/python3.py b/codewof/style/style_checkers/python3.py new file mode 100644 index 000000000..e3303b6e9 --- /dev/null +++ b/codewof/style/style_checkers/python3.py @@ -0,0 +1,158 @@ +"""Style checking code for Python 3 code.""" + +import re +import os.path +import uuid +import subprocess +from pathlib import Path +from django.conf import settings +from django.core.exceptions import ObjectDoesNotExist +from django.db.models import F +from style.utils import ( + CHARACTER_DESCRIPTIONS, + get_language_info, + get_article, +) +from style.models import Error + +LINE_RE = re.compile(r':(?P\d+):(?P\d+): (?P\w\d+) (?P.*)$') +CHARACTER_RE = re.compile(r'\'(?P.*)\'') +TEMP_FILE_ROOT = settings.STYLE_CHECKER_TEMP_FILES_ROOT +TEMP_FILE_EXT = '.py' +# Create folder if it does not exist +Path(TEMP_FILE_ROOT).mkdir(parents=True, exist_ok=True) +PYTHON3_DETAILS = get_language_info('python3') + + +def python3_style_check(code): + """Run the flake8 style check on provided code. + + Args: + code (str): String of user code. + + Returns: + List of dictionaries of style checker result data. + """ + # Write file to HDD + filename = str(uuid.uuid4()) + TEMP_FILE_EXT + filepath = Path(os.path.join(TEMP_FILE_ROOT, filename)) + f = open(filepath, 'w') + f.write(code) + f.close() + + # Read file with flake8 + checker_result = subprocess.run( + [ + '/docker_venv/bin/flake8', + filepath, + '--config=' + PYTHON3_DETAILS['checker-config'], + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + # Process results + result_text = checker_result.stdout.decode('utf-8') + is_example_code = code == PYTHON3_DETAILS['example_code'] + result_data = process_results(result_text, is_example_code) + + # Delete file from HDD + filepath.unlink() + + # Send results + return result_data + + +def process_results(result_text, is_example_code): + """Process results into data for response. + + Args: + result_text (str): Text output from style checker. + is_example_code (bool): True if provided code matches the example code. + + Returns: + List of dictionaries of result data. + """ + issues = [] + for line in result_text.split('\n'): + issue_data = process_line(line, is_example_code) + if issue_data: + issues.append(issue_data) + return issues + + +def process_line(line_text, is_example_code): + """ + Process style error by matching database entry and incrementing count. + + Note: Could at extracting parts of this function to a generic + utility function. + + Args: + line_text (str): Text of style checker result. + is_example_code (bool): True if program was provided example code. + + Returns: + Dictionary of information about style error. + """ + issue_data = dict() + re_result = re.search(LINE_RE, line_text) + if re_result: + line_number = re_result.group('line') + error_code = re_result.group('error_code') + error_message = re_result.group('error_message') + + try: + error = Error.objects.get( + language='python3', + code=error_code, + ) + # Increment error occurence count, if not example code + if not is_example_code: + error.count = F('count') + 1 + error.save() + + if error.title_templated: + error_title = render_text(error.title, error_message) + error_solution = render_text(error.solution, error_message) + else: + error_title = error.title + error_solution = error.solution + + issue_data = { + 'code': error_code, + 'title': error_title, + 'line_number': line_number, + 'solution': error_solution, + 'explanation': error.explanation, + } + except ObjectDoesNotExist: + # If error is not defined in database. + issue_data = { + 'code': error_code, + 'title': error_message, + 'line_number': line_number, + } + return issue_data + + +def render_text(template, error_message): + """Render title text from error message contents. + + Args: + template (str): Template for formatting text. + error_message (str): Original style error message. + + Returns: + Rendered title text. + """ + re_result = re.search(CHARACTER_RE, error_message) + character = re_result.group('character') + character_description = CHARACTER_DESCRIPTIONS[character] + template_data = { + 'character': character, + 'character_description': character_description, + 'article': get_article(character_description), + } + title = template.format(**template_data) + return title diff --git a/codewof/style/style_checkers/python3_data.py b/codewof/style/style_checkers/python3_data.py new file mode 100644 index 000000000..9866b10cc --- /dev/null +++ b/codewof/style/style_checkers/python3_data.py @@ -0,0 +1,1139 @@ +"""Data for Python 3 style errors. + +The following fields are rendered as HTML. + - solution + - explanation +""" + +DATA = { + "E101": { + "original_message": "indentation contains mixed spaces and tabs", + "title_templated": False, + "title": "Line is indented using a mixture of spaces and tabs.", + "solution": "You should indent your code using only spaces.", + "explanation": "Python expects the indentation method to be consistent line to line. Spaces are the preferred indentation method." + }, + "E111": { + "original_message": "indentation is not a multiple of four", + "title_templated": False, + "title": "Line has an indentation level that is not a multiple of four.", + "solution": "Ensure that the first indentation level is 4 spaces, the second indentation level is 8 spaces and so on.", + "explanation": "" + }, + "E112": { + "original_message": "expected an indented block", + "title_templated": False, + "title": "Line is not indented at the correct level.", + "solution": "Add indentation to this line until it is indented at the correct level.", + "explanation": "" + }, + "E113": { + "original_message": "unexpected indentation", + "title_templated": False, + "title": "Line is indented when it shouldn't be.", + "solution": "Remove indentation from this line until it is indented at the correct level.", + "explanation": "" + }, + "E114": { + "original_message": "indentation is not a multiple of four (comment)", + "title_templated": False, + "title": "Line has an indentation level that is not a multiple of four.", + "solution": "Ensure that the first indentation level is 4 spaces, the second indentation level is 8 spaces and so on.", + "explanation": "" + }, + "E115": { + "original_message": "expected an indented block (comment)", + "title_templated": False, + "title": "Line is not indented at the correct level.", + "solution": "Add indentation to this line until it is indented at the correct level.", + "explanation": "Comments should be indented relative to the code block they are in." + }, + "E116": { + "original_message": "unexpected indentation (comment)", + "title_templated": False, + "title": "Line is indented when it shouldn't be.", + "solution": "Remove indentation from this line until it is indented at the correct level.", + "explanation": "" + }, + "E117": { + "original_message": "over-indented", + "title_templated": False, + "title": "Line has too many indentation levels.", + "solution": "Remove indentation from this line until it is indented at the correct level.", + "explanation": "" + }, + # E121 ignored by default + "E121": { + "original_message": "continuation line under-indented for hanging indent", + "title_templated": False, + "title": "Line is less indented than it should be.", + "solution": "Add indentation to this line until it is indented at the correct level.", + "explanation": "" + }, + "E122": { + "original_message": "continuation line missing indentation or outdented", + "title_templated": False, + "title": "Line is not indented as far as it should be or is indented too far.", + "solution": "Add or remove indentation levels until it is indented at the correct level.", + "explanation": "" + }, + # E123 ignored by default + "E123": { + "original_message": "closing bracket does not match indentation of opening bracket’s line", + "title_templated": False, + "title": "Line has a closing bracket that does not match the indentation level of the line that the opening bracket started on.", + "solution": "Add or remove indentation of the closing bracket so it matches the indentation of the line that the opening bracket is on.", + "explanation": "" + }, + "E124": { + "original_message": "closing bracket does not match visual indentation", + "title_templated": False, + "title": "Line has a closing bracket that does not match the indentation of the opening bracket.", + "solution": "Add or remove indentation of the closing bracket so it matches the indentation of the opening bracket.", + "explanation": "" + }, + "E125": { + "original_message": "continuation line with same indent as next logical line", + "title_templated": False, + "title": "Line has a continuation that should be indented one extra level so that it can be distinguished from the next logical line.", + "solution": "Add an indentation level to the line continuation so that it is indented one more level than the next logical line.", + "explanation": "Continuation lines should not be indented at the same level as the next logical line. Instead, they should be indented to one more level so as to distinguish them from the next line." + }, + # E126 ignored by default + "E126": { + "original_message": "continuation line over-indented for hanging indent", + "title_templated": False, + "title": "Line is indented more than it should be.", + "solution": "Remove indentation from this line until it is indented at the correct level.", + "explanation": "" + }, + "E127": { + "original_message": "continuation line over-indented for visual indent", + "title_templated": False, + "title": "Line is indented more than it should be.", + "solution": "Remove indentation from this line until it is indented at the correct level.", + "explanation": "" + }, + "E128": { + "original_message": "continuation line under-indented for visual indent", + "title_templated": False, + "title": "Line is indented less than it should be.", + "solution": "Add indentation to this line until it is indented at the correct level.", + "explanation": "" + }, + "E129": { + "original_message": "visually indented line with same indent as next logical line", + "title_templated": False, + "title": "Line has the same indentation as the next logical line.", + "solution": "Add an indentation level to the visually indented line so that it is indented one more level than the next logical line.", + "explanation": "A visually indented line that has the same indentation as the next logical line is hard to read." + }, + "E131": { + "original_message": "continuation line unaligned for hanging indent", + "title_templated": False, + "title": "Line is not aligned correctly for a hanging indent.", + "solution": "Add or remove indentation so that the lines are aligned with each other.", + "explanation": "" + }, + # E133 ignored by default + # "E133": { + # "original_message": "closing bracket is missing indentation", + # "title_templated": False, + # "title": "", + # "solution": "", + # "explanation": "", + # }, + "E201": { + "original_message": "whitespace after '{character}'", + "title_templated": True, + "title": "Line contains {article} {character_description} that has a space after it.", + "solution": "Remove any spaces that appear after the `{character}` character.", + "explanation": "" + }, + "E202": { + "original_message": "whitespace before '{character}'", + "title_templated": True, + "title": "Line contains {article} {character_description} that has a space before it.", + "solution": "Remove any spaces that appear before the `{character}` character.", + "explanation": "" + }, + "E203": { + "original_message": "whitespace before '{character}'", + "title_templated": True, + "title": "Line contains {article} {character_description} that has a space before it.", + "solution": "Remove any spaces that appear before the `{character}` character.", + "explanation": "" + }, + "E211": { + "original_message": "whitespace before '{character}'", + "title_templated": True, + "title": "Line contains {article} {character_description} that has a space before it.", + "solution": "Remove any spaces that appear before the `{character}` character.", + "explanation": "" + }, + "E221": { + "original_message": "multiple spaces before operator", + "title_templated": False, + "title": "Line has multiple spaces before an operator.", + "solution": "Remove any extra spaces that appear before the operator on this line.", + "explanation": "" + }, + "E222": { + "original_message": "multiple spaces after operator", + "title_templated": False, + "title": "Line has multiple spaces after an operator.", + "solution": "Remove any extra spaces that appear after the operator on this line.", + "explanation": "" + }, + "E223": { + "original_message": "tab before operator", + "title_templated": False, + "title": "Line contains a tab character before an operator.", + "solution": "Remove any tab characters that appear before the operator on this line. Operators should only have one space before them.", + "explanation": "" + }, + "E224": { + "original_message": "tab after operator", + "title_templated": False, + "title": "Line contains a tab character after an operator.", + "solution": "Remove any tab characters that appear after the operator on this line. Operators should only have one space after them.", + "explanation": "" + }, + "E225": { + "original_message": "missing whitespace around operator", + "title_templated": False, + "title": "Line is missing whitespace around an operator.", + "solution": "Ensure there is one space before and after all operators.", + "explanation": "" + }, + # E226 ignored by default + "E226": { + "original_message": "missing whitespace around arithmetic operator", + "title_templated": False, + "title": "Line is missing whitespace around an arithmetic operator (`+`, `-`, `/` and `*`).", + "solution": "Ensure there is one space before and after all arithmetic operators (`+`, `-`, `/` and `*`).", + "explanation": "" + }, + "E227": { + "original_message": "missing whitespace around bitwise or shift operator", + "title_templated": False, + "title": "Line is missing whitespace around a bitwise or shift operator (`<<`, `>>`, `&`, `|`, `^`).", + "solution": "Ensure there is one space before and after all bitwise and shift operators (`<<`, `>>`, `&`, `|`, `^`).", + "explanation": "" + }, + "E228": { + "original_message": "missing whitespace around modulo operator", + "title_templated": False, + "title": "Line is missing whitespace around a modulo operator (`%`).", + "solution": "Ensure there is one space before and after the modulo operator (`%`).", + "explanation": "" + }, + "E231": { + "original_message": "missing whitespace after ‘,’, ‘;’, or ‘:’", + "title_templated": False, + "title": "Line is missing whitespace around one of the following characters: `,` `;` and `:`.", + "solution": "Ensure there is one space before and after any of the following characters: `,` `;` and `:`.", + "explanation": "" + }, + # E241 ignored by default + "E241": { + "original_message": "multiple spaces after ‘,’", + "title_templated": False, + "title": "Line has multiple spaces after the `,` character.", + "solution": "Ensure there is one space before and after any `,` characters.", + "explanation": "" + }, + # E242 ignored by default + "E242": { + "original_message": "tab after ‘,’", + "title_templated": False, + "title": "Line contains a tab character after the `,` character.", + "solution": "Remove any tab characters and ensure there is one space before and after any `,` characters.", + "explanation": "" + }, + "E251": { + "original_message": "unexpected spaces around keyword / parameter equals", + "title_templated": False, + "title": "Line contains spaces before or after the `=` in a function definition.", + "solution": "Remove any spaces that appear either before or after the `=` character in your function definition.", + "explanation": "" + }, + "E261": { + "original_message": "at least two spaces before inline comment", + "title_templated": False, + "title": "Line contains an inline comment that does not have 2 spaces before it.", + "solution": "Ensure that your inline comment has 2 spaces before the `#` character.", + "explanation": "" + }, + "E262": { + "original_message": "inline comment should start with ‘# ‘", + "title_templated": False, + "title": "Comments have a space between the `#` character and the comment message.", + "solution": "Ensure that your inline comment has a space between the `#` character and the comment message.", + "explanation": "" + }, + "E265": { + "original_message": "block comment should start with ‘# ‘", + "title_templated": False, + "title": "Comments should start with a `#` character and have one space between the `#` character and the comment itself.", + "solution": "Ensure that your block comment has a space between the `#` character and the comment message.", + "explanation": "" + }, + "E266": { + "original_message": "too many leading ‘#’ for block comment", + "title_templated": False, + "title": "Comments should only start with a single `#` character.", + "solution": "Ensure your comment only starts with one `#` character.", + "explanation": "" + }, + "E271": { + "original_message": "multiple spaces after keyword", + "title_templated": False, + "title": "Line contains more than one space after a keyword.", + "solution": "Ensure there is only one space after any keywords.", + "explanation": "" + }, + "E272": { + "original_message": "multiple spaces before keyword", + "title_templated": False, + "title": "Line contains more than one space before a keyword.", + "solution": "Ensure there is only one space before any keywords.", + "explanation": "" + }, + "E273": { + "original_message": "tab after keyword", + "title_templated": False, + "title": "Line contains a tab character after a keyword.", + "solution": "Ensure there is only one space after any keywords.", + "explanation": "" + }, + "E274": { + "original_message": "tab before keyword", + "title_templated": False, + "title": "Line contains a tab character before a keyword.", + "solution": "Ensure there is only one space before any keywords.", + "explanation": "" + }, + "E275": { + "original_message": "missing whitespace after keyword", + "title_templated": False, + "title": "Line is missing a space after a keyword.", + "solution": "Ensure there is one space after any keywords.", + "explanation": "" + }, + "E301": { + "original_message": "expected 1 blank line, found 0", + "title_templated": False, + "title": "Line is missing a blank line between the methods of a class.", + "solution": "Add a blank line in between your class methods.", + "explanation": "" + }, + "E302": { + "original_message": "expected 2 blank lines, found 0", + "title_templated": False, + "title": "Two blank lines are expected before and after each function or class.", + "solution": "Ensure there are two blank lines before and after each function and class.", + "explanation": "" + }, + "E303": { + "original_message": "too many blank lines (3)", + "title_templated": False, + "title": "Too many blank lines.", + "solution": "Ensure there are only two blank lines between functions and classes and one blank line between methods of a class.", + "explanation": "" + }, + "E304": { + "original_message": "blank lines found after function decorator", + "title_templated": False, + "title": "Line contains a blank line after a function decorator.", + "solution": "Ensure that there are no blank lines between a function decorator and the function it is decorating.", + "explanation": "" + }, + "E305": { + "original_message": "expected 2 blank lines after end of function or class", + "title_templated": False, + "title": "Functions and classes should have two blank lines after them.", + "solution": "Ensure that functions and classes should have two blank lines after them.", + "explanation": "" + }, + "E306": { + "original_message": "expected 1 blank line before a nested definition", + "title_templated": False, + "title": "Nested definitions should have one blank line before them.", + "solution": "Ensure there is a blank line above your nested definition.", + "explanation": "" + }, + "E401": { + "original_message": "multiple imports on one line", + "title_templated": False, + "title": "Line contains imports from different modules on the same line.", + "solution": "Ensure import statements from different modules occur on their own line.", + "explanation": "" + }, + "E402": { + "original_message": "module level import not at top of file", + "title_templated": False, + "title": "Module imports should be at the top of the file and there should be no statements in between module level imports.", + "solution": "Ensure all import statements are at the top of the file and there are no statements in between imports.", + "explanation": "" + }, + "E501": { + "original_message": "line too long (82 > 79 characters)", + "title_templated": False, + "title": "Line is longer than 79 characters.", + "solution": "You should rewrite your long line of code by breaking it down across multiple lines.", + "explanation": "By making sure your lines of code are not too complicated means it's easier to understand by other people. Also by limiting the line width makes it possible to have several files open side-by-side, and works well when using code review tools that present the two versions in adjacent columns." + }, + "E502": { + "original_message": "the backslash is redundant between brackets", + "title_templated": False, + "title": "There is no need for a backslash (`\\`) between brackets.", + "solution": "Remove any backslashes between brackets.", + "explanation": "" + }, + "E701": { + "original_message": "multiple statements on one line (colon)", + "title_templated": False, + "title": "Line contains multiple statements.", + "solution": "Make sure that each statement is on its own line.", + "explanation": "This improves readability." + }, + "E702": { + "original_message": "multiple statements on one line (semicolon)", + "title_templated": False, + "title": "Line contains multiple statements.", + "solution": "Make sure that each statement is on its own line.", + "explanation": "This improves readability of your code." + }, + "E703": { + "original_message": "statement ends with a semicolon", + "title_templated": False, + "title": "Line ends in a semicolon (`;`).", + "solution": "Remove the semicolon from the end of the line, these are not used in Python 3.", + "explanation": "" + }, + # E704 ignored by default + "E704": { + "original_message": "multiple statements on one line (def)", + "title_templated": False, + "title": "Line contains multiple statements.", + "solution": "Make sure multiple statements of a function definition are on their own separate lines.", + "explanation": "" + }, + "E711": { + "original_message": "comparison to None should be ‘if cond is None:’", + "title_templated": False, + "title": "Comparisons to objects such as `True`, `False`, and `None` should use `is` or `is not` instead of `==` and `!=`.", + "solution": "Replace `!=` with `is not` and `==` with `is`.", + "explanation": "This makes your code easier to read, as it's using words rather than symbols." + }, + "E712": { + "original_message": "comparison to True should be ‘if cond is True:’ or ‘if cond:’", + "title_templated": False, + "title": "Comparisons to objects such as `True`, `False`, and `None` should use `is` or `is not` instead of `==` and `!=`.", + "solution": "Replace `!=` with `is not` and `==` with `is`.", + "explanation": "This makes your code easier to read, as it's using words rather than symbols." + }, + "E713": { + "original_message": "test for membership should be ‘not in’", + "title_templated": False, + "title": "When testing whether or not something is in an object use the form `x not in the_object` instead of `not x in the_object`.", + "solution": "Use the form `not x in the_object` instead of `x not in the_object`.", + "explanation": "This improves readability of your code as it reads more naturally." + }, + "E714": { + "original_message": "test for object identity should be ‘is not’", + "title_templated": False, + "title": "When testing for object identity use the form `x is not None` rather than `not x is None`.", + "solution": "Use the form `x is not None` rather than `not x is None`.", + "explanation": "This improves readability of your code as it reads more naturally." + }, + "E721": { + "original_message": "do not compare types, use ‘isinstance()’", + "title_templated": False, + "title": "You should compare an objects type by using `isinstance()` instead of `==`. This is because `isinstance` can handle subclasses as well.", + "solution": "Use `if isinstance(dog, Animal)` instead of `if type(dog) == Animal`.", + "explanation": "" + }, + "E722": { + "original_message": "do not use bare except, specify exception instead", + "title_templated": False, + "title": "Except block is calling all exceptions, instead it should catch a specific exception.", + # TODO: Add a simple multi-except example + "solution": "Add the specific exception the block is expected to catch, you may need to use multiple `except` blocks if you were catching multiple exceptions.", + "explanation": "This helps other to know exactly what the `except` is expected to catch." + }, + "E731": { + "original_message": "do not assign a lambda expression, use a def", + "title_templated": False, + "title": "Line assigns a lambda expression instead of defining it as a function using `def`.", + "solution": "Define the line as a function using `def`.", + "explanation": "The primary reason for this is debugging. Lambdas show as `` in tracebacks, where functions will display the function’s name." + }, + "E741": { + "original_message": "do not use variables named `l`, `O`, or `I`", + "title_templated": False, + "title": "Line uses one of the variables named `l`, `O`, or `I`", + "solution": "Change the names of these variables to something more descriptive.", + "explanation": "Variables named `l`, `O`, or `I` can be very hard to read. This is because the letter `I` and the letter `l` are easily confused, and the letter `O` and the number `0` can be easily confused." + }, + "E742": { + "original_message": "do not define classes named `l`, `O`, or `I`", + "title_templated": False, + "title": "Line contains a class named `l`, `O`, or `I`", + "solution": "Change the names of these classes to something more descriptive.", + "explanation": "Classes named `l`, `O`, or `I` can be very hard to read. This is because the letter `I` and the letter `l` are easily confused, and the letter `O` and the number `0` can be easily confused." + }, + "E743": { + "original_message": "do not define functions named `l`, `O`, or `I`", + "title_templated": False, + "title": "Line contains a function named `l`, `O`, or `I`", + "solution": "Change the names of these functions to something more descriptive.", + "explanation": "Functions named `l`, `O`, or `I` can be very hard to read. This is because the letter `I` and the letter `l` are easily confused, and the letter `O` and the number `0` can be easily confused." + }, + "E999": { + "original_message": "Syntax error", + "title_templated": False, + "title": "Program failed to compile.", + "solution": "Make sure your code is working.", + "explanation": "" + }, + # TODO: Continue from this point onwards with checking text and adding templating boolean + "W191": { + "original_message": "indentation contains tabs", + "title_templated": False, + "title": "Line contains tabs when only spaces are expected.", + "solution": "Replace any tabs in your indentation with spaces.", + "explanation": "Using a consistent character for whitespace makes it much easier for editors to read your file." + }, + "W291": { + "original_message": "trailing whitespace", + "title_templated": False, + "title": "Line contains whitespace after the final character.", + "solution": "Remove any extra whitespace at the end of each line.", + "explanation": "" + }, + "W292": { + "original_message": "no newline at end of file", + "title_templated": False, + "title": "Files should end with a newline.", + "solution": "Add a newline to the end of your file.", + "explanation": "All text files should automatically end with a new line character, but some code editors can allow you to remove it." + }, + "W293": { + "original_message": "blank line contains whitespace", + "title_templated": False, + "title": "Blank lines should not contain any tabs or spaces.", + "solution": "Remove any whitespace from blank lines.", + "explanation": "" + }, + "W391": { + "original_message": "blank line at end of file", + "title_templated": False, + "title": "There are either zero, two, or more than two blank lines at the end of your file.", + "solution": "Ensure there is only one blank line at the end of your file.", + "explanation": "" + }, + # W503 ignored by default + # This seems contradicitng... https://lintlyci.github.io/Flake8Rules/rules/W503.html + "W503": { + "original_message": "line break before binary operator", + "title_templated": False, + "title": "Line break is before a binary operator.", + "solution": "", + "explanation": "" + }, + # W504 ignored by default + # same as above https://lintlyci.github.io/Flake8Rules/rules/W504.html + "W504": { + "original_message": "line break after binary operator", + "title_templated": False, + "title": "Line break is after a binary operator.", + "solution": "", + "explanation": "" + }, + # W505 ignored by default + "W505": { + "original_message": "doc line too long (82 > 79 characters)", + "title_templated": False, + "title": "Line is longer than 79 characters.", + "solution": "You should rewrite your long line of code by breaking it down across multiple lines.", + "explanation": "By making sure your lines of code are not too complicated means it's easier to understand by other people. Also by limiting the line width makes it possible to have several files open side-by-side, and works well when using code review tools that present the two versions in adjacent columns." + }, + "W601": { + "original_message": ".has_key() is deprecated, use ‘in’", + "title_templated": False, + "title": "`.has_key()` was deprecated in Python 2. It is recommended to use the `in` operator instead.", + "solution": """ +Use `in` instead of `.has_key()`. + +For example: + +``` +if 8054 in postcodes: +``` +""", + "explanation": "" + }, + "W602": { + "original_message": "deprecated form of raising exception", + "title_templated": False, + "title": "Using `raise ExceptionType, message` is not supported.", + "solution": "Instead of using `raise ExceptionType, 'Error message'`, passing the text as a parameter to the exception, like `raise ExceptionType('Error message')`.", + "explanation": "" + }, + "W603": { + "original_message": "‘<>’ is deprecated, use ‘!=’", + "title_templated": False, + "title": "`<>` has been removed in Python 3.", + "solution": "Replace any occurences of `<>` with `!=`.", + "explanation": "The `!=` is the common programming symbols for stating not equal." + }, + "W604": { + "original_message": "backticks are deprecated, use ‘repr()’", + "title_templated": False, + "title": "Backticks have been removed in Python 3.", + "solution": "Use the built-in function `repr()` instead.", + "explanation": "" + }, + "W605": { + "original_message": "invalid escape sequence ‘x’", + "title_templated": False, + "title": "Backslash is used to escape a character that cannot be escaped.", + "solution": "Either don't use the backslash, or check your string is correct.", + "explanation": "" + }, + "W606": { + "original_message": "`async` and `await` are reserved keywords starting with Python 3.7", + "title_templated": False, + "title": "`async` and `await` are reserved names", + "solution": "Do not name variables or functions as `async` or `await`.", + "explanation": "" + }, + "D100": { + "original_message": "Missing docstring in public module", + "title_templated": False, + "title": "Module (the term for the Python file) should have a docstring.", + "solution": "Add a docstring to your module.", + "explanation": """ +A docstring is a special comment at the top of your module that briefly explains the purpose of the module. It should have 3 sets of quotes to start and finish the comment. + +For example: + +``` +\"\"\"This file calculates required dietary requirements for kiwis. +``` +""" + }, + "D101": { + "original_message": "Missing docstring in public class", + "title_templated": False, + "title": "Class should have a docstring.", + "solution": "Add a docstring to your class.", + "explanation": """ +A docstring is a special comment at the top of your class that briefly explains the purpose of the class. It should have 3 sets of quotes to start and finish the comment. + +For example: + +``` +class Kiwi(): + \"\"\"Represents a kiwi bird from New Zealand.\"\"\" +``` +""" + }, + "D102": { + "original_message": "Missing docstring in public method", + "title_templated": False, + "title": "Methods should have docstrings.", + "solution": "Add a docstring to your method.", + "explanation": """ +A docstring is a special comment at the top of your method that briefly explains the purpose of the method. It should have 3 sets of quotes to start and finish the comment. + +For example: + +``` +class Kiwi(): + \"\"\"Represents a kiwi bird from New Zealand.\"\"\" + + def eat_plants(amount): + \"\"\"Calculates calories from the given amount of plant food and eats it.\"\"\" +``` +""" + }, + "D103": { + "original_message": "Missing docstring in public function", + "title_templated": False, + "title": "This function should have a docstring.", + "solution": "Add a docstring to your function.", + "explanation": """ +A docstring is a special comment at the top of your module that briefly explains the purpose of the function. It should have 3 sets of quotes to start and finish the comment. + +For example: + +``` +def get_waypoint_latlng(number): + \"\"\"Return the latitude and longitude values of the waypoint.\"\"\" +``` +""" + }, + "D104": { + "original_message": "Missing docstring in public package", + "title_templated": False, + "title": "Packages should have docstrings.", + "solution": "Add a docstring to your package.", + "explanation": "" + }, + "D105": { + "original_message": "Missing docstring in magic method", + "title_templated": False, + "title": "Magic methods should have docstrings.", + "solution": "Add a docstring to your magic method.", + "explanation": "" + }, + "D106": { + "original_message": "Missing docstring in public nested class", + "title_templated": False, + "title": "Public nested classes should have docstrings.", + "solution": "Add a docstring to your public nested class.", + "explanation": "" + }, + "D107": { + "original_message": "Missing docstring in __init__", + "title_templated": False, + "title": "The `__init__` method should have a docstring.", + "solution": "Add a docstring to your `__init__` method.", + "explanation": "This helps understand how objects of your class are created." + }, + "D200": { + "original_message": "One-line docstring should fit on one line with quotes", + "title_templated": False, + "title": "Docstrings that are one line long should fit on one line with quotes.", + "solution": "Put your docstring on one line with quotes.", + "explanation": "" + }, + "D201": { + "original_message": "No blank lines allowed before function docstring", + "title_templated": False, + "title": "Function docstrings should not have blank lines before them.", + "solution": "Remove any blank lines before your function docstring.", + "explanation": "" + }, + "D202": { + "original_message": "No blank lines allowed after function docstring", + "title_templated": False, + "title": "Function docstrings should not have blank lines after them.", + "solution": "Remove any blank lines after your function docstring.", + "explanation": "" + }, + "D203": { + "original_message": "1 blank line required before class docstring", + "title_templated": False, + "title": "Class docstrings should have 1 blank line before them.", + "solution": "Insert 1 blank line before your class docstring.", + "explanation": "" + }, + "D204": { + "original_message": "1 blank line required after class docstring", + "title_templated": False, + "title": "Class docstrings should have 1 blank line after them.", + "solution": "Insert 1 blank line after your class docstring.", + "explanation": "This improves the readability of your code." + }, + "D205": { + "original_message": "1 blank line required between summary line and description", + "title_templated": False, + "title": "There should be 1 blank line between the summary line and the description.", + "solution": "Insert 1 blank line between the summary line and the description.", + "explanation": """ +This makes larger docstrings easier to read. + +For example: + +``` +def get_stock_level(book): + \"\"\"Return the stock level for the given book. + + This calculates the stock level across multiple stores in the city, + excluding books reserved for customers. + + Args: + book (str): Name of the book. + + Returns: + String of publication date in the format of 'DD/MM/YYYY'. + \"\"\" +``` +""" + }, + "D206": { + "original_message": "Docstring should be indented with spaces, not tabs", + "title_templated": False, + "title": "Docstrings should be indented using spaces, not tabs.", + "solution": "Make sure your docstrings are indented using spaces instead of tabs.", + "explanation": "" + }, + "D207": { + "original_message": "Docstring is under-indented", + "title_templated": False, + "title": "Docstring is under-indented.", + "solution": "Add indentation levels to your docstring until it is at the correct indentation level.", + "explanation": "" + }, + "D208": { + "original_message": "Docstring is over-indented", + "title_templated": False, + "title": "Docstring is over-indented.", + "solution": "Remove indentation levels from your docstring until it is at the correct indentation level.", + "explanation": "" + }, + "D209": { + "original_message": "Multi-line docstring closing quotes should be on a separate line", + "title_templated": False, + "title": "Docstrings that are longer than one line should have closing quotes on a separate line.", + "solution": "Put the closing quotes of your docstring on a separate line.", + "explanation": """ +This makes larger docstrings easier to read. + +For example: + +``` +def get_stock_level(book): + \"\"\"Return the stock level for the given book. + + This calculates the stock level across multiple stores in the city, + excluding books reserved for customers. + + Args: + book (str): Name of the book. + + Returns: + String of publication date in the format of 'DD/MM/YYYY'. + \"\"\" +``` +""" + }, + "D210": { + "original_message": "No whitespaces allowed surrounding docstring text", + "title_templated": False, + "title": "Text in docstrings should not be surrounded by whitespace.", + "solution": "Remove any whitespace from the start and end of your docstring.", + "explanation": "" + }, + "D211": { + "original_message": "No blank lines allowed before class docstring", + "title_templated": False, + "title": "Class docstrings should not have blank lines before them.", + "solution": "Remove any blank lines before your class docstring.", + "explanation": "" + }, + "D212": { + "original_message": "Multi-line docstring summary should start at the first line", + "title_templated": False, + "title": "Docstrings that are more than one line long should start at the first line.", + "solution": """ +Ensure your docstring starts on the first line with quotes. + +For example: + +``` +def get_stock_level(book): + \"\"\"Return the stock level for the given book. +``` +""", + "explanation": "" + }, + "D213": { + "original_message": "Multi-line docstring summary should start at the second line", + "title_templated": False, + "title": "Docstrings that are more than one line long should start at the second line.", + "solution": "Ensure your docstring starts on the second line, which is the first line without quotes.", + "explanation": "" + }, + "D214": { + "original_message": "Section is over-indented", + "title_templated": False, + "title": "Section is indented by too many levels.", + "solution": "Remove indentation levels from this section until it is at the correct indentation level.", + "explanation": "" + }, + "D215": { + "original_message": "Section underline is over-indented", + "title_templated": False, + "title": "Section underline is indented by too many levels.", + "solution": "Remove indentation levels from this section underline until it is at the correct indentation level.", + "explanation": "" + }, + "D300": { + "original_message": "Use \"\"\"triple double quotes\"\"\"", + "title_templated": False, + "title": "Use \"\"\"triple double quotes\"\"\" around docstrings.", + "solution": "Use \"\"\" triple double quotes around your docstring.", + "explanation": "" + }, + "D301": { + "original_message": "Use r\"\"\" if any backslashes in a docstring", + "title_templated": False, + "title": "Use r\"\"\" if there are any backslashes in a docstring.", + "solution": "Use r\"\"\" at the beginning of your docstring if it contains any backslashes.", + "explanation": "" + }, + "D302": { + "original_message": "Use u\"\"\" for Unicode docstrings", + "title_templated": False, + "title": "Use u\"\"\" for docstrings that contain Unicode.", + "solution": "Use u\"\"\" at the beginning of your docstring if it contains any Unicode.", + "explanation": "" + }, + "D400": { + "original_message": "First line should end with a period", + "title_templated": False, + "title": "The first line in docstrings should end with a period.", + "solution": "Add a period to the end of the first line in your docstring. It should be a short summary of your code, if it's too long you may need to break it apart into multiple sentences.", + "explanation": "" + }, + "D401": { + "original_message": "First line should be in imperative mood", + "title_templated": False, + "title": "The first line in docstrings should read like a command.", + "solution": "Ensure the first line in your docstring reads like a command, not a description. For example 'Do this' instead of 'Does this', 'Return this' instead of 'Returns this'.", + "explanation": "" + }, + "D402": { + "original_message": "First line should not be the function’s signature", + "title_templated": False, + "title": "The first line in docstrings should not be a copy of the function’s definition.", + "solution": "Rewrite the docstring to describe the purpose of the function.", + "explanation": "" + }, + "D403": { + "original_message": "First word of the first line should be properly capitalized", + "title_templated": False, + "title": "The first word in the first line should be capitalised.", + "solution": "Capitalise the first word.", + "explanation": "" + }, + "D404": { + "original_message": "First word of the docstring should not be 'This'", + "title_templated": False, + "title": "First word of the docstring should not be 'This'.", + "solution": "Rephrase the docstring so that the first word is not 'This'.", + "explanation": "" + }, + "D405": { + "original_message": "Section name should be properly capitalized", + "title_templated": False, + "title": "Section name should be properly capitalised.", + "solution": "Capitalise the section name.", + "explanation": "" + }, + "D406": { + "original_message": "Section name should end with a newline", + "title_templated": False, + "title": "Section names should end with a newline.", + "solution": "Add a newline after the section name.", + "explanation": "" + }, + "D407": { + "original_message": "Missing dashed underline after section", + "title_templated": False, + "title": "Section names should have a dashed line underneath them.", + "solution": "Add a dashed line underneath the section name.", + "explanation": "" + }, + "D408": { + "original_message": "Section underline should be in the line following the section’s name", + "title_templated": False, + "title": "Dashed line should be on the line following the section's name.", + "solution": "Put the dashed underline on the line immediately following the section name.", + "explanation": "" + }, + "D409": { + "original_message": "Section underline should match the length of its name", + "title_templated": False, + "title": "Dashed section underline should match the length of the section's name.", + "solution": "Add or remove dashes from the dashed underline until it matches the length of the section name.", + "explanation": "" + }, + "D410": { + "original_message": "Missing blank line after section", + "title_templated": False, + "title": "Section should have a blank line after it.", + "solution": "Add a blank line after the section.", + "explanation": "" + }, + "D411": { + "original_message": "Missing blank line before section", + "title_templated": False, + "title": "Section should have a blank line before it.", + "solution": "Add a blank line before the section.", + "explanation": "" + }, + "D412": { + "original_message": "No blank lines allowed between a section header and its content", + "title_templated": False, + "title": "There should be no blank lines between a section header and its content.", + "solution": "Remove any blank lines that are between the section header and its content.", + "explanation": "" + }, + "D413": { + "original_message": "Missing blank line after last section", + "title_templated": False, + "title": "The last section in a docstring should have a blank line after it.", + "solution": "Add a blank line after the last section in your docstring.", + "explanation": "" + }, + "D414": { + "original_message": "Section has no content", + "title_templated": False, + "title": "Sections in docstrings must have content.", + "solution": "Add content to the section in your docstring.", + "explanation": "" + }, + "D415": { + "original_message": "First line should end with a period, question mark, or exclamation point", + "title_templated": False, + "title": "The first line in your docstring should end with a period, question mark, or exclamation point.", + "solution": "Add a period, question mark, or exclamation point to the end of the first line in your docstring.", + "explanation": "" + }, + "D416": { + "original_message": "Section name should end with a colon", + "title_templated": False, + "title": "Section names should end with a colon.", + "solution": "Add a colon to the end of your section name.", + "explanation": "" + }, + "D417": { + "original_message": "Missing argument descriptions in the docstring", + "title_templated": False, + "title": "Docstrings should include argument descriptions.", + "solution": "Add argument descriptions to your docstring.", + "explanation": "" + }, + "N801": { + "original_message": "class names should use CapWords convention", + "title_templated": False, + "title": "Class names should use the CapWords convention.", + "solution": "Edit your class names to follow the CapWords convention, where each word is capitalised with no spaces.", + "explanation": "" + }, + "N802": { + "original_message": "function name should be lowercase", + "title_templated": False, + "title": "Function names should be lowercase.", + "solution": "Edit your function names to be lowercase, with underscores for spaces.", + "explanation": "" + }, + "N803": { + "original_message": "argument name should be lowercase", + "title_templated": False, + "title": "Argument names should be lowercase.", + "solution": "Edit your function names to be lowercase, with underscores for spaces.", + "explanation": "" + }, + "N804": { + "original_message": "first argument of a classmethod should be named `cls`", + "title_templated": False, + "title": "The first argument of a classmethod should be named `cls`.", + "solution": "Edit the first argument in your classmethod to be named `cls`.", + "explanation": "This is the common term that Python programmers use for a classmethod." + }, + "N805": { + "original_message": "first argument of a method should be named `self`", + "title_templated": False, + "title": "The first argument of a method should be named `self`.", + "solution": "Edit the first argument in your method to be named `self`.", + "explanation": "This is the common term that Python programmers use for the object of a class's method." + }, + "N806": { + "original_message": "variable in function should be lowercase", + "title_templated": False, + "title": "Variables in functions should be lowercase.", + "solution": "Edit the variable in your function to be lowercase.", + "explanation": "" + }, + "N807": { + "original_message": "function name should not start and end with `__`", + "title_templated": False, + "title": "Function names should not start and end with `__` (double underscore).", + "solution": "Edit the function name so that it does not start and end with `__`", + "explanation": "" + }, + "N811": { + "original_message": "constant imported as non constant", + "title_templated": False, + "title": "Import that should have been imported as a constant has been imported as a non constant.", + "solution": "Edit the import to be imported as a constant (use all capital letters in the 'import as...' name).", + "explanation": "" + }, + "N812": { + "original_message": "lowercase imported as non lowercase", + "title_templated": False, + "title": "Import that should have been imported as lowercase has been imported as non lowercase", + "solution": "Edit the import to be imported as lowercase (use lowercase in the `import as` name).", + "explanation": "" + }, + "N813": { + "original_message": "camelcase imported as lowercase", + "title_templated": False, + "title": "Import that should have been imported as camelCase has been imported as lowercase.", + "solution": "Edit the import to be imported as camelCase (use camelCase in the 'import as ...' name).", + "explanation": "" + }, + "N814": { + "original_message": "camelcase imported as constant", + "title_templated": False, + "title": "Import that should have been imported as camelCase has been imported as a constant.", + "solution": "Edit the import to be imported as camelCase (use camelCase in the 'import as ...' name).", + "explanation": "" + }, + "N815": { + "original_message": "mixedCase variable in class scope", + "title_templated": False, + "title": "Mixed case variable used in the class scope", + "solution": "Edit the variable name so that it doesn't use mixedCase.", + "explanation": "" + }, + "N816": { + "original_message": "mixedCase variable in global scope", + "title_templated": False, + "title": "Mixed case variable used in the global scope", + "solution": "Edit the variable name so that it doesn't use mixedCase.", + "explanation": "" + }, + "N817": { + "original_message": "camelcase imported as acronym", + "title_templated": False, + "title": "Import that should have been imported as camelCase has been imported as an acronym.", + "solution": "Edit the import to be imported as camelCase (use camelCase in the 'import as...' name).", + "explanation": "" + }, + "Q000": { + "original_message": "Remove bad quotes", + "title_templated": False, + "title": "Use single quotes (') instead of double quotes (\").", + "solution": "Replace any double quotes with single quotes.", + "explanation": "" + }, + "Q001": { + "original_message": "Remove bad quotes from multiline string", + "title_templated": False, + "title": "Use single quotes (') instead of double quotes (\") in multiline strings.", + "solution": "Replace any double quotes in your multiline string with single quotes.", + "explanation": "" + }, + "Q002": { + "original_message": "Remove bad quotes from docstring", + "title_templated": False, + "title": "Use single quotes (') instead of double quotes (\") in docstrings.", + "solution": "Replace any double quotes in your docstring with single quotes.", + "explanation": "" + }, + "Q003": { + "original_message": "Change outer quotes to avoid escaping inner quotes", + "title_templated": False, + "title": "Change outer quotes to avoid escaping inner quotes.", + "solution": "Make either the outer quotes single (`'`) and the inner quotes double (`\"`), or the outer quotes double and the inner quotes single.", + "explanation": "" + }, +} diff --git a/codewof/style/urls.py b/codewof/style/urls.py new file mode 100644 index 000000000..766d89dfd --- /dev/null +++ b/codewof/style/urls.py @@ -0,0 +1,12 @@ +"""URL routing for style application.""" + +from django.urls import path +from . import views + +app_name = 'style' +urlpatterns = [ + path('', views.HomeView.as_view(), name='home'), + path('/', views.LanguageStyleCheckerView.as_view(), name='language'), + path('/statistics/', views.LanguageStatisticsView.as_view(), name='language_statistics'), + path('ajax/check/', views.check_code, name='check_code'), +] diff --git a/codewof/style/utils.py b/codewof/style/utils.py new file mode 100644 index 000000000..a1de9c12d --- /dev/null +++ b/codewof/style/utils.py @@ -0,0 +1,101 @@ +"""Utilities for the style checker application.""" + +from django.conf import settings +from django.template.loader import render_to_string + + +CHARACTER_DESCRIPTIONS = { + '(': 'opening bracket', + ')': 'closing bracket', + '[': 'opening square bracket', + ']': 'closing square bracket', + '{': 'opening curly bracket', + '}': 'closing curly bracket', + "'": 'single quote', + '"': 'double quote', + ':': 'colon', + ';': 'semicolon', + ' ': 'space', + ',': 'comma', + '.': 'full stop', +} + + +def get_language_slugs(): + """Return all programming language slugs. + + Returns: + Iteratable of programming language slugs. + """ + return settings.STYLE_CHECKER_LANGUAGES.keys() + + +def get_language_info(slug): + """Return information about a programming language. + + Args: + slug (str): The slug of the given language. + + Returns: + Dictionary of information about the given programming language. + """ + return settings.STYLE_CHECKER_LANGUAGES.get(slug, dict()) + + +def get_article(word): + """Return English article for word. + + Returns 'an' if word starts with vowel. Technically + it should check the word sound, compared to the + letter but this shouldn't occur with our words. + + Args: + word (str): Word to create article for. + + Returns: + 'a' or 'an' (str) depending if word starts with vowel. + """ + if word[0].lower() in 'aeiou': + return 'an' + else: + return 'a' + + +def render_results_as_html(issues): + """Render style issue data as HTML. + + Args: + issues (list): List of style issues. + + Returns: + HTML string. + """ + result_html = render_to_string( + 'style/component/result.html', + { + 'issues': issues, + 'issue_count': len(issues), + } + ) + return result_html + + +def render_results_as_text(user_code, issues): + """Render style issue data as HTML. + + Args: + user_code (str): String of user code. + issues (list): List of style issues. + + Returns: + String of text. + """ + result_text = render_to_string( + 'style/component/result.txt', + { + 'user_code': user_code, + 'issues': issues, + 'issue_count': len(issues), + } + ) + return result_text diff --git a/codewof/style/views.py b/codewof/style/views.py new file mode 100644 index 000000000..4f49535f3 --- /dev/null +++ b/codewof/style/views.py @@ -0,0 +1,105 @@ +"""Views for style application.""" + +import json +from django.conf import settings +from django.http import JsonResponse, Http404 +from django.views.generic import ( + TemplateView, + ListView, +) +from style.style_checkers.python3 import python3_style_check +from style.models import Error +from style.utils import ( + render_results_as_html, + render_results_as_text, + get_language_slugs, + get_language_info, + CHARACTER_DESCRIPTIONS, +) + +LANGUAGE_PATH_TEMPLATE = 'style/{}.html' + + +class HomeView(ListView): + """View for style homepage.""" + + template_name = 'style/home.html' + context_object_name = 'languages' + + def get_queryset(self): + """Get iterate of languages for page.""" + return settings.STYLE_CHECKER_LANGUAGES + + +class LanguageStyleCheckerView(TemplateView): + """View for a language style checker.""" + + template_name = 'style/language.html' + + def get_context_data(self, **kwargs): + """Get additional context data for template.""" + context = super().get_context_data(**kwargs) + language_slug = self.kwargs.get('language', '') + language_info = get_language_info(language_slug) + # If language not found + if not language_info: + raise Http404 + context['language'] = language_info + context['language_header'] = 'style/language-components/{}-header.html'.format(language_slug) + context['language_subheader'] = 'style/language-components/{}-subheader.html'.format(language_slug) + context['language_js'] = 'js/style_checkers/{}.js'.format(language_slug) + context['MAX_CHARACTER_COUNT'] = settings.STYLE_CHECKER_MAX_CHARACTER_COUNT + return context + + +class LanguageStatisticsView(TemplateView): + """View for a language statistics.""" + + template_name = 'style/language-statistics.html' + + def get_context_data(self, **kwargs): + """Get additional context data for template.""" + context = super().get_context_data(**kwargs) + language_slug = self.kwargs.get('language', '') + language_info = get_language_info(language_slug) + # If language not found + if not language_info: + raise Http404 + context['language'] = language_info + context['language_header'] = 'style/language-components/{}-header.html'.format(language_slug) + context['issues'] = Error.objects.filter(language=language_slug).order_by('-count', 'code') + if context['issues']: + context['max_count'] = context['issues'][0].count + context['characters'] = list(CHARACTER_DESCRIPTIONS.keys()) + return context + + +def check_code(request): + """Check the user's code for style issues. + + Args: + request (Request): AJAX request from user. + + Returns: + JSON response with result. + """ + # TODO: Provide message for failure + result = { + 'success': False, + } + if request.is_ajax(): + request_json = json.loads(request.body.decode('utf-8')) + user_code = request_json['user_code'] + language = request_json['language'] + is_valid_length = 0 < len(user_code) <= settings.STYLE_CHECKER_MAX_CHARACTER_COUNT + is_valid_language = language in get_language_slugs() + if is_valid_length and is_valid_language: + if language == 'python3': + result_data = python3_style_check(user_code) + result['success'] = True + else: + # TODO: else raise error language isn't supported + pass + result['result_html'] = render_results_as_html(result_data) + result['result_text'] = render_results_as_text(user_code, result_data) + return JsonResponse(result) diff --git a/codewof/templates/base.html b/codewof/templates/base.html index 2f956192b..ddf51d461 100644 --- a/codewof/templates/base.html +++ b/codewof/templates/base.html @@ -1,4 +1,4 @@ -{% load static i18n activeurl svg %} +{% load static i18n activeurl svg django_bootstrap_breadcrumbs i18n %} @@ -53,8 +53,9 @@ {% if request %} {% activeurl %}