Skip to content

Commit abf8d78

Browse files
Merge pull request #785 from geekygirlsarah/main
Add unit tests and optimize algorithm a bit
2 parents be128bf + c0ad605 commit abf8d78

File tree

7 files changed

+224
-39
lines changed

7 files changed

+224
-39
lines changed

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ Resolves #xxxx
3030
<!-- Please replace this line with a description of your changes. -->
3131

3232

33+
## AI bots used in the process of making this PR
34+
35+
<!-- Please replace this line with any information on any bots you used and how they were used -->
36+
<!-- AI is allowed, but please review the suggested changes and ensure they work correctly. -->
37+
<!-- We're also curious how bots were used and how they helped you make this PR. -->
38+
3339
## (If editing website code) Please add screenshots
3440

3541
<!--
@@ -53,7 +59,7 @@ Resolves #xxxx
5359
- [ ] I tested my changes locally to ensure they work
5460
- [ ] For language files, I have validated the edited files are valid JSON and data shows up correctly
5561
- [ ] For website code edits, I have added or edited any appropriate unit tests for my changes
56-
62+
- [ ] If I used artificial intelligence (AI), I have reviewed the suggested changes and ensured they work correctly
5763

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

web/models.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class MetaStructure:
1212
Holds info about how the structure is divided into categories and
1313
concepts
1414
"""
15+
_cached_files = {}
1516

1617
def __init__(self, key, name):
1718
"""
@@ -23,12 +24,18 @@ def __init__(self, key, name):
2324
"""
2425
self.key = key
2526
self.name = name
27+
28+
if key in MetaStructure._cached_files:
29+
self.categories = MetaStructure._cached_files[key]
30+
return
31+
2632
meta_structure_file_path = os.path.join(
2733
"web", "thesauruses", "_meta", f"{key}.json")
2834
with open(meta_structure_file_path, 'r', encoding='UTF-8') as meta_structure_file:
2935
meta_structure_file_json = json.load(meta_structure_file)
3036

3137
self.categories = meta_structure_file_json["categories"]
38+
MetaStructure._cached_files[key] = self.categories
3239

3340

3441
class Language:
@@ -210,6 +217,37 @@ def concept_comment(self, concept_key):
210217
return comment
211218

212219

220+
def is_concept_complete(self, concept_key):
221+
"""
222+
Returns a Boolean if the concept has either code or a comment.
223+
"""
224+
if self.concept_unknown(concept_key):
225+
return False
226+
if not self.concept_implemented(concept_key):
227+
return True # If explicitly marked as not-implemented, we consider it "complete" in terms of knowledge
228+
return bool(self.concept_code(concept_key) or self.concept_comment(concept_key))
229+
230+
def is_category_incomplete(self, category_concepts_keys):
231+
"""
232+
Returns a Boolean if ANY concept in the category is unknown or missing code/comment.
233+
"""
234+
for key in category_concepts_keys:
235+
if self.concept_unknown(key):
236+
return True
237+
if self.concept_implemented(key) and not (self.concept_code(key) or self.concept_comment(key)):
238+
return True
239+
return False
240+
241+
def has_any_implemented_in_category(self, category_concepts_keys):
242+
"""
243+
Returns True if at least one concept in the category is known AND implemented.
244+
"""
245+
for key in category_concepts_keys:
246+
if not self.concept_unknown(key) and self.concept_implemented(key):
247+
return True
248+
return False
249+
250+
213251
class MissingLanguageError(Exception):
214252
"""Error for when a requested language is not defined in `meta.json`"""
215253
def __init__(self, key):
@@ -232,19 +270,28 @@ def __init__(self, structure, language_key, language_name, language_version):
232270

233271
class MetaInfo:
234272
"""Holds info about structures and languages"""
273+
_cached_structures = None
274+
_cached_languages = None
235275

236276
def __init__(self):
237277
"""
238278
Initializes MetaInfo object with meta language information
239279
240280
:rtype: None
241281
"""
282+
if MetaInfo._cached_structures is not None:
283+
self.structures = MetaInfo._cached_structures
284+
self.languages = MetaInfo._cached_languages
285+
return
286+
242287
meta_info_file_path = os.path.join(
243288
"web", "thesauruses", "meta_info.json")
244289
with open(meta_info_file_path, 'r', encoding='UTF-8') as meta_file:
245290
meta_info_json = json.load(meta_file)
246291
self.structures = meta_info_json["structures"]
247292
self.languages = meta_info_json["languages"]
293+
MetaInfo._cached_structures = self.structures
294+
MetaInfo._cached_languages = self.languages
248295

249296

250297
def language_name(self, language_key):

web/tests/test_db_models.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from django.test import TestCase
2+
from web.models import SiteVisit, LookupData
3+
4+
class TestDbModels(TestCase):
5+
def test_site_visit_creation(self):
6+
visit = SiteVisit.objects.create(
7+
url="http://example.com",
8+
user_agent="Mozilla/5.0",
9+
referer="http://google.com"
10+
)
11+
self.assertIsNotNone(visit.id)
12+
self.assertEqual(visit.url, "http://example.com")
13+
self.assertEqual(visit.user_agent, "Mozilla/5.0")
14+
self.assertEqual(visit.referer, "http://google.com")
15+
self.assertIsNotNone(visit.date_time)
16+
17+
def test_lookup_data_creation(self):
18+
visit = SiteVisit.objects.create(
19+
url="http://example.com",
20+
user_agent="Mozilla/5.0",
21+
referer="http://google.com"
22+
)
23+
lookup = LookupData.objects.create(
24+
language1="python",
25+
version1="3",
26+
language2="javascript",
27+
version2="es6",
28+
structure="data_types",
29+
site_visit=visit
30+
)
31+
self.assertIsNotNone(lookup.id)
32+
self.assertEqual(lookup.language1, "python")
33+
self.assertEqual(lookup.version1, "3")
34+
self.assertEqual(lookup.language2, "javascript")
35+
self.assertEqual(lookup.version2, "es6")
36+
self.assertEqual(lookup.structure, "data_types")
37+
self.assertEqual(lookup.site_visit, visit)

web/tests/test_models.py

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -144,15 +144,54 @@ def test_language_get_concept_code(self):
144144
self.assertEqual(language.concept_code("concept3"), "")
145145
self.assertEqual(language.concept_code("concept4"), "line1\nline2")
146146

147-
def test_language_get_concept_comment(self):
148-
"""test Language#get_concept_comment"""
149-
language = self.dummy_language
150-
151-
# Test unknown concept
152-
self.assertEqual(language.concept_comment("12345"), "")
153-
154-
# Test known concept
155-
self.assertEqual(language.concept_comment("concept1"), "")
156-
self.assertEqual(language.concept_comment("concept2"), "My comment")
157-
self.assertEqual(language.concept_comment("concept3"), "")
158-
self.assertEqual(language.concept_comment("concept4"), "")
147+
def test_language_versions(self):
148+
"""test Language#versions"""
149+
language = Language("python", "Python")
150+
versions = language.versions()
151+
self.assertGreater(len(versions), 0)
152+
self.assertIn("3", versions)
153+
154+
def test_language_load_filled_concepts(self):
155+
"""test Language#load_filled_concepts"""
156+
language = Language("python", "Python")
157+
# Python 3 has data_types structure
158+
response = language.load_filled_concepts("data_types", "3")
159+
response_json = json.loads(response)
160+
self.assertEqual(response_json["meta"]["language"], "python")
161+
self.assertEqual(response_json["meta"]["structure"], "data_types")
162+
self.assertIn("concepts", response_json)
163+
# Check if some basic concept exists
164+
self.assertIn("boolean", response_json["concepts"])
165+
166+
def test_language_load_comparison(self):
167+
"""test Language#load_comparison"""
168+
language = Language("python", "Python")
169+
response = language.load_comparison("data_types", "javascript", "ECMAScript 2023", "3")
170+
response_json = json.loads(response)
171+
self.assertEqual(response_json["meta"]["language_1"], "python")
172+
self.assertEqual(response_json["meta"]["language_2"], "javascript")
173+
self.assertIn("concepts1", response_json)
174+
self.assertIn("concepts2", response_json)
175+
176+
def test_metainfo_language_methods(self):
177+
"""test MetaInfo language related methods"""
178+
self.assertEqual(self.metainfo.language_name("python"), "Python")
179+
lang = self.metainfo.language("python")
180+
self.assertIsInstance(lang, Language)
181+
self.assertEqual(lang.key, "python")
182+
183+
def test_metainfo_load_languages(self):
184+
"""test MetaInfo#load_languages"""
185+
structure = self.metainfo.structure("data_types")
186+
langs = self.metainfo.load_languages([("python", "3"), ("javascript", "ECMAScript 2023")], structure)
187+
self.assertEqual(len(langs), 2)
188+
self.assertEqual(langs[0].key, "python")
189+
self.assertEqual(langs[1].key, "javascript")
190+
191+
def test_metainfo_load_languages_missing_structure(self):
192+
"""test MetaInfo#load_languages with missing structure"""
193+
from web.models import MissingStructureError
194+
structure = self.metainfo.structure("data_types")
195+
with self.assertRaises(MissingStructureError):
196+
# python 3 definitely has data_types, but let's try something that doesn't exist
197+
self.metainfo.load_languages([("python", "non_existent_version")], structure)

