From 681d9d1b9528edaa20288876568ff4c6c9a1f04c Mon Sep 17 00:00:00 2001 From: renzon Date: Sat, 17 Mar 2018 13:57:04 -0300 Subject: [PATCH] Implemented Chapters close #274 --- contrib/env-sample | 1 + pythonpro/modules/admin.py | 13 +++- pythonpro/modules/chapters_urls.py | 8 ++ pythonpro/modules/chapters_views.py | 8 ++ pythonpro/modules/facade.py | 40 ++++++++-- .../modules/fixtures/pythonpro_chapters.json | 1 + .../modules/fixtures/pythonpro_sections.json | 1 + pythonpro/modules/migrations/0006_chapter.py | 28 +++++++ pythonpro/modules/models.py | 14 ++++ pythonpro/modules/modules_views.py | 4 +- pythonpro/modules/sections_views.py | 6 +- .../templates/chapters/chapter_detail.html | 31 ++++++++ .../templates/modules/module_detail.html | 13 +++- .../templates/sections/section_detail.html | 10 +++ pythonpro/modules/tests/test_chapters_view.py | 75 +++++++++++++++++++ .../modules/tests/test_module_detail_view.py | 36 ++++++++- pythonpro/tests/test_urls.py | 2 +- pythonpro/urls.py | 1 + 18 files changed, 271 insertions(+), 21 deletions(-) create mode 100644 pythonpro/modules/chapters_urls.py create mode 100644 pythonpro/modules/chapters_views.py create mode 100644 pythonpro/modules/fixtures/pythonpro_chapters.json create mode 100644 pythonpro/modules/fixtures/pythonpro_sections.json create mode 100644 pythonpro/modules/migrations/0006_chapter.py create mode 100644 pythonpro/modules/templates/chapters/chapter_detail.html create mode 100644 pythonpro/modules/tests/test_chapters_view.py diff --git a/contrib/env-sample b/contrib/env-sample index fec9cf53..132018fd 100644 --- a/contrib/env-sample +++ b/contrib/env-sample @@ -2,6 +2,7 @@ DEBUG=True SECRET_KEY=Change for a secret here ALLOWED_HOSTS=localhost, 127.0.0.1 SENTRY_DSN= +DATABASE_URL= # Amazon S3 configuration DJANGO_AWS_ACCESS_KEY_ID= diff --git a/pythonpro/modules/admin.py b/pythonpro/modules/admin.py index dc6c29b4..63eb9d9e 100644 --- a/pythonpro/modules/admin.py +++ b/pythonpro/modules/admin.py @@ -1,16 +1,21 @@ from django.contrib import admin from ordered_model.admin import OrderedModelAdmin -from pythonpro.modules.models import Section, Module +from pythonpro.modules.models import Section, Module, Chapter + + +class ModuleAdmin(OrderedModelAdmin): + list_display = 'title slug order move_up_down_links'.split() class SectionAdmin(OrderedModelAdmin): list_display = 'title slug module order move_up_down_links'.split() -class ModuleAdmin(OrderedModelAdmin): - list_display = 'title slug order move_up_down_links'.split() +class ChapterAdmin(OrderedModelAdmin): + list_display = 'title slug section order move_up_down_links'.split() -admin.site.register(Section, SectionAdmin) admin.site.register(Module, ModuleAdmin) +admin.site.register(Section, SectionAdmin) +admin.site.register(Chapter, ChapterAdmin) diff --git a/pythonpro/modules/chapters_urls.py b/pythonpro/modules/chapters_urls.py new file mode 100644 index 00000000..dd123526 --- /dev/null +++ b/pythonpro/modules/chapters_urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from pythonpro.modules import chapters_views + +app_name = 'chapters' +urlpatterns = [ + path('/', chapters_views.detail, name='detail'), +] diff --git a/pythonpro/modules/chapters_views.py b/pythonpro/modules/chapters_views.py new file mode 100644 index 00000000..724ab3b0 --- /dev/null +++ b/pythonpro/modules/chapters_views.py @@ -0,0 +1,8 @@ +from django.shortcuts import render + +from pythonpro.modules import facade + + +def detail(request, slug): + ctx = {'chapter': facade.get_chapter_with_contents(slug=slug)} + return render(request, 'chapters/chapter_detail.html', context=ctx) diff --git a/pythonpro/modules/facade.py b/pythonpro/modules/facade.py index 0f5d77bb..dfc0bf1f 100644 --- a/pythonpro/modules/facade.py +++ b/pythonpro/modules/facade.py @@ -1,6 +1,6 @@ from django.db.models import Prefetch -from pythonpro.modules.models import Section as _Section, Module as _Module +from pythonpro.modules.models import Section as _Section, Module as _Module, Chapter as _Chapter def get_all_modules(): @@ -11,16 +11,46 @@ def get_all_modules(): return tuple(_Module.objects.order_by('order')) -def get_module_sections(slug): +def get_module_with_sections_and_chapters(slug): """ - Search for a module with respective sections - :param slug: module slugs + Search for a module with respective sections and chapters + :param slug: module's slug :return: Module with respective section on attribute sections """ return _Module.objects.filter(slug=slug).prefetch_related( Prefetch( 'section_set', - queryset=_Section.objects.order_by('order'), + queryset=_Section.objects.order_by('order').prefetch_related( + Prefetch( + 'chapter_set', + queryset=_Chapter.objects.order_by('order'), + to_attr='chapters' + ) + ), to_attr='sections') ).get() + + +def get_section_with_module_and_chapters(slug): + """ + Search for a section with respective module and chapters + :param slug: section's slug + :return: Section + """ + return _Section.objects.filter(slug=slug).select_related('module').prefetch_related( + Prefetch( + 'chapter_set', + queryset=_Chapter.objects.order_by('order'), + to_attr='chapters' + ) + ).get() + + +def get_chapter_with_contents(slug): + """ + Search for a chapter respective to slug with it's module and section + :param slug: chapter's slug + :return: Chapter + """ + return _Chapter.objects.filter(slug=slug).select_related('section').select_related('section__module').get() diff --git a/pythonpro/modules/fixtures/pythonpro_chapters.json b/pythonpro/modules/fixtures/pythonpro_chapters.json new file mode 100644 index 00000000..9faef80d --- /dev/null +++ b/pythonpro/modules/fixtures/pythonpro_chapters.json @@ -0,0 +1 @@ +[{"model": "modules.chapter", "pk": 1, "fields": {"order": 0, "title": "Introdu\u00e7\u00e3o", "description": "Nesse cap\u00edtulo voc\u00ea vai conferir a motiva\u00e7\u00e3o do curso, ou seja, como o jogo ficar\u00e1 ap\u00f3s a implementa\u00e7\u00e3o. Logo depois voc\u00ea vai aprender como instalar o Python em seu sistema operacional, editar c\u00f3digo e fazer pequenos testes no console.", "slug": "introducao-python-birds", "section": 1}}, {"model": "modules.chapter", "pk": 2, "fields": {"order": 1, "title": "Tipos Embutidos", "description": "Nesse cap\u00edtulo voc\u00ea vai aprender sobre os tipos embutidos. Eles formam um conjunto b\u00e1sicos de classes que serve para modelar o seu programa como n\u00fameros e palavras.", "slug": "tipos-embutidos", "section": 1}}] \ No newline at end of file diff --git a/pythonpro/modules/fixtures/pythonpro_sections.json b/pythonpro/modules/fixtures/pythonpro_sections.json new file mode 100644 index 00000000..d86a1f7b --- /dev/null +++ b/pythonpro/modules/fixtures/pythonpro_sections.json @@ -0,0 +1 @@ +[{"model": "modules.section", "pk": 1, "fields": {"order": 0, "title": "Programa\u00e7\u00e3o Procedural", "description": "Nessa se\u00e7\u00e3o voc\u00ea vai aprender programa\u00e7\u00e3o procedural. Esse paradigma consiste em voc\u00ea definir a resolu\u00e7\u00e3o de um problema, passo a passo, de forma linear. Funciona como uma receita culin\u00e1ria, onde cada passado \u00e9 definido exatamente um depois do outro.", "slug": "programacao-procedural", "module": 1}}, {"model": "modules.section", "pk": 2, "fields": {"order": 1, "title": "Orienta\u00e7\u00e3o a Objetos", "description": "Depois de aprende o paradigma procedural na se\u00e7\u00e3o anterior chega hora de conhecer outro: a Orienta\u00e7\u00e3o a Objetos (OO). Voc\u00ea vai aprender sobre classes e seus componentes, heran\u00e7a e utilizar esses conceitos para implementar o jogo Python Birds. Como toda mudan\u00e7a de paradigma, demora um tempo para se acostumar, mas \u00e9 importante aprender bem OO porque ela utilizada em in\u00fameras bibliotecas e frameworks.", "slug": "orientacao-a-objetos", "module": 1}}] \ No newline at end of file diff --git a/pythonpro/modules/migrations/0006_chapter.py b/pythonpro/modules/migrations/0006_chapter.py new file mode 100644 index 00000000..a7164b41 --- /dev/null +++ b/pythonpro/modules/migrations/0006_chapter.py @@ -0,0 +1,28 @@ +# Generated by Django 2.0.3 on 2018-03-17 16:03 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('modules', '0005_auto_20180314_1817'), + ] + + operations = [ + migrations.CreateModel( + name='Chapter', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order', models.PositiveIntegerField(db_index=True, editable=False)), + ('title', models.CharField(max_length=50)), + ('description', models.TextField()), + ('slug', models.SlugField(unique=True)), + ('section', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='modules.Section')), + ], + options={ + 'ordering': ['section', 'order'], + }, + ), + ] diff --git a/pythonpro/modules/models.py b/pythonpro/modules/models.py index 828a45dd..469b18e4 100644 --- a/pythonpro/modules/models.py +++ b/pythonpro/modules/models.py @@ -69,3 +69,17 @@ def get_absolute_url(self): def parent(self): return self.module + + +class Chapter(Content): + section = models.ForeignKey('Section', on_delete=models.CASCADE) + order_with_respect_to = 'section' + + class Meta: + ordering = ['section', 'order'] + + def get_absolute_url(self): + return reverse('chapters:detail', kwargs={'slug': self.slug}) + + def parent(self): + return self.section diff --git a/pythonpro/modules/modules_views.py b/pythonpro/modules/modules_views.py index 3fab7c28..89275f26 100644 --- a/pythonpro/modules/modules_views.py +++ b/pythonpro/modules/modules_views.py @@ -2,12 +2,12 @@ from django.shortcuts import render # Create your views here. -from pythonpro.modules.facade import get_module_sections, get_all_modules +from pythonpro.modules.facade import get_module_with_sections_and_chapters, get_all_modules @login_required def detail(request, slug): - module = get_module_sections(slug) + module = get_module_with_sections_and_chapters(slug) return render(request, 'modules/module_detail.html', {'module': module}) diff --git a/pythonpro/modules/sections_views.py b/pythonpro/modules/sections_views.py index 4aaf3f23..56bd86d4 100644 --- a/pythonpro/modules/sections_views.py +++ b/pythonpro/modules/sections_views.py @@ -1,8 +1,8 @@ -from django.shortcuts import render, get_object_or_404 +from django.shortcuts import render -from pythonpro.modules.models import Section +from pythonpro.modules import facade def detail(request, slug): - ctx = {'section': get_object_or_404(Section, slug=slug)} + ctx = {'section': facade.get_section_with_module_and_chapters(slug=slug)} return render(request, 'sections/section_detail.html', ctx) diff --git a/pythonpro/modules/templates/chapters/chapter_detail.html b/pythonpro/modules/templates/chapters/chapter_detail.html new file mode 100644 index 00000000..de3604d8 --- /dev/null +++ b/pythonpro/modules/templates/chapters/chapter_detail.html @@ -0,0 +1,31 @@ +{% extends 'core/base.html' %} +{% load static %} + +{% block title %}{{ chapter.title }}{% endblock %} + +{% block body %} + +
+
+
+

