Merge pull request #12 from fossunited/nice-urls
Implemented nice urls for courses, topics, sketches and profiles
This commit is contained in:
@@ -91,7 +91,7 @@ app_include_js = "/assets/community/js/community.js"
|
|||||||
# Hook on document methods and events
|
# Hook on document methods and events
|
||||||
|
|
||||||
doc_events = {
|
doc_events = {
|
||||||
"User": {
|
"User": {
|
||||||
"after_insert": "community.community.doctype.community_member.community_member.create_member_from_user"
|
"after_insert": "community.community.doctype.community_member.community_member.create_member_from_user"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,3 +127,31 @@ scheduler_events = {
|
|||||||
#
|
#
|
||||||
# auto_cancel_exempted_doctypes = ["Auto Repeat"]
|
# auto_cancel_exempted_doctypes = ["Auto Repeat"]
|
||||||
|
|
||||||
|
from .routing import install_regex_converter
|
||||||
|
install_regex_converter()
|
||||||
|
|
||||||
|
# Add all simple route rules here
|
||||||
|
primary_rules = [
|
||||||
|
{"from_route": "/sketches", "to_route": "sketches"},
|
||||||
|
{"from_route": "/sketches/<sketch>", "to_route": "sketches/sketch"},
|
||||||
|
{"from_route": "/courses", "to_route": "courses"},
|
||||||
|
{"from_route": "/courses/<course>", "to_route": "courses/course"},
|
||||||
|
{"from_route": "/courses/<course>/<topic>", "to_route": "courses/topic"},
|
||||||
|
{"from_route": "/courses/<course>/<topic>", "to_route": "courses/topic"}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Any frappe default URL is blocked by profile-rules, add it here to unblock it
|
||||||
|
whitelist = [
|
||||||
|
"/login",
|
||||||
|
"/update-password",
|
||||||
|
"/update-profile",
|
||||||
|
"/third-party-apps"
|
||||||
|
]
|
||||||
|
whitelist_rules = [{"from_route": p, "to_route": p[1:]} for p in whitelist]
|
||||||
|
|
||||||
|
# regex rule to match all profiles
|
||||||
|
profile_rules = [
|
||||||
|
{"from_route": "/<regex('[a-z0-9-]{5,}'):username>", "to_route": "profiles/profile"},
|
||||||
|
]
|
||||||
|
|
||||||
|
website_route_rules = primary_rules + whitelist_rules + profile_rules
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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'])
|
||||||
|
|||||||
@@ -13,6 +13,14 @@ class LMSSketch(Document):
|
|||||||
def get_owner_name(self):
|
def get_owner_name(self):
|
||||||
return get_userinfo(self.owner)['full_name']
|
return get_userinfo(self.owner)['full_name']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sketch_id(self):
|
||||||
|
"""Returns the numeric part of the name.
|
||||||
|
|
||||||
|
For example, the skech_id will be "123" for sketch with name "SKETCH-123".
|
||||||
|
"""
|
||||||
|
return self.name.replace("SKETCH-", "")
|
||||||
|
|
||||||
def get_livecode_url(self):
|
def get_livecode_url(self):
|
||||||
doc = frappe.get_cached_doc("LMS Settings")
|
doc = frappe.get_cached_doc("LMS Settings")
|
||||||
return doc.livecode_url
|
return doc.livecode_url
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
17
community/lms/test_utils.py
Normal file
17
community/lms/test_utils.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import unittest
|
||||||
|
from .utils import slugify
|
||||||
|
|
||||||
|
class TestSlugify(unittest.TestCase):
|
||||||
|
def test_simple(self):
|
||||||
|
self.assertEquals(slugify("hello-world"), "hello-world")
|
||||||
|
self.assertEquals(slugify("Hello World"), "hello-world")
|
||||||
|
self.assertEquals(slugify("Hello, World!"), "hello-world")
|
||||||
|
|
||||||
|
def test_duplicates(self):
|
||||||
|
self.assertEquals(
|
||||||
|
slugify("Hello World", ['hello-world']),
|
||||||
|
"hello-world-2")
|
||||||
|
|
||||||
|
self.assertEquals(
|
||||||
|
slugify("Hello World", ['hello-world', 'hello-world-2']),
|
||||||
|
"hello-world-3")
|
||||||
30
community/lms/utils.py
Normal file
30
community/lms/utils.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
RE_SLUG_NOTALLOWED = re.compile("[^a-z0-9]+")
|
||||||
|
|
||||||
|
def slugify(title, used_slugs=[]):
|
||||||
|
"""Converts title to a slug.
|
||||||
|
|
||||||
|
If a list of used slugs is specified, it will make sure the generated slug
|
||||||
|
is not one of them.
|
||||||
|
|
||||||
|
>>> slugify("Hello World!")
|
||||||
|
'hello-world'
|
||||||
|
>>> slugify("Hello World!", ['hello-world'])
|
||||||
|
'hello-world-2'
|
||||||
|
>>> slugify("Hello World!", ['hello-world', 'hello-world-2'])
|
||||||
|
'hello-world-3'
|
||||||
|
"""
|
||||||
|
slug = RE_SLUG_NOTALLOWED.sub('-', title.lower()).strip('-')
|
||||||
|
used_slugs = set(used_slugs)
|
||||||
|
|
||||||
|
if slug not in used_slugs:
|
||||||
|
return slug
|
||||||
|
|
||||||
|
count = 2
|
||||||
|
while True:
|
||||||
|
new_slug = f"{slug}-{count}"
|
||||||
|
if new_slug not in used_slugs:
|
||||||
|
return new_slug
|
||||||
|
count = count+1
|
||||||
|
|
||||||
25
community/routing.py
Normal file
25
community/routing.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"""Utilities for making custom routing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from werkzeug.routing import BaseConverter, Map
|
||||||
|
from werkzeug.datastructures import ImmutableDict
|
||||||
|
|
||||||
|
class RegexConverter(BaseConverter):
|
||||||
|
"""werkzeug converter that supports custom regular expression.
|
||||||
|
|
||||||
|
The `install_regex_converter` function must be called before using
|
||||||
|
regex converter in rules.
|
||||||
|
"""
|
||||||
|
def __init__(self, map, regex):
|
||||||
|
super().__init__(map)
|
||||||
|
self.regex = regex
|
||||||
|
|
||||||
|
def install_regex_converter():
|
||||||
|
"""Installs the RegexConvetor to the default converters supported by werkzeug.
|
||||||
|
|
||||||
|
This allows specifing rules using regex. For example:
|
||||||
|
|
||||||
|
/profiles/<regex("[a-z0-9]{5,}"):username>
|
||||||
|
"""
|
||||||
|
default_converters = dict(Map.default_converters, regex=RegexConverter)
|
||||||
|
Map.default_converters = ImmutableDict(default_converters)
|
||||||
@@ -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/topic?course={{course.name}}&topic={{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 %}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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?course={{course.name}}">{{course.title}}</a></h5>
|
|
||||||
<p class="card-text">{{ frappe.utils.md_to_html(course.description[:250]) }}</p>
|
|
||||||
<a href="/courses/course?course={{course.name}}" class="card-link">See more →</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/topic?course={{course.name}}&topic={{topic.name}}">{{topic.title}}</a></h5>
|
|
||||||
<p class="card-text">{{topic.description}}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
@@ -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?course={{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>
|
||||||
|
|
||||||
|
|||||||
@@ -2,36 +2,32 @@ import frappe
|
|||||||
|
|
||||||
def get_context(context):
|
def get_context(context):
|
||||||
context.no_cache = 1
|
context.no_cache = 1
|
||||||
course_name = get_queryparam("course", '/courses')
|
|
||||||
context.course = get_course(course_name)
|
|
||||||
|
|
||||||
topic_name = get_queryparam("topic", '/courses?course=' + course_name)
|
try:
|
||||||
context.topic = get_topic(course_name, topic_name)
|
course_slug = frappe.form_dict['course']
|
||||||
|
topic_slug = frappe.form_dict['topic']
|
||||||
|
except KeyError:
|
||||||
|
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()
|
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.exceptions.DoesNotExistError:
|
|
||||||
raise frappe.NotFound
|
|
||||||
return course
|
|
||||||
|
|
||||||
def get_topic(course_name, topic_name):
|
|
||||||
try:
|
|
||||||
topic = frappe.get_doc('LMS Topic', topic_name)
|
|
||||||
except frappe.exceptions.DoesNotExistError:
|
|
||||||
raise frappe.NotFound
|
|
||||||
if topic.course != course_name:
|
|
||||||
raise frappe.NotFound
|
|
||||||
return topic
|
|
||||||
|
|||||||
25
community/www/profiles/profile.html
Normal file
25
community/www/profiles/profile.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{% extends "templates/web.html" %}
|
||||||
|
{% block page_content %}
|
||||||
|
<div class="py-20 row">
|
||||||
|
{% if user.photo %}
|
||||||
|
<div class="col-sm-2 border border-dark">
|
||||||
|
<img src="{{ user.photo }}" alt="{{ user.full_name }}">
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="col-sm-2">
|
||||||
|
<div class="standard-image" style="font-size: 30px;">{{ user.abbr }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="col">
|
||||||
|
<h1>{{ user.full_name }}</h1>
|
||||||
|
{% if user.short_intro %}
|
||||||
|
<p class="lead"> {{ user.short_intro }} </p>
|
||||||
|
{% endif %}
|
||||||
|
{% if user.bio %}
|
||||||
|
<p class="markdown-style"> {{ frappe.utils.md_to_html(user.bio) }} </p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
<!-- this is a sample default web page template -->
|
||||||
18
community/www/profiles/profile.py
Normal file
18
community/www/profiles/profile.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
def get_context(context):
|
||||||
|
context.no_cache = 1
|
||||||
|
|
||||||
|
username = frappe.form_dict.get('username')
|
||||||
|
user = username and get_user(username)
|
||||||
|
if not user:
|
||||||
|
context.template = "www/404.html"
|
||||||
|
|
||||||
|
user.abbr = "".join([s[0] for s in user.full_name.split()])
|
||||||
|
context.user = user
|
||||||
|
|
||||||
|
def get_user(username):
|
||||||
|
try:
|
||||||
|
return frappe.get_doc("Community Member", username)
|
||||||
|
except frappe.DoesNotExistError:
|
||||||
|
return
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<div class='container pb-5'>
|
<div class='container pb-5'>
|
||||||
<h1>Recent Sketches</h1>
|
<h1>Recent Sketches</h1>
|
||||||
|
|
||||||
<a href="/sketches/sketch?sketch=new">Create a New Sketch</a>
|
<a href="/sketches/new">Create a New Sketch</a>
|
||||||
</div>
|
</div>
|
||||||
<div class='container'>
|
<div class='container'>
|
||||||
<div class="row row-cols-1 row-cols-xl-5 row-cols-lg-4 row-cols-md-3 row-cols-sm-2 ">
|
<div class="row row-cols-1 row-cols-xl-5 row-cols-lg-4 row-cols-md-3 row-cols-sm-2 ">
|
||||||
@@ -21,13 +21,13 @@
|
|||||||
<div class="col mb-4">
|
<div class="col mb-4">
|
||||||
<div class="card sketch-card" style="width: 200px;">
|
<div class="card sketch-card" style="width: 200px;">
|
||||||
<div class="card-img-top">
|
<div class="card-img-top">
|
||||||
<a href="/sketches/sketch?sketch={{sketch.name}}">
|
<a href="/sketches/{{sketch.sketch_id}}">
|
||||||
{{ sketch.to_svg() }}
|
{{ sketch.to_svg() }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
<div class="sketch-title">
|
<div class="sketch-title">
|
||||||
<a href="sketches/sketch?sketch={{sketch.name}}">{{sketch.title}}</a>
|
<a href="sketches/{{sketch.sketch_id}}">{{sketch.title}}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="sketch-author">
|
<div class="sketch-author">
|
||||||
by {{sketch.get_owner_name()}}
|
by {{sketch.get_owner_name()}}
|
||||||
|
|||||||
@@ -2,8 +2,19 @@ import frappe
|
|||||||
|
|
||||||
def get_context(context):
|
def get_context(context):
|
||||||
context.no_cache = 1
|
context.no_cache = 1
|
||||||
course_name = get_queryparam("sketch", '/sketches')
|
|
||||||
context.sketch = get_sketch(course_name)
|
try:
|
||||||
|
sketch_id = frappe.form_dict["sketch"]
|
||||||
|
except KeyError:
|
||||||
|
context.template = "www/404.html"
|
||||||
|
return
|
||||||
|
|
||||||
|
sketch = get_sketch(sketch_id)
|
||||||
|
if not sketch:
|
||||||
|
context.template = "www/404.html"
|
||||||
|
return
|
||||||
|
|
||||||
|
context.sketch = sketch
|
||||||
context.livecode_url = get_livecode_url()
|
context.livecode_url = get_livecode_url()
|
||||||
context.editable = is_editable(context.sketch, frappe.session.user)
|
context.editable = is_editable(context.sketch, frappe.session.user)
|
||||||
|
|
||||||
@@ -19,15 +30,8 @@ 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_sketch(sketch_id):
|
||||||
try:
|
if sketch_id == 'new':
|
||||||
return frappe.form_dict[name]
|
|
||||||
except KeyError:
|
|
||||||
frappe.local.flags.redirect_location = redirect_when_not_found
|
|
||||||
raise frappe.Redirect
|
|
||||||
|
|
||||||
def get_sketch(name):
|
|
||||||
if name == 'new':
|
|
||||||
sketch = frappe.new_doc('LMS Sketch')
|
sketch = frappe.new_doc('LMS Sketch')
|
||||||
sketch.name = "new"
|
sketch.name = "new"
|
||||||
sketch.title = "New Sketch"
|
sketch.title = "New Sketch"
|
||||||
@@ -35,7 +39,8 @@ def get_sketch(name):
|
|||||||
return sketch
|
return sketch
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
name = "SKETCH-" + sketch_id
|
||||||
return frappe.get_doc('LMS Sketch', name)
|
return frappe.get_doc('LMS Sketch', name)
|
||||||
except frappe.exceptions.DoesNotExistError:
|
except frappe.exceptions.DoesNotExistError:
|
||||||
raise frappe.NotFound
|
return
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user