From 7beadf7518489150beb47b81d72ba5f4e60ab5fa Mon Sep 17 00:00:00 2001 From: Sarah Withee <2601974+geekygirlsarah@users.noreply.github.com> Date: Sat, 27 Dec 2025 15:07:23 -0500 Subject: [PATCH 1/3] Add additional unit tests for DB models, meta/lang testing, template tags, and views --- web/tests/test_db_models.py | 37 ++++++++++++++++++++ web/tests/test_models.py | 63 +++++++++++++++++++++++++++------- web/tests/test_templatetags.py | 17 +++++++++ web/tests/test_views.py | 32 +++++++++++++++++ 4 files changed, 137 insertions(+), 12 deletions(-) create mode 100644 web/tests/test_db_models.py create mode 100644 web/tests/test_templatetags.py diff --git a/web/tests/test_db_models.py b/web/tests/test_db_models.py new file mode 100644 index 000000000..ae75184aa --- /dev/null +++ b/web/tests/test_db_models.py @@ -0,0 +1,37 @@ +from django.test import TestCase +from web.models import SiteVisit, LookupData + +class TestDbModels(TestCase): + def test_site_visit_creation(self): + visit = SiteVisit.objects.create( + url="http://example.com", + user_agent="Mozilla/5.0", + referer="http://google.com" + ) + self.assertIsNotNone(visit.id) + self.assertEqual(visit.url, "http://example.com") + self.assertEqual(visit.user_agent, "Mozilla/5.0") + self.assertEqual(visit.referer, "http://google.com") + self.assertIsNotNone(visit.date_time) + + def test_lookup_data_creation(self): + visit = SiteVisit.objects.create( + url="http://example.com", + user_agent="Mozilla/5.0", + referer="http://google.com" + ) + lookup = LookupData.objects.create( + language1="python", + version1="3", + language2="javascript", + version2="es6", + structure="data_types", + site_visit=visit + ) + self.assertIsNotNone(lookup.id) + self.assertEqual(lookup.language1, "python") + self.assertEqual(lookup.version1, "3") + self.assertEqual(lookup.language2, "javascript") + self.assertEqual(lookup.version2, "es6") + self.assertEqual(lookup.structure, "data_types") + self.assertEqual(lookup.site_visit, visit) diff --git a/web/tests/test_models.py b/web/tests/test_models.py index a630c2b86..62d236cb6 100644 --- a/web/tests/test_models.py +++ b/web/tests/test_models.py @@ -144,15 +144,54 @@ def test_language_get_concept_code(self): self.assertEqual(language.concept_code("concept3"), "") self.assertEqual(language.concept_code("concept4"), "line1\nline2") - def test_language_get_concept_comment(self): - """test Language#get_concept_comment""" - language = self.dummy_language - - # Test unknown concept - self.assertEqual(language.concept_comment("12345"), "") - - # Test known concept - self.assertEqual(language.concept_comment("concept1"), "") - self.assertEqual(language.concept_comment("concept2"), "My comment") - self.assertEqual(language.concept_comment("concept3"), "") - self.assertEqual(language.concept_comment("concept4"), "") + def test_language_versions(self): + """test Language#versions""" + language = Language("python", "Python") + versions = language.versions() + self.assertGreater(len(versions), 0) + self.assertIn("3", versions) + + def test_language_load_filled_concepts(self): + """test Language#load_filled_concepts""" + language = Language("python", "Python") + # Python 3 has data_types structure + response = language.load_filled_concepts("data_types", "3") + response_json = json.loads(response) + self.assertEqual(response_json["meta"]["language"], "python") + self.assertEqual(response_json["meta"]["structure"], "data_types") + self.assertIn("concepts", response_json) + # Check if some basic concept exists + self.assertIn("boolean", response_json["concepts"]) + + def test_language_load_comparison(self): + """test Language#load_comparison""" + language = Language("python", "Python") + response = language.load_comparison("data_types", "javascript", "ECMAScript 2023", "3") + response_json = json.loads(response) + self.assertEqual(response_json["meta"]["language_1"], "python") + self.assertEqual(response_json["meta"]["language_2"], "javascript") + self.assertIn("concepts1", response_json) + self.assertIn("concepts2", response_json) + + def test_metainfo_language_methods(self): + """test MetaInfo language related methods""" + self.assertEqual(self.metainfo.language_name("python"), "Python") + lang = self.metainfo.language("python") + self.assertIsInstance(lang, Language) + self.assertEqual(lang.key, "python") + + def test_metainfo_load_languages(self): + """test MetaInfo#load_languages""" + structure = self.metainfo.structure("data_types") + langs = self.metainfo.load_languages([("python", "3"), ("javascript", "ECMAScript 2023")], structure) + self.assertEqual(len(langs), 2) + self.assertEqual(langs[0].key, "python") + self.assertEqual(langs[1].key, "javascript") + + def test_metainfo_load_languages_missing_structure(self): + """test MetaInfo#load_languages with missing structure""" + from web.models import MissingStructureError + structure = self.metainfo.structure("data_types") + with self.assertRaises(MissingStructureError): + # python 3 definitely has data_types, but let's try something that doesn't exist + self.metainfo.load_languages([("python", "non_existent_version")], structure) diff --git a/web/tests/test_templatetags.py b/web/tests/test_templatetags.py new file mode 100644 index 000000000..48578667e --- /dev/null +++ b/web/tests/test_templatetags.py @@ -0,0 +1,17 @@ +from django.test import TestCase +from django.template import Context, Template + +class TestTemplateTags(TestCase): + def test_concept_card_tag(self): + template = Template( + "{% load templatetags %}" + "{% concept_card code comment %}" + ) + context = Context({ + 'code': 'print("Hello")', + 'comment': 'A simple print statement' + }) + rendered = template.render(context) + + self.assertIn('print("Hello")', rendered) + self.assertIn('A simple print statement', rendered) diff --git a/web/tests/test_views.py b/web/tests/test_views.py index b5e5b5a62..6cc7872ba 100644 --- a/web/tests/test_views.py +++ b/web/tests/test_views.py @@ -269,4 +269,36 @@ def test_single_concept_view_valid_language_version(self): self.assertIn('meta', response_data) self.assertIn('concepts', response_data) + def test_api_compare_valid(self): + """Test api_compare with valid languages and versions""" + url = reverse('api.compare', kwargs={ + 'structure_key': 'data_types', + 'lang1': 'python', + 'version1': '3', + 'lang2': 'javascript', + 'version2': 'ECMAScript 2009' + }) + response = self.client.get(url) + self.assertEqual(response.status_code, HTTPStatus.OK) + response_data = response.json() + self.assertIn('meta', response_data) + self.assertIn('concepts1', response_data) + self.assertIn('concepts2', response_data) + self.assertEqual(response_data['meta']['language_1'], 'python') + self.assertEqual(response_data['meta']['language_2'], 'javascript') + + def test_concepts_view_valid_params(self): + """Test concepts view with valid parameters that should return 200""" + url = reverse('index') + '?concept=data_types&lang=python%3B3&lang=javascript%3BECMAScript%202023' + response = self.client.get(url) + self.assertEqual(response.status_code, HTTPStatus.OK) + self.assertTemplateUsed(response, 'concepts.html') + + def test_concepts_view_legacy_params(self): + """Test concepts view with legacy lang1/lang2 parameters""" + url = reverse('compare') + '?concept=data_types&lang1=python%3B3&lang2=javascript%3BECMAScript%202023' + response = self.client.get(url) + self.assertEqual(response.status_code, HTTPStatus.OK) + self.assertTemplateUsed(response, 'concepts.html') + From 126c9361112bd4b25523a691378475e20311149d Mon Sep 17 00:00:00 2001 From: Sarah Withee <2601974+geekygirlsarah@users.noreply.github.com> Date: Sat, 27 Dec 2025 15:17:06 -0500 Subject: [PATCH 2/3] Optimize models and views 1. Refactored web/models.py * Added helper methods to the Language class to encapsulate "completeness" logic: * is_concept_complete(concept_key): Checks if a concept has code or a comment. * is_category_incomplete(category_concepts_keys): Determines if any concept in a category is missing required data. * has_any_implemented_in_category(category_concepts_keys): Checks if a language implements at least one concept in a category. * Implemented simple in-memory caching for MetaInfo and MetaStructure to avoid redundant file I/O operations when loading JSON metadata and structure files. 2. Optimized web/views.py * Simplified the concepts view by leveraging the new Language model methods, making the logic much more readable and maintainable. * Optimized syntax highlighting by pre-fetching lexers for each language once per request, instead of looking them up for every single concept. * Updated concepts_data, format_code_for_display, and format_comment_for_display to accept pre-fetched lexers, reducing overhead during page rendering. 3. Verification * Verified that all existing unit tests pass. * Ensured that the logic for determining "incomplete" status for languages and categories remains consistent with the original implementation but is now more efficient. --- web/models.py | 47 ++++++++++++++++++++++++++++++++++++++++ web/views.py | 59 ++++++++++++++++++++++++++++----------------------- 2 files changed, 80 insertions(+), 26 deletions(-) diff --git a/web/models.py b/web/models.py index abec3088a..c7c1dea89 100644 --- a/web/models.py +++ b/web/models.py @@ -12,6 +12,7 @@ class MetaStructure: Holds info about how the structure is divided into categories and concepts """ + _cached_files = {} def __init__(self, key, name): """ @@ -23,12 +24,18 @@ def __init__(self, key, name): """ self.key = key self.name = name + + if key in MetaStructure._cached_files: + self.categories = MetaStructure._cached_files[key] + return + meta_structure_file_path = os.path.join( "web", "thesauruses", "_meta", f"{key}.json") with open(meta_structure_file_path, 'r', encoding='UTF-8') as meta_structure_file: meta_structure_file_json = json.load(meta_structure_file) self.categories = meta_structure_file_json["categories"] + MetaStructure._cached_files[key] = self.categories class Language: @@ -210,6 +217,37 @@ def concept_comment(self, concept_key): return comment + def is_concept_complete(self, concept_key): + """ + Returns a Boolean if the concept has either code or a comment. + """ + if self.concept_unknown(concept_key): + return False + if not self.concept_implemented(concept_key): + return True # If explicitly marked as not-implemented, we consider it "complete" in terms of knowledge + return bool(self.concept_code(concept_key) or self.concept_comment(concept_key)) + + def is_category_incomplete(self, category_concepts_keys): + """ + Returns a Boolean if ANY concept in the category is unknown or missing code/comment. + """ + for key in category_concepts_keys: + if self.concept_unknown(key): + return True + if self.concept_implemented(key) and not (self.concept_code(key) or self.concept_comment(key)): + return True + return False + + def has_any_implemented_in_category(self, category_concepts_keys): + """ + Returns True if at least one concept in the category is known AND implemented. + """ + for key in category_concepts_keys: + if not self.concept_unknown(key) and self.concept_implemented(key): + return True + return False + + class MissingLanguageError(Exception): """Error for when a requested language is not defined in `meta.json`""" def __init__(self, key): @@ -232,6 +270,8 @@ def __init__(self, structure, language_key, language_name, language_version): class MetaInfo: """Holds info about structures and languages""" + _cached_structures = None + _cached_languages = None def __init__(self): """ @@ -239,12 +279,19 @@ def __init__(self): :rtype: None """ + if MetaInfo._cached_structures is not None: + self.structures = MetaInfo._cached_structures + self.languages = MetaInfo._cached_languages + return + meta_info_file_path = os.path.join( "web", "thesauruses", "meta_info.json") with open(meta_info_file_path, 'r', encoding='UTF-8') as meta_file: meta_info_json = json.load(meta_file) self.structures = meta_info_json["structures"] self.languages = meta_info_json["languages"] + MetaInfo._cached_structures = self.structures + MetaInfo._cached_languages = self.languages def language_name(self, language_key): diff --git a/web/views.py b/web/views.py index 1496166aa..0514efb79 100644 --- a/web/views.py +++ b/web/views.py @@ -188,35 +188,34 @@ def concepts(request): meta_structure.key ) + lexers = [get_highlighter(lang.key) for lang in languages] all_categories = [] for (category_key, category) in meta_structure.categories.items(): - concepts_list = [concepts_data(key, name, languages) for (key, name) in category.items()] + concept_keys = list(category.keys()) + concepts_list = [concepts_data(key, name, languages, lexers) for (key, name) in category.items()] category_entry = { "key": category_key, "concepts": concepts_list, - "is_incomplete": [False] * len(languages) + "is_incomplete": [] } - for i in range(len(languages)): - is_incomplete = True - for concept in concepts_list: - if not languages[i].concept_unknown(concept["key"]) and \ - languages[i].concept_implemented(concept["key"]): - is_incomplete = False - if languages[i].concept_unknown(concept["key"]) or \ - (languages[i].concept_implemented(concept["key"]) and \ - not languages[i].concept_code(concept["key"]) and \ - not languages[i].concept_comment(concept["key"]) ): - category_entry["is_incomplete"][i] = True - break - if is_incomplete: - category_entry["is_incomplete"][i] = True + + for lang in languages: + is_incomplete = False + # If nothing in this category is implemented for this language + if not lang.has_any_implemented_in_category(concept_keys): + is_incomplete = True + # OR if at least one concept is missing code/comment + elif lang.is_category_incomplete(concept_keys): + is_incomplete = True + + category_entry["is_incomplete"].append(is_incomplete) + all_categories.append(category_entry) - for lang in languages: - booleans = [category["is_incomplete"][languages.index(lang)] for category in all_categories] - lang._is_incomplete = any(booleans) + for i, lang in enumerate(languages): + lang._is_incomplete = any(cat["is_incomplete"][i] for cat in all_categories) return render_concepts(request, languages, meta_structure, all_categories) @@ -321,20 +320,22 @@ def get_highlighter(language): return lexer # Helper functions -def format_code_for_display(concept_key, lang): +def format_code_for_display(concept_key, lang, lexer=None): """ Returns the formatted HTML formatted syntax-highlighted text for a concept key (from a meta language file) and a language :param concept_key: name of the key to format :param lang: language to format it (in meta language/syntax highlighter format) + :param lexer: optional pre-fetched lexer :return: string with code with applied HTML formatting """ if lang.concept_unknown(concept_key) or lang.concept_code(concept_key) is None: return "Unknown" if lang.concept_implemented(concept_key): - lexer = get_highlighter(lang.key) + if lexer is None: + lexer = get_highlighter(lang.key) return highlight( lang.concept_code(concept_key), lexer, @@ -357,22 +358,28 @@ def format_comment_for_display(concept_key, lang): return lang.concept_comment(concept_key) -def concepts_data(key, name, languages): +def concepts_data(key, name, languages, lexers=None): """ Generates the comparision object of a single concept :param key: key of the concept :param name: name of the concept :param languages: list of languages to compare / get a reference for + :param lexers: optional list of pre-fetched lexers corresponding to languages :return: string with code with applied HTML formatting """ + data = [] + for i, lang in enumerate(languages): + lexer = lexers[i] if lexers else None + data.append({ + "code": format_code_for_display(key, lang, lexer), + "comment": format_comment_for_display(key, lang) + }) + return { "key": key, "name": name, - "data": [{ - "code": format_code_for_display(key, lang), - "comment": format_comment_for_display(key, lang) - } for lang in languages ], + "data": data, } From c0ad60521e8128dbee5d702d7054b0024f4f8495 Mon Sep 17 00:00:00 2001 From: Sarah Withee <2601974+geekygirlsarah@users.noreply.github.com> Date: Sat, 27 Dec 2025 15:24:31 -0500 Subject: [PATCH 3/3] Revise PR template with some additional AI requests --- .github/PULL_REQUEST_TEMPLATE.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 581951191..3a52e5869 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -30,6 +30,12 @@ Resolves #xxxx +## AI bots used in the process of making this PR + + + + + ## (If editing website code) Please add screenshots