refactor: added slugs to course and topics to make nice urls

- the slug is autogenerated from the title
- the slug of a topic is unique among all the topics of that course
This commit is contained in:
Anand Chitipothu
2021-04-06 15:49:17 +05:30
parent 6620ecf0c8
commit 175bd19a51
11 changed files with 121 additions and 83 deletions

View File

@@ -9,6 +9,7 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"title", "title",
"slug",
"description", "description",
"section_break_3", "section_break_3",
"is_published", "is_published",
@@ -47,12 +48,20 @@
{ {
"fieldname": "column_break_5", "fieldname": "column_break_5",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"description": "The slug of the course. Autogenerated from the title if not specified.",
"fieldname": "slug",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Slug",
"unique": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_published_field": "is_published", "is_published_field": "is_published",
"links": [], "links": [],
"modified": "2021-03-19 15:44:47.411705", "modified": "2021-04-06 15:33:08.870313",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Course", "name": "LMS Course",
@@ -71,7 +80,7 @@
"write": 1 "write": 1
} }
], ],
"search_fields": "title", "search_fields": "slug",
"sort_field": "creation", "sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"title_field": "title", "title_field": "title",

View File

@@ -3,8 +3,29 @@
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
# import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from ...utils import slugify
class LMSCourse(Document): class LMSCourse(Document):
pass def before_save(self):
if not self.slug:
self.slug = self.generate_slug(title=self.title)
def generate_slug(self, title):
result = frappe.get_all(
'LMS Course',
fields=['slug'])
slugs = set([row['slug'] for row in result])
return slugify(title, used_slugs=slugs)
def get_topic(self, slug):
"""Returns the topic with given slug in this course as a Document.
"""
result = frappe.get_all(
"LMS Topic",
filters={"course": self.name, "slug": slug})
if result:
row = result[0]
return frappe.get_doc('LMS Topic', row['name'])

View File

