Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ Resolves #xxxx
<!-- Please replace this line with a description of your changes. -->


## AI bots used in the process of making this PR

<!-- Please replace this line with any information on any bots you used and how they were used -->
<!-- AI is allowed, but please review the suggested changes and ensure they work correctly. -->
<!-- We're also curious how bots were used and how they helped you make this PR. -->

## (If editing website code) Please add screenshots

<!--
Expand All @@ -53,7 +59,7 @@ Resolves #xxxx
- [ ] I tested my changes locally to ensure they work
- [ ] For language files, I have validated the edited files are valid JSON and data shows up correctly
- [ ] For website code edits, I have added or edited any appropriate unit tests for my changes

- [ ] If I used artificial intelligence (AI), I have reviewed the suggested changes and ensured they work correctly

## Any additional comments or things to be aware of while reviewing?

Expand Down
47 changes: 47 additions & 0 deletions web/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand All @@ -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:
Expand Down Expand Up @@ -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):
Expand All @@ -232,19 +270,28 @@ 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):
"""
Initializes MetaInfo object with meta language information

: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):
Expand Down
37 changes: 37 additions & 0 deletions web/tests/test_db_models.py
Original file line number Diff line number Diff line change
@@ -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)
63 changes: 51 additions & 12 deletions web/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
17 changes: 17 additions & 0 deletions web/tests/test_templatetags.py
Original file line number Diff line number Diff line change
@@ -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)
32 changes: 32 additions & 0 deletions web/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')


59 changes: 33 additions & 26 deletions web/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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,
Expand All @@ -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,
}


Expand Down
Loading