Skip to content

Commit e4c00aa

Browse files
renzonrenzon
authored andcommitted
Implemented Course Certificate
close #1979
1 parent 9925098 commit e4c00aa

10 files changed

Lines changed: 191 additions & 1 deletion

File tree

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ coverage.xml
5353
# Django stuff:
5454
*.log
5555
local_settings.py
56+
staticfiles/
5657

5758
# Flask stuff:
5859
instance/
5.99 KB
Loading
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{% extends 'core/base_without_nav.html' %}
2+
{% load dashboard_tags %}
3+
{% load static %}
4+
{% block title %}Certificado do Curso {{ module.title }}{% endblock %}
5+
{% block description %}Certificado do Curso {{ module.title }}{% endblock %}
6+
{% block head %}
7+
8+
{% endblock head %}
9+
10+
{% block body %}
11+
<div class="colors-pallet d-none d-md-block">
12+
<div class="color-1 pallet"></div>
13+
<div class="color-2 pallet"></div>
14+
<div class="color-3 pallet"></div>
15+
<div class="color-4 pallet"></div>
16+
</div>
17+
<div class="container">
18+
<div class="row">
19+
<div class="col text-center">
20+
<h1 class="pythonpro-title mt-5"><span class="python">Python</span> Pro</h1>
21+
<h2 class="pythonpro-subtitle mb-4 mb-md-5">Python para Profissionais</h2>
22+
</div>
23+
24+
</div>
25+
<div class="row">
26+
<div class="col text-center">
27+
<h1 class="mb-5">Certificado</h1>
28+
</div>
29+
</div>
30+
<div class="row">
31+
<div class="col mb-4 font-weight-bold">
32+
Conferimos que {{ user.first_name }} concluiu {% widthratio module.progress 1 100 %}% do curso
33+
{{ module.title }}
34+
com carga horária de {{ module.duration|duration }},
35+
tendo assistido {{ module.total_watched_time|duration }} de video aulas e
36+
finalizado {{ module.finished_topics_count }} aulas de um total de {{ module.topics_count }}.
37+
</div>
38+
</div>
39+
<div class="row font-weight-bold mb-5">
40+
<div class="col text-right">
41+
São José dos Campos, {% now 'd' %} de {% now 'F' %} de {% now 'Y' %}.
42+
</div>
43+
</div>
44+
<div class="row">
45+
<div class="col">
46+
<div class="col text-center">
47+
<img src="{% static 'dashboard/img/assinatura-renzo.jpg' %}" alt="Assinatura de Renzo Nuccitelli"/>
48+
<br/>
49+
50+
________________________________
51+
<br/>
52+
Renzo Nuccitelli
53+
</div>
54+
55+
</div>
56+
</div>
57+
</div>
58+
59+
{% endblock body %}
60+
61+
{% block footer %}{% endblock footer %}

‎pythonpro/dashboard/templates/dashboard/home.html‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ <h2 class="mb-4">Progresso por Módulo</h2>
6262
<div class="col-12 col-lg-6 mb-md-4 mb-4">
6363
<div class="card">
6464
<div class="card-header bg-primary text-light">
65+
<a href="{{ module_progress.get_certificate_url }}" class="btn-sm btn-warning">Certificado &raquo;</a>
6566
Módulo: {{ module_progress.title }}
6667
</div>
6768
<div class="card-body">

‎pythonpro/dashboard/templatetags/dashboard_tags.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ def duration(value):
99
else:
1010
minutes, seconds = divmod(duration, 60)
1111
hours, minutes = divmod(minutes, 60)
12-
return f'{hours:02d}:{minutes:02d}:{seconds:02d}'
12+
if hours > 0:
13+
output = f'{hours:02d} hora(s) {minutes:02d} minuto(s)s {seconds:02d} segundo(s)'
14+
else:
15+
output = f'{minutes:02d} minuto(s)s {seconds:02d} segundo(s)'
16+
return output
1317

1418

1519
register = template.Library()

‎pythonpro/dashboard/urls.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@
66
urlpatterns = [
77
path('topic_interaction', views.topic_interaction, name='topic_interaction'),
88
path('', views.home, name='home'),
9+
path('certificados/<slug:module_slug>', views.certificate, name='certificate'),
910
]

‎pythonpro/dashboard/views.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@
99
from pythonpro.modules.models import Topic
1010

1111

