Studying for the Japanese HTML5 Professional Certification by creating a Django app
Can you think of a more eloquent title?
I am studying for the HTML5 Professional Certification. Not only is some of it new information for me, but it is all in Japanese, my third language. My study method? Go through the official study book, take notes on each sample question, add vocabulary to anki, then revisit the notes every 10 questions or so and convert them from pen-and-paper to digital. I wanted to be able to this anywhere, from any computer, so I started a little website where I could add each studied question.
github :: Web application (ask me for login credentials)
So this is self-study for the Japanese language, for Django, and for the HTML5 test itself.
This is essentially a CRUD application with a (R)ead all functionality and no (current) ability to (U)pdate nor (D)elete. As a personal project, I can just use the built-in django admin panel or manually change to database myself.
Model
Here is the basic model upon which the whole app operates.
from django.db import models
class Question(models.Model):
question_code = models.CharField(max_length=10, unique=True) # Example: "1-1", "3-11"
question_in_japanese = models.TextField()
questions_in_english = models.TextField()
possible_answers = models.JSONField()
answer = models.TextField()
notes = models.TextField(blank=True, null=True)
link = models.URLField(blank=True, null=True)
def __str__(self): #defines how instances of my model are represented as strings
return self.question_code
class Vocabulary(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name='vocabularies')
#Cascade on delete always delights me,
#like "Hey, you don't need to be here anymore!"
japanese_word = models.CharField(max_length=100)
pronunciation = models.CharField(max_length=100)
english_translation = models.CharField(max_length=100)
def __str__(self):
return f"{self.japanese_word} ({self.pronunciation}) - {self.english_translation}"
Index — (R)ead all
And here’s the controller for the index views (which is confusingly named views.py):
def index(request):
question_list = sort_chapters_and_questions()
context = {"question_list": question_list}
#render() function takes the request object as its first argument, a template name
#as its second argument and a dictionary as its optional third argument
return render(request, "questions/index.html", context)
#helper functions actually in a separate file
def sort_chapters_and_questions():
questions = Question.objects.annotate(
chapter_number=Cast(Substr('question_code', 1, 1), IntegerField()),
question_number=Cast(Substr('question_code', 3), IntegerField())
).order_by('chapter_number','question_number')
return questions
Here’s the view code for index:
{% for question in question_list %}
<div class="index-question-body">
<a class="question-code" href="{% url 'questions:detail' question.question_code %}">{{ question.question_code }}</a><br>
<div class="question-in-japanese">{{ question.question_in_japanese }}</div>
<div class="question-in-english">{{ question.questions_in_english }}</div>
<div class="possible-answers">
<ol type="A">
{% for answer in question.possible_answers %}
<li class="possible-answer">{{ answer }}</li>
{% endfor %}
</ol>
</div>
<hr>
</div>
{% endfor %}
And some beautifully simple css:
/*index.html grid*/
.index-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
row-gap: 15px;
column-gap: 15px;
justify-items: stretch;
justify-content: space-evenly;
}
/* Media query to limit to a maximum of 3 columns on larger screens */
@media (min-width: 960px) {
.index-container {
grid-template-columns: repeat(3, 1fr);
}
}
/* Media query for small screens */
@media (max-width: 360px) {
.index-container {
grid-template-columns: 90vw;
}
}
.index-question-body {
padding: 20px;
background-color: #f9f9f9;
border: 1px solid #ccc;
border-radius: 5px;
box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.3);
}
/*Questions list (index.html)*/
.question-code {
font-size: 1em;
}
.question-in-japanese{
display: inline-block;
margin-left: 5px;
}
.question-in-english {
text-decoration-color: #616161;
background-color: rgb(228, 228, 228);
margin-left: 5px;
padding-left: 5px;
margin-bottom: 5px;
}
.possible-answers {
border-left: 6px solid #616161;
background-color: lightgrey;
font-style: italic;
padding: 1px 2px;
font-size: smaller;
overflow-wrap: break-word;/*ensures wrapping text especially on mobile*/
}
hr {
border: none;
height: 3px; /* Set height of the HR */
background-color: orangered; /* Color of the HR */
}
Detail — (R)ead
Here’s the controller for the detailed view of each question (which includes vocabulary specific to that question):
def detail(request, question_code):
question = get_object_or_404(Question, question_code=question_code)
vocab_list = question.vocabularies.all() # Get related vocabulary entries
context = {
"question": question,
"vocab_list": vocab_list,
}
return render(request, "questions/detail.html", context)
And that awesome URL slug is as simple as this:
path("<str:question_code>/", views.detail, name="detail"),
And here’s the view code for details:
<h1>{{ question }} Details</h1>
<div class="question-body">
<div class="question-in-japanese">{{ question.question_in_japanese }}</div>
<div class="question-in-english">{{ question.questions_in_english }}</div>
<div class="possible-answers">
<ol type="A">
{% for answer in question.possible_answers %}
<li class="possible-answer">{{ answer }}</li>
{% endfor %}
</ol>
</div>
<div class="answer dropdown">
<button class="dropbtn">Answer</button>
<div class="dropdown-content">{{ question.answer }}</div>
</div>
{% if question.notes %}
<div class="notes">{{ question.notes|linebreaksbr }}</div><!--properly display line breaks-->
{% endif %}
{% if question.link %}
<div class="question-link"><a href="{{ question.link }}">Relevant Link</a></div>
{% endif %}
</div>
<hr>
<!--TABLE OF VOCABULARY-->
<h2>Vocabulary</h2>
<div style="overflow-x:auto;">
<table border="1" class="vocabulary-table">
<thead class="table-header">
<tr>
<th>漢字</th><!--chinese characters-->
<th>読み方</th><!--pronunciation-->
<th>英語</th><!--english meaning-->
</tr>
</thead>
<tbody class="table-body">
{% for vocab in vocab_list %}
<tr>
<td class="kanji">{{ vocab.japanese_word }}</td>
<td>{{ vocab.pronunciation }}</td>
<td>{{ vocab.english_translation }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
And I can’t forget the cool css only solution to “reveal” the answer:
.dropbtn {
margin-left: 5px;
background-color: #4CAF50;
color: white;
padding: 16px;
font-size: 16px;
border: none;
cursor: pointer;
}
/* Dropdown Content (Hidden by Default) */
.dropdown-content {
display: none;
position: absolute;
background-color: #e0f0da;
min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
z-index: 1;
padding: 8px;
}
/* Show the dropdown menu on hover */
.dropdown:hover .dropdown-content {
display: block;
}
All Vocab
Here’s the controller of “all vocab”:
def allvocab(request):
sorted_questions = sort_chapters_and_questions()
vocab_list = Vocabulary.objects.filter(question__in=sorted_questions).order_by(
Cast(Substr('question__question_code', 1, 1), IntegerField()),
Cast(Substr('question__question_code', 3), IntegerField())
)
#don't include duplicates
unique_vocab_list = remove_duplicates_by_attribute(vocab_list, 'japanese_word')
context = {
"vocab_list": unique_vocab_list,
}
return render(request, "questions/allvocab.html", context)
#helper function is actually in another file
def remove_duplicates_by_attribute(queryset, attribute):
seen = set()
return [item for item in queryset if not (getattr(item, attribute) in seen or seen.add(getattr(item, attribute)))]
# This compicated list comprehension is the below pseudo-code
# Include each item in queryset
# only if its attribute value has not been 'seen' before.
# Iterate over each item object in queryset
# and check if the following condition is "not True":
# Check if the attribute value of the current item is already in the 'seen' set (True):
# **If this is True, the entire condition evaluates to True**
# Thus, DON'T ADD this item to the returned list.
# If the attribute value is not already in the 'seen' set (False):
# The 'seen.add(item.attribute)' method is executed, which adds the attribute value to the 'seen' set.
# **The add method returns 'None', which is equivalent to 'False' in a boolean context;**
# Therefore, the condition evaluates to False, and the item is added to the returned list.
List comprehension always take me a while to write and wrap my head around it, especially this one. It isn’t really a time saver for me.
And here’s the view of “all vocab”:
<h1>All Vocab</h1>
<div style="overflow-x:auto;">
<table border="1" class="vocabulary-table">
<thead class="table-header">
<tr>
<th>#</th>
<th>漢字</th>
<th>読み方</th>
<th>英語</th>
</tr>
</thead>
<tbody class="table-body">
{% for vocab in vocab_list %}
<tr>
<td><a class="question-code" href="{% url 'questions:detail' vocab.question %}">{{ vocab.question }}</a></td>
<td class="kanji">{{ vocab.japanese_word }}</td>
<td>{{ vocab.pronunciation }}</td>
<td>{{ vocab.english_translation }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
I love this css hover function for the table:
table {
width: 100%;
max-width: 800px;
border-collapse: collapse;
margin: 0 auto; /* Center the form horizontally */
padding: 20px; /* Example padding around the form */
border-radius: 20px;
overflow: hidden;
}
th {
background-color: #4c4c4c;
color: white;
text-align: center;
padding: 8px;
border: 1px solid transparent;
}
td {
padding: 8px;
text-align: left;
border: 1px solid transparent;
}
tr:nth-child(odd) {
background-color: rgb(236, 236, 236);
}
tr:nth-child(even) {
background-color: rgb(208, 208, 208);
}
tr:hover {
background-color: rgb(251, 218, 206)/*This one right here*/
Add a Question
The controller for the “Add a Question” form:
def add(request):
if request.method == "POST":
question_code = request.POST.get("question_code")
question_in_japanese = request.POST.get("question_in_japanese")
question_in_english = request.POST.get("question_in_english")
possible_answers = [
request.POST.get("choice_a"),
request.POST.get("choice_b"),
request.POST.get("choice_c"),
request.POST.get("choice_d"),
request.POST.get("choice_e"),
]
answer = request.POST.get("answer")
notes = request.POST.get("notes")
link = request.POST.get("link")
newly_created_question = Question.objects.create(
question_code=question_code,
question_in_japanese=question_in_japanese,
questions_in_english=question_in_english,
possible_answers=possible_answers,
answer=answer,
notes=notes,
link=link,
)
# Create related Vocabulary entries
for i in range(1, 6):
japanese_word = request.POST.get(f"j{i}")
pronunciation = request.POST.get(f"p{i}")
english_translation = request.POST.get(f"e{i}")
if japanese_word and pronunciation and english_translation:
Vocabulary.objects.create(
question=newly_created_question,
japanese_word=japanese_word,
pronunciation=pronunciation,
english_translation=english_translation
)
return HttpResponseRedirect(reverse("questions:success"))
# Always return an HttpResponseRedirect after successfully dealing
# with POST data. This prevents data from being posted twice if a
# user hits the Back button.
return render(request, "questions/add.html")
I’m sure this could probably be refactored (helper functions, etc.), as it is kinda long.
Note as this is a personal project, there is not yet any specific server-side validation nor SQL injection protection (Don’t get any ideas!).
Here is the view for the “add a question” form:
<h1>Add a question</h1>
<form id="main-form" action="{% url 'questions:add' %}" method="POST" autocomplete="off">
{% csrf_token %}
<fieldset>
<legend>Question Information</legend><!--I love the look of these-->
<label for="question-code">Code</label>
<input type="text" id="question-code" name="question_code" placeholder="5-5" required pattern="\d+-\d+" autofocus>
<label for="question-japanese">日本語</label>
<input type="text" id="question-japanese" name="question_in_japanese" placeholder="質問" required>
<label for="question-english">English</label>
<input type="text" id="question-english" name="question_in_english" placeholder="Question" required>
</fieldset>
<fieldset>
<legend>Possible Answers</legend>
<ol type="A">
<li><input type="text" name="choice_a"></li>
<li><input type="text" name="choice_b"></li>
<li><input type="text" name="choice_c"></li>
<li><input type="text" name="choice_d"></li>
<li><input type="text" name="choice_e"></li>
</ol>
</fieldset>
<fieldset>
<legend>Answer</legend>
<input type="text" name="answer" placeholder="answer" required>
</fieldset>
<fieldset>
<legend>Extras</legend>
<label for="notes">Notes</label>
<textarea rows="4" id="notes" name="notes" placeholder="notes"></textarea><br>
<label for="link">Link</label>
<input type="url" id="link" name="link" placeholder="url">
</fieldset>
<fieldset>
<legend>Vocabulary</legend>
<div style="overflow-x:auto;">
<table border="1">
<tr>
<th>漢字</th>
<th>読み方</th>
<th>英語</th>
</tr>
{% for i in "123456" %}
<tr>
<td><input type="text" name="j{{ i }}" placeholder="日本語"></td>
<td><input type="text" name="p{{ i }}" placeholder="ひらがな"></td>
<td><input type="text" name="e{{ i }}" placeholder="English"></td>
</tr>
{% endfor %}
</table>
</div>
</fieldset>
<div class="button-container">
<label for="confirm-modal" class="submit-label">Add this Question</label>
<button type="reset" class="reset">Clear Form</button>
</div>
</form>
<!-- Hidden Checkbox -->
<input type="checkbox" id="confirm-modal" class="modal-checkbox">
<!-- Modal -->
<div class="modal">
<div class="modal-content">
<p>You sure you wanna add this question?</p>
<div class="modal-buttons">
<label for="confirm-modal" class="submit" onclick="document.getElementById('main-form').submit();">Yes, add it!</label>
<!--Just, like, a tiny little bit of javascript for a modal-->
<label for="confirm-modal" class="reset">Cancel</label>
</div>
</div>
</div>
Here’s a tiny “text box can only be resized vertically” css trick that I didn’t know:
textarea {
resize: vertical; /*only allows vertical resizing using the grabber*/
}
Navbar
ul.nav {
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
background-color: orangered;
border-radius: 8px;
}
li.nav {
float: right;
}
li.nav a {
display: block;
color: white;
text-align: center;
padding: 12px 12px;
text-decoration: none;
font-size: 1.5rem;
}
li.nav a:hover {
background-color: rgb(209, 56, 0);
}
li.nav a.active {
color: #FFFFFF;
background: orangered;
text-shadow: 0 0 5px #FFF, 0 0 10px #FFF, 0 0 15px #FFF, 0 0 20px #49ff18, 0 0 30px #49FF18, 0 0 40px #49FF18, 0 0 55px #49FF18, 0 0 75px #49ff18;
color: #FFFFFF;
background: orangered;
}
Credit goes to the “Vegas” style from here.
Simple emoji in the html:
<nav>
<ul class="nav">
<li class="nav">
<a href="{% url 'questions:add' %}"
class="{% if request.resolver_match.url_name == 'add' %}active{% endif %}">
➕</a>
</li>
<li class="nav">
<a href="{% url 'questions:allvocab' %}"
class="{% if request.resolver_match.url_name == 'allvocab' %}active{% endif %}">
🇯🇵</a>
</li>
<li class="nav">
<a href="{% url 'questions:index' %}"
class="{% if request.resolver_match.url_name == 'index' %}active{% endif %}">
📖</a>
</li>
<li class="nav">
<a href="{% url 'landing_page' %}"
class="{% if request.resolver_match.url_name == 'landing_page' %}active{% endif %}">
🏠</a>
</li>
</ul>
</nav>
And?
How is my studying going? Well, I am currently (August 26, 2024) somewhere in the 30s of Chapter 1. I have a long way to actually study, but I consider this project to be complete!