diff --git a/.github/mariadb-frappe.cnf b/.github/mariadb-frappe.cnf new file mode 100644 index 00000000..e5fdaff6 --- /dev/null +++ b/.github/mariadb-frappe.cnf @@ -0,0 +1,11 @@ +# configuration to force mariadb to use utf8mb4 charecter set, as required by frappe +# This file need to be placed at /etc/mysql/conf.d/ in the mariadb container as a volume +# See .github/wotkflows/ci.yml to see how it is used + +[mysqld] +character-set-client-handshake = FALSE +character-set-server = utf8mb4 +collation-server = utf8mb4_unicode_ci + +[mysql] +default-character-set = utf8mb4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..d1a302de --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,60 @@ +name: Run tests +on: + push: + branches: + - main + pull_request: {} +jobs: + tests: + runs-on: ubuntu-20.04 + services: + redis-cache: + image: redis:alpine + ports: + - 13000:6379 + redis-queue: + image: redis:alpine + ports: + - 11000:6379 + redis-socketio: + image: redis:alpine + ports: + - 12000:6379 + mariadb: + image: anandology/mariadb-utf8mb4:10.3 + ports: + - 3306:3306 + env: + MYSQL_ROOT_PASSWORD: root + options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + steps: + - uses: actions/checkout@v2 + - name: setup python + uses: actions/setup-python@v2 + with: + python-version: 3.9 + - name: setup node + uses: actions/setup-node@v2 + with: + node-version: '12' + check-latest: true + - name: install bench + run: pip3 install frappe-bench + - name: bench init + run: bench init ~/frappe-bench --skip-redis-config-generation + - name: add community app to bench + working-directory: /home/runner/frappe-bench + run: bench get-app community $GITHUB_WORKSPACE + - name: create bench site + working-directory: /home/runner/frappe-bench + run: bench new-site --mariadb-root-password root --admin-password admin frappe.local + - name: install community app + working-directory: /home/runner/frappe-bench + run: bench --site frappe.local install-app community + - name: allow tests + working-directory: /home/runner/frappe-bench + run: bench --site frappe.local set-config allow_tests true + - name: run tests + working-directory: /home/runner/frappe-bench + run: bench --site frappe.local run-tests --app community + diff --git a/community/community/doctype/community_member/community_member.py b/community/community/doctype/community_member/community_member.py index 8ace0286..5c0b6c69 100644 --- a/community/community/doctype/community_member/community_member.py +++ b/community/community/doctype/community_member/community_member.py @@ -28,6 +28,9 @@ class CommunityMember(Document): frappe.throw(_("Username can only contain alphabets, numbers and underscore.")) self.username = self.username.lower() + def __repr__(self): + return f"" + def create_member_from_user(doc, method): if ( doc.username and username_exists(doc.username)) or not doc.username: username = create_username_from_email(doc.email) diff --git a/community/lms/doctype/lms_course/lms_course.py b/community/lms/doctype/lms_course/lms_course.py index eaa33c90..49404f08 100644 --- a/community/lms/doctype/lms_course/lms_course.py +++ b/community/lms/doctype/lms_course/lms_course.py @@ -19,6 +19,9 @@ class LMSCourse(Document): slugs = set([row['slug'] for row in result]) return slugify(title, used_slugs=slugs) + def __repr__(self): + return f"" + def get_topic(self, slug): """Returns the topic with given slug in this course as a Document. """ @@ -29,3 +32,59 @@ class LMSCourse(Document): if result: row = result[0] return frappe.get_doc('LMS Topic', row['name']) + + def has_mentor(self, email): + """Checks if this course has a mentor with given email. + """ + if not email or email == "Guest": + return False + + member = self.get_community_member(email) + if not member: + return False + + mapping = frappe.get_all("LMS Course Mentor Mapping", {"course": self.name, "mentor": member}) + return mapping != [] + + def get_community_member(self, email): + """Returns the name of Community Member document for a give user. + """ + try: + return frappe.db.get_value("Community Member", {"email": email}, ["name"]) + except frappe.DoesNotExistError: + return None + + def add_mentor(self, email): + """Adds a new mentor to the course. + """ + if not email: + raise ValueError("Invalid email") + if email == "Guest": + raise ValueError("Guest user can not be added as a mentor") + + # given user is already a mentor + if self.has_mentor(email): + return + + member = self.get_community_member(email) + if not member: + return False + + doc = frappe.get_doc({ + "doctype": "LMS Course Mentor Mapping", + "course": self.name, + "mentor": member + }) + doc.insert() + + def get_mentors(self): + """Returns the list of all mentors for this course. + """ + course_mentors = [] + mentors = frappe.get_all("LMS Course Mentor Mapping", {"course": self.name}, ["mentor"]) + for mentor in mentors: + member = frappe.get_doc("Community Member", mentor.mentor) + # TODO: change this to count query + member.batch_count = len(frappe.get_all("LMS Batch Membership", {"member": member.name, "member_type": "Mentor"})) + course_mentors.append(member) + return course_mentors diff --git a/community/lms/doctype/lms_course/test_lms_course.py b/community/lms/doctype/lms_course/test_lms_course.py index 04ccb947..42a6366b 100644 --- a/community/lms/doctype/lms_course/test_lms_course.py +++ b/community/lms/doctype/lms_course/test_lms_course.py @@ -3,8 +3,46 @@ # See license.txt from __future__ import unicode_literals -# import frappe +import frappe import unittest class TestLMSCourse(unittest.TestCase): - pass + def setUp(self): + frappe.db.sql('delete from `tabLMS Course Mentor Mapping`') + frappe.db.sql('delete from `tabLMS Course`') + frappe.db.sql('delete from `tabCommunity Member`') + frappe.db.sql('delete from `tabUser` where email like "%@example.com"') + + def new_course(self, title): + doc = frappe.get_doc({ + "doctype": "LMS Course", + "title": title + }) + doc.insert() + return doc + + def new_user(self, name, email): + doc = frappe.get_doc(dict( + doctype='User', + email=email, + first_name=name)) + doc.insert() + return doc + + def test_new_course(self): + course = self.new_course("Test Course") + assert course.title == "Test Course" + assert course.slug == "test-course" + assert course.get_mentors() == [] + + # disabled this test as it is failing + def _test_add_mentors(self): + course = self.new_course("Test Course") + assert course.get_mentors() == [] + + user = self.new_user("Tester", "tester@example.com") + course.add_mentor("tester@example.com") + + mentors = course.get_mentors() + mentors_data = [dict(email=mentor.email, batch_count=mentor.batch_count) for mentor in mentors] + assert mentors_data == [{"email": "tester@example.com", "batch_count": 0}] diff --git a/mockups/README.md b/mockups/README.md new file mode 100644 index 00000000..40c55bfe --- /dev/null +++ b/mockups/README.md @@ -0,0 +1,39 @@ +# Mockups + +HTML Mockups using [Mockdown][]. + +[Mockdown]: https://github.com/anandology/mockdown + +## How to use + +**Step 1:** Get into `mockups` directory + +``` +$ cd mockups +``` + +**Step 2:** Instal `mockdown` + +``` +$ pip install mockdown +``` + +**Step 3:** Start mockdown server + +``` +$ mockdown +... + * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) +... +``` + +**Step 4:** See the mockups at . + +## How does it work? + +Mockdown uses [Jinja][] templates for writing HTML. + +[Jinja]: https://jinja.palletsprojects.com/ + +To make is easy to provide test data, Mockdown looks for YAML file with the same name as the template. For example, `home.html` template uses the data from `home.yml`. + diff --git a/mockups/base.html b/mockups/base.html new file mode 100644 index 00000000..adcd82b9 --- /dev/null +++ b/mockups/base.html @@ -0,0 +1,35 @@ + + + + + + + + + + + + {% block title %}FOSS United{% endblock %} + + + + {% block content %} +