web/tests/test_templatetags.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from django.test import TestCase
2+
from django.template import Context, Template
3+
4+
class TestTemplateTags(TestCase):
5+
def test_concept_card_tag(self):
6+
template = Template(
7+
"{% load templatetags %}"
8+
"{% concept_card code comment %}"
9+
)
10+
context = Context({
11+
'code': 'print("Hello")',
12+
'comment': 'A simple print statement'
13+
})
14+
rendered = template.render(context)
15+
16+
self.assertIn('print("Hello")', rendered)
17+
self.assertIn('A simple print statement', rendered)

web/tests/test_views.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,4 +269,36 @@ def test_single_concept_view_valid_language_version(self):
269269
self.assertIn('meta', response_data)
270270
self.assertIn('concepts', response_data)
271271

272+
def test_api_compare_valid(self):
273+
"""Test api_compare with valid languages and versions"""
274+
url = reverse('api.compare', kwargs={
275+
'structure_key': 'data_types',
276+
'lang1': 'python',
277+
'version1': '3',
278+
'lang2': 'javascript',
279+
'version2': 'ECMAScript 2009'
280+
})
281+
response = self.client.get(url)
282+
self.assertEqual(response.status_code, HTTPStatus.OK)
283+
response_data = response.json()
284+
self.assertIn('meta', response_data)
285+
self.assertIn('concepts1', response_data)
286+
self.assertIn('concepts2', response_data)
287+
self.assertEqual(response_data['meta']['language_1'], 'python')
288+
self.assertEqual(response_data['meta']['language_2'], 'javascript')
289+
290+
def test_concepts_view_valid_params(self):
291+
"""Test concepts view with valid parameters that should return 200"""
292+
url = reverse('index') + '?concept=data_types&lang=python%3B3&lang=javascript%3BECMAScript%202023'
293+
response = self.client.get(url)
294+
self.assertEqual(response.status_code, HTTPStatus.OK)
295+
self.assertTemplateUsed(response, 'concepts.html')
296+
297+
def test_concepts_view_legacy_params(self):
298+
"""Test concepts view with legacy lang1/lang2 parameters"""
299+
url = reverse('compare') + '?concept=data_types&lang1=python%3B3&lang2=javascript%3BECMAScript%202023'
300+
response = self.client.get(url)
301+
self.assertEqual(response.status_code, HTTPStatus.OK)
302+
self.assertTemplateUsed(response, 'concepts.html')
303+
272304

