Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 0.77.0 #1955

Merged
merged 5 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,4 @@ celerybeat-schedule
docker-compose.*.yml

.python-version
/locustfile.py
8 changes: 8 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
Release Notes
=============

Version 0.77.0
--------------

- Filter out not live programs (#1954)
- Product Page: about section Show More toggle updates (#1949)
- Course Page: More enrollment dates updates (#1951)
- wsgi tuning setup config (#1947)

Version 0.76.1 (Released October 11, 2023)
--------------

Expand Down
2 changes: 1 addition & 1 deletion cms/templatetags/expand.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def expand(text):
pre = str(expand_here[0])
post = "".join([str(sib) for sib in expand_here[1].find_next_siblings()])

output = f'<!-- pre -->{pre}<!-- /pre --><p class="expand_here_container"><a href="#" class="expand_here_link" data-expand-body="{container_uuid}">Show More</a></p><div class="expand_here_body" id="exp{container_uuid}">{str(expand_here[1])}{post}</div>'
output = f'<!-- pre -->{pre}<!-- /pre --><div class="expand_here_body" id="exp{container_uuid}">{str(expand_here[1])}{post}</div><p class="expand_here_container"><a href="#" class="expand_here_link fade" data-expand-body="{container_uuid}">Show More</a></p>'
elif len(text.split("\n\n")) > 1:
(pre, post) = text.split("\n\n", maxsplit=1)

Expand Down
2 changes: 2 additions & 0 deletions courses/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,8 @@ def _enroll_learner_into_associated_programs():
then the change_status of that program_enrollment is checked to ensure it equals None.
"""
for program in run.course.programs:
if not program.live:
continue
program_enrollment, _ = ProgramEnrollment.objects.get_or_create(
user=user,
program=program,
Expand Down
68 changes: 68 additions & 0 deletions docs/source/configuration/uwsgi_tuning.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
=======================================
Setting up uWsgi tuning for MITx Online
=======================================

This setup satisfies the testing to help with tuning as mentioned in this `Discusssion Post <https://github.com/mitodl/hq/discussions/393>`_

Largely borrowed from work on OCW studio:

| `Adding uWSGI stats <https://github.com/mitodl/ocw-studio/pull/1898/>`_
| `Tuning the App <https://github.com/mitodl/ocw-studio/pull/1886/>`_


******************
To set up locally:
******************

Set up uwsgitop
---------------
1. Install uwsgitop: ``docker compose run --rm web poetry add uwsgitop``
2. Set UWSGI_RELOAD_ON_RSS in your .env to a high value (e.g. 500)
3. Set UWSGI_MAX_REQUESTS in your .env to a high value (e.g. 10000)
4. ``docker compose build``
5. ``docker compose up``
6. In a new terminal window/tab, ``docker compose exec web uwsgitop /tmp/uwsgi-stats.sock``
7. You should see your application's memory usage without usage. Ready to go.


Set up Locust
-------------
1. Install Locust: ``docker compose run --rm web poetry add locust``
2. Add locust to your docker-compose.yml locally, under services:

.. code-block:: shell

locust:
image: locustio/locust
ports:
- "8089:8089"
volumes:
- ./:/src
command: >
-f /src/locustfile.py

3. Add the following to the web block, at the level of, and directly after, ``build``:

.. code-block:: shell

deploy:
resources:
limits:
cpus: "2"
memory: "1g"

4. Add locustfile.py. There is an example file at ``locustfile.py.example`` in the root of the repo. ``cp locustfile.py.example locustfile.py`` will copy it over as is. Change variables and/or add tests as needed.

Put it all together
-------------------

1. Run ``docker-compose build``
2. Run ``docker-compose up``
3. You can use locust from ``http://0.0.0.0:8089/`` in a browser
4. You can use uwsgitop in a terminal with ``docker compose exec web uwsgitop /tmp/uwsgi-stats.sock``

******************
To test:
******************

Coming soon!
16 changes: 16 additions & 0 deletions frontend/public/scss/product-page/product-details.scss
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ body.new-design {
div.expand_here_body.open {
max-height: 400rem;
}

a.expand_here_link {
opacity: 0;
transition: opacity 225ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

a.expand_here_link.fade {
opacity: 1;
transition: opacity 300ms cubic-bezier(0.55, 0.085, 0.68, 0.53);
}
}

section.about-this-class {
Expand Down Expand Up @@ -250,6 +260,12 @@ body.new-design {
padding-left: 0;
background-color: transparent;
}
button.more-dates-link.enrolled {
color: $green-darker;
cursor: default;
text-decoration-line: none;

}
}
}

Expand Down
27 changes: 22 additions & 5 deletions frontend/public/src/components/CourseInfoBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ const getStartDateText = (run: BaseCourseRun, isArchived: boolean = false) => {
: "Start Anytime"
}

const getStartDateForRun = (run: BaseCourseRun) => {
return run && !emptyOrNil(run.start_date) && !run.is_self_paced
? formatPrettyDate(moment(new Date(run.start_date)))
: "Start Anytime"
}

export default class CourseInfoBox extends React.PureComponent<CourseInfoBoxProps> {
state = {
showMoreEnrollDates: false
Expand Down Expand Up @@ -60,7 +66,7 @@ export default class CourseInfoBox extends React.PureComponent<CourseInfoBoxProp
className="more-dates-link"
onClick={() => this.setRunEnrollDialog(run)}
>
{getStartDateText(run)}
{getStartDateForRun(run)}
</button>
) : (
<form action="/enrollments/" method="post">
Expand All @@ -71,7 +77,7 @@ export default class CourseInfoBox extends React.PureComponent<CourseInfoBoxProp
/>
<input type="hidden" name="run" value={run ? run.id : ""} />
<button type="submit" className="more-dates-link">
{getStartDateText(run)}
{getStartDateForRun(run)}
</button>
</form>
)}
Expand All @@ -87,11 +93,18 @@ export default class CourseInfoBox extends React.PureComponent<CourseInfoBoxProp
return !currentUser || !currentUser.id ? (
<>
<a href={routes.login} className="more-dates-link">
{getStartDateText(run)}
{getStartDateForRun(run)}
</a>
</>
) : null
}
renderEnrolledDateLink(run: EnrollmentFlaggedCourseRun) {
return (
<button className="more-dates-link enrolled">
{getStartDateText(run)} - Enrolled
</button>
)
}

render() {
const { courses, courseRuns } = this.props
Expand All @@ -115,9 +128,13 @@ export default class CourseInfoBox extends React.PureComponent<CourseInfoBoxProp
const moreEnrollableCourseRuns = courseRuns && courseRuns.length > 1
if (moreEnrollableCourseRuns) {
courseRuns.forEach((courseRun, index) => {
if (!courseRun.is_enrolled) {
if (courseRun.id !== run.id) {
startDates.push(
<li key={index}>{this.renderEnrollNowDateLink(courseRun)}</li>
<li key={index}>
{courseRun.is_enrolled
? this.renderEnrolledDateLink(courseRun)
: this.renderEnrollNowDateLink(courseRun)}
</li>
)
}
})
Expand Down
35 changes: 19 additions & 16 deletions frontend/public/src/components/CourseProductDetailEnroll.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,14 @@ export class CourseProductDetailEnroll extends React.Component<
currentCourseRun: courseRun
})
}
getFirstUnexpiredRun = () => {
const { courses, courseRuns } = this.props
return courseRuns
? courses && courses[0].next_run_id
? courseRuns.find(elem => elem.id === courses[0].next_run_id)
: courseRuns[0]
: null
}

getCurrentCourseRun = (): EnrollmentFlaggedCourseRun => {
return this.state.currentCourseRun
Expand Down Expand Up @@ -402,7 +410,11 @@ export class CourseProductDetailEnroll extends React.Component<
)
}

renderEnrolledButton(run: EnrollmentFlaggedCourseRun, startDate: any) {
renderEnrolledButton(run: EnrollmentFlaggedCourseRun) {
const startDate =
run && !emptyOrNil(run.start_date)
? moment(new Date(run.start_date))
: null
const waitingForCourseToBeginMessage = moment().isBefore(startDate) ? (
<p style={{ fontSize: "16px" }}>
Enrolled and waiting for the course to begin.
Expand Down Expand Up @@ -499,17 +511,12 @@ export class CourseProductDetailEnroll extends React.Component<
} = this.props
const showNewDesign = checkFeatureFlag("mitxonline-new-product-page")

let run =
!this.getCurrentCourseRun() && !courseRuns
? null
: !this.getCurrentCourseRun() && courseRuns
? this.getFirstUnenrolledCourseRun()
: this.getCurrentCourseRun()

if (run) this.updateDate(run)

let product = run && run.products ? run.products[0] : null
let run,
product = null
if (courseRuns) {
run = this.getFirstUnexpiredRun()
product = run && run.products ? run.products[0] : null
this.updateDate(run)
const thisScope = this
courseRuns.map(courseRun => {
// $FlowFixMe
Expand All @@ -524,18 +531,14 @@ export class CourseProductDetailEnroll extends React.Component<
})
})
}
const startDate =
run && !emptyOrNil(run.start_date)
? moment(new Date(run.start_date))
: null

return (
<>
{
// $FlowFixMe: isLoading null or undefined
<Loader key="product_detail_enroll_loader" isLoading={isLoading}>
<>
{this.renderEnrolledButton(run, startDate)}
{this.renderEnrolledButton(run)}
{this.renderEnrollLoginButton()}
{this.renderEnrollNowButton(run, product)}

Expand Down
13 changes: 12 additions & 1 deletion frontend/public/src/containers/ProductDetailEnrollApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,24 @@ import CourseProductDetailEnroll from "../components/CourseProductDetailEnroll"
import ProgramProductDetailEnroll from "../components/ProgramProductDetailEnroll"

const expandExpandBlock = (event: MouseEvent) => {
event.preventDefault()
const blockTarget = event.target

if (blockTarget instanceof HTMLElement) {
const block = blockTarget.getAttribute("data-expand-body")
if (block) {
const elem = document.querySelector(`div#exp${block}`)
elem && elem.classList && elem.classList.toggle("open")
if (elem && elem.classList && elem.classList.contains("open")) {
event.srcElement.innerText = "Show Less"
} else {
event.srcElement.classList.remove("fade")
setTimeout(() => {
requestAnimationFrame(() => {
event.srcElement.innerText = "Show More"
event.srcElement.classList.add("fade")
})
}, 225) // timeout
}
}
}
}
Expand Down
92 changes: 92 additions & 0 deletions locustfile.py.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import logging
from locust import HttpUser, task, between, events


def _get_product_json(response_json, page_type):
logging.info(f'User was displayed {response_json["count"]} {page_type}(s)')
pages = response_json["results"][:5]
return [page["readable_id"] for page in pages]


class DjangoAdminUser(HttpUser):
wait_time = between(1, 5)

def on_start(self):
self.client.get("/admin/login/")
self.login()

def login(self):
csrf_token = self.client.cookies['csrftoken']
self.client.post(
"/admin/login/",
{
"username": "[email protected]",
"password": "edx",
"csrfmiddlewaretoken": csrf_token,
},
headers={"Referer": "/admin/login/"},
)

@task
def navigate_through_course_areas(self):
logging.info(f"{self} has started to surf")
self._navigate_to_home_page()
programs, courses = self._navigate_to_catalog()
self._navigate_to_product_page("program", programs)
self._navigate_to_product_page("course", courses)
self._navigate_to_checkout_page()
logging.info(f"{self} has gone off to learn")

def _navigate_to_catalog(self):
endpoints = [
"/api/users/me",
"/api/departments/",
"/api/users/me",
]
logging.info(f"User {self} opening catalog")
self._navigate_to_endpoints(endpoints)
with self.client.get("/api/programs/?page=1&live=true", catch_response=True) as response:
program_json = _get_product_json(response.json(), "program")
with self.client.get("/api/courses/?page=1&live=true&page__live=true&courserun_is_enrollable=true",
catch_response=True) as response:
course_json = _get_product_json(response.json(), "course")
return program_json, course_json

def _navigate_to_home_page(self):
logging.info("User (%r) opening Home Page")
with self.client.get("/api/users/me", catch_response=True) as response:
logging.info(response.status_code)

def _navigate_to_checkout_page(self):
logging.info("User (%r) opening Checkout Page")
endpoints = [
"/api/users/me",
"/api/checkout/cart",
]
self._navigate_to_endpoints(endpoints)

def _navigate_to_product_page(self, product_type, response_json):
logging.info(response_json)
for page in response_json:
logging.info(f"User opened {product_type} page for {page}")
endpoints = [
"/api/users/me",
"/api/program_enrollments/",
"/api/users/me",
f"/api/course_runs/?relevant_to={page}",
]
if product_type == "course":
endpoints += [
f"/api/courses/?readable_id={page}&live=true",
]
elif product_type == "program":
endpoints += [
f"/api/programs/?readable_id={page}",
]
self._navigate_to_endpoints(endpoints)

def _navigate_to_endpoints(self, endpoints):
for endpoint in endpoints:
logging.info(f"endpoint: {endpoint}")
with self.client.get(endpoint, catch_response=True) as response:
logging.info(f"status: {response.status_code}")
Loading