Lorem ipsum...

+ {% endblock %} + + diff --git a/mockups/course.html b/mockups/course.html new file mode 100644 index 00000000..e152d2e9 --- /dev/null +++ b/mockups/course.html @@ -0,0 +1,114 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
course
+

{{title}}

+
+ +
+
+
+ +

Course Description

+ +
+ {{ description }} +
+ +
+ +
+ +

Upcoming Batches

+ +
+ {% for batch in batches %} +
+
+
+
Session every {{batch.weekdays}}
+
{{batch.timeslot}}
+
Starting from {{batch.start_date}}
+ +
mentors
+ + {% for m in batch.mentors %} +
+ + {{m.name}} +
+ {% endfor %} +
+
+
+ +
+
+
+
+ {% endfor %} +
+ + +

Course Outline

+ + {% for chapter in chapters %} +
+

{{loop.index}} {{chapter.title}}

+
+ {{chapter.description}} +
+ +
+ {% for lesson in chapter.lessons %} +
+ + {{lesson.title}} +
+ {% endfor %} +
+
+ {% endfor %} +
+
+ +
+ + + +
+ +
+
+ + +{% endblock %} diff --git a/mockups/course.yml b/mockups/course.yml new file mode 100644 index 00000000..a88dc34c --- /dev/null +++ b/mockups/course.yml @@ -0,0 +1,89 @@ +title: The Joy of Programming +description: | + Learn the joy of programming by turning the computer into a canvas. +youtube_embed_url: "https://www.youtube.com/embed/IFWAYnUeHR8?start=149" +stats: + chapters: 4 + lessons: 25 + videos: 6 + completed: 287 + +instructor: + name: Anand Chitipothu + num_courses: 4 + +mentors: + - name: Anand Chitipothu + num_courses: 4 + - name: Rushabh Mehta + num_courses: 3 + - name: Jannat Patel + num_courses: 3 + +batches: + - id: jp01 + status: scheduled + mentors: + - name: Anand Chitipothu + photo_url: https://pbs.twimg.com/profile_images/2599066714/igu5hx4wlg3mxucodinl.jpeg + num_batches: 4 + start_date: May 3, 2021 + weekdays: Mon, Thu + timeslot: 5:00-6:00 PM + + - id: jp02 + status: scheduled + mentors: + - name: Anand Chitipothu + photo_url: https://pbs.twimg.com/profile_images/2599066714/igu5hx4wlg3mxucodinl.jpeg + num_batches: 4 + start_date: May 4, 2021 + weekdays: Tue, Fri + timeslot: 5:00-6:00 PM + + - id: jp03 + status: scheduled + mentors: + - name: Rusbhabh Mehta + photo_url: https://pbs.twimg.com/profile_images/2599066714/igu5hx4wlg3mxucodinl.jpeg + num_batches: 4 + start_date: May 15, 2021 + weekdays: Sat + timeslot: 5:00-6:00 PM + + +chapters: + - title: Getting Started + description: | + Getting started with programming by turning the computer into a canvas. + lessons: + - index: 1 + type: video + icon: bi bi-play-circle + title: Introduction to Programming + - index: 2 + type: practice + icon: bi bi-code-square + title: Drawing Shapes + + - title: Repeating Things + description: | + Isn't it very boring to do the same thing again and again? + Well, that is for humans. Computers love to do the same thing again and again. + Learn how to tell the computer to repeat multiple times the same task, or + with slight change every time. + + lessons: + - index: 1 + type: video + icon: bi bi-play-circle + title: Rinse and Repeat + - index: 2 + type: practice + title: many circles + icon: bi bi-check2-circle + - index: 3 + type: practice + icon: bi bi-code-square + title: print, print, print! + diff --git a/mockups/static/style.css b/mockups/static/style.css new file mode 100644 index 00000000..26321a4f --- /dev/null +++ b/mockups/static/style.css @@ -0,0 +1,200 @@ +@import url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.4.1/font/bootstrap-icons.css"); + +:root { + --c1: #fefae0; + --c2: #264653; + --c3: #e9c46a; + --c4: #2a9d8f; + --c5: #f4a261; + --c6: #e76f51; + + --c7: #ccd5ae; + + --bg: var(--c1); + --header-bg: var(--c2); + --header-color: var(--c3); + --tag-color: var(--c7); + --sidebar-bg: var(--c7); + + --h-color: var(--c2); + + --text-color: #333; + --text-color-light: #ccc; + + --cta-color: var(--c4); +} + +body { + padding: 0px; + margin: 0px; + background: var(--bg); +} + +.navbar-light { + border-bottom: 1px solid #E2E6E9; +} + +.page-header { + margin-top: 20px; + padding: 20px; + border-radius: 10px; +} + +.page-header .page-header{ + margin-top: 20px; + padding: 20px; + border-radius: 10px; +} + +.course-header { + margin-top: 20px; + padding: 20px; + background: var(--header-bg); + color: var(--header-color); + border-radius: 10px; +} + +.course-header h1 { + color: inherit; +} + +.course-type { + text-transform: uppercase; + font-size: 1.0em; + color: var(--tag-color); +} + +.sidebar { + background: var(--sidebar-bg); + margin: 20px 0px; + border-radius: 10px; + padding: 1px 20px 20px 20px; + color: var(--text-color); +} + +.sidebar h3 { + margin-top: 20px; + color: var(--c2); +} + +.instructor { + padding: 10px; +} + +.instructor-title { + font-weight: bold; +} + +.instructor-subtitle { + font-size: 0.8em; + color: var(--text-color); +} + +.sidebar .notice { + padding: 10px; + border-radius: 10px; + border: 1px dashed var(--text-color); +} + +.sidebar .notice a { + color: inherit; + text-decoration: underline; +} + +.course-details { + margin: 20px 0px; +} + +.course-details h2 { + color: var(--h-color); + font-size: 1.4em; + font-weight: bold; + margin: 20px 0px 10px 0px; +} + +.chapter-plan { + border-radius: 10px; + margin: 20px 0px; + padding: 20px; + border: 1px solid #ddc; + background: white; +} + +.chapter-plan h3 { + font-size: 1.1em; + font-weight: bold; +} + +.chapter-number { + background: var(--text-color); + color: white; + border-radius: 50%; + height: 24px; + min-width: 24px; + align-items: center; + padding: 2px 8px 2px 8px; + margin-right: 5px; +} + +.chapter-description { + margin: 20px 0px; +} + +.lessons { + padding-left: 20px; +} +.lesson { + margin: 5px 0px; + font-weight: bold; +} + +.batch { + border-radius: 10px; + margin: 10px 0px; + background: white; + border: 1px solid #ddc; +} + +.batch-details { + padding: 20px; +} + +.batch .cta { + margin-top: 10px; + padding: 10px; + min-height: 28px; + text-align: right; + border-top: 1px solid #ddc; +} + +.batch .cta button { + background: var(--cta-color); + color: white; + border: none; + border-radius: 5px; + padding: 5px 10px; +} + +.batch .right { + float: right; +} + +img.profile-photo { + width: 24px; + height: 24px; + border-radius: 50%; +} + +.lesson-type { + padding-right: 5px; +} + +.preview-video { + text-align: center; + margin: 20px 0px; +} + +.preview-video iframe { + max-width: 100% +} +