web/views.py

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -188,35 +188,34 @@ def concepts(request):
188188
meta_structure.key
189189
)
190190

191+
lexers = [get_highlighter(lang.key) for lang in languages]
191192
all_categories = []
192193

193194
for (category_key, category) in meta_structure.categories.items():
194-
concepts_list = [concepts_data(key, name, languages) for (key, name) in category.items()]
195+
concept_keys = list(category.keys())
196+
concepts_list = [concepts_data(key, name, languages, lexers) for (key, name) in category.items()]
195197

196198
category_entry = {
197199
"key": category_key,
198200
"concepts": concepts_list,
199-
"is_incomplete": [False] * len(languages)
201+
"is_incomplete": []
200202
}
201-
for i in range(len(languages)):
202-
is_incomplete = True
203-
for concept in concepts_list:
204-
if not languages[i].concept_unknown(concept["key"]) and \
205-
languages[i].concept_implemented(concept["key"]):
206-
is_incomplete = False
207-
if languages[i].concept_unknown(concept["key"]) or \
208-
(languages[i].concept_implemented(concept["key"]) and \
209-
not languages[i].concept_code(concept["key"]) and \
210-
not languages[i].concept_comment(concept["key"]) ):
211-
category_entry["is_incomplete"][i] = True
212-
break
213-
if is_incomplete:
214-
category_entry["is_incomplete"][i] = True
203+
204+
for lang in languages:
205+
is_incomplete = False
206+
# If nothing in this category is implemented for this language
207+
if not lang.has_any_implemented_in_category(concept_keys):
208+
is_incomplete = True
209+
# OR if at least one concept is missing code/comment
210+
elif lang.is_category_incomplete(concept_keys):
211+
is_incomplete = True
212+
213+
category_entry["is_incomplete"].append(is_incomplete)
214+
215215
all_categories.append(category_entry)
216216

