Skip to content

Commit

Permalink
Add course view for searching and adding team courses.
Browse files Browse the repository at this point in the history
  • Loading branch information
dirkcuys committed Jan 23, 2024
1 parent bd07119 commit d071f49
Show file tree
Hide file tree
Showing 8 changed files with 323 additions and 1 deletion.
123 changes: 123 additions & 0 deletions frontend/components/dashboard/CourseList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import React, {useState, useEffect} from 'react';
import ApiHelper from "../../helpers/ApiHelper";
import moment from "moment"; //TODO


const CourseTable = ({courses, totalPages}) => <>
<div className="table-responsive d-none d-md-block" data-aos='fade'>
<table className="table">
<thead>
<tr>
<td>Title</td>
<td>Provider</td>
<td>Date Added</td>
<td></td>
</tr>
</thead>
<tbody>
{
courses.map(course => {
const date = moment(course.created_at).format('MMM D, YYYY')
const classes = course.unlisted ? 'bg-cream-dark' : '';

return(
<tr key={course.id} className={`${classes}`}>
<td><a href={course.course_page_path}>{`${course.unlisted ? "[UNLISTED] " : ""}${course.title}`}</a></td>
<td>{course.provider}</td>
<td>{date}</td>
<td><a href={course.course_edit_path} className="p2pu-btn btn btn-sm dark">edit</a></td>
</tr>
)
})
}
</tbody>
</table>
</div>

<div className="d-md-none">
{
courses.map((course, index) => {
const date = moment(course.created_at).format('MMM D, YYYY')
const classes = course.unlisted ? 'bg-cream-dark' : '';
const delay = index * 100;

return(
<div className={`meeting-card p-2 ${classes}`} key={course.id} data-aos='fade-up' data-aos-delay={delay}>
<a className="bold" href={course.course_page_path}>{`${course.unlisted ? "[UNLISTED] " : ""}${course.title}`}</a>

<div className="d-flex">
<div className="pr-2">
<div className="bold">Provider</div>
<div className="bold">Date Added</div>
</div>

<div className="flex-grow px-2">
<div className="">{ course.provider }</div>
<div className="">{ date }</div>
</div>
</div>

<a href={ course.course_edit_path } className="p2pu-btn btn btn-sm dark m-0 my-2">edit</a>
</div>
)
})
}
</div>

{
totalPages > 1 &&
<nav aria-label="Page navigation">
<ul className="pagination">
<li className={`page-item ${currentPage == 1 ? 'disabled' : ''}`}>
<a className="page-link" href="" aria-label="Previous" onClick={this.prevPage}>
<span aria-hidden="true">&laquo;</span>
<span className="sr-only">Previous</span>
</a>
</li>
<li className="page-item">
<span className="page-link disabled">{`Page ${currentPage} of ${totalPages}`}</span>
</li>
<li className={`page-item ${currentPage == totalPages ? 'disabled' : ''}`}>
<a className="page-link" href="" aria-label="Next" onClick={this.nextPage}>
<span aria-hidden="true">&raquo;</span>
<span className="sr-only">Next</span>
</a>
</li>
</ul>
</nav>
}

</>


const CourseList = props => {
const {teamId} = props;
const [courseList, setCourseList] = useState({courses: [], count: 0, offset:0, limit: 20});
useEffect(() => {
const api = new ApiHelper('courses');
const onSuccess = (data) => {
setCourseList({
courses: data.items,
count: data.count,
offset: data.offset,
limit: data.limit
})
}
const params = {
limit: courseList.limit,
offset: courseList.offset,
team: true,
}
api.fetchResource({ callback: onSuccess, params })
}, [])
return (
<>
<CourseTable
courses={courseList.courses}
totalPages={1}
/>
</>
);
}

export default CourseList;
9 changes: 9 additions & 0 deletions frontend/components/dashboard/FacilitatorDashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import TeamInvitationsTable from './TeamInvitationsTable'
import OrganizerTeamInvitations from './OrganizerTeamInvitations'
import FacilitatorProfile from './FacilitatorProfile'
import Announcements from './Announcements'
import CourseList from './CourseList'