@@ -1,15 +1,15 @@
{ {
"actions": [], "actions": [],
"allow_guest_to_view": 1, "allow_guest_to_view": 1,
"autoname": "format:{title}",
"creation": "2021-03-02 07:20:41.686573", "creation": "2021-03-02 07:20:41.686573",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"course", "course",
"preview",
"title", "title",
"slug",
"preview",
"description", "description",
"order", "order",
"sections" "sections"
@@ -50,11 +50,17 @@
"fieldtype": "Table", "fieldtype": "Table",
"label": "Sections", "label": "Sections",
"options": "LMS Section" "options": "LMS Section"
},
{
"description": "The slug of the topic. Autogenerated from the title if not specified.",
"fieldname": "slug",
"fieldtype": "Data",
"label": "Slug"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-03-05 17:08:55.580189", "modified": "2021-04-06 14:12:48.514062",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Topic", "name": "LMS Topic",

View File

@@ -6,12 +6,28 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from .section_parser import SectionParser from .section_parser import SectionParser
from ...utils import slugify
class LMSTopic(Document): class LMSTopic(Document):
def before_save(self): def before_save(self):
sections = SectionParser().parse(self.description) course = self.get_course()
if not self.slug:
self.slug = self.generate_slug(title=self.title)
sections = SectionParser().parse(self.description or "")
self.sections = [self.make_lms_section(i, s) for i, s in enumerate(sections)] self.sections = [self.make_lms_section(i, s) for i, s in enumerate(sections)]
def get_course(self):
return frappe.get_doc("LMS Course", self.course)
def generate_slug(self, title):
result = frappe.get_all(
'LMS Topic',
filters={'course': self.course},
fields=['slug'])
slugs = set([row['slug'] for row in result])
return slugify(title, used_slugs=slugs)
def get_sections(self): def get_sections(self):
return sorted(self.sections, key=lambda s: s.index) return sorted(self.sections, key=lambda s: s.index)
@@ -22,3 +38,6 @@ class LMSTopic(Document):
s.contents = section.contents s.contents = section.contents
s.index = index s.index = index
return s return s

View File

@@ -1,6 +1,5 @@
{% extends "templates/base.html" %} {% extends "templates/base.html" %}
{% block title %}{{ 'Courses' }}{% endblock %} {% block title %}{{ 'Courses' }}{% endblock %}
{% from "www/courses/macros/card.html" import course_card, topic_card %}
{% block head_include %} {% block head_include %}
<meta name="description" content="Courses" /> <meta name="description" content="Courses" />
<meta name="keywords" content="Courses {{course.title}}" /> <meta name="keywords" content="Courses {{course.title}}" />
@@ -34,16 +33,16 @@
aria-selected="false">Discussions</a> aria-selected="false">Discussions</a>
</li> </li>
{% endif %} {% endif %}
</ul> </ul>
</div> </div>
<div class="tab-content"> <div class="tab-content">
<div class="tab-pane fade py-4 show active" role="tabpanel" id="home"> <div class="tab-pane fade py-4 show active" role="tabpanel" id="home">
<div>{{ frappe.utils.md_to_html(course.description) }}</div>
<div class='container'> <div class='container'>
<div>{{ frappe.utils.md_to_html(course.description) }}</div>
<div class="list-group"> <div class="list-group">
{% for topic in course.topics %} {% for topic in course.topics %}
<div class="list-group-item"> <div class="list-group-item">
<h5><a href="/courses/{{course.name}}/{{topic.name}}">{{topic.title}}</a></h5> <h5><a href="/courses/{{course.slug}}/{{topic.slug}}">{{topic.title}}</a></h5>
<div>{{topic.preview | markdown }}</div> <div>{{topic.preview | markdown }}</div>
</div> </div>
{% endfor %} {% endfor %}

View File

@@ -16,14 +16,15 @@ def get_context(context):
context.current_batch = context.memberships[0].batch context.current_batch = context.memberships[0].batch
context.author = context.memberships[0].member context.author = context.memberships[0].member
def get_course(name): def get_course(slug):
course = frappe.db.get_value('LMS Course', name, course = frappe.db.get_value('LMS Course', {"slug": slug},
['name', 'title', 'description'], as_dict=1) ['name', 'slug', 'title', 'description'], as_dict=1)
course['topics'] = frappe.db.get_all('LMS Topic', course['topics'] = frappe.db.get_all('LMS Topic',
filters={ filters={
'course': name 'course': course['name']
}, },
fields=['name', 'title', 'preview'], fields=['name', 'slug', 'title', 'preview'],
order_by='creation' order_by='creation'
) )
return course return course

View File

@@ -1,24 +1,34 @@
{% extends "templates/base.html" %} {% extends "templates/base.html" %}
{% block title %}{{ 'Courses' }}{% endblock %} {% block title %}{{ 'Courses' }}{% endblock %}
{% from "www/courses/macros/card.html" import course_card %}
{% block head_include %} {% block head_include %}
<meta name="description" content="{{ 'Courses' }}" /> <meta name="description" content="{{ 'Courses' }}" />
<meta name="keywords" content="Courses" /> <meta name="keywords" content="Courses" />
<style> <style>
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<section class="top-section" style="padding: 1rem 0rem;"> <section class="top-section" style="padding: 1rem 0rem;">
<div class='container pb-5'> <div class='container pb-5'>
<h1>{{ 'Courses' }}</h1> <h1>{{ 'Courses' }}</h1>
</div> </div>
<div class='container'> <div class='container'>
<div class="row mt-5"> <div class="row mt-5">
{% for course in courses %} {% for course in courses %}
{{ course_card(course) }} {{ course_card(course) }}
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</section> </section>
{% endblock %} {% endblock %}
{% macro course_card(course) %}
<div class="card mb-5 w-100">
<div class="card-body">
<h5 class="card-title"><a href="/courses/{{course.slug}}">{{course.title}}</a></h5>
<p class="card-text">{{ frappe.utils.md_to_html(course.description[:250]) }}</p>
<a href="/courses/{{course.slug}}" class="card-link">See more &rarr;</a>
</div>
</div>
{% endmacro %}

View File

@@ -7,6 +7,6 @@ def get_context(context):
def get_courses(): def get_courses():
courses = frappe.get_all( courses = frappe.get_all(
"LMS Course", "LMS Course",
fields=['name', 'title', 'description'] fields=['name', 'slug', 'title', 'description']
) )
return courses return courses

View File

@@ -1,19 +0,0 @@
{% macro course_card(course) %}
<div class="card mb-5 w-100">
<div class="card-body">
<h5 class="card-title"><a href="/courses/{{course.name}}">{{course.title}}</a></h5>
<p class="card-text">{{ frappe.utils.md_to_html(course.description[:250]) }}</p>
<a href="/courses/{{course.name}}" class="card-link">See more &rarr;</a>
</div>
</div>
{% endmacro %}
{% macro topic_card(course, topic) %}
<div class="card mb-5 w-100">
<div class="card-body">
<h5 class="card-title"><a href="/courses/{{course.name}}/{{topic.name}}">{{topic.title}}</a></h5>
<p class="card-text">{{topic.description}}</p>
</div>
</div>
{% endmacro %}

View File

@@ -24,7 +24,7 @@
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item" aria-current="page"><a href="/courses">Courses</a></li> <li class="breadcrumb-item" aria-current="page"><a href="/courses">Courses</a></li>
<li class="breadcrumb-item" aria-current="page"><a href="/courses/{{course.name}}">{{course.title}}</a></li> <li class="breadcrumb-item" aria-current="page"><a href="/courses/{{course.slug}}">{{course.title}}</a></li>
</ol> </ol>
</nav> </nav>

View File

@@ -4,38 +4,30 @@ def get_context(context):
context.no_cache = 1 context.no_cache = 1
try: try:
course_name = get_queryparam("course", '/courses') course_slug = frappe.form_dict['course']
context.course = get_course(course_name) topic_slug = frappe.form_dict['topic']
except KeyError:
topic_name = get_queryparam("topic", '/courses/' + course_name)
context.topic = get_topic(course_name, topic_name)
context.livecode_url = get_livecode_url()
except frappe.DoesNotExistError:
context.template = 'www/404.html' context.template = 'www/404.html'
return
course = get_course(course_slug)
topic = course and course.get_topic(topic_slug)
if not topic:
context.template = 'www/404.html'
return
context.course = course
context.topic = topic
context.livecode_url = get_livecode_url()
def notfound(context):
context.template = 'www/404.html'
def get_livecode_url(): def get_livecode_url():
doc = frappe.get_doc("LMS Settings") doc = frappe.get_doc("LMS Settings")
return doc.livecode_url return doc.livecode_url
def get_queryparam(name, redirect_when_not_found): def get_course(slug):
try: course = frappe.db.get_value('LMS Course', {"slug": slug}, ["name"], as_dict=1)
return frappe.form_dict[name] return course and frappe.get_doc('LMS Course', course['name'])
except KeyError:
frappe.local.flags.redirect_location = redirect_when_not_found
raise frappe.Redirect
def get_course(name):
try:
course = frappe.get_doc('LMS Course', name)
except frappe.DoesNotExistError:
raise
return course
def get_topic(course_name, topic_name):
try:
topic = frappe.get_doc('LMS Topic', topic_name)
except frappe.DoesNotExistError:
raise
if topic.course != course_name:
raise frappe.DoesNotExistError()
return topic