{{ chapter.title }}

+
Descrição
+
+
    +
  • {{ chapter.description }}
  • +
+
+
+
+
+{% endblock body %} diff --git a/pythonpro/modules/templates/modules/module_detail.html b/pythonpro/modules/templates/modules/module_detail.html index c8db8f4a..c99173fb 100644 --- a/pythonpro/modules/templates/modules/module_detail.html +++ b/pythonpro/modules/templates/modules/module_detail.html @@ -31,11 +31,20 @@

{{ module.title }}

    {% for section in module.sections %}
  1. {{ section.title }}
  2. +
    +
      + {% for chapter in section.chapters %} +
    1. {{ chapter.title }}
    2. + {% empty %} +
    3. Nenhum Capítulo definido ainda
    4. + {% endfor %} +
    +
    {% empty %}
  3. Nenhuma seção definida ainda
  4. {% endfor %} - -
+ + diff --git a/pythonpro/modules/templates/sections/section_detail.html b/pythonpro/modules/templates/sections/section_detail.html index 12a173df..38955c8d 100644 --- a/pythonpro/modules/templates/sections/section_detail.html +++ b/pythonpro/modules/templates/sections/section_detail.html @@ -25,6 +25,16 @@

{{ section.title }}

  • {{ section.description }}
  • +
    Capítulos
    +
    +
      + {% for chapter in section.chapters %} +
    1. {{ chapter.title }}
    2. + {% empty %} +
    3. Nenhum Capítulo definido ainda
    4. + {% endfor %} +
    +
    diff --git a/pythonpro/modules/tests/test_chapters_view.py b/pythonpro/modules/tests/test_chapters_view.py new file mode 100644 index 00000000..9da58d3e --- /dev/null +++ b/pythonpro/modules/tests/test_chapters_view.py @@ -0,0 +1,75 @@ +import pytest +from django.urls import reverse +from model_mommy import mommy + +from pythonpro.django_assertions import dj_assert_contains +from pythonpro.modules.models import Section, Module, Chapter + + +@pytest.fixture +def module(db): + return mommy.make(Module) + + +@pytest.fixture +def section(module): + return mommy.make(Section, module=module) + + +@pytest.fixture +def chapter(section): + return mommy.make(Chapter, section=section) + + +@pytest.fixture +def chapters(section): + return mommy.make(Chapter, 2, section=section) + + +@pytest.fixture +def resp_section(client, django_user_model, section, chapters): + user = mommy.make(django_user_model) + client.force_login(user) + return client.get(reverse('sections:detail', kwargs={'slug': section.slug})) + + +def test_chapter_title_on_section(resp_section, chapters): + for chapter in chapters: + dj_assert_contains(resp_section, chapter.title) + + +def test_chapter_url_on_section(resp_section, chapters): + for chapter in chapters: + dj_assert_contains(resp_section, chapter.get_absolute_url()) + + +@pytest.fixture +def resp(client, chapter, django_user_model): + user = mommy.make(django_user_model) + client.force_login(user) + return client.get(reverse('chapters:detail', kwargs={'slug': chapter.slug})) + + +def test_status_code(resp): + assert resp.status_code == 200 + + +def test_breadcrumb_module(resp, module): + dj_assert_contains( + resp, + f'' + ) + + +def test_breadcrumb_section(resp, section): + dj_assert_contains( + resp, + f'' + ) +# +# +# def test_breadcrumb_current(resp, section): +# dj_assert_contains( +# resp, +# f'' +# ) diff --git a/pythonpro/modules/tests/test_module_detail_view.py b/pythonpro/modules/tests/test_module_detail_view.py index c9117504..7caa151c 100644 --- a/pythonpro/modules/tests/test_module_detail_view.py +++ b/pythonpro/modules/tests/test_module_detail_view.py @@ -5,7 +5,7 @@ from pythonpro.django_assertions import dj_assert_contains from pythonpro.modules import facade -from pythonpro.modules.models import Section, Module +from pythonpro.modules.models import Section, Module, Chapter def generate_resp(slug, client): @@ -79,10 +79,38 @@ def python_birds(modules): @pytest.fixture -def resp_with_user(client_with_user, sections, python_birds): +def resp_with_sections(client_with_user, sections, python_birds): return client_with_user.get(reverse('modules:detail', kwargs={'slug': python_birds.slug})) -def test_sections_urls(resp_with_user, sections): +def test_section_titles(resp_with_sections, sections): for section in sections: - dj_assert_contains(resp_with_user, section.get_absolute_url()) + dj_assert_contains(resp_with_sections, section.title) + + +def test_section_urls(resp_with_sections, sections): + for section in sections: + dj_assert_contains(resp_with_sections, section.get_absolute_url()) + + +@pytest.fixture +def chapters(sections): + result = [] + for section in sections: + result.extend(mommy.make(Chapter, 2, section=section)) + return result + + +@pytest.fixture +def resp_with_chapters(client_with_user, python_birds, sections, chapters): + return resp_with_sections(client_with_user, sections, python_birds) + + +def test_chapter_titles(resp_with_chapters, chapters): + for chapter in chapters: + dj_assert_contains(resp_with_chapters, chapter.title) + + +def test_chapter_urls(resp_with_chapters, chapters): + for chapter in chapters: + dj_assert_contains(resp_with_chapters, chapter.get_absolute_url()) diff --git a/pythonpro/tests/test_urls.py b/pythonpro/tests/test_urls.py index b2d39bc1..253b1c17 100644 --- a/pythonpro/tests/test_urls.py +++ b/pythonpro/tests/test_urls.py @@ -2,4 +2,4 @@ def test_urls_len(): - assert 12 == len(urlpatterns) + assert 13 == len(urlpatterns) diff --git a/pythonpro/urls.py b/pythonpro/urls.py index e28243d4..a87449dd 100644 --- a/pythonpro/urls.py +++ b/pythonpro/urls.py @@ -30,6 +30,7 @@ path('discourse/', include('pythonpro.discourse.urls')), path('modulos/', include('pythonpro.modules.module_urls')), path('secoes/', include('pythonpro.modules.sections_urls')), + path('capitulos/', include('pythonpro.modules.chapters_urls')), path('', include('pythonpro.core.urls')), ]