import 'react-tabs/style/react-tabs.css';
import 'aos/dist/aos.css';
Expand Down Expand Up @@ -214,6 +215,7 @@ export default class FacilitatorDashboard extends React.Component {
<Tab><span className="minicaps bold text-xs">Team members</span></Tab>
<Tab><span className="minicaps bold text-xs">Pending invitations</span></Tab>
<Tab><span className="minicaps bold text-xs">Invite new members</span></Tab>
<Tab><span className="minicaps bold text-xs">Course list</span></Tab>
</TabList>
<TabPanel>
<div data-aos='fade' data-aos-anchor-placement="top-bottom">
Expand Down Expand Up @@ -242,6 +244,13 @@ export default class FacilitatorDashboard extends React.Component {
/>
</div>
</TabPanel>
<TabPanel>
<div data-aos='fade' data-aos-anchor-placement="top-bottom">
<CourseList
teamId={this.props.teamId}
/>
</div>
</TabPanel>
</Tabs>
<div className="text-right">
<a href={this.props.editTeamUrl}>Edit team information</a>
Expand Down
2 changes: 1 addition & 1 deletion frontend/helpers/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const API_ENDPOINTS = {
},
courses: {
baseUrl: '/api/courses/?',
searchParams: ['q', 'topics', 'order', 'course_id', 'user', 'include_unlisted', 'limit', 'offset']
searchParams: ['q', 'topics', 'order', 'course_id', 'user', 'include_unlisted', 'limit', 'offset', 'team']
},
learningCirclesTopics: {
baseUrl: '/api/learningcircles/topics/?',
Expand Down
134 changes: 134 additions & 0 deletions frontend/team-courses.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import React from 'react'
import ReactDOM from 'react-dom'
import ErrorBoundary from './components/error-boundary'

import SearchProvider from 'p2pu-components/dist/Search/SearchProvider'
import SearchCourses from 'p2pu-components/dist/Courses/SearchCourses'
import CourseCard from 'p2pu-components/dist/Courses/CourseCard'
//import BrowseCourses from 'p2pu-components/dist/Courses/Browse'
import SearchAndFilter from 'p2pu-components/dist/Courses/SearchAndFilter'
import SearchSummary from 'p2pu-components/dist/Courses/SearchSummary'
import SearchBar from 'p2pu-components/dist/Search/SearchBar'

import OrderCoursesForm from 'p2pu-components/dist/Courses/OrderCoursesForm'
import TopicsFilterForm from 'p2pu-components/dist/Courses/TopicsFilterForm'
import LanguageFilterForm from 'p2pu-components/dist/Courses/LanguageFilterForm'
import OerFilterForm from 'p2pu-components/dist/Courses/OerFilterForm'
import FacilitatorGuideFilterForm from 'p2pu-components/dist/Courses/FacilitatorGuideFilterForm'

import { t } from 'ttag';



const element = document.getElementById('team-courses')

const user = element.dataset.user === "AnonymousUser" ? null : element.dataset.user;
const teamId = element.dataset.teamId === "None" ? null : element.dataset.teamId;
const teamName = element.dataset.teamName === "None" ? null : element.dataset.teamName;
const userIsOrganizer = element.dataset.userIsOrganizer === "None" ? null : element.dataset.userIsOrganizer;


const BrowseCourses = props => {
const { results, updateQueryParams, onSelectResult, columnBreakpoints, isLoading } = props;

if (isLoading){
return <></>;
}

return (
<div className="search-results">
{
results.map((course, index) => (
<CourseCard
key={`course-card-${index}`}
id={`course-card-${index}`}
course={course}
updateQueryParams={updateQueryParams}
courseLink={props.courseLink}
moreInfo={props.moreInfo}
onSelectResult={onSelectResult}
buttonText={t`Add this course`}
classes="mb-4"
/>
))
}
</div>
);
}

// TODO dedup with frontend/components/learning_circle_form/CourseSelection.jsx
const CustomCourseSearch = (props) => {
return (
<>
<SearchBar
updateQueryParams={props.updateQueryParams}
q={props.q}
/>

<a data-bs-toggle="collapse" href="#searchFilters" role="button" aria-expanded="false" aria-controls="searchFilters">
Advanced options <i className="fa fa-chevron-down"></i>
</a>

<div id="searchFilters" className="collapse">
<div className="col-12">
<OrderCoursesForm {...props} />
</div>
<div className="col-12">
<TopicsFilterForm {...props} />
</div>
<div className="col-12">
<LanguageFilterForm {...props} />
</div>
<div className="col-12">
<FacilitatorGuideFilterForm {...props} />
</div>
<div className="col-12">
<OerFilterForm {...props} />
</div>
</div>

<SearchSummary {...props} />
<BrowseCourses {...props} onSelectResult={ e=> console.log(e) }/>
</>
);
}


const TeamCourseSelection = props => {

const handleSelectResult = selected => {
props.updateFormData({ course: selected })
scrollToTop()
}

return (
<div id="team-course-selection">
<h1>Select team courses</h1>
<SearchProvider
columnBreakpoints={props.showHelp ? 1 : 3}
searchSubject={'courses'}
initialState={{languages: ['en']}}
onSelectResult={handleSelectResult}
origin={window.location.origin}
scrollContainer={'#team-course-selection'}
NoResultsComponent={() => <p className="my-4">{`There are no matching courses.`}<a className="btn p2pu-btn btn-secondary" href={`${window.location.origin}${window.location.pathname}`}>Start over</a></p>}
>
<CustomCourseSearch/>
</SearchProvider>
</div>
);
}

ReactDOM.render(
<ErrorBoundary scope="team-courses">
<TeamCourseSelection
user={user}
teamId={teamId}
teamName={teamName}
userIsOrganizer={userIsOrganizer}
isMemberTeam={element.dataset.isMemberTeam}
isStaff={element.dataset.isStaff}
/>
</ErrorBoundary>,
element
)
2 changes: 2 additions & 0 deletions studygroups/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from studygroups.views import FacilitatorDashboard
from studygroups.views import OrganizerGuideForm
from studygroups.views import TeamUpdate
from studygroups.views import TeamCourseList
from studygroups.views import MessageView
from studygroups.views import MeetingRecap
from studygroups.views import MeetingRecapDismiss
Expand Down Expand Up @@ -124,6 +125,7 @@
url(r'^organize/teammembership/(?P<team_id>[\d]+)/(?P<user_id>[\d]+)/delete/$', TeamMembershipDelete.as_view(), name='studygroups_teammembership_delete'),
url(r'^organize/team/(?P<team_id>[\d]+)/member/invite/$', TeamInvitationCreate.as_view(), name='studygroups_team_member_invite'),
url(r'^organize/team/(?P<team_id>[\d]+)/edit/$', TeamUpdate.as_view(), name='studygroups_team_edit'),
url(r'^organize/team/(?P<team_id>[\d]+)/course_list/$', TeamCourseList.as_view(), name='studygroups_team_course_list'),

url(r'^get-organizer-guide/$', OrganizerGuideForm.as_view(), name='studygroups_organizer_guide_form'),

Expand Down
10 changes: 10 additions & 0 deletions studygroups/views/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,7 @@ def get(self, request):
"include_unlisted": schema.boolean(),
"facilitator_guide": schema.boolean(),
"course_list": schema.integer(),
"team": schema.boolean(),
}
data = schema.django_get_to_dict(request.GET)
clean_data, errors = schema.validate(query_schema, data)
Expand Down Expand Up @@ -607,6 +608,15 @@ def get(self, request):
# TODO should this parameter only be available for teams?
# TODO should this take the course list id, or be associated with the current users team membership?

if clean_data.get('team'):
team_membership = TeamMembership.objects.active().filter(user=request.user).first()
if not team_membership:
errors = { 'team': ['Facilitator not part of a team']}
return json_response(request, {"status": "error", "errors": errors})
team = TeamMembership.objects.active().filter(user=request.user).first().team
courses = courses.filter(courselist__team=team)


data = {
'count': courses.count()
}
Expand Down
8 changes: 8 additions & 0 deletions studygroups/views/organizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from django.utils import timezone
from django.utils.translation import ugettext as _
from django.views.generic.base import View
from django.views.generic.base import TemplateView
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
from django.views.generic import ListView
from django.contrib import messages
Expand Down Expand Up @@ -213,4 +214,11 @@ def form_valid(self, form):
return super().form_valid(form)


@method_decorator(user_is_team_organizer, name='dispatch')
class TeamCourseList(TemplateView):

template_name = 'studygroups/team_courses.html'

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
return context
36 changes: 36 additions & 0 deletions templates/studygroups/team_courses.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{% extends 'base.html' %}
{% load render_bundle from webpack_loader %}
{% load static %}
{% load i18n %}

{% block page_title %}
{% trans "Team Courses" %}
{% endblock %}


{% block content %}
<div
id='team-courses'
data-user="{{ request.user }}"
data-team-id="{{ team_id }}"
data-team-role="{{ team_role }}"
data-team-name="{{ team_name }}"
data-team-organizer-name="{{ team_organizer_name }}"
data-user-is-organizer="{{ user_is_organizer }}"
{% if is_member_team %}
data-is-member-team="true"
data-member-support-url="{{ member_support_url }}"
{% endif %}
{% if request.user.is_staff %}
data-is-staff="true"
data-member-support-url="{{ member_support_url }}"
{% endif %}
>
<div class="loader"></div>
</div>

{% endblock %}

{% block scripts %}
{% render_bundle 'team-courses' %}
{% endblock %}

0 comments on commit d071f49

Please sign in to comment.