12+
@login_required
13+
def certificate(request, module_slug):
14+
module_progresses = content_statistics_domain.calculate_module_progresses(request.user)
15+
ctx = {}
16+
for module in module_progresses:
17+
if module.slug == module_slug:
18+
ctx['module'] = module
19+
break
20+
return render(request, 'dashboard/certificate.html', ctx)
21+
22+
1223
@login_required
1324
def home(request):
1425
topics = list(content_statistics_domain.calculate_topic_interaction_history(request.user))
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import pytest
2+
from django.urls import reverse
3+
from model_mommy import mommy
4+
5+
from pythonpro.dashboard.models import TopicInteraction
6+
from pythonpro.dashboard.templatetags.dashboard_tags import duration
7+
from pythonpro.django_assertions import dj_assert_contains
8+
from pythonpro.modules.models import Chapter, Module, Section, Topic
9+
10+
11+
# Tests user interacted with all topics
12+
@pytest.fixture
13+
def module(db):
14+
return mommy.make(Module)
15+
16+
17+
@pytest.fixture
18+
def sections(module):
19+
models = []
20+
models.extend(mommy.make(Section, 2, module=module))
21+
return models
22+
23+
24+
@pytest.fixture
25+
def chapters(sections):
26+
models = []
27+
for s in sections:
28+
models.extend(mommy.make(Chapter, 2, section=s))
29+
return models
30+
31+
32+
TOPIC_DURATION = 100
33+
34+
35+
@pytest.fixture
36+
def topics(chapters):
37+
models = []
38+
for c in chapters:
39+
models.extend(mommy.make(Topic, 2, chapter=c, duration=TOPIC_DURATION))
40+
return models
41+
42+
43+
@pytest.fixture
44+
def interactions(topics, logged_user):
45+
models = []
46+
for t in topics:
47+
models.append(
48+
mommy.make(
49+
TopicInteraction,
50+
user=logged_user,
51+
topic=t,
52+
topic_duration=TOPIC_DURATION,
53+
total_watched_time=TOPIC_DURATION // 4,
54+
max_watched_time=TOPIC_DURATION // 2
55+
)
56+
)
57+
return models
58+
59+
60+
@pytest.fixture
61+
def resp(client_with_lead, interactions, module):
62+
return client_with_lead.get(
63+
reverse('dashboard:certificate', kwargs={'module_slug': module.slug}),
64+
secure=True
65+
)
66+
67+
68+
def test_status_code(resp, module):
69+
assert resp.status_code == 200
70+
71+
72+
def test_module_title_is_present(resp, module):
73+
dj_assert_contains(resp, module.title)
74+
75+
76+
def test_user_name_is_present(resp, logged_user):
77+
dj_assert_contains(resp, logged_user.first_name)
78+
79+
80+
def test_user_progress(resp):
81+
dj_assert_contains(resp, 'concluiu 50%')
82+
83+
84+
def test_user_watched_time(resp, interactions):
85+
dj_assert_contains(resp, duration(len(interactions) * TOPIC_DURATION // 4))
86+
87+
88+
def test_module_total_duration(resp, topics):
89+
dj_assert_contains(resp, duration(len(topics) * TOPIC_DURATION))
90+
91+
92+
def test_finished_topics(resp):
93+
dj_assert_contains(resp, 'finalizado 0 aulas')
94+
95+
96+
def test_total_topis(resp, topics):
97+
dj_assert_contains(resp, f'total de {len(topics)}')

‎pythonpro/domain/tests/test_certificate_list.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import List
2+
13
import pytest
24
from django.urls import reverse
35
from model_mommy import mommy
@@ -71,6 +73,11 @@ def test_module_title_is_present_on_card(resp, modules):
7173
dj_assert_contains(resp, f'Módulo: {m.title}')
7274

7375

76+
def test_module_certificate_link(resp, modules: List[Module]):
77+
for m in modules:
78+
dj_assert_contains(resp, f'href="{m.get_certificate_url()}"')
79+
80+
7481
def test_module_percentage_style_on_card(resp, logged_user):
7582
for m in pythonpro.domain.content_statistics_domain.calculate_module_progresses(logged_user):
7683
dj_assert_contains(resp, f'style="width: {m.progress:.0%}"')

‎pythonpro/modules/models.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,13 @@ def get_absolute_url(self):
115115
"""Must return the absolute url for this content"""
116116
return reverse('modules:detail', kwargs={'slug': self.slug})
117117

118+
def get_certificate_url(self):
119+
"""
120+
Generate Module's certificate url
121+
:return:
122+
"""
123+
return reverse('dashboard:certificate', kwargs={'module_slug': self.slug})
124+
118125
def _next_content_query_set(self):
119126
return Module.objects.filter(order=self.order + 1)
120127

0 commit comments

Comments
 (0)