217-
for lang in languages:
218-
booleans = [category["is_incomplete"][languages.index(lang)] for category in all_categories]
219-
lang._is_incomplete = any(booleans)
217+
for i, lang in enumerate(languages):
218+
lang._is_incomplete = any(cat["is_incomplete"][i] for cat in all_categories)
220219

221220
return render_concepts(request, languages, meta_structure, all_categories)
222221

@@ -321,20 +320,22 @@ def get_highlighter(language):
321320
return lexer
322321

323322
# Helper functions
324-
def format_code_for_display(concept_key, lang):
323+
def format_code_for_display(concept_key, lang, lexer=None):
325324
"""
326325
Returns the formatted HTML formatted syntax-highlighted text for a concept key (from a meta
327326
language file) and a language
328327
329328
:param concept_key: name of the key to format
330329
:param lang: language to format it (in meta language/syntax highlighter format)
330+
:param lexer: optional pre-fetched lexer
331331
:return: string with code with applied HTML formatting
332332
"""
333333

334334
if lang.concept_unknown(concept_key) or lang.concept_code(concept_key) is None:
335335
return "Unknown"
336336
if lang.concept_implemented(concept_key):
337-
lexer = get_highlighter(lang.key)
337+
if lexer is None:
338+
lexer = get_highlighter(lang.key)
338339
return highlight(
339340
lang.concept_code(concept_key),
340341
lexer,
@@ -357,22 +358,28 @@ def format_comment_for_display(concept_key, lang):
357358
return lang.concept_comment(concept_key)
358359

359360

360-
def concepts_data(key, name, languages):
361+
def concepts_data(key, name, languages, lexers=None):
361362
"""
362363
Generates the comparision object of a single concept
363364
364365
:param key: key of the concept
365366
:param name: name of the concept
366367
:param languages: list of languages to compare / get a reference for
368+
:param lexers: optional list of pre-fetched lexers corresponding to languages
367369
:return: string with code with applied HTML formatting
368370
"""
371+
data = []
372+
for i, lang in enumerate(languages):
373+
lexer = lexers[i] if lexers else None
374+
data.append({
375+
"code": format_code_for_display(key, lang, lexer),
376+
"comment": format_comment_for_display(key, lang)
377+
})
378+
369379
return {
370380
"key": key,
371381
"name": name,
372-
"data": [{
373-
"code": format_code_for_display(key, lang),
374-
"comment": format_comment_for_display(key, lang)
375-
} for lang in languages ],
382+
"data": data,
376383
}
377384

378385

0 commit comments

Comments
 (0)