diff --git a/custom_registration/views.py b/custom_registration/views.py index 3e1563771..558714c29 100644 --- a/custom_registration/views.py +++ b/custom_registration/views.py @@ -29,6 +29,7 @@ from studygroups.models import TeamMembership from studygroups.models import Profile from studygroups.forms import TeamMembershipForm +from studygroups.models.learningcircle import StudyGroup from uxhelpers.utils import json_response from api import schema from .models import create_user @@ -309,8 +310,14 @@ def delete(self, request, *args, **kwargs): user.username = random_username user.save() - # delete any active or future learning circles - user.studygroup_set.update(deleted_at=timezone.now()) + for lc in StudyGroup.objects.active().filter(facilitator__user=user): + if lc.facilitator_set.count() == 1: + lc.deleted_at = timezone.now() + + if lc.facilitator_set.count() > 1: + lc.facilitator_set.filter(user=user).delete() + + lc.save() # delete profile data profile = user.profile diff --git a/docker-compose.yml b/docker-compose.yml index 217c3a550..176c0dff2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,6 +2,8 @@ version: '3' services: postgres: image: postgres:11 + ports: + - 5432:5432 environment: - POSTGRES_HOST_AUTH_METHOD=trust volumes: diff --git a/e2e/fixtures/test_studygroups.json b/e2e/fixtures/test_studygroups.json index 7c5c810c7..680aac24d 100644 --- a/e2e/fixtures/test_studygroups.json +++ b/e2e/fixtures/test_studygroups.json @@ -33,7 +33,7 @@ "longitude" : "-87.650050", "language" : "en", "place_id" : "", - "facilitator": 1, + "created_by": 1, "venue_name": "Harold Washington", "venue_address": "123 Street", "venue_details": "3rd floor", @@ -45,6 +45,16 @@ "model": "studygroups.studygroup", "pk": 1 }, +{ + "fields": { + "added_at": "2015-03-23T15:18:39.462Z", + "user": 1, + "study_group": 1 + }, + "model": "studygroups.facilitator", + "pk": 1 +}, + { "fields": { "created_at": "2015-03-23T15:19:04.318Z", @@ -57,7 +67,7 @@ "end_date": "2015-03-23", "duration": 120, "timezone": "US/Central", - "facilitator": 1, + "created_by": 1, "venue_name": "Harold Washington", "venue_address": "123 Street", "venue_details": "3rd floor", @@ -71,6 +81,16 @@ "model": "studygroups.studygroup", "pk": 2 }, +{ + "fields": { + "added_at": "2015-03-23T15:18:39.462Z", + "user": 1, + "study_group": 2 + }, + "model": "studygroups.facilitator", + "pk": 2 +}, + { "fields": { "created_at": "2015-03-25T14:35:02.227Z", @@ -83,7 +103,7 @@ "end_date": "2015-03-23", "duration": 120, "timezone": "US/Central", - "facilitator": 1, + "created_by": 1, "venue_name": "Harold Washington", "venue_address": "123 Street", "venue_details": "3rd floor", @@ -97,6 +117,15 @@ "model": "studygroups.studygroup", "pk": 3 }, +{ + "fields": { + "added_at": "2015-03-23T15:18:39.462Z", + "user": 1, + "study_group": 3 + }, + "model": "studygroups.facilitator", + "pk": 3 +}, { "fields": { "created_at": "2015-03-25T15:55:44.525Z", @@ -109,7 +138,7 @@ "end_date": "2015-03-23", "duration": 120, "timezone": "US/Central", - "facilitator": 1, + "created_by": 1, "venue_name": "Harold Washington", "venue_address": "123 Street", "venue_details": "3rd floor", @@ -122,5 +151,14 @@ }, "model": "studygroups.studygroup", "pk": 4 +}, +{ + "fields": { + "added_at": "2015-03-23T15:18:39.462Z", + "user": 1, + "study_group": 4 + }, + "model": "studygroups.facilitator", + "pk": 4 } ] diff --git a/e2e/tests/test_learning_circle_creation.py b/e2e/tests/test_learning_circle_creation.py index 15307c5c4..adaec1f61 100644 --- a/e2e/tests/test_learning_circle_creation.py +++ b/e2e/tests/test_learning_circle_creation.py @@ -155,7 +155,7 @@ def test_save_draft_learning_circle(self): self.wait.until(expected_conditions.url_changes('%s%s' % (self.live_server_url, '/en/studygroup/create/'))) saved_studygroup = StudyGroup.objects.filter(draft=True).last() - self.assertEqual(saved_studygroup.facilitator, facilitator) + self.assertEqual(saved_studygroup.created_by, facilitator) self.assertTrue(expected_conditions.url_to_be('{}/en/studygroup/{}/'.format(self.live_server_url, saved_studygroup.id))) @@ -196,7 +196,7 @@ def test_publish_learning_circle(self): self.wait.until(expected_conditions.url_changes('%s%s' % (self.live_server_url, '/en/studygroup/create/'))) published_studygroup = StudyGroup.objects.published().last() - self.assertEqual(published_studygroup.facilitator, facilitator) + self.assertEqual(published_studygroup.created_by, facilitator) self.assertTrue(expected_conditions.url_to_be('{}/en/studygroup/{}/'.format(self.live_server_url, published_studygroup.id))) diff --git a/e2e/tests/test_learning_circle_manage.py b/e2e/tests/test_learning_circle_manage.py index 96508b25a..b6061a5e7 100644 --- a/e2e/tests/test_learning_circle_manage.py +++ b/e2e/tests/test_learning_circle_manage.py @@ -13,6 +13,7 @@ from studygroups.models import Course from studygroups.models import StudyGroup from studygroups.models import Meeting +from studygroups.models import Facilitator from custom_registration.models import create_user from datetime import timedelta, time @@ -56,8 +57,9 @@ def tearDownClass(cls): def setUp(self): self.facilitator = create_user('hi@example.net', 'bowie', 'wowie', 'password') sg = StudyGroup.objects.get(pk=1) - sg.facilitator = self.facilitator + sg.created_by = self.facilitator sg.save() + Facilitator.objects.create(study_group=sg, user=self.facilitator) self.study_group = sg meeting_date = timezone.now() - timedelta(days=2) meeting = Meeting.objects.create(study_group=sg, meeting_date=meeting_date.date(), meeting_time=time(18, 0)) diff --git a/frontend/components/create-learning-circle-page.jsx b/frontend/components/create-learning-circle-page.jsx index bde0474b4..73c5dd5f9 100644 --- a/frontend/components/create-learning-circle-page.jsx +++ b/frontend/components/create-learning-circle-page.jsx @@ -53,9 +53,11 @@ export default class CreateLearningCirclePage extends React.Component { ...this.props.learningCircle, meetings: meetings }, + team: this.props.team, showModal: false, showHelp: window.screen.width > DESKTOP_BREAKPOINT, user: this.props.user, + userId: this.props.userId, errors: {}, alert: { show: false }, isSaving: false, @@ -290,7 +292,9 @@ export default class CreateLearningCirclePage extends React.Component { currentTab={this.state.currentTab} allTabs={this.allTabs} changeTab={this.changeTab} + userId={this.props.userId} learningCircle={this.state.learningCircle} + team={this.state.team} errors={this.state.errors} onCancel={this.onCancel} onSubmitForm={this.onSubmitForm} diff --git a/frontend/components/dashboard/FacilitatorDashboard.jsx b/frontend/components/dashboard/FacilitatorDashboard.jsx index 4a9a61cfd..0db25b32e 100644 --- a/frontend/components/dashboard/FacilitatorDashboard.jsx +++ b/frontend/components/dashboard/FacilitatorDashboard.jsx @@ -10,11 +10,9 @@ import Notification from './Notification'; import DiscourseTable from './DiscourseTable'; import CoursesTable from './CoursesTable'; import MemberLearningCirclesTable from './MemberLearningCirclesTable'; -import UpcomingLearningCirclesTable from './UpcomingLearningCirclesTable'; import CurrentLearningCirclesTable from './CurrentLearningCirclesTable'; import ActiveLearningCirclesTable from './ActiveLearningCirclesTable'; import CompletedLearningCirclesTable from './CompletedLearningCirclesTable'; -import UpcomingMeetings from './UpcomingMeetings'; import RecommendedResources from "./RecommendedResources"; import GlobalSuccesses from "./GlobalSuccesses"; import InstagramFeed from "./InstagramFeed"; diff --git a/frontend/components/dashboard/TeamInvitationsTable.jsx b/frontend/components/dashboard/TeamInvitationsTable.jsx index b10be9bb7..a0f64a649 100644 --- a/frontend/components/dashboard/TeamInvitationsTable.jsx +++ b/frontend/components/dashboard/TeamInvitationsTable.jsx @@ -77,7 +77,7 @@ export default class TeamMembersTable extends Component { if (this.state.teamMembers.length === 0) { return(
-
You don't have any team members.
+
You don't have any pending invitations.
) } diff --git a/frontend/components/dashboard/UpcomingLearningCirclesTable.jsx b/frontend/components/dashboard/UpcomingLearningCirclesTable.jsx deleted file mode 100644 index 09465e476..000000000 --- a/frontend/components/dashboard/UpcomingLearningCirclesTable.jsx +++ /dev/null @@ -1,149 +0,0 @@ -import React, { Component } from "react"; -import ApiHelper from "../../helpers/ApiHelper"; -import moment from "moment"; - -const PAGE_LIMIT = 5; - -export default class LearningCirclesTable extends Component { - constructor(props) { - super(props); - this.state = { - learningCircles: [], - limit: PAGE_LIMIT, - count: 0, - }; - } - - componentDidMount() { - this.populateResources(); - } - - populateResources = (params={}) => { - const api = new ApiHelper('learningCircles'); - - const onSuccess = (data) => { - this.setState({ learningCircles: data.items, count: data.count, offset: data.offset, limit: data.limit }) - } - - const defaultParams = { limit: this.state.limit, offset: this.state.offset, user: this.props.user, draft: true, scope: "upcoming", team_id: this.props.teamId } - - api.fetchResource({ callback: onSuccess, params: { ...defaultParams, ...params } }) - } - - nextPage = (e) => { - e.preventDefault(); - const params = { limit: PAGE_LIMIT, offset: this.state.offset + this.state.learningCircles.length }; - this.populateResources(params) - } - - prevPage = (e) => { - e.preventDefault(); - const params = { limit: PAGE_LIMIT, offset: this.state.offset - PAGE_LIMIT }; - this.populateResources(params) - } - - - render() { - const totalPages = Math.ceil(this.state.count / PAGE_LIMIT); - const currentPage = Math.ceil((this.state.offset + this.state.learningCircles.length) / PAGE_LIMIT); - - if (this.state.learningCircles.length === 0) { - return( -
-
No upcoming learning circles.
-
- ) - } - - return ( -
-
- - - - - - { this.props.teamId && } - - - { (this.props.user || this.props.userIsOrganizer) && } - - - - { - this.state.learningCircles.map(lc => { - const date = lc.next_meeting_date ? moment(lc.next_meeting_date).format('MMM D, YYYY') : "n/a"; - const classes = lc.draft ? 'bg-cream-dark' : ''; - - return( - - - - { this.props.teamId && } - - - { (this.props.user || this.props.userIsOrganizer) && } - - ) - }) - } - -
Learning circleVenueFacilitatorSignupsFirst meeting
{`${lc.draft ? "[DRAFT] " : ""}${lc.name}`}{ lc.venue }{ lc.facilitator }{ lc.signup_count }{ date }manage
-
- -
- { - this.state.learningCircles.map(lc => { - const date = lc.next_meeting_date ? moment(lc.next_meeting_date).format('MMM D, YYYY') : "n/a"; - const classes = lc.draft ? 'bg-cream-dark' : ''; - - return( -
- {`${lc.draft ? "[DRAFT] " : ""}${lc.name}`} - -
-
-
Venue
-
Signups
-
First Meeting
-
- -
-
{ lc.venue }
-
{ lc.signup_count }
-
{ date }
-
-
- - { (this.props.user || this.props.userIsOrganizer) && manage } -
- ) - }) - } -
- { - totalPages > 1 && - - } -
- ); - } -} diff --git a/frontend/components/dashboard/UpcomingMeetings.jsx b/frontend/components/dashboard/UpcomingMeetings.jsx deleted file mode 100644 index 8c539095c..000000000 --- a/frontend/components/dashboard/UpcomingMeetings.jsx +++ /dev/null @@ -1,117 +0,0 @@ -import React, { Component } from "react"; -import moment from "moment"; -import AOS from 'aos'; - -import ApiHelper from "../../helpers/ApiHelper"; -import { DEFAULT_LC_IMAGE } from "../../helpers/constants" - - -export default class UpcomingMeetings extends Component { - constructor(props) { - super(props); - this.state = { - meetings: [], - errors: [] - }; - } - - componentDidMount() { - this.populateResources(); - AOS.init(); - } - - componentDidUpdate() { - AOS.refresh(); - } - - populateResources = () => { - const api = new ApiHelper('landingPage'); - - const onSuccess = (data) => { - if (data.status && data.status === "error") { - return this.setState({ errors: data.errors }) - } - - this.setState({ meetings: data.items }) - } - - api.fetchResource({ callback: onSuccess, params: { scope: this.props.scope } }) - } - - generateFormattedMeetingDate = (nextMeeting) => { - if (moment().isSame(nextMeeting, 'day')) { - return 'Today' - } else if (moment().add(1, 'day').isSame(nextMeeting, 'day')) { - return 'Tomorrow' - } else { - return nextMeeting.format('dddd, MMM Do') - } - } - - render() { - if (this.state.errors.length > 0) { - return( -
-
- Check out our organizer materials if you’re interested in starting a team. -
-
- ) - }; - - if (this.state.meetings.length === 0) { - return( -
-
No upcoming meetings.
-
- ) - } - - return ( -
- { - this.state.meetings.map((meeting, index) => { - const nextMeeting = moment(`${meeting.next_meeting_date} ${meeting.meeting_time}`); - const formattedMeetingDate = this.generateFormattedMeetingDate(nextMeeting); - const formattedStartTime = nextMeeting.format('h:mma'); - const formattedCity = meeting.city.replace(/United States of America/, 'USA'); - const delay = index * 100; - const imageSrc = meeting.image_url || DEFAULT_LC_IMAGE; - - return( -
-
- - - -
- -
-
- -
- today - {`${formattedMeetingDate} at ${formattedStartTime}`} -
- -
- location_on - {formattedCity} -
-
- -
-

- {meeting.facilitator} is facilitating {meeting.name} at { meeting.venue } -

-
- -
-
- ) - }) - } -
- ); - } -} diff --git a/frontend/components/learning_circle_form/FinalizeSection.jsx b/frontend/components/learning_circle_form/FinalizeSection.jsx index 2d4f15412..a9104d56b 100644 --- a/frontend/components/learning_circle_form/FinalizeSection.jsx +++ b/frontend/components/learning_circle_form/FinalizeSection.jsx @@ -1,7 +1,42 @@ -import React from 'react' -import { TextareaWithLabel } from 'p2pu-components' +import React, { useState } from 'react' +import { TextareaWithLabel, SelectWithLabel } from 'p2pu-components' -const FinalizeSection = (props) => ( +const FinalizeSection = (props) => { + + const [showSelfRemovalWarning, setShowSelfRemovalWarning] = useState(false); + + const populateTeamOptions = (team) => { + return team.map(teamMember => { + return {label: teamMember.firstName + ' ' + teamMember.lastName + ', ' + teamMember.email, + value: teamMember.id} + }) + } + + const handleFacilitatorSelect = (value) => { + if(props.learningCircle.facilitators) { + const removedFacilitators = props.learningCircle.facilitators.filter(x => value.facilitators === null || !value.facilitators.includes(x)); + if(removedFacilitators.includes(props.userId)) { + setShowSelfRemovalWarning(true); + } + } + props.updateFormData(value) + } + + const addCurrentUserToFacilitators = () => { + if(props.learningCircle.facilitators) { + props.learningCircle.facilitators.push(props.userId); + } + else { + props.learningCircle.facilitators = [props.userId]; + } + setShowSelfRemovalWarning(false); + } + + const hasTeam = () => { + return props.team.length > 0; + } + + return (
( id={'id_facilitator_concerns'} errorMessage={props.errors.facilitator_concerns} /> +
+ + {(!hasTeam()) && This feature is only available to teams} + {(showSelfRemovalWarning) && +
+

You are removing yourself as a facilitator and will therefore no longer have access to this learning circle.

+ +
+ } +
-); + ); +}; export default FinalizeSection; diff --git a/frontend/components/learning_circle_form/FormTabs.jsx b/frontend/components/learning_circle_form/FormTabs.jsx index c610e33b1..206ddeba0 100644 --- a/frontend/components/learning_circle_form/FormTabs.jsx +++ b/frontend/components/learning_circle_form/FormTabs.jsx @@ -31,7 +31,7 @@ export default class FormTabs extends React.Component{ ['city', 'country', 'venue_name', 'venue_details', 'venue_address', 'language'], ['start_date', 'meeting_time', 'timezone'], ['name', 'description', 'course_description', 'signup_question', 'venue_website'], - ['facilitator_goal', 'facilitator_concerns'], + ['facilitator_goal', 'facilitator_concerns', 'facilitators'], ]; const tabErrors = tabFields.map( tab => Object.keys(errors).filter(e => tab.indexOf(e) != -1)); @@ -91,6 +91,8 @@ export default class FormTabs extends React.Component{

Step 5: Finalize

diff --git a/frontend/components/weekly-meetings-list.jsx b/frontend/components/weekly-meetings-list.jsx index 98bdac303..71dcd20bd 100644 --- a/frontend/components/weekly-meetings-list.jsx +++ b/frontend/components/weekly-meetings-list.jsx @@ -94,7 +94,6 @@ export default class WeeklyMeetingsList extends React.Component { this._handleWeekChange(nextWeek) }>{interpolate(gettext("Week of %s"), [nextWeek.format('MMM. D, YYYY')])}  this._handleWeekChange(moment().startOf('week')) }>{gettext("This week")} { week.map(day => ) } -

{gettext("View all meetings")}

); } diff --git a/frontend/helpers/constants.js b/frontend/helpers/constants.js index cf9bee229..0f6201699 100644 --- a/frontend/helpers/constants.js +++ b/frontend/helpers/constants.js @@ -49,14 +49,6 @@ export const API_ENDPOINTS = { baseUrl: '/api/courses/topics/?', searchParams: [] }, - stats: { - baseUrl: '/api/landing-page-stats/?', - searchParams: [] - }, - landingPage: { - baseUrl: '/api/landing-page-learning-circles/?', - searchParams: ['team', 'scope'] - }, images: { postUrl: '/api/upload_image/' }, diff --git a/frontend/learning-circle-create.jsx b/frontend/learning-circle-create.jsx index 5d2962c03..a7133f8ff 100644 --- a/frontend/learning-circle-create.jsx +++ b/frontend/learning-circle-create.jsx @@ -6,7 +6,8 @@ import CreateLearningCirclePage from './components/create-learning-circle-page' const element = document.getElementById('create-learning-circle-form') const user = element.dataset.user === "AnonymousUser" ? null : element.dataset.user; +const userId = window.currentUserId; const learningCircle = window.lc; const tinymceScriptSrc = "/static/js/vendor/tinymce/tinymce.min.js"; -ReactDOM.render(, element) +ReactDOM.render(, element) diff --git a/requirements.txt b/requirements.txt index 6ccc8bb12..91c79e3dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -51,7 +51,7 @@ phonenumberslite==8.12.38 Pillow==8.4.0 premailer==3.10.0 prompt-toolkit==3.0.23 -psycopg2-binary==2.8.6 +psycopg2-binary==2.9.3 pycparser==2.21 pygal==3.0.0 PyJWT==2.3.0 diff --git a/static/sass/_learningcircle.scss b/static/sass/_learningcircle.scss index 484603c11..33ef3acfa 100644 --- a/static/sass/_learningcircle.scss +++ b/static/sass/_learningcircle.scss @@ -13,4 +13,10 @@ } } +.lc-co-facilitator-input { + margin-bottom: 200px; +} +.rm-facilitator-warning { + margin-top: 20px; +} diff --git a/studygroups/admin.py b/studygroups/admin.py index 096c1b1d6..b9f53c04e 100644 --- a/studygroups/admin.py +++ b/studygroups/admin.py @@ -26,10 +26,13 @@ def get_queryset(self, request): class StudyGroupAdmin(admin.ModelAdmin): inlines = [ApplicationInline] - list_display = ['course', 'city', 'facilitator', 'start_date', 'day', 'signup_open', 'uuid'] + list_display = ['course', 'city', 'facilitators', 'start_date', 'day', 'signup_open', 'uuid'] exclude = ['deleted_at'] - search_fields = ['course__title', 'uuid', 'city', 'facilitator__first_name'] - raw_id_fields = ['course', 'facilitator'] + search_fields = ['course__title', 'uuid', 'city', 'facilitator__user__first_name', 'facilitator__user__email'] + raw_id_fields = ['course', 'created_by'] + + def facilitators(self, study_group): + return study_group.facilitators_display() def get_queryset(self, request): return super().get_queryset(request).active() @@ -101,7 +104,7 @@ def get_form(self, request, obj=None, **kwargs): def save_model(self, request, obj, form, change): if obj.study_group: obj.course = obj.study_group.course - obj.user = obj.study_group.facilitator + obj.user = obj.study_group.created_by super().save_model(request, obj, form, change) diff --git a/studygroups/api_urls.py b/studygroups/api_urls.py index 7d1a0406b..933810e50 100644 --- a/studygroups/api_urls.py +++ b/studygroups/api_urls.py @@ -29,8 +29,6 @@ url(r'^signup/$', views.SignupView.as_view(), name='api_learningcircles_signup'), url(r'^learning-circle/$', views.LearningCircleCreateView.as_view(), name='api_learningcircles_create'), url(r'^learning-circle/(?P[\d]+)/$', views.LearningCircleUpdateView.as_view(), name='api_learningcircles_update'), - url(r'^landing-page-learning-circles/$', views.LandingPageLearningCirclesView.as_view(), name='api_learningcircles_meetings'), - url(r'^landing-page-stats/$', views.LandingPageStatsView.as_view(), name='api_landing_page_stats'), url(r'^upload_image/$', views.ImageUploadView.as_view(), name='api_image_upload'), url(r'^learning-circles-map/$', views.LearningCirclesMapView.as_view(), name='api_learningcircles_map'), url(r'^instagram-feed/$', views.InstagramFeed.as_view(), name='api_instagram_feed'), diff --git a/studygroups/charts.py b/studygroups/charts.py index bea4d82d9..f37eee712 100644 --- a/studygroups/charts.py +++ b/studygroups/charts.py @@ -784,8 +784,9 @@ def get_data(self): counts = [] for sg in study_groups: - facilitator = sg.facilitator - sg_count = StudyGroup.objects.published().filter(start_date__lte=sg.start_date, facilitator=facilitator).count() + facilitator = sg.created_by + # TODO + sg_count = StudyGroup.objects.published().filter(start_date__lte=sg.start_date, created_by=facilitator).count() counts.append(sg_count) counter = Counter(counts) diff --git a/studygroups/decorators.py b/studygroups/decorators.py index 19b3ccfb2..681c67632 100644 --- a/studygroups/decorators.py +++ b/studygroups/decorators.py @@ -7,6 +7,7 @@ from django.utils.translation import ugettext as _ from studygroups.models import StudyGroup +from studygroups.models import Facilitator from studygroups.models import Team from studygroups.models import TeamMembership from studygroups.models import get_study_group_organizers @@ -30,8 +31,8 @@ def decorated(*args, **kwargs): # TODO this logic should be in the model study_group = get_object_or_404(StudyGroup, pk=study_group_id) if args[0].user.is_staff \ - or args[0].user == study_group.facilitator \ - or TeamMembership.objects.active().filter(user=args[0].user, role=TeamMembership.ORGANIZER).exists() and args[0].user in get_study_group_organizers(study_group): + or Facilitator.objects.filter(user=args[0].user, study_group=study_group).exists() \ + or study_group.team and TeamMembership.objects.active().filter(user=args[0].user, role=TeamMembership.ORGANIZER, team=study_group.team).exists(): return func(*args, **kwargs) raise PermissionDenied return login_required(decorated) diff --git a/studygroups/fixtures/test_studygroups.json b/studygroups/fixtures/test_studygroups.json index 9cd7c0fb4..dbf4eb414 100644 --- a/studygroups/fixtures/test_studygroups.json +++ b/studygroups/fixtures/test_studygroups.json @@ -52,7 +52,7 @@ "language" : "en", "place_id" : "", "online": false, - "facilitator": 1, + "created_by": 1, "venue_name": "Harold Washington", "venue_address": "123 Street", "venue_details": "3rd floor", @@ -64,6 +64,15 @@ "model": "studygroups.studygroup", "pk": 1 }, +{ + "fields": { + "added_at": "2015-03-23T15:18:39.462Z", + "user": 1, + "study_group": 1 + }, + "model": "studygroups.facilitator", + "pk": 1 +}, { "fields": { "created_at": "2015-03-23T15:19:04.318Z", @@ -76,7 +85,7 @@ "end_date": "2015-03-23", "duration": 120, "timezone": "US/Central", - "facilitator": 1, + "created_by": 1, "venue_name": "Harold Washington", "venue_address": "123 Street", "venue_details": "3rd floor", @@ -91,6 +100,15 @@ "model": "studygroups.studygroup", "pk": 2 }, +{ + "fields": { + "added_at": "2015-03-23T15:18:39.462Z", + "user": 1, + "study_group": 2 + }, + "model": "studygroups.facilitator", + "pk": 2 +}, { "fields": { "created_at": "2015-03-25T14:35:02.227Z", @@ -103,7 +121,7 @@ "end_date": "2015-03-23", "duration": 120, "timezone": "US/Central", - "facilitator": 2, + "created_by": 2, "venue_name": "Harold Washington", "venue_address": "123 Street", "venue_details": "3rd floor", @@ -118,6 +136,15 @@ "model": "studygroups.studygroup", "pk": 3 }, +{ + "fields": { + "added_at": "2015-03-23T15:18:39.462Z", + "user": 2, + "study_group": 3 + }, + "model": "studygroups.facilitator", + "pk": 3 +}, { "fields": { "created_at": "2015-03-25T15:55:44.525Z", @@ -130,7 +157,7 @@ "end_date": "2015-03-23", "duration": 120, "timezone": "US/Central", - "facilitator": 2, + "created_by": 2, "venue_name": "Harold Washington", "venue_address": "123 Street", "venue_details": "3rd floor", @@ -144,5 +171,14 @@ }, "model": "studygroups.studygroup", "pk": 4 +}, +{ + "fields": { + "added_at": "2015-03-23T15:18:39.462Z", + "user": 2, + "study_group": 4 + }, + "model": "studygroups.facilitator", + "pk": 4 } ] diff --git a/studygroups/ics.py b/studygroups/ics.py index f8f2ac30d..2def6fc66 100644 --- a/studygroups/ics.py +++ b/studygroups/ics.py @@ -12,8 +12,12 @@ def make_meeting_ics(meeting): event.add('dtstart', meeting.meeting_datetime()) event.add('dtend', meeting.meeting_datetime_end()) - organizer = vCalAddress('MAILTO:{}'.format(study_group.facilitator.email)) - organizer.params['cn'] = vText(study_group.facilitator.first_name) + # Only use the first facilitator or default to created_by + facilitator = study_group.created_by + if study_group.facilitator_set.count(): + facilitator = study_group.facilitator_set.first().user + organizer = vCalAddress('MAILTO:{}'.format(facilitator.email)) + organizer.params['cn'] = vText(facilitator.first_name) organizer.params['role'] = vText('Facilitator') event['organizer'] = organizer event['location'] = vText('{}, {}, {}, {}'.format( diff --git a/studygroups/management/commands/add_team_to_learning_circles.py b/studygroups/management/commands/add_team_to_learning_circles.py deleted file mode 100644 index ba6e88a45..000000000 --- a/studygroups/management/commands/add_team_to_learning_circles.py +++ /dev/null @@ -1,16 +0,0 @@ -from django.core.management.base import BaseCommand, CommandError - -from studygroups.models import StudyGroup -import requests - -class Command(BaseCommand): - help = 'Associate learning circles to the team of the facilitator' - - def handle(self, *args, **options): - study_groups = StudyGroup.objects.active() - for study_group in study_groups: - if study_group.facilitator.teammembership_set.active().count(): - study_group.team = study_group.facilitator.teammembership_set.active().first().team - study_group.save() - print("Added study group to team {}: {}".format(study_group.id, study_group.team_id)) - diff --git a/studygroups/migrations/0163_facilitator.py b/studygroups/migrations/0163_facilitator.py new file mode 100644 index 000000000..30042ff99 --- /dev/null +++ b/studygroups/migrations/0163_facilitator.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.13 on 2022-07-04 11:29 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('studygroups', '0162_auto_20220614_0508'), + ] + + operations = [ + migrations.CreateModel( + name='Facilitator', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('added_at', models.DateTimeField(auto_now_add=True)), + ('study_group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='studygroups.studygroup')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/studygroups/migrations/0164_facilitator_data_migration.py b/studygroups/migrations/0164_facilitator_data_migration.py new file mode 100644 index 000000000..d1df71b49 --- /dev/null +++ b/studygroups/migrations/0164_facilitator_data_migration.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2.13 on 2022-07-12 13:32 + +from django.db import migrations + +def create_facilitator_entries(apps, schema_editor): + StudyGroup = apps.get_model('studygroups', 'StudyGroup') + Facilitator = apps.get_model('studygroups', 'Facilitator') + + for sg in StudyGroup.objects.all(): + f = Facilitator.objects.create(study_group=sg, user=sg.facilitator) + Facilitator.objects.filter(pk=f.pk).update(added_at=sg.created_at) + + +class Migration(migrations.Migration): + + dependencies = [ + ('studygroups', '0163_facilitator'), + ] + + operations = [ + migrations.RunPython(create_facilitator_entries), + ] diff --git a/studygroups/migrations/0165_rename_facilitator_studygroup_created_by.py b/studygroups/migrations/0165_rename_facilitator_studygroup_created_by.py new file mode 100644 index 000000000..bb0d9bd95 --- /dev/null +++ b/studygroups/migrations/0165_rename_facilitator_studygroup_created_by.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.13 on 2022-08-03 09:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('studygroups', '0164_facilitator_data_migration'), + ] + + operations = [ + migrations.RenameField( + model_name='studygroup', + old_name='facilitator', + new_name='created_by', + ), + ] diff --git a/studygroups/models/__init__.py b/studygroups/models/__init__.py index a9da281d1..ad7e00ee2 100644 --- a/studygroups/models/__init__.py +++ b/studygroups/models/__init__.py @@ -18,6 +18,7 @@ from .announcement import Announcement from .profile import Profile from .learningcircle import StudyGroup +from .learningcircle import Facilitator from .learningcircle import Meeting from .learningcircle import Application from .learningcircle import Reminder @@ -171,9 +172,9 @@ def weekly_update_data(today, team=None): # TODO should creation date or start date determine lc # _facilitator_groups = StudyGroup.objects.published().filter( - facilitator=OuterRef('facilitator'), + created_by=OuterRef('created_by'), ## TODO - this uses creator rather than facilitator start_date__lte=OuterRef('start_date') - ).order_by().values('facilitator').annotate(number=Count('pk')) + ).order_by().values('created_by').annotate(number=Count('pk')) upcoming_studygroups = StudyGroup.objects.published().annotate( lc_number=_facilitator_groups.values('number')[:1] @@ -338,8 +339,9 @@ def get_active_facilitators(): studygroup_count=Count( Case( When( - studygroup__draft=False, studygroup__deleted_at__isnull=True, - then=F('studygroup__id') + facilitator__study_group__deleted_at__isnull=True, + facilitator__study_group__draft=False, + then=F('facilitator__study_group__id') ), default=Value(0), output_field=IntegerField() @@ -349,20 +351,21 @@ def get_active_facilitators(): latest_end_date=Max( Case( When( - studygroup__draft=False, - studygroup__deleted_at__isnull=True, - then='studygroup__end_date' + facilitator__study_group__draft=False, + facilitator__study_group__deleted_at__isnull=True, + then='facilitator__study_group__end_date' ) ) ), learners_count=Sum( Case( When( - studygroup__draft=False, - studygroup__deleted_at__isnull=True, - studygroup__application__deleted_at__isnull=True, - studygroup__application__accepted_at__isnull=False, then=1 + facilitator__study_group__draft=False, + facilitator__study_group__deleted_at__isnull=True, + facilitator__study_group__application__deleted_at__isnull=True, + facilitator__study_group__application__accepted_at__isnull=False, then=1 ), + default=Value(0), output_field=IntegerField() ) ) diff --git a/studygroups/models/learningcircle.py b/studygroups/models/learningcircle.py index 139be04cf..6a26b26a5 100644 --- a/studygroups/models/learningcircle.py +++ b/studygroups/models/learningcircle.py @@ -28,6 +28,9 @@ import uuid import random import string +import logging + +logger = logging.getLogger(__name__) # TODO remove organizer model - only use Facilitator model + Team Membership @@ -38,6 +41,12 @@ def __str__(self): return self.user.__str__() +class Facilitator(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + study_group = models.ForeignKey('studygroups.StudyGroup', on_delete=models.CASCADE) + added_at = models.DateTimeField(auto_now_add=True) + + class StudyGroupQuerySet(SoftDeleteQuerySet): def published(self): @@ -63,7 +72,7 @@ class StudyGroup(LifeTimeTrackingModel): longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) place_id = models.CharField(max_length=256, blank=True) # Algolia place_id online = models.BooleanField(default=False) # indicate if the meetings will take place online - facilitator = models.ForeignKey(User, on_delete=models.CASCADE) + created_by = models.ForeignKey(User, on_delete=models.CASCADE) start_date = models.DateField() # This field caches first_meeting.meeting_date meeting_time = models.TimeField() end_date = models.DateField() # This field caches last_meeting.meeting_date @@ -101,12 +110,12 @@ def save(self, *args, **kwargs): # use course.title if name is not set if self.name is None: self.name = self.course.title - super().save(*args, **kwargs) if created: - # if the facilitator is part of a team, set the team field - if self.facilitator.teammembership_set.active().count(): - self.team = self.facilitator.teammembership_set.active().first().team - self.save() + # if the creator is part of a team, set the team field + if self.created_by.teammembership_set.active().count(): + self.team = self.created_by.teammembership_set.active().first().team + super().save(*args, **kwargs) + def day(self): return calendar.day_name[self.start_date.weekday()] @@ -180,6 +189,15 @@ def feedback_status(self): return 'pending' return 'todo' + def facilitators_display(self): + facilitators = [f.user.first_name for f in self.facilitator_set.all()] + if not len(facilitators): + logger.error(f'Learning circle with no facilitators! pk={self.pk}') + return _('Unknown') + if len(facilitators) == 1: + return facilitators[0] + else: + return _('%(first)s and %(last)s') % {'first': ', '.join(facilitators[:-1]), 'last': facilitators[-1]} @property @@ -188,17 +206,25 @@ def weeks(self): def to_dict(self): sg = self # TODO - this logic is repeated in the API class + facilitators = [f.user.first_name for f in sg.facilitator_set.all()] + if not len(facilitators): + logger.error(f'Bad learning circle : {sg.pk}') + facilitators = ['Unknown'] + facilitators_legacy = ' and '.join(filter(lambda x: x, [', '.join(facilitators[:-1]), facilitators[-1]])) + data = { "id": sg.pk, "name": sg.name, - "course": sg.course.id, - "course_title": sg.course.title, - "description": sg.description, - "course_description": sg.course_description, + "facilitator": facilitators_legacy, + "facilitators": facilitators, "venue_name": sg.venue_name, "venue_details": sg.venue_details, "venue_address": sg.venue_address, "venue_website": sg.venue_website, + "course": sg.course.id, + "course_title": sg.course.title, + "course_description": sg.course_description, + "description": sg.description, "city": sg.city, "region": sg.region, "country": sg.country, @@ -208,22 +234,21 @@ def to_dict(self): "place_id": sg.place_id, "online": sg.online, "language": sg.language, + "day": sg.day(), "start_date": sg.start_date, "start_datetime": self.local_start_date(), - "weeks": sg.weeks, "meeting_time": sg.meeting_time.strftime('%H:%M'), - "duration": sg.duration, "timezone": sg.timezone, "timezone_display": sg.timezone_display(), + "end_time": sg.end_time(), + "duration": sg.duration, # not in API endpoint + "weeks": sg.weeks, + "url": reverse('studygroups_view_study_group', args=(sg.id,)), "signup_question": sg.signup_question, "facilitator_goal": sg.facilitator_goal, "facilitator_concerns": sg.facilitator_concerns, - "day": sg.day(), - "end_time": sg.end_time(), - "facilitator": sg.facilitator.first_name + " " + sg.facilitator.last_name, - "signup_count": sg.application_set.active().count(), "draft": sg.draft, - "url": reverse('studygroups_view_study_group', args=(sg.id,)), + "signup_count": sg.application_set.active().count(), "signup_url": reverse('studygroups_signup', args=(slugify(sg.venue_name, allow_unicode=True), sg.id,)), } next_meeting = self.next_meeting() @@ -480,11 +505,14 @@ def generate_meeting_reminder(meeting): reminder.study_group_meeting = meeting context = { - 'facilitator': meeting.study_group.facilitator, 'study_group': meeting.study_group, 'next_meeting': meeting, 'reminder': reminder, } + if meeting.study_group.facilitator_set.count() > 1: + context['facilitator_names'] = meeting.study_group.facilitators_display() + else: + context['facilitator_name'] = meeting.study_group.facilitators_display() timezone.activate(pytz.timezone(meeting.study_group.timezone)) with use_language(meeting.study_group.language): reminder.email_subject = render_to_string_ctx( diff --git a/studygroups/models/team.py b/studygroups/models/team.py index 0d5dddd19..491b644df 100644 --- a/studygroups/models/team.py +++ b/studygroups/models/team.py @@ -7,11 +7,11 @@ from django.utils.timezone import now from django_bleach.models import BleachField - from .base import LifeTimeTrackingModel import uuid + class Team(models.Model): name = models.CharField(max_length=128) subtitle = models.CharField(max_length=256, default=_('Join your neighbors to learn something together. Learning circles meet weekly for 6-8 weeks, and are free to join.')) @@ -65,6 +65,14 @@ class TeamMembership(LifeTimeTrackingModel): def __str__(self): return 'Team membership: {}'.format(self.user.__str__()) + + def to_dict(self): + return { + 'id': self.user.pk, + 'email': self.user.email, + 'firstName': self.user.first_name, + 'lastName': self.user.last_name + } class TeamInvitation(models.Model): @@ -83,9 +91,9 @@ def __str__(self): def get_study_group_organizers(study_group): """ Return the organizers for the study group """ - team_membership = TeamMembership.objects.active().filter(user=study_group.facilitator) - if team_membership.count() == 1: - organizers = team_membership.first().team.teammembership_set.active().filter(role=TeamMembership.ORGANIZER).values('user') + team = study_group.team + if team: + organizers = team.teammembership_set.active().filter(role=TeamMembership.ORGANIZER).values('user') return User.objects.filter(pk__in=organizers) return [] diff --git a/studygroups/signals.py b/studygroups/signals.py index 106cace39..1bfa076f1 100644 --- a/studygroups/signals.py +++ b/studygroups/signals.py @@ -4,6 +4,7 @@ from django.core.mail import EmailMultiAlternatives, send_mail from django.conf import settings from django.utils import timezone +from django.utils.translation import ugettext as _ from studygroups.email_helper import render_html_with_css @@ -43,23 +44,33 @@ def handle_new_application(sender, instance, created, **kwargs): } ).strip('\n') + facilitators = [f'{f.user.first_name} {f.user.last_name}' for f in application.study_group.facilitator_set.all()] + if len(facilitators) == 0: + names = _('Unkown') + elif len(facilitators) == 1: + names = facilitators[0] + else: + names = _('%(first)s and %(last)s') % {'first': ', '.join(facilitators[:-1]), 'last': facilitators[-1]} + learner_signup_html = render_html_with_css( 'studygroups/email/learner_signup.html', { 'application': application, 'advice': advice, + 'facilitator_first_last_names': names, } ) learner_signup_body = html_body_to_text(learner_signup_html) to = [application.email] # CC facilitator and put in reply-to + facilitator_emails = set(application.study_group.facilitator_set.all().values_list('user__email', flat=True)) welcome_message = EmailMultiAlternatives( learner_signup_subject, learner_signup_body, settings.DEFAULT_FROM_EMAIL, to, - cc=[application.study_group.facilitator.email], - reply_to=[application.study_group.facilitator.email] + cc=facilitator_emails, + reply_to=facilitator_emails ) welcome_message.attach_alternative(learner_signup_html, 'text/html') welcome_message.send() @@ -90,9 +101,9 @@ def handle_new_study_group_creation(sender, instance, created, **kwargs): subject, text_body, settings.DEFAULT_FROM_EMAIL, - [study_group.facilitator.email], + [study_group.created_by.email], cc=cc, - reply_to=[study_group.facilitator.email] + cc + reply_to=[study_group.created_by.email] + cc ) notification.attach_alternative(html_body, 'text/html') notification.send() diff --git a/studygroups/tasks.py b/studygroups/tasks.py index cf6408d47..765442c37 100644 --- a/studygroups/tasks.py +++ b/studygroups/tasks.py @@ -35,15 +35,13 @@ def _send_facilitator_survey(study_group): - facilitator_name = study_group.facilitator.first_name path = reverse('studygroups_facilitator_survey', kwargs={'study_group_uuid': study_group.uuid}) base_url = f'{settings.PROTOCOL}://{settings.DOMAIN}' survey_url = base_url + path context = { 'study_group': study_group, - 'facilitator': study_group.facilitator, - 'facilitator_name': facilitator_name, + 'show_dash_link': True, 'survey_url': survey_url, 'course_title': study_group.course.title, 'study_group_name': study_group.name, @@ -54,7 +52,7 @@ def _send_facilitator_survey(study_group): 'studygroups/email/facilitator_survey', context ) - to = [study_group.facilitator.email] + to = [f.user.email for f in study_group.facilitator_set.all()] cc = [settings.DEFAULT_FROM_EMAIL] message = EmailMultiAlternatives( @@ -99,7 +97,6 @@ def _send_learner_survey(application): ) querystring = '?learner={}'.format(application.uuid) survey_url = base_url + path + querystring - facilitator_email = application.study_group.facilitator.email context = { 'learner_name': application.name, @@ -118,7 +115,7 @@ def _send_learner_survey(application): txt, settings.DEFAULT_FROM_EMAIL, to, - reply_to=[facilitator_email] + reply_to=[facilitator.user.email for facilitator in application.study_group.facilitator_set.all()] ) notification.attach_alternative(html, 'text/html') notification.send() @@ -183,7 +180,7 @@ def send_meeting_reminder(reminder): text_body, sender, [email], - reply_to=[reminder.study_group.facilitator.email] + reply_to=[facilitator.user.email for facilitator in reminder.study_group.facilitator_set.all()] ) reminder_email.attach_alternative(html_body, 'text/html') # attach icalendar event @@ -195,7 +192,9 @@ def send_meeting_reminder(reminder): reminder_email.attach(part) reminder_email.send() except Exception as e: + # TODO - this swallows any exception in the code logger.exception('Could not send email to ', email, exc_info=e) + # Send to facilitator without RSVP & unsubscribe links try: base_url = f'{settings.PROTOCOL}://{settings.DOMAIN}' @@ -205,7 +204,8 @@ def send_meeting_reminder(reminder): email_body = re.sub(r'RSVP_NO_LINK', dashboard_link, email_body) context = { - "facilitator": reminder.study_group.facilitator, + "facilitator_names": reminder.study_group.facilitators_display(), + "show_dash_link": True, "reminder": reminder, "message": email_body, } @@ -218,13 +218,13 @@ def send_meeting_reminder(reminder): subject, text_body, sender, - [reminder.study_group.facilitator.email] + [facilitator.user.email for facilitator in reminder.study_group.facilitator_set.all()] ) reminder_email.attach_alternative(html_body, 'text/html') reminder_email.send() except Exception as e: - logger.exception('Could not send email to ', reminder.study_group.facilitator.email, exc_info=e) + logger.exception('Could not send email to facilitator', exc_info=e) # TODO - Exception masks other errors! # If called directly, be sure to activate language to use for constructing URLs @@ -253,7 +253,7 @@ def send_reminder(reminder): context ) text_body = html_body_to_text(html_body) - to += [reminder.study_group.facilitator.email] + to += [facilitator.user.email for facilitator in reminder.study_group.facilitator_set.all()] sender = 'P2PU <{0}>'.format(settings.DEFAULT_FROM_EMAIL) try: reminder_email = EmailMultiAlternatives( @@ -262,7 +262,7 @@ def send_reminder(reminder): sender, [], bcc=to, - reply_to=[reminder.study_group.facilitator.email], + reply_to=[facilitator.user.email for facilitator in reminder.study_group.facilitator_set.all()] ) reminder_email.attach_alternative(html_body, 'text/html') reminder_email.send() @@ -295,7 +295,7 @@ def _send_meeting_wrapup(meeting): subject, text_body, settings.DEFAULT_FROM_EMAIL, - to=[study_group.facilitator.email], + to=[facilitator.user.email for facilitator in study_group.facilitator_set.all()] ) message.attach_alternative(html_body, 'text/html') try: @@ -566,3 +566,52 @@ def anonymize_signups(): for application in applications: application.anonymize() + + +@shared_task +def send_cofacilitator_email(study_group_id, user_id, actor_user_id): + user = User.objects.get(pk=user_id) + actor = User.objects.get(pk=actor_user_id) + context = { + "study_group": StudyGroup.objects.get(pk=study_group_id), + "facilitator": user, + "actor": actor, + } + subject = render_to_string_ctx('studygroups/email/facilitator_added-subject.txt', context).strip('\n') + html_body = render_html_with_css('studygroups/email/facilitator_added.html', context) + text_body = html_body_to_text(html_body) + to = [user.email] + + msg = EmailMultiAlternatives( + subject, + text_body, + settings.DEFAULT_FROM_EMAIL, + to, + reply_to=[actor.email]) + msg.attach_alternative(html_body, 'text/html') + msg.send() + + +@shared_task +def send_cofacilitator_removed_email(study_group_id, user_id, actor_user_id): + user = User.objects.get(pk=user_id) + actor = User.objects.get(pk=actor_user_id) + context = { + "study_group": StudyGroup.objects.get(pk=study_group_id), + "facilitator": user, + "actor": actor, + } + subject = render_to_string_ctx('studygroups/email/facilitator_removed-subject.txt', context).strip('\n') + html_body = render_html_with_css('studygroups/email/facilitator_removed.html', context) + text_body = html_body_to_text(html_body) + to = [user.email] + + msg = EmailMultiAlternatives( + subject, + text_body, + settings.DEFAULT_FROM_EMAIL, + to, + reply_to=[actor.email] + ) + msg.attach_alternative(html_body, 'text/html') + msg.send() diff --git a/studygroups/tests/api/test_course_api.py b/studygroups/tests/api/test_course_api.py index 553f69699..0132e05e2 100644 --- a/studygroups/tests/api/test_course_api.py +++ b/studygroups/tests/api/test_course_api.py @@ -177,7 +177,7 @@ def test_team_unlisted(self): # create team with 2 users organizer = create_user('organ@team.com', 'organ', 'test', '1234', False) faci1 = create_user('faci1@team.com', 'faci1', 'test', '1234', False) - StudyGroup.objects.filter(pk=1).update(facilitator=faci1) + StudyGroup.objects.filter(pk=1).update(created_by=faci1) mail.outbox = [] # create team diff --git a/studygroups/tests/api/test_feedback_api.py b/studygroups/tests/api/test_feedback_api.py index b1212d097..4a881a90b 100644 --- a/studygroups/tests/api/test_feedback_api.py +++ b/studygroups/tests/api/test_feedback_api.py @@ -15,6 +15,7 @@ from studygroups.models import TeamMembership from studygroups.models import Meeting from studygroups.models import Feedback +from studygroups.models import Facilitator from studygroups.views import LearningCircleListView from custom_registration.models import create_user from django.contrib.auth.models import User @@ -34,8 +35,9 @@ def setUp(self): user.save() self.facilitator = user sg = StudyGroup.objects.get(pk=1) - sg.facilitator = user + sg.created_by = user sg.save() + Facilitator.objects.create(study_group=sg, user=user) meeting = Meeting() meeting.study_group = sg diff --git a/studygroups/tests/api/test_landing_page_api.py b/studygroups/tests/api/test_landing_page_api.py index c2282be6c..918be89df 100644 --- a/studygroups/tests/api/test_landing_page_api.py +++ b/studygroups/tests/api/test_landing_page_api.py @@ -23,50 +23,6 @@ class TestLandingPageApi(TestCase): fixtures = ['test_courses.json', 'test_studygroups.json'] - def test_landing_page_learning_circles(self): - c = Client() - meeting_1 = Meeting.objects.create( - study_group_id=1, - meeting_date=datetime.date(2017,10,25), - meeting_time=datetime.time(17,30), - ) - meeting_2 = Meeting.objects.create( - study_group_id=2, - meeting_date=datetime.date(2017,10,26), - meeting_time=datetime.time(17,30), - ) - meeting_3 = Meeting.objects.create( - study_group_id=3, - meeting_date=datetime.date(2017,10,27), - meeting_time=datetime.time(17,30), - ) - meeting_4 = Meeting.objects.create( - study_group_id=4, - meeting_date=datetime.date(2017,10,31), - meeting_time=datetime.time(17,30), - ) - - with freeze_time("2017-10-24 17:55:34"): - resp = c.get('/api/landing-page-learning-circles/') - self.assertEqual(resp.status_code, 200) - self.assertEqual(len(resp.json()["items"]), 3) - - with freeze_time("2017-10-25 17:55:34"): - resp = c.get('/api/landing-page-learning-circles/') - self.assertEqual(resp.status_code, 200) - self.assertEqual(len(resp.json()["items"]), 3) - - with freeze_time("2017-10-31 17:55:34"): - resp = c.get('/api/landing-page-learning-circles/') - self.assertEqual(resp.status_code, 200) - self.assertEqual(len(resp.json()["items"]), 3) - - with freeze_time("2017-11-30 17:55:34"): - resp = c.get('/api/landing-page-learning-circles/') - self.assertEqual(resp.status_code, 200) - self.assertEqual(len(resp.json()["items"]), 3) - - def test_learning_circles_map_view(self): c = Client() diff --git a/studygroups/tests/api/test_learning_circle_api.py b/studygroups/tests/api/test_learning_circle_api.py index f6c01f704..86d8644f9 100644 --- a/studygroups/tests/api/test_learning_circle_api.py +++ b/studygroups/tests/api/test_learning_circle_api.py @@ -9,6 +9,7 @@ from freezegun import freeze_time from studygroups.models import StudyGroup +from studygroups.models import Facilitator from studygroups.models import Profile from studygroups.models import Course from studygroups.models import generate_all_meetings @@ -29,7 +30,7 @@ class TestLearningCircleApi(TestCase): def setUp(self): with patch('custom_registration.signals.send_email_confirm_email'): - user = create_user('faci@example.net', 'b', 't', 'password', False) + user = create_user('faci@example.net', 'Bobjanechris', 'Trailer', 'password', False) user.save() self.facilitator = user @@ -80,6 +81,8 @@ def test_create_learning_circle(self): "studygroup_url": "{}://{}/en/studygroup/{}/".format(settings.PROTOCOL, settings.DOMAIN, lc.pk) }) self.assertEqual(StudyGroup.objects.all().count(), 5) + self.assertEqual(lc.facilitator_set.all().count(), 1) + self.assertEqual(lc.facilitator_set.first().user_id, lc.created_by_id) self.assertEqual(lc.course.id, 3) self.assertEqual(lc.name, "Test learning circle") self.assertEqual(lc.description, 'Lets learn something') @@ -93,6 +96,76 @@ def test_create_learning_circle(self): self.assertIn('community@localhost', mail.outbox[0].cc) + def test_create_learning_circle_with_facilitator_set(self): + cofacilitator = create_user('cofaci@example.net', 'ba', 'ta', 'password', False) + c = Client() + c.login(username='faci@example.net', password='password') + data = { + "name": "Test learning circle", + "course": 3, + "description": "Lets learn something", + "course_description": "A real great course", + "venue_name": "75 Harrington", + "venue_details": "top floor", + "venue_address": "75 Harrington", + "city": "Cape Town", + "country": "South Africa", + "country_en": "South Africa", + "region": "Western Cape", + "latitude": 3.1, + "longitude": "1.3", + "place_id": "1", + "online": "false", + "language": "en", + "meetings": [ + { "meeting_date": "2018-02-12", "meeting_time": "17:01" }, + { "meeting_date": "2018-02-19", "meeting_time": "17:01" }, + ], + "meeting_time": "17:01", + "duration": 50, + "timezone": "UTC", + "image": "/media/image.png", + "facilitator_concerns": "blah blah", + "facilitators": [cofacilitator.pk], + } + url = '/api/learning-circle/' + self.assertEqual(StudyGroup.objects.all().count(), 4) + + with patch('studygroups.views.api.send_cofacilitator_email.delay') as send_cofacilitator_email: + resp = c.post(url, data=json.dumps(data), content_type='application/json') + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.json(), { + "status": "error", + "errors": { + "facilitators": ["Facilitator not part of a team"], + } + }) + + team = Team.objects.create(name='awesome team') + TeamMembership.objects.create(team=team, user=self.facilitator, role=TeamMembership.ORGANIZER) + resp = c.post(url, data=json.dumps(data), content_type='application/json') + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.json(), { + "status": "error", + "errors": { + "facilitators": ["Facilitators not part of the same team"], + } + }) + + TeamMembership.objects.create(team=team, user=cofacilitator, role=TeamMembership.MEMBER) + resp = c.post(url, data=json.dumps(data), content_type='application/json') + self.assertEqual(resp.status_code, 200) + lc = StudyGroup.objects.all().last() + self.assertEqual(resp.json(), { + "status": "created", + "studygroup_url": "{}://{}/en/studygroup/{}/".format(settings.PROTOCOL, settings.DOMAIN, lc.pk) + }) + self.assertEqual(StudyGroup.objects.all().count(), 5) + self.assertEqual(lc.facilitator_set.all().count(), 2) + self.assertIn(cofacilitator.id, lc.facilitator_set.all().values_list('user_id', flat=True)) + self.assertEqual(len(mail.outbox), 2) + + def test_create_learning_circle_without_name_or_course_description(self): c = Client() c.login(username='faci@example.net', password='password') @@ -173,6 +246,7 @@ def test_create_learning_circle_and_publish(self): } url = '/api/learning-circle/' self.assertEqual(StudyGroup.objects.all().count(), 4) + self.assertEqual(len(mail.outbox), 0) resp = c.post(url, data=json.dumps(data), content_type='application/json') self.assertEqual(resp.status_code, 200) lc = StudyGroup.objects.all().last() @@ -181,6 +255,7 @@ def test_create_learning_circle_and_publish(self): "studygroup_url": "{}://{}/en/studygroup/{}/".format(settings.PROTOCOL, settings.DOMAIN, lc.pk) }) self.assertEqual(StudyGroup.objects.all().count(), 5) + self.assertEqual(lc.facilitator_set.count(), 1) self.assertEqual(lc.course.id, 3) self.assertEqual(lc.draft, False) self.assertEqual(lc.name, "Test learning circle") @@ -189,10 +264,13 @@ def test_create_learning_circle_and_publish(self): self.assertEqual(lc.start_date, datetime.date(2018,2,12)) self.assertEqual(lc.meeting_time, datetime.time(17,1)) self.assertEqual(lc.meeting_set.all().count(), 2) + self.assertEqual(lc.reminder_set.count(), 2) self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].subject, 'Your “{}” learning circle in {} has been created!'.format(lc.name, lc.city)) self.assertIn('faci@example.net', mail.outbox[0].to) self.assertIn('community@localhost', mail.outbox[0].cc) + # TODO test that correct faciltators are mentioned in reminders + @freeze_time('2018-01-20') @@ -381,6 +459,7 @@ def test_update_learning_circle(self): data['course'] = 1 data["description"] = "Lets learn something else" data["name"] = "A new LC name" + data["facilitators"] = [f.user_id for f in lc.facilitator_set.all()] # date shouldn't matter, but lets make it after the lc started with freeze_time('2019-03-01'): @@ -440,6 +519,7 @@ def test_update_learning_circle_date(self): }) self.assertEqual(StudyGroup.objects.all().count(), 5) self.assertEqual(lc.meeting_set.active().count(), 2) + data["facilitators"] = [f.user_id for f in lc.facilitator_set.all()] # update more than 2 days before start @@ -505,6 +585,7 @@ def test_update_draft_learning_circle_date(self): }) self.assertEqual(StudyGroup.objects.all().count(), 5) self.assertEqual(lc.meeting_set.active().count(), 2) + data["facilitators"] = [f.user_id for f in lc.facilitator_set.all()] # update less than 2 days before with freeze_time("2018-12-14"): @@ -542,6 +623,130 @@ def test_update_draft_learning_circle_date(self): self.assertEqual(lc.meeting_set.active().count(), 2) + + def test_update_learning_circle_facilitators(self): + cofacilitator = create_user('cofaci@example.net', 'badumorum', 'ta', 'password', False) + + self.facilitator.profile.email_confirmed_at = timezone.now() + self.facilitator.profile.save() + c = Client() + c.login(username='faci@example.net', password='password') + data = { + "course": 3, + "description": "Lets learn something", + "course_description": "A real great course", + "venue_name": "75 Harrington", + "venue_details": "top floor", + "venue_address": "75 Harrington", + "city": "Cape Town", + "country": "South Africa", + "country_en": "South Africa", + "region": "Western Cape", + "latitude": 3.1, + "longitude": "1.3", + "place_id": "4", + "online": "false", + "language": "en", + "meeting_time": "17:01", + "duration": 50, + "timezone": "UTC", + "image": "/media/image.png", + "draft": False, + "meetings": [ + { "meeting_date": "2018-02-12", "meeting_time": "17:01" }, + { "meeting_date": "2018-02-19", "meeting_time": "17:01" }, + ], + } + url = '/api/learning-circle/' + self.assertEqual(StudyGroup.objects.all().count(), 4) + + resp = c.post(url, data=json.dumps(data), content_type='application/json') + self.assertEqual(resp.status_code, 200) + lc = StudyGroup.objects.all().last() + self.assertEqual(resp.json(), { + "status": "created", + "studygroup_url": "{}://{}/en/studygroup/{}/".format(settings.PROTOCOL, settings.DOMAIN, lc.pk) + }) + self.assertEqual(StudyGroup.objects.all().count(), 5) + + # Update learning circle + lc = StudyGroup.objects.all().last() + self.assertFalse(lc.draft) + url = '/api/learning-circle/{}/'.format(lc.pk) + data["facilitators"] = [self.facilitator.pk, cofacilitator.pk] + + with patch('studygroups.views.api.send_cofacilitator_email.delay') as send_cofacilitator_email: + resp = c.post(url, data=json.dumps(data), content_type='application/json') + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.json(), { + "status": "error", + "errors": { + "facilitators": ["Facilitator not part of a team"], + } + }) + + team = Team.objects.create(name='Team Awesome') + lc.team = team + lc.save() + TeamMembership.objects.create(team=team, user=self.facilitator) + resp = c.post(url, data=json.dumps(data), content_type='application/json') + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.json(), { + "status": "error", + "errors": { + "facilitators": ["Facilitators not part of the same team"], + } + }) + + TeamMembership.objects.create(team=team, user=cofacilitator) + resp = c.post(url, data=json.dumps(data), content_type='application/json') + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.json()['status'], 'updated') + self.assertTrue(send_cofacilitator_email.called) + + self.assertIn(self.facilitator.first_name, lc.reminder_set.first().email_body) + self.assertIn(cofacilitator.first_name, lc.reminder_set.first().email_body) + + c = Client() + c.login(username='cofaci@example.net', password='password') + resp = c.post(url, data=json.dumps(data), content_type='application/json') + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.json()['status'], 'updated') + + c = Client() + c.login(username='faci@example.net', password='password') + + data["facilitators"] = [] + resp = c.post(url, data=json.dumps(data), content_type='application/json') + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.json(), { + "status": "error", + "errors": { + "facilitators": ["Cannot remove all faclitators from a learning circle"], + } + }) + + with patch('studygroups.views.api.send_cofacilitator_removed_email.delay') as send_cofacilitator_removed_email: + data["facilitators"] = [cofacilitator.id] + resp = c.post(url, data=json.dumps(data), content_type='application/json') + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.json()['status'], 'updated') + self.assertTrue(send_cofacilitator_removed_email.called) + + self.assertNotIn(self.facilitator.first_name, lc.reminder_set.first().email_body) + self.assertIn(cofacilitator.first_name, lc.reminder_set.first().email_body) + + resp = c.post(url, data=json.dumps(data), content_type='application/json') + self.assertEqual(resp.status_code, 403) + + c = Client() + c.login(username='cofaci@example.net', password='password') + resp = c.post(url, data=json.dumps(data), content_type='application/json') + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.json()['status'], 'updated') + + + @freeze_time('2018-01-20') def test_publish_learning_circle(self): self.facilitator.profile.email_confirmed_at = timezone.now() @@ -585,6 +790,7 @@ def test_publish_learning_circle(self): self.assertEqual(StudyGroup.objects.all().count(), 5) self.assertEqual(lc.meeting_set.all().count(), 2) data['draft'] = False + data['facilitators'] = [lc.created_by_id] # Update learning circle url = '/api/learning-circle/{}/'.format(lc.pk) resp = c.post(url, data=json.dumps(data), content_type='application/json') @@ -1083,7 +1289,7 @@ def test_get_learning_circles_by_team(self): team = facilitator2.teammembership_set.active().first().team sgdata = dict( course=Course.objects.first(), - facilitator=facilitator2, + created_by=facilitator2, description='blah', venue_name='ACME public library', venue_address='ACME rd 1', @@ -1160,8 +1366,9 @@ def test_get_learning_circles_by_user(self): request = factory.get('/api/learningcircles/?user=true') user = self.facilitator sg = StudyGroup.objects.get(pk=2) - sg.facilitator = user + sg.created_by = user sg.save() + Facilitator.objects.create(study_group=sg, user=user) request.user = user diff --git a/studygroups/tests/api/test_teams_api.py b/studygroups/tests/api/test_teams_api.py index 9fa89d6e5..ffbc9958c 100644 --- a/studygroups/tests/api/test_teams_api.py +++ b/studygroups/tests/api/test_teams_api.py @@ -85,7 +85,7 @@ def test_team_data(self): team = Team.objects.get(pk=1) organizer = User.objects.get(pk=1) - organizer_studygroups_count = StudyGroup.objects.filter(facilitator=organizer).count() + organizer_studygroups_count = StudyGroup.objects.filter(created_by=organizer).count() self.assertEqual(team_json["member_count"], team.teammembership_set.active().count()) self.assertEqual(team_json["facilitators"][0]["first_name"], organizer.first_name) diff --git a/studygroups/tests/test_facilitator_views.py b/studygroups/tests/test_facilitator_views.py index 8da957d44..68261d6af 100644 --- a/studygroups/tests/test_facilitator_views.py +++ b/studygroups/tests/test_facilitator_views.py @@ -12,6 +12,7 @@ from freezegun import freeze_time from studygroups.models import Course +from studygroups.models import Facilitator from studygroups.models import StudyGroup from studygroups.models import Meeting from studygroups.models import Application @@ -126,8 +127,9 @@ def assertForbidden(url): def test_facilitator_access(self): user = create_user('bob@example.net', 'bob', 'test', 'password') sg = StudyGroup.objects.get(pk=1) - sg.facilitator = user + sg.created_by = user sg.save() + Facilitator.objects.create(study_group=sg, user=user) c = Client() c.login(username='bob@example.net', password='password') def assertAllowed(url): @@ -161,7 +163,7 @@ def test_create_study_group(self): resp = c.post('/en/studygroup/create/legacy/', data) sg = StudyGroup.objects.last() self.assertRedirects(resp, '/en/studygroup/{}/'.format(sg.pk)) - study_groups = StudyGroup.objects.filter(facilitator=user) + study_groups = StudyGroup.objects.filter(created_by=user) self.assertEquals(study_groups.count(), 1) lc = study_groups.first() self.assertEquals(study_groups.first().meeting_set.count(), 6) @@ -182,7 +184,7 @@ def test_publish_study_group(self, handle_new_facilitator): with freeze_time('2018-07-20'): resp = c.post('/api/learning-circle/', data=json.dumps(self.STUDY_GROUP_DATA), content_type='application/json') self.assertEqual(resp.json()['status'], 'created') - study_groups = StudyGroup.objects.filter(facilitator=user) + study_groups = StudyGroup.objects.filter(created_by=user) self.assertEqual(study_groups.count(), 1) lc = study_groups.first() self.assertEqual(lc.meeting_set.count(), 6) @@ -202,7 +204,7 @@ def test_publish_study_group_email_unconfirmed(self, handle_new_facilitator): with freeze_time('2018-07-20'): resp = c.post('/api/learning-circle/', data=json.dumps(self.STUDY_GROUP_DATA), content_type='application/json') self.assertEqual(resp.json()['status'], 'created') - study_groups = StudyGroup.objects.filter(facilitator=user) + study_groups = StudyGroup.objects.filter(created_by=user) self.assertEqual(study_groups.count(), 1) lc = study_groups.first() resp = c.post('/en/studygroup/{0}/publish/'.format(lc.pk)) @@ -221,7 +223,7 @@ def test_draft_study_group_actions_disabled(self, handle_new_facilitator): with freeze_time('2018-07-20'): resp = c.post('/api/learning-circle/', data=json.dumps(self.STUDY_GROUP_DATA), content_type='application/json') self.assertEqual(resp.json()['status'], 'created') - study_groups = StudyGroup.objects.filter(facilitator=user) + study_groups = StudyGroup.objects.filter(created_by=user) self.assertEqual(study_groups.count(), 1) self.assertEqual(study_groups.first().meeting_set.count(), 6) @@ -279,7 +281,7 @@ def test_update_study_group_legacy_view(self): resp = c.post('/en/studygroup/create/legacy/', data) sg = StudyGroup.objects.last() self.assertRedirects(resp, '/en/studygroup/{}/'.format(sg.pk)) - study_groups = StudyGroup.objects.filter(facilitator=user) + study_groups = StudyGroup.objects.filter(created_by=user) self.assertEquals(study_groups.count(), 1) lc = study_groups.first() self.assertEquals(study_groups.first().meeting_set.active().count(), 6) @@ -355,7 +357,7 @@ def test_study_group_unicode_venue_name(self, handle_new_facilitator): sgd['start_date'] = (datetime.datetime.now() + datetime.timedelta(weeks=2)).date().isoformat() resp = c.post('/api/learning-circle/', data=json.dumps(sgd), content_type='application/json') self.assertEqual(resp.json()['status'], 'created') - study_groups = StudyGroup.objects.filter(facilitator=user) + study_groups = StudyGroup.objects.filter(created_by=user) study_group = study_groups.first() self.assertEqual(study_groups.count(), 1) self.assertEqual(study_group.meeting_set.count(), 6) @@ -377,7 +379,7 @@ def test_create_study_group_venue_name_validation(self, handle_new_facilitator): with freeze_time('2019-07-20'): resp = c.post('/en/studygroup/create/legacy/', data) self.assertEquals(resp.status_code, 200) - study_groups = StudyGroup.objects.filter(facilitator=user) + study_groups = StudyGroup.objects.filter(created_by=user) self.assertEquals(study_groups.count(), 0) @@ -391,7 +393,7 @@ def test_edit_meeting(self, current_app): with freeze_time('2018-07-20'): resp = c.post('/api/learning-circle/', data=json.dumps(self.STUDY_GROUP_DATA), content_type='application/json') self.assertEqual(resp.json()['status'], 'created') - study_groups = StudyGroup.objects.filter(facilitator=user) + study_groups = StudyGroup.objects.filter(created_by=user) self.assertEqual(study_groups.count(), 1) lc = study_groups.first() self.assertEqual(lc.meeting_set.count(), 6) @@ -606,7 +608,7 @@ def test_dont_send_blank_sms(self, send_message): def test_user_accept_invitation(self): organizer = create_user('organ@team.com', 'organ', 'test', '1234', False) faci1 = create_user('faci1@team.com', 'faci1', 'test', '1234', False) - StudyGroup.objects.filter(pk=1).update(facilitator=faci1) + StudyGroup.objects.filter(pk=1).update(created_by=faci1) # create team team = Team.objects.create(name='test team') @@ -629,7 +631,7 @@ def test_user_accept_invitation(self): def test_user_reject_invitation(self): organizer = create_user('organ@team.com', 'organ', 'test', '1234', False) faci1 = create_user('faci1@team.com', 'faci1', 'test', '1234', False) - StudyGroup.objects.filter(pk=1).update(facilitator=faci1) + StudyGroup.objects.filter(pk=1).update(created_by=faci1) # create team team = Team.objects.create(name='test team') @@ -712,7 +714,7 @@ def test_cant_edit_used_course(self): course = Course.objects.create(**course_data) sg = StudyGroup.objects.get(pk=1) sg.course = course - sg.facilitator = user2 + sg.created_by = user2 sg.save() c = Client() c.login(username='bob@example.net', password='password') @@ -741,7 +743,7 @@ def test_study_group_facilitator_survey(self): course = Course.objects.create(**course_data) sg = StudyGroup.objects.get(pk=1) sg.course = course - sg.facilitator = facilitator + sg.created_by = facilitator sg.save() c = Client() c.login(username='hi@example.net', password='password') diff --git a/studygroups/tests/test_learner_views.py b/studygroups/tests/test_learner_views.py index 100dee23f..107eeba3b 100644 --- a/studygroups/tests/test_learner_views.py +++ b/studygroups/tests/test_learner_views.py @@ -10,6 +10,7 @@ from unittest.mock import patch from studygroups.models import StudyGroup +from studygroups.models import Facilitator from studygroups.models import Meeting from studygroups.models import Application from studygroups.models import Rsvp @@ -78,7 +79,7 @@ def test_application_welcome_message(self): # Make sure notification was sent self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].to[0], self.APPLICATION_DATA['email']) - self.assertEqual(mail.outbox[0].cc[0], study_group.facilitator.email) + self.assertEqual(mail.outbox[0].cc[0], study_group.created_by.email) self.assertIn('The first meeting will be on Monday, 23 March at 6:30 p.m.', mail.outbox[0].body) @@ -113,8 +114,9 @@ def test_update_application_bug_564(self): facilitator = create_user('hi@example.net', 'bowie', 'wowie', 'password') mail.outbox = [] sg = StudyGroup.objects.get(pk=1) - sg.facilitator = facilitator + sg.created_by = facilitator sg.save() + Facilitator.objects.create(study_group=sg, user=facilitator) c = Client() c.login(username='hi@example.net', password='password') user1 = {'study_group': sg.pk, 'name': 'bob', 'email': 'bob@mail.com', 'mobile': '+27112223333'} @@ -147,8 +149,9 @@ def test_update_application_bug_564_2(self): facilitator = create_user('hi@example.net', 'bowie', 'wowie', 'password') mail.outbox = [] sg = StudyGroup.objects.get(pk=1) - sg.facilitator = facilitator + sg.created_by = facilitator sg.save() + Facilitator.objects.create(study_group=sg, user=facilitator) c = Client() c.login(username='hi@example.net', password='password') user1 = {'study_group': sg.pk, 'name': 'bob', 'mobile': '+27112223333'} @@ -260,7 +263,7 @@ def test_receive_sms(self): self.assertEqual(len(mail.outbox), 1) self.assertTrue(mail.outbox[0].subject.find(signup_data['name']) > 0) self.assertTrue(mail.outbox[0].subject.find(signup_data['mobile']) > 0) - self.assertIn(StudyGroup.objects.get(pk=1).facilitator.email, mail.outbox[0].to) + self.assertIn(StudyGroup.objects.get(pk=1).created_by.email, mail.outbox[0].to) self.assertIn('admin@localhost', mail.outbox[0].bcc) mail.outbox = [] @@ -269,7 +272,7 @@ def test_receive_sms(self): self.assertEqual(len(mail.outbox), 1) self.assertTrue(mail.outbox[0].subject.find(signup_data['mobile']) > 0) - self.assertNotIn(StudyGroup.objects.get(pk=1).facilitator.email, mail.outbox[0].to) + self.assertNotIn(StudyGroup.objects.get(pk=1).created_by.email, mail.outbox[0].to) self.assertIn('admin@localhost', mail.outbox[0].to) @@ -297,7 +300,7 @@ def test_receive_sms_rsvp(self): self.assertEqual(len(mail.outbox), 1) self.assertTrue(mail.outbox[0].subject.find('+12812347890') > 0) self.assertTrue(mail.outbox[0].subject.find('Test User') > 0) - self.assertIn(StudyGroup.objects.get(pk=1).facilitator.email, mail.outbox[0].to) + self.assertIn(StudyGroup.objects.get(pk=1).created_by.email, mail.outbox[0].to) self.assertIn('{0}/{1}/rsvp/?user=%2B12812347890&study_group=1&meeting_date={2}&attending=yes&sig='.format(settings.DOMAIN, get_language(), urllib.parse.quote(next_meeting.meeting_datetime().isoformat())), mail.outbox[0].body) self.assertIn('{0}/{1}/rsvp/?user=%2B12812347890&study_group=1&meeting_date={2}&attending=no&sig='.format(settings.DOMAIN, get_language(), urllib.parse.quote(next_meeting.meeting_datetime().isoformat())), mail.outbox[0].body) diff --git a/studygroups/tests/test_models.py b/studygroups/tests/test_models.py index 77fd84986..de69ab535 100644 --- a/studygroups/tests/test_models.py +++ b/studygroups/tests/test_models.py @@ -209,7 +209,7 @@ def test_new_study_group_email(self): mail.outbox = [] sg = StudyGroup( course=Course.objects.first(), - facilitator=facilitator, + created_by=facilitator, description='blah', venue_name='ACME publich library', venue_address='ACME rd 1', diff --git a/studygroups/tests/test_organizer_views.py b/studygroups/tests/test_organizer_views.py index 492c63a69..56f3bd498 100644 --- a/studygroups/tests/test_organizer_views.py +++ b/studygroups/tests/test_organizer_views.py @@ -51,7 +51,8 @@ def test_organizer_access(self): TeamMembership.objects.create(team=team, user=faci1, role=TeamMembership.MEMBER) sg = StudyGroup.objects.get(pk=1) - sg.facilitator = faci1 + sg.created_by = faci1 + sg.team = team sg.save() c = Client() @@ -136,14 +137,6 @@ def test_organizer_dash(self): organizer = create_user('organ@team.com', 'organ', 'test', 'password', False) faci1 = create_user('faci1@team.com', 'faci1', 'test', 'password', False) faci2 = create_user('faci2@team.com', 'faci2', 'test', 'password', False) - - sg = StudyGroup.objects.get(pk=1) - sg.facilitator = faci1 - sg.save() - - sg = StudyGroup.objects.get(pk=2) - sg.facilitator = faci2 - sg.save() # create team team = Team.objects.create(name='test team') @@ -151,6 +144,16 @@ def test_organizer_dash(self): TeamMembership.objects.create(team=team, user=faci1, role=TeamMembership.MEMBER) TeamMembership.objects.create(team=team, user=faci2, role=TeamMembership.MEMBER) + sg = StudyGroup.objects.get(pk=1) + sg.created_by = faci1 + sg.team = team + sg.save() + + sg = StudyGroup.objects.get(pk=2) + sg.created_by = faci2 + sg.team = team + sg.save() + c = Client() c.login(username='organ@team.com', password='password') resp = c.get('/en/organize/') @@ -181,8 +184,8 @@ def test_weekly_report(self): TeamMembership.objects.create(team=team, user=organizer, role=TeamMembership.ORGANIZER) TeamMembership.objects.create(team=team, user=faci1, role=TeamMembership.MEMBER) - StudyGroup.objects.filter(pk=1).update(facilitator=faci1, team=team) - StudyGroup.objects.filter(pk=3).update(facilitator=faci1, team=team) + StudyGroup.objects.filter(pk=1).update(created_by=faci1, team=team) + StudyGroup.objects.filter(pk=3).update(created_by=faci1, team=team) StudyGroup.objects.filter(pk=3).update(deleted_at=timezone.now()) diff --git a/studygroups/tests/test_report_views.py b/studygroups/tests/test_report_views.py index bb2c9b126..035f95f4d 100644 --- a/studygroups/tests/test_report_views.py +++ b/studygroups/tests/test_report_views.py @@ -82,7 +82,7 @@ def test_study_group_final_report_with_no_responses(self): course = Course.objects.create(**course_data) sg = StudyGroup.objects.get(pk=1) sg.course = course - sg.facilitator = facilitator + sg.created_by = facilitator sg.save() data = dict(self.APPLICATION_DATA) @@ -121,7 +121,7 @@ def test_study_group_final_report_with_only_facilitator_response(self): course = Course.objects.create(**course_data) sg = StudyGroup.objects.get(pk=1) sg.course = course - sg.facilitator = facilitator + sg.created_by = facilitator sg.save() data = dict(self.APPLICATION_DATA) @@ -163,7 +163,7 @@ def test_study_group_final_report_with_responses(self): course = Course.objects.create(**course_data) sg = StudyGroup.objects.get(pk=1) sg.course = course - sg.facilitator = facilitator + sg.created_by = facilitator sg.save() data = dict(self.APPLICATION_DATA) diff --git a/studygroups/tests/test_tasks.py b/studygroups/tests/test_tasks.py index 228641049..964a0c38c 100644 --- a/studygroups/tests/test_tasks.py +++ b/studygroups/tests/test_tasks.py @@ -13,6 +13,7 @@ from studygroups.models import Course from studygroups.models import StudyGroup from studygroups.models import Meeting +from studygroups.models import Facilitator from studygroups.models import Feedback from studygroups.models import Application from studygroups.models import Reminder @@ -250,7 +251,7 @@ def test_facilitator_reminder_email_links(self, send_message): send_reminder(reminder) self.assertEqual(len(mail.outbox), 2) # should be sent to facilitator & application self.assertEqual(mail.outbox[0].to[0], data['email']) - self.assertEqual(mail.outbox[1].to[0], sg.facilitator.email) + self.assertEqual(mail.outbox[1].to[0], sg.created_by.email) self.assertFalse(send_message.called) self.assertNotIn('{0}/{1}/rsvp/'.format(settings.DOMAIN, get_language()), mail.outbox[1].alternatives[0][0]) self.assertIn('{0}/{1}/'.format(settings.DOMAIN, get_language()), mail.outbox[1].alternatives[0][0]) @@ -315,7 +316,7 @@ def test_send_meeting_wrap(self, _send_meeting_wrapup): def test_send_weekly_report(self): organizer = create_user('organ@team.com', 'organ', 'test', '1234', False) faci1 = create_user('faci1@team.com', 'faci', 'test', 'password', False) - StudyGroup.objects.filter(pk=1).update(facilitator=faci1) + StudyGroup.objects.filter(pk=1).update(created_by=faci1) mail.outbox = [] team = Team.objects.create(name='test team') @@ -350,7 +351,7 @@ def test_send_weekly_report(self): def test_dont_send_weekly_report(self): organizer = create_user('organ@team.com', 'organ', 'test', '1234', False) faci1 = create_user('faci1@team.com', 'faci', 'test', 'password', False) - StudyGroup.objects.filter(pk=1).update(facilitator=faci1) + StudyGroup.objects.filter(pk=1).update(created_by=faci1) mail.outbox = [] team = Team.objects.create(name='test team') @@ -484,7 +485,7 @@ def test_facilitator_survey_email(self): send_facilitator_survey(sg) self.assertEqual(len(mail.outbox), 1) self.assertIn('{0}/en/studygroup/{1}/facilitator_survey/'.format(settings.DOMAIN, sg.uuid), mail.outbox[0].body) - self.assertIn(sg.facilitator.email, mail.outbox[0].to) + self.assertIn(sg.created_by.email, mail.outbox[0].to) self.assertNotEqual(sg.facilitator_survey_sent_at, None) diff --git a/studygroups/urls.py b/studygroups/urls.py index 80d46e8f3..f0ae8aaf4 100644 --- a/studygroups/urls.py +++ b/studygroups/urls.py @@ -25,7 +25,6 @@ from studygroups.views import StudyGroupFacilitatorSurvey from studygroups.views import StudyGroupDidNotHappen from studygroups.views import LeaveTeam -from studygroups.views import MeetingList from studygroups.views import TeamMembershipDelete from studygroups.views import TeamInvitationCreate from studygroups.views import InvitationConfirm @@ -123,7 +122,6 @@ url(r'^organize/$', views.organize, name='studygroups_organize'), url(r'^organize/(?P[\d]+)/$', views.organize_team, name='studygroups_organize_team'), url(r'^organize/studygroups/$', StudyGroupList.as_view(), name='studygroups_organizer_studygroup_list'), - url(r'^organize/studygroup_meetings/$', MeetingList.as_view(), name='studygroups_organizer_studygroup_meetings'), url(r'^organize/teammembership/(?P[\d]+)/(?P[\d]+)/delete/$', TeamMembershipDelete.as_view(), name='studygroups_teammembership_delete'), url(r'^organize/team/(?P[\d]+)/member/invite/$', TeamInvitationCreate.as_view(), name='studygroups_team_member_invite'), url(r'^organize/team/(?P[\d]+)/edit/$', TeamUpdate.as_view(), name='studygroups_team_edit'), diff --git a/studygroups/views/api.py b/studygroups/views/api.py index b7c86e381..cf4e76046 100644 --- a/studygroups/views/api.py +++ b/studygroups/views/api.py @@ -30,6 +30,7 @@ from studygroups.decorators import user_is_team_organizer from studygroups.models import Course from studygroups.models import StudyGroup +from studygroups.models import Facilitator from studygroups.models import Application from studygroups.models import Meeting from studygroups.models import Reminder @@ -42,7 +43,10 @@ from studygroups.models import get_json_response from studygroups.models.course import course_platform_from_url from studygroups.models.team import eligible_team_by_email_domain +from studygroups.models.team import get_team_users from studygroups.models.learningcircle import generate_meeting_reminder +from studygroups.tasks import send_cofacilitator_email +from studygroups.tasks import send_cofacilitator_removed_email from uxhelpers.utils import json_response @@ -63,7 +67,7 @@ def to_json(sg): data = { "name": sg.name, "course_title": sg.course.title, - "facilitator": sg.facilitator.first_name + " " + sg.facilitator.last_name, + "facilitator": sg.created_by.first_name + " " + sg.created_by.last_name, "venue": sg.venue_name, "venue_address": sg.venue_address + ", " + sg.city, "city": sg.city, @@ -96,6 +100,8 @@ def __init__(self, value, search_type='raw', **kwargs): def serialize_learning_circle(sg): + + facilitators = [f.user.first_name for f in sg.facilitator_set.all()] data = { "course": { "id": sg.course.pk, @@ -107,7 +113,8 @@ def serialize_learning_circle(sg): }, "id": sg.id, "name": sg.name, - "facilitator": sg.facilitator.first_name, + "facilitator": sg.facilitators_display(), + "facilitators": facilitators, "venue": sg.venue_name, "venue_address": sg.venue_address + ", " + sg.city, "venue_website": sg.venue_website, @@ -196,16 +203,16 @@ def get(self, request): if errors != {}: return json_response(request, {"status": "error", "errors": errors}) - study_groups = StudyGroup.objects.published().filter(members_only=False).prefetch_related('course', 'meeting_set', 'application_set').order_by('id') + study_groups = StudyGroup.objects.published().filter(members_only=False).prefetch_related('course', 'meeting_set', 'application_set', 'facilitator_set', 'facilitator_set__user').order_by('id') if 'draft' in request.GET: study_groups = StudyGroup.objects.active().order_by('id') if 'id' in request.GET: id = request.GET.get('id') study_groups = StudyGroup.objects.filter(pk=int(id)) + if 'user' in request.GET: - user_id = request.user.id - study_groups = study_groups.filter(facilitator=user_id) + study_groups = study_groups.filter(facilitator__user=request.user) if 'online' in request.GET: online = clean_data.get('online') @@ -262,8 +269,7 @@ def get(self, request): 'venue_name', 'venue_address', 'venue_details', - 'facilitator__first_name', - 'facilitator__last_name', + 'facilitator__user__first_name', config='simple' ) ).filter(search=tsquery) @@ -626,7 +632,23 @@ def _meetings_validator(meetings): return mtngs, None +def _facilitators_validator(facilitators): + # TODO - check that its a list, facilitator exists + if facilitators is None: + return [], None + if not isinstance(facilitators, list): + return None, 'Invalid facilitators' + results = list(map(schema.integer(), facilitators)) + errors = list(filter(lambda x: x, map(lambda x: x[1], results))) + fcltrs = list(map(lambda x: x[0], results)) + if errors: + return None, 'Invalid facilitator data' + else: + return fcltrs, None + + def _make_learning_circle_schema(request): + post_schema = { "name": schema.text(length=128, required=False), "course": schema.chain([ @@ -655,6 +677,7 @@ def _make_learning_circle_schema(request): "duration": schema.integer(required=True), "timezone": schema.text(required=True, length=128), "signup_question": schema.text(length=256), + "facilitators": _facilitators_validator, "facilitator_goal": schema.text(length=256), "facilitator_concerns": schema.text(length=256), "image_url": schema.chain([ @@ -677,6 +700,18 @@ def post(self, request): logger.debug('schema error {0}'.format(json.dumps(errors))) return json_response(request, {"status": "error", "errors": errors}) + if len(data.get('facilitators', [])) > 0: + team_membership = TeamMembership.objects.active().filter(user=request.user).first() + if not team_membership: + errors = { 'facilitators': ['Facilitator not part of a team']} + return json_response(request, {"status": "error", "errors": errors}) + team = TeamMembership.objects.active().filter(user=request.user).first().team + team_list = team.teammembership_set.active().values_list('user', flat=True) + if not all(item in team_list for item in data.get('facilitators', [])): + errors = { 'facilitators': ['Facilitators not part of the same team']} + return json_response(request, {"status": "error", "errors": errors}) + + # start and end dates need to be set for db model to be valid start_date = data.get('meetings')[0].get('meeting_date') end_date = data.get('meetings')[-1].get('meeting_date') @@ -686,7 +721,7 @@ def post(self, request): name=data.get('name', None), course=data.get('course'), course_description=data.get('course_description', None), - facilitator=request.user, + created_by=request.user, description=data.get('description'), venue_name=data.get('venue_name'), venue_address=data.get('venue_address'), @@ -725,7 +760,15 @@ def post(self, request): study_group.draft = data.get('draft', True) study_group.save() - # notification about new study group is sent at this point, but no associated meetings exists, which implies that the reminder can't use the date of the first meeting + + # add all facilitators + facilitators = set([request.user.id] + data.get('facilitators')) # make user a facilitator + for user_id in facilitators: + f = Facilitator(study_group=study_group, user_id=user_id) + f.save() + if user_id != request.user.id: + send_cofacilitator_email.delay(study_group.id, user_id, request.user.id) + generate_meetings_from_dates(study_group, data.get('meetings', [])) studygroup_url = f"{settings.PROTOCOL}://{settings.DOMAIN}" + reverse('studygroups_view_study_group', args=(study_group.id,)) @@ -746,6 +789,20 @@ def post(self, request, *args, **kwargs): if errors != {}: return json_response(request, {"status": "error", "errors": errors}) + if len(data.get('facilitators', [])) == 0: + errors = { 'facilitators': ['Cannot remove all faclitators from a learning circle']} + return json_response(request, {"status": "error", "errors": errors}) + + if len(data.get('facilitators', [])) > 1: + if not study_group.team: + errors = { 'facilitators': ['Facilitator not part of a team']} + return json_response(request, {"status": "error", "errors": errors}) + + team_list = TeamMembership.objects.active().filter(team=study_group.team).values_list('user', flat=True) + if not all(item in team_list for item in data.get('facilitators', [])): + errors = { 'facilitators': ['Facilitators not part of the same team']} + return json_response(request, {"status": "error", "errors": errors}) + # determine if meeting reminders should be regenerated regenerate_reminders = any([ study_group.name != data.get('name'), @@ -754,6 +811,7 @@ def post(self, request, *args, **kwargs): study_group.venue_details != data.get('venue_details'), study_group.venue_details != data.get('venue_details'), study_group.language != data.get('language'), + set(study_group.facilitator_set.all().values_list('user_id', flat=True)) != set(data.get('facilitators')), ]) # update learning circle @@ -792,6 +850,19 @@ def post(self, request, *args, **kwargs): study_group.save() generate_meetings_from_dates(study_group, data.get('meetings', [])) + # update facilitators + current_facilicators_ids = study_group.facilitator_set.all().values_list('user_id', flat=True) + updated_facilitators = data.get('facilitators') + to_delete = study_group.facilitator_set.exclude(user_id__in=updated_facilitators) + for facilitator in to_delete: + send_cofacilitator_removed_email.delay(study_group.id, facilitator.user_id, request.user.id) + to_delete.delete() + to_add = [f_id for f_id in updated_facilitators if f_id not in current_facilicators_ids] + for user_id in to_add: + f = Facilitator(study_group=study_group, user_id=user_id) + f.save() + send_cofacilitator_email.delay(study_group.pk, user_id, request.user.id) + if regenerate_reminders: for meeting in study_group.meeting_set.active(): # if the reminder hasn't already been sent, regenerate it @@ -855,82 +926,6 @@ def post(self, request): return json_response(request, {"status": "created"}) -class LandingPageLearningCirclesView(View): - """ return upcoming learning circles for landing page """ - def get(self, request): - - query_schema = { - "scope": schema.text(), - } - data = schema.django_get_to_dict(request.GET) - clean_data, errors = schema.validate(query_schema, data) - if errors != {}: - return json_response(request, {"status": "error", "errors": errors}) - - study_groups_unsliced = StudyGroup.objects.published() - - if 'scope' in request.GET and request.GET.get('scope') == "team": - user = request.user - team_ids = TeamMembership.objects.active().filter(user=user).values("team") - - if team_ids.count() == 0: - return json_response(request, { "status": "error", "errors": ["User is not on a team."] }) - - team_members = TeamMembership.objects.active().filter(team__in=team_ids).values("user") - study_groups_unsliced = study_groups_unsliced.filter(facilitator__in=team_members) - - # get learning circles with image & upcoming meetings - study_groups = study_groups_unsliced.filter( - meeting__meeting_date__gte=timezone.now(), - ).annotate( - next_meeting_date=Min('meeting__meeting_date') - ).order_by('next_meeting_date')[:3] - - # if there are less than 3 with upcoming meetings and an image - if study_groups.count() < 3: - # pad with learning circles with the most recent meetings - past_study_groups = study_groups_unsliced.filter( - meeting__meeting_date__lt=timezone.now(), - ).annotate( - next_meeting_date=Max('meeting__meeting_date') - ).order_by('-next_meeting_date') - study_groups = list(study_groups) + list(past_study_groups[:3-study_groups.count()]) - data = { - 'items': [ serialize_learning_circle(sg) for sg in study_groups ] - } - return json_response(request, data) - - -class LandingPageStatsView(View): - """ Return stats for the landing page """ - """ - - Number of active learning circles - - Number of cities where learning circle happened - - Number of facilitators who ran at least 1 learning circle - - Number of learning circles to date - """ - def get(self, request): - study_groups = StudyGroup.objects.published().filter( - meeting__meeting_date__gte=timezone.now() - ).annotate( - next_meeting_date=Min('meeting__meeting_date') - ) - cities = StudyGroup.objects.published().filter( - latitude__isnull=False, - longitude__isnull=False, - ).distinct('city').values('city') - learning_circle_count = StudyGroup.objects.published().count() - facilitators = StudyGroup.objects.active().distinct('facilitator').values('facilitator') - cities_s = list(set([c['city'].split(',')[0].strip() for c in cities])) - data = { - "active_learning_circles": study_groups.count(), - "cities": len(cities_s), - "facilitators": facilitators.count(), - "learning_circle_count": learning_circle_count - } - return json_response(request, data) - - class ImageUploadView(View): def post(self, request): form = ImageForm(request.POST, request.FILES) @@ -1029,7 +1024,7 @@ def serialize_team_data(team): } members = team.teammembership_set.active().values('user') - studygroup_count = StudyGroup.objects.published().filter(facilitator__in=members).count() + studygroup_count = StudyGroup.objects.published().filter(team=team).count() serialized_team["studygroup_count"] = studygroup_count diff --git a/studygroups/views/drf.py b/studygroups/views/drf.py index 3453c0dab..86e8b8fb5 100644 --- a/studygroups/views/drf.py +++ b/studygroups/views/drf.py @@ -14,6 +14,7 @@ from studygroups.models import TeamMembership from studygroups.models import TeamInvitation from studygroups.models import Meeting +from studygroups.models import Facilitator from studygroups.models import get_study_group_organizers @@ -31,26 +32,21 @@ class Meta: class IsGroupFacilitator(permissions.BasePermission): def check_permission(self, user, study_group): - if user.is_staff or user == study_group.facilitator \ - or TeamMembership.objects.active().filter(user=user, role=TeamMembership.ORGANIZER).exists() and user in get_study_group_organizers(study_group): + if user.is_staff \ + or Facilitator.objects.filter(user=user, study_group=study_group).exists() \ + or study_group.team and TeamMembership.objects.active().filter(user=user, role=TeamMembership.ORGANIZER, team=study_group.team).exists(): return True return False - def has_permission(self, request, view): meeting_id = request.data.get('study_group_meeting') meeting = Meeting.objects.get(pk=meeting_id) return self.check_permission(request.user, meeting.study_group) - def has_object_permission(self, request, view, obj): """ give access to staff, user and team organizer """ study_group = obj.study_group_meeting.study_group - if request.user.is_staff \ - or request.user == study_group.facilitator \ - or TeamMembership.objects.active().filter(user=request.user, role=TeamMembership.ORGANIZER).exists() and request.user in get_study_group_organizers(study_group): - return True - return False + return self.check_permission(request.user, study_group) class FeedbackViewSet( @@ -70,8 +66,8 @@ def has_object_permission(self, request, view, obj): """ give access to staff, user and team organizer """ study_group = obj if request.user.is_staff \ - or request.user == study_group.facilitator \ - or TeamMembership.objects.active().filter(user=request.user, role=TeamMembership.ORGANIZER).exists() and request.user in get_study_group_organizers(study_group): + or Facilitator.objects.filter(user=request.user, study_group=study_group).exists() \ + or study_group.team and TeamMembership.objects.active().filter(user=request.user, role=TeamMembership.ORGANIZER, team=study_group.team).exists(): return True return False @@ -93,10 +89,8 @@ class StudyGroupRatingViewSet( class IsATeamOrganizer(permissions.BasePermission): def has_object_permission(self, request, view, obj): - """ give access to staff, user and team organizer """ - study_group = obj + """ give access to staff and team organizer """ if request.user.is_staff \ - or request.user == study_group.facilitator \ or TeamMembership.objects.active().filter(user=request.user, role=TeamMembership.ORGANIZER).exists(): return True return False diff --git a/studygroups/views/facilitate.py b/studygroups/views/facilitate.py index 9c1db1c2d..f141cbff8 100644 --- a/studygroups/views/facilitate.py +++ b/studygroups/views/facilitate.py @@ -35,6 +35,7 @@ from studygroups.models import TeamMembership from studygroups.models import TeamInvitation from studygroups.models import StudyGroup +from studygroups.models import Facilitator from studygroups.models import Meeting from studygroups.models import Course from studygroups.models import Application @@ -239,8 +240,8 @@ def dispatch(self, request, *args, **kwargs): course = self.get_object() if not request.user.is_staff and course.created_by != request.user: raise PermissionDenied - other_study_groups = StudyGroup.objects.active().filter(course=course).exclude(facilitator=request.user) - study_groups = StudyGroup.objects.active().filter(course=course, facilitator=request.user) + other_study_groups = StudyGroup.objects.active().filter(course=course).exclude(facilitator__user=request.user) + study_groups = StudyGroup.objects.active().filter(course=course, facilitator__user=request.user) if study_groups.count() > 1 or other_study_groups.count() > 0: messages.warning(request, _('This course is being used by other learning circles and cannot be edited, please create a new course to make changes')) url = reverse('studygroups_facilitator') @@ -282,6 +283,10 @@ def get_context_data(self, **kwargs): context = super(StudyGroupCreate, self).get_context_data(**kwargs) context['RECAPTCHA_SITE_KEY'] = settings.RECAPTCHA_SITE_KEY # required for inline signup context['hide_footer'] = True + context['team'] = [] + if self.request.user.is_authenticated and TeamMembership.objects.active().filter(user=self.request.user).exists(): + team = TeamMembership.objects.active().filter(user=self.request.user).get().team + context['team'] = json.dumps([t.to_dict() for t in team.teammembership_set.active()]) return context @@ -298,9 +303,9 @@ def get_initial(self): def form_valid(self, form): study_group = form.save(commit=False) - study_group.facilitator = self.request.user - + study_group.created_by = self.request.user study_group.save() + Facilitator.objects.create(user=self.request.user, study_group=study_group) meeting_dates = generate_all_meeting_dates( study_group.start_date, study_group.meeting_time, form.cleaned_data['weeks'] ) @@ -321,6 +326,12 @@ def get_context_data(self, **kwargs): self.object = self.get_object() context = super().get_context_data(**kwargs) context['meetings'] = [m.to_json() for m in self.object.meeting_set.active()] + context['facilitators'] = [f.user_id for f in self.object.facilitator_set.all()] + # only do this if + # a) the currently authenticated user is in a team + # or b) if it's a super user and the learning circle is part of a team + if self.request.user.is_staff and self.object.team or TeamMembership.objects.active().filter(user=self.request.user).exists(): + context['team'] = [t.to_json() for t in self.object.team.teammembership_set.active()] context['hide_footer'] = True if Reminder.objects.filter(study_group=self.object, edited_by_facilitator=True, sent_at__isnull=True).exists(): context['reminders_edited'] = True @@ -378,7 +389,7 @@ class StudyGroupPublish(SingleObjectMixin, View): def post(self, request, *args, **kwargs): study_group = self.get_object() - profile = study_group.facilitator.profile + profile = study_group.created_by.profile # TODO if profile.email_confirmed_at is None: messages.warning(self.request, _("You need to confirm your email address before you can publish a learning circle.")); else: diff --git a/studygroups/views/learner.py b/studygroups/views/learner.py index dbc5b0f2f..46538674d 100644 --- a/studygroups/views/learner.py +++ b/studygroups/views/learner.py @@ -40,6 +40,9 @@ import cities import json import urllib +import logging + +logger = logging.getLogger(__name__) class TeamPage(DetailView): @@ -52,12 +55,11 @@ def get_context_data(self, **kwargs): context = super(TeamPage, self).get_context_data(**kwargs) two_weeks = (datetime.datetime.now() - datetime.timedelta(weeks=2)).date() - team_users = TeamMembership.objects.active().filter(team=self.object).values('user') study_group_ids = Meeting.objects.active()\ .filter(meeting_date__gte=timezone.now())\ .values('study_group') study_groups = StudyGroup.objects.published()\ - .filter(facilitator__in=team_users)\ + .filter(team=self.object)\ .filter(id__in=study_group_ids, signup_open=True)\ .order_by('start_date') @@ -138,11 +140,6 @@ def signup(request, location, study_group_id): #if study_group.venue_address: # context['map_url'] = "https://www.google.com/maps/search/?api=1&query={}".format(urllib.parse.quote(study_group.venue_address)) - team_membership = TeamMembership.objects.active().filter(user=study_group.facilitator).first() - if team_membership: - context['team_name'] = team_membership.team.name - context['team_page_slug'] = team_membership.team.page_slug - return render(request, 'studygroups/signup.html', context) @@ -277,9 +274,8 @@ def receive_sms(request): if signups.count() == 1: signup = signups.first() context['signup'] = signup - # TODO i18n - subject = 'New SMS reply from {0} <{1}>'.format(signup.name, sender) - to += [ signup.study_group.facilitator.email ] + subject = _('New SMS reply from {0} <{1}>').format(signup.name, sender) + to += [facilitator.user.email for facilitator in signup.study_group.facilitator_set.all()] next_meeting = signups.first().study_group.next_meeting() # TODO - replace this check with a check to see if the meeting reminder has been sent if next_meeting and next_meeting.meeting_datetime() - timezone.now() < datetime.timedelta(days=2): @@ -344,7 +340,7 @@ def get(self, request, *args, **kwargs): 'study_group_name': study_group.name, 'course_title': study_group.course.title, 'learner_uuid': application.uuid, - 'facilitator_name': study_group.facilitator.first_name, + 'facilitator_names': study_group.facilitators_display(), } if goal_met: context['goal_met'] = goal_met @@ -354,7 +350,7 @@ def get(self, request, *args, **kwargs): 'study_group_uuid': study_group.uuid, 'study_group_name': study_group.name, 'course_title': study_group.course.title, - 'facilitator_name': study_group.facilitator.first_name, + 'facilitator_names': study_group.facilitators_display(), } return render(request, self.template_name, context) diff --git a/studygroups/views/organizer.py b/studygroups/views/organizer.py index 845d5c7e2..5b323812c 100644 --- a/studygroups/views/organizer.py +++ b/studygroups/views/organizer.py @@ -30,6 +30,7 @@ from studygroups.decorators import user_is_organizer from studygroups.decorators import user_is_team_member from studygroups.decorators import user_is_team_organizer +from studygroups.decorators import user_is_staff from studygroups.forms import OrganizerGuideForm from studygroups.forms import TeamForm @@ -79,7 +80,7 @@ def organize_team(request, team_id): members = team.teammembership_set.active().values('user') team_users = User.objects.filter(pk__in=members) - study_groups = StudyGroup.objects.published().filter(facilitator__in=team_users) + study_groups = StudyGroup.objects.published().filter(team=team) facilitators = team_users invitations = TeamInvitation.objects.filter(team=team, responded_at__isnull=True) active_study_groups = study_groups.filter( @@ -101,31 +102,12 @@ def organize_team(request, team_id): return render(request, 'studygroups/organize.html', context) -@method_decorator(user_is_organizer, name='dispatch') +@method_decorator(user_is_staff, name='dispatch') class StudyGroupList(ListView): model = StudyGroup def get_queryset(self): - study_groups = StudyGroup.objects.published() - if not self.request.user.is_staff: - team_users = get_team_users(self.request.user) - study_groups = study_groups.filter(facilitator__in=team_users) - return study_groups - - -@method_decorator(user_is_organizer, name='dispatch') -class MeetingList(ListView): - model = Meeting - paginate_by = 10 - - def get_queryset(self): - study_groups = StudyGroup.objects.published() - if not self.request.user.is_staff: - team_users = get_team_users(self.request.user) - study_groups = study_groups.filter(facilitator__in=team_users) - - meetings = Meeting.objects.active().filter(study_group__in=study_groups) - return meetings + return StudyGroup.objects.published() @method_decorator(user_is_organizer, name='dispatch') diff --git a/studygroups/views/staff.py b/studygroups/views/staff.py index 5aa3576b4..749cea987 100644 --- a/studygroups/views/staff.py +++ b/studygroups/views/staff.py @@ -22,6 +22,7 @@ from django.db.models import Prefetch from django.db.models import OuterRef from django.db.models import Subquery +from django.db.models import F, Case, When, Value, Sum, IntegerField from studygroups.models import Application @@ -119,7 +120,25 @@ def get(self, request, *args, **kwargs): class ExportFacilitatorsView(ListView): def get_queryset(self): - return User.objects.all().prefetch_related('studygroup_set', 'studygroup_set__course') + learning_circles = StudyGroup.objects.select_related('course').published().filter(facilitator__user_id=OuterRef('pk')).order_by('-start_date') + return User.objects.all().annotate( + learning_circle_count=Sum( + Case( + When( + facilitator__study_group__deleted_at__isnull=True, + facilitator__study_group__draft=False, + then=Value(1), + facilitator__user__id=F('id') + ), + default=Value(0), output_field=IntegerField() + ) + ) + ).annotate( + last_learning_circle_date=Subquery(learning_circles.values('start_date')[:1]), + last_learning_circle_name=Subquery(learning_circles.values('name')[:1]), + last_learning_circle_course=Subquery(learning_circles.values('course__title')[:1]), + last_learning_circle_venue=Subquery(learning_circles.values('venue_name')[:1]) + ) def csv(self, **kwargs): @@ -141,23 +160,17 @@ def csv(self, **kwargs): writer.writerow(field_names) for user in self.object_list: data = [ - ' '.join([user.first_name ,user.last_name]), + ' '.join([user.first_name, user.last_name]), user.email, user.date_joined, user.last_login, user.profile.communication_opt_in if user.profile else False, - user.studygroup_set.active().count() + user.learning_circle_count, + user.last_learning_circle_date, + user.last_learning_circle_name, + user.last_learning_circle_course, + user.last_learning_circle_venue, ] - last_study_group = user.studygroup_set.active().order_by('start_date').last() - if last_study_group: - data += [ - last_study_group.start_date, - last_study_group.name, - last_study_group.course.title, - last_study_group.venue_name - ] - else: - data += ['', '', ''] writer.writerow(data) return response @@ -171,8 +184,8 @@ def get(self, request, *args, **kwargs): class ExportStudyGroupsView(ListView): def get_queryset(self): - return StudyGroup.objects.all().prefetch_related('course', 'facilitator', 'meeting_set').annotate( - learning_circle_number=RawSQL("RANK() OVER(PARTITION BY facilitator_id ORDER BY created_at ASC)", []) + return StudyGroup.objects.all().prefetch_related('course', 'facilitator_set', 'meeting_set').annotate( + learning_circle_number=RawSQL("RANK() OVER(PARTITION BY created_by_id ORDER BY created_at ASC)", []) ) def csv(self, **kwargs): @@ -188,8 +201,8 @@ def csv(self, **kwargs): 'draft', 'course id', 'course title', - 'facilitator', - 'faciltator email', + 'created by', + 'created by email', 'learning_circle_number', 'location', 'city', @@ -204,6 +217,7 @@ def csv(self, **kwargs): 'learner survey', 'learner survey responses', 'did not happen', + 'facilitator count', ] writer = csv.writer(response) writer.writerow(field_names) @@ -217,8 +231,8 @@ def csv(self, **kwargs): 'yes' if sg.draft else 'no', sg.course.id, sg.course.title, - ' '.join([sg.facilitator.first_name, sg.facilitator.last_name]), - sg.facilitator.email, + ' '.join([sg.created_by.first_name, sg.created_by.last_name]), + sg.created_by.email, sg.learning_circle_number, ' ' .join([sg.venue_name, sg.venue_address]), sg.city, @@ -240,9 +254,9 @@ def csv(self, **kwargs): data += [''] data += [sg.application_set.active().count()] - # team - if sg.facilitator.teammembership_set.active().count(): - data += [sg.facilitator.teammembership_set.active().first().team.name] + + if sg.team: + data += [sg.team.name] else: data += [''] @@ -260,6 +274,7 @@ def csv(self, **kwargs): data += [learner_survey] data += [sg.learnersurveyresponse_set.count()] data += [sg.did_not_happen] + data += [sg.facilitator_set.count()] writer.writerow(data) return response diff --git a/surveys/fixtures/test_studygroups.json b/surveys/fixtures/test_studygroups.json index 7c5c810c7..b52e4296c 100644 --- a/surveys/fixtures/test_studygroups.json +++ b/surveys/fixtures/test_studygroups.json @@ -33,7 +33,7 @@ "longitude" : "-87.650050", "language" : "en", "place_id" : "", - "facilitator": 1, + "created_by": 1, "venue_name": "Harold Washington", "venue_address": "123 Street", "venue_details": "3rd floor", @@ -46,6 +46,16 @@ "pk": 1 }, { + "fields": { + "added_at": "2015-03-23T15:18:39.462Z", + "user": 1, + "study_group": 1 + }, + "model": "studygroups.facilitator", + "pk": 1 +}, +{ + "fields": { "created_at": "2015-03-23T15:19:04.318Z", "updated_at": "2015-03-23T15:18:39.462Z", @@ -57,7 +67,7 @@ "end_date": "2015-03-23", "duration": 120, "timezone": "US/Central", - "facilitator": 1, + "created_by": 1, "venue_name": "Harold Washington", "venue_address": "123 Street", "venue_details": "3rd floor", @@ -72,6 +82,16 @@ "pk": 2 }, { + "fields": { + "added_at": "2015-03-23T15:18:39.462Z", + "user": 1, + "study_group": 2 + }, + "model": "studygroups.facilitator", + "pk": 2 +}, +{ + "fields": { "created_at": "2015-03-25T14:35:02.227Z", "updated_at": "2015-03-23T15:18:39.462Z", @@ -83,7 +103,7 @@ "end_date": "2015-03-23", "duration": 120, "timezone": "US/Central", - "facilitator": 1, + "created_by": 1, "venue_name": "Harold Washington", "venue_address": "123 Street", "venue_details": "3rd floor", @@ -97,6 +117,15 @@ "model": "studygroups.studygroup", "pk": 3 }, +{ + "fields": { + "added_at": "2015-03-23T15:18:39.462Z", + "user": 1, + "study_group": 3 + }, + "model": "studygroups.facilitator", + "pk": 3 +}, { "fields": { "created_at": "2015-03-25T15:55:44.525Z", @@ -109,7 +138,7 @@ "end_date": "2015-03-23", "duration": 120, "timezone": "US/Central", - "facilitator": 1, + "created_by": 1, "venue_name": "Harold Washington", "venue_address": "123 Street", "venue_details": "3rd floor", @@ -122,5 +151,14 @@ }, "model": "studygroups.studygroup", "pk": 4 +}, +{ + "fields": { + "added_at": "2015-03-23T15:18:39.462Z", + "user": 1, + "study_group": 4 + }, + "model": "studygroups.facilitator", + "pk": 4 } ] diff --git a/surveys/models.py b/surveys/models.py index d6d4910dd..2f63a4c0e 100644 --- a/surveys/models.py +++ b/surveys/models.py @@ -108,12 +108,12 @@ def normalize_data(typeform_response): } answers['facilitator'] = { 'field_title': 'Facilitator', - 'answer': typeform_response.study_group.facilitator.email, + 'answer': typeform_response.study_group.created_by.email, } - if hasattr(typeform_response.study_group.facilitator, 'teammembership'): + if typeform_response.study_group.team: answers['team'] = { 'field_title': 'Team', - 'answer': typeform_response.study_group.facilitator.teammembership.team.name + 'answer': typeform_response.study_group.team.name } return answers diff --git a/templates/email_base.html b/templates/email_base.html index 36445a83e..4a0eafbfe 100644 --- a/templates/email_base.html +++ b/templates/email_base.html @@ -64,7 +64,7 @@ {% block body %}{% endblock %} - {% if team or facilitator or user %} + {% if team or facilitator or user or show_dash_link %} {% url 'account_settings' as account_settings_url %} diff --git a/templates/studygroups/email/facilitator_added-subject.txt b/templates/studygroups/email/facilitator_added-subject.txt new file mode 100644 index 000000000..4459baac6 --- /dev/null +++ b/templates/studygroups/email/facilitator_added-subject.txt @@ -0,0 +1 @@ +{% load i18n %}{% blocktrans with learning_circle_name=study_group.name %}You have been added as a co-facilitator to {{learning_circle_name}}{% endblocktrans %} diff --git a/templates/studygroups/email/facilitator_added.html b/templates/studygroups/email/facilitator_added.html new file mode 100644 index 000000000..1998d04ea --- /dev/null +++ b/templates/studygroups/email/facilitator_added.html @@ -0,0 +1,15 @@ +{% extends 'email_base.html' %} +{% load i18n %} +{% block body %} +

{% blocktrans with facilitator_name=facilitator.first_name %}Hi {{facilitator_name}},{% endblocktrans %}

+ +{% url 'studygroups_view_study_group' study_group.pk as manage_url %} +

{% blocktrans with learning_circle=study_group.name actor_name=actor.first_name start_date=study_group.start_date|date:"F j, Y" %} +{{actor_name}} has added you as an additional facilitator to the learning circle “{{learning_circle}}” starting on {{start_date}}. +{% endblocktrans %}

+ +

{% blocktrans %}The P2PU Knowledge Base has plenty of resources to help you get started with your learning circle. Here is a checklist for getting started and the section on co-facilitation.{% endblocktrans %} + +

{% trans "Cheers" %},

+

P2PU

+{% endblock %} diff --git a/templates/studygroups/email/facilitator_meeting_reminder.html b/templates/studygroups/email/facilitator_meeting_reminder.html index 7b6613fa0..6f3af9a6b 100644 --- a/templates/studygroups/email/facilitator_meeting_reminder.html +++ b/templates/studygroups/email/facilitator_meeting_reminder.html @@ -2,7 +2,7 @@ {% load i18n %} {% block body %} -

{% blocktrans with facilitator_name=facilitator.first_name %}Hi {{facilitator_name}},{% endblocktrans %}

+

{% blocktrans with facilitator_names=facilitator_names %}Hi {{facilitator_names}}.{% endblocktrans %}

{% trans "The following message has been sent to your learning circle." %}

diff --git a/templates/studygroups/email/facilitator_meeting_reminder.txt b/templates/studygroups/email/facilitator_meeting_reminder.txt index 13c806ecf..708b3fd62 100644 --- a/templates/studygroups/email/facilitator_meeting_reminder.txt +++ b/templates/studygroups/email/facilitator_meeting_reminder.txt @@ -1,4 +1,4 @@ -{% extends 'email_base.txt' %}{% load i18n %}{% block body %}{% blocktrans with facilitator_name=facilitator.first_name %}Hi {{facilitator_name}},{% endblocktrans %} +{% extends 'email_base.txt' %}{% load i18n %}{% block body %}{% blocktrans with facilitator_names=facilitator_names %}Hi {{facilitator_names}}.{% endblocktrans %} {% trans "The following message has been sent to your learning circle." %} diff --git a/templates/studygroups/email/facilitator_meeting_wrapup.html b/templates/studygroups/email/facilitator_meeting_wrapup.html index 552b4dd29..cdec6bf89 100644 --- a/templates/studygroups/email/facilitator_meeting_wrapup.html +++ b/templates/studygroups/email/facilitator_meeting_wrapup.html @@ -30,7 +30,7 @@ -

{% blocktrans with first_name=study_group.facilitator.first_name %}Hi {{first_name}}{% endblocktrans %},

+

{% blocktrans with first_names=study_group.facilitators_display %}Hi {{first_names}}{% endblocktrans %},

{% trans "How did your learning circle go today?" %}

diff --git a/templates/studygroups/email/facilitator_removed-subject.txt b/templates/studygroups/email/facilitator_removed-subject.txt new file mode 100644 index 000000000..d84304afa --- /dev/null +++ b/templates/studygroups/email/facilitator_removed-subject.txt @@ -0,0 +1 @@ +{% load i18n %}{% blocktrans with learning_circle_name=study_group.name %}You've been removed as facilitator from {{learning_circle_name}}{% endblocktrans %} diff --git a/templates/studygroups/email/facilitator_removed.html b/templates/studygroups/email/facilitator_removed.html new file mode 100644 index 000000000..006401e31 --- /dev/null +++ b/templates/studygroups/email/facilitator_removed.html @@ -0,0 +1,14 @@ +{% extends 'email_base.html' %} +{% load i18n %} +{% load extras %} +{% block body %} +

{% blocktrans with facilitator_name=facilitator.first_name %}Hi {{facilitator_name}},{% endblocktrans %}

+ +{% url 'studygroups_signup' location=study_group.venue_name|unicode_slugify study_group_id=study_group.pk as signup_url %} +

{% blocktrans with learning_circle=study_group.name %}You have been removed as a facilitator from the following learning circle "{{learning_circle}}".{% endblocktrans %}

+ +

{% trans "If you believe this was done in error, please contact your co-facilitator(s)." %}

+ +

{% trans "Cheers" %},

+

P2PU

+{% endblock %} diff --git a/templates/studygroups/email/facilitator_survey.html b/templates/studygroups/email/facilitator_survey.html index 78e0a7be3..1a79e2a54 100644 --- a/templates/studygroups/email/facilitator_survey.html +++ b/templates/studygroups/email/facilitator_survey.html @@ -17,7 +17,7 @@ } -

{% blocktrans %}Hi {{facilitator_name}},{% endblocktrans %}

+

{% blocktrans with facilitator_names=study_group.facilitators_display %}Hi {{facilitator_names}}{% endblocktrans %}

{% trans "Can you take a moment to share some final reflections about your learning circle? Your experience and feedback on this course is a big help for future facilitators!" %}

diff --git a/templates/studygroups/email/facilitator_survey.txt b/templates/studygroups/email/facilitator_survey.txt index 1a7a0009d..44dec6102 100644 --- a/templates/studygroups/email/facilitator_survey.txt +++ b/templates/studygroups/email/facilitator_survey.txt @@ -1,5 +1,5 @@ {% load i18n %} -{% blocktrans %}Hi {{facilitator_name}},{% endblocktrans %} +{% blocktrans with facilitator_names=study_group.facilitators_display %}Hi {{facilitator_names}}{% endblocktrans %} {% trans "Can you take a moment to share some final reflections about your learning circle? Your experience and feedback on this course is a big help for future facilitators!" %} diff --git a/templates/studygroups/email/learner_signup.html b/templates/studygroups/email/learner_signup.html index 55b136cea..0774da81b 100644 --- a/templates/studygroups/email/learner_signup.html +++ b/templates/studygroups/email/learner_signup.html @@ -22,11 +22,19 @@ {% endblocktrans %}

+{% if study_group.facilitator_set.all.count > 1 %} +

{% trans "Meet your facilitators" %}

+{% else %}

{% trans "Meet your facilitator" %}

+{% endif %} {% with answers=application.get_signup_questions %} {% if answers.goals %} -

{% blocktrans with facilitator_first_name=study_group.facilitator.first_name facilitator_last_name=study_group.facilitator.last_name%}Your facilitator {{facilitator_first_name}} {{facilitator_last_name}} is copied on this email. If you have any questions for them you can reply to this message. To help your facilitator better assist you, we’re including your answers to the signup questions below.{% endblocktrans %}

+{% if study_group.facilitator_set.all.count == 1 %} +

{% blocktrans with facilitator_first_name=study_group.facilitator_set.first.user.first_name facilitator_last_name=study_group.facilitator_set.first.user.last_name %}Your facilitator {{facilitator_first_name}} {{facilitator_last_name}} is copied on this email. If you have any questions for them you can reply to this message. To help your facilitator better assist you, we’re including your answers to the signup questions below.{% endblocktrans %}

+{% else %} +

{% blocktrans %}Your facilitators {{facilitator_first_last_names}} are copied on this email. If you have any questions for them you can reply to this message. To help your facilitator better assist you, we’re including your answers to the signup questions below.{% endblocktrans %}

+{% endif %}
  • {% trans "Q: What is your goal for taking this learning circle?" %}
  • @@ -42,7 +50,11 @@

    {% trans "Meet your facilitator" %}

    {% endif %}
{% else %} -

{% blocktrans with facilitator_first_name=study_group.facilitator.first_name facilitator_last_name=study_group.facilitator.last_name%}Your facilitator {{facilitator_first_name}} {{facilitator_last_name}} is copied on this email. If you have any questions for them you can reply to this message.{% endblocktrans %}

+{% if study_group.facilitator_set.all.count == 1 %} +

{% blocktrans with facilitator_first_name=study_group.facilitator_set.first.user.first_name facilitator_last_name=study_group.facilitator_set.first.user.last_name %}Your facilitator {{facilitator_first_name}} {{facilitator_last_name}} is copied on this email. If you have any questions for them you can reply to this message.{% endblocktrans %}

+{% else %} +

{% blocktrans %}Your facilitators {{facilitator_first_last_names}} are copied on this email. If you have any questions for them you can reply to this message.{% endblocktrans %}

+{% endif %} {% endif %} {% endwith %} diff --git a/templates/studygroups/email/learning_circle_created.html b/templates/studygroups/email/learning_circle_created.html index 019a3b95d..772ac060e 100644 --- a/templates/studygroups/email/learning_circle_created.html +++ b/templates/studygroups/email/learning_circle_created.html @@ -7,7 +7,7 @@ {% url 'studygroups_generate_course_discourse_topic' study_group.course.pk as course_discourse_url %} {% url 'studygroups_signup' location=study_group.venue_name|unicode_slugify study_group_id=study_group.pk as study_group_url%} -

{% blocktrans with name=study_group.facilitator.first_name %}Hi {{name}}{% endblocktrans %},

+

{% blocktrans with name=study_group.created_by.first_name %}Hi {{name}}{% endblocktrans %},

{% blocktrans with studygroup_name=study_group.name city=study_group.city %}Congratulations! Your “{{studygroup_name}}” learning circle in {{city}} has been created.{% endblocktrans %}

diff --git a/templates/studygroups/email/meeting_reminder.html b/templates/studygroups/email/meeting_reminder.html index d171a3d08..504aa13e0 100644 --- a/templates/studygroups/email/meeting_reminder.html +++ b/templates/studygroups/email/meeting_reminder.html @@ -14,7 +14,11 @@ {{venue_address}}
{% endblocktrans %}

-

{% blocktrans with facilitator_name=study_group.facilitator.first_name %}Your facilitator, {{facilitator_name}}, is expecting you! Please RSVP below.{% endblocktrans %}

+{% if facilitator_name %} +

{% blocktrans %}Your facilitator, {{facilitator_name}}, is expecting you! Please RSVP below.{% endblocktrans %}

+{% elif facilitator_names %} +

{% blocktrans %}Your facilitators, {{facilitator_names}}, are expecting you! Please RSVP below.{% endblocktrans %}

+{% endif %} diff --git a/templates/studygroups/email/weekly-update.html b/templates/studygroups/email/weekly-update.html index 08aaa3a60..8b0d63267 100644 --- a/templates/studygroups/email/weekly-update.html +++ b/templates/studygroups/email/weekly-update.html @@ -185,9 +185,7 @@

{{ studygroup.name }} {{ studygroup.venue_name|title }} - {% blocktrans with first_name=studygroup.facilitator.first_name|title last_name=studygroup.facilitator.last_name|title email=studygroup.facilitator.email %} - {{first_name}} {{last_name}} - {% endblocktrans %} + {% for facilitator in studygroup.facilitator_set.all %}{% if forloop.last and not forloop.first %} and {% else %}{% if not forloop.first %}, {% endif %}{% endif %}{{facilitator.user.first_name}} {{facilitator.user.last_name}}{% endfor %} {% if staff_update %}{{ studygroup.team.name }}{% endif %} {{ studygroup.application_set.active.count }} @@ -236,7 +234,9 @@

{% trans "Meetings this week" %}

{{ study_group.name }} {{ study_group.venue_name }} - {{ study_group.facilitator.first_name }} {{ study_group.facilitator.last_name }} + + {% for facilitator in study_group.facilitator_set.all %}{% if forloop.last and not forloop.first %} and {% else %}{% if not forloop.first %}, {% endif %}{% endif %}{{facilitator.user.first_name}} {{facilitator.user.last_name}}{% endfor %} + {% if staff_update %}{{ study_group.team.name }}{% endif %} {{ study_group.application_set.active.count}} {{ meeting.meeting_date|date:"l N j" }} at {{ meeting.meeting_time|time:"fA" }} {{study_group.timezone_display}} @@ -279,7 +279,9 @@

{{ study_group.name }} {{ study_group.venue_name }} - {{ study_group.facilitator.first_name }} {{ study_group.facilitator.last_name }} + + {% for facilitator in study_group.facilitator_set.all %}{% if forloop.last and not forloop.first %} and {% else %}{% if not forloop.first %}, {% endif %}{% endif %}{{facilitator.user.first_name}} {{facilitator.user.last_name}}{% endfor %} + {% if staff_update %}{{ study_group.team.name }}{% endif %} {{ study_group.application_set.active.count}} {{ study_group.last_meeting.meeting_date|date:"l N j" }} at {{ study_group.last_meeting.meeting_time|time:"fA" }} {{study_group.timezone_display}} @@ -313,8 +315,8 @@

  • {% blocktrans with studygroup_name=study_group.name %}{{studygroup_name}}{% endblocktrans %}

    - {% blocktrans with venue=study_group.venue_name first_name=study_group.facilitator.first_name last_name=study_group.facilitator.last_name %} - Facilitated by {{first_name}} {{last_name}} at {{venue}} + {% blocktrans with venue=study_group.venue_name first_names=study_group.facilitators_display %} + Facilitated by {{first_names}} at {{venue}} {% endblocktrans %}

    {% if feedback_response.rating %} @@ -427,9 +429,9 @@

    {% blocktrans count new_guides_count=new_facilitator_guides.count %} - Our community added {{new_guides_count}} new facilitator guide in the past 3 months. + Our community added {{new_guides_count}} new facilitator guide in the past 3 months {% plural %} - Our community added {{new_guides_count}} new facilitator guides in the past 3 months. + Our community added {{new_guides_count}} new facilitator guides in the past 3 months {% endblocktrans %}

    diff --git a/templates/studygroups/learner_survey.html b/templates/studygroups/learner_survey.html index 02d6ffdf1..ee358fc44 100644 --- a/templates/studygroups/learner_survey.html +++ b/templates/studygroups/learner_survey.html @@ -17,7 +17,7 @@ data-course="{{course_title}}" data-goal-rating="{{goal_met}}" data-learner-uuid="{{learner_uuid}}" - data-facilitator-name="{{facilitator_name}}"> + data-facilitator-name="{{facilitator_names}}"> diff --git a/templates/studygroups/signup.html b/templates/studygroups/signup.html index e5bfc9ba0..1bab2dd19 100644 --- a/templates/studygroups/signup.html +++ b/templates/studygroups/signup.html @@ -75,7 +75,9 @@

    {{study_group.name}}

    face
    - {% blocktrans with first_name=study_group.facilitator.first_name %}Facilitated by {{first_name}}{% endblocktrans %} + {% blocktrans with first_name=study_group.facilitator.first_name %}Facilitated by {% endblocktrans %} + + {% for f in study_group.facilitator_set.all %}{% if forloop.last and not forloop.first %} and {% else %}{% if not forloop.first %}, {% endif %}{% endif %}{{f.user.first_name}}{% endfor %}
    @@ -144,14 +146,15 @@

    {{study_group.name}}

    -
    +
    + {% for facilitator in study_group.facilitator_set.all %}
    - {% if study_group.facilitator.profile.avatar %} - Facilitator image + {% if facilitator.user.profile.avatar %} + Facilitator image {% else %} {% endif %} @@ -160,29 +163,30 @@

    {{study_group.name}}

    {% trans "Facilitated by" %}

    -

    {{ study_group.facilitator.first_name }}

    - {% if study_group.facilitator.profile.bio %} -

    {{ study_group.facilitator.profile.bio }}

    +

    {{ facilitator.user.first_name }}

    + {% if facilitator.user.profile.bio %} +

    {{ facilitator.user.profile.bio }}

    {% endif %}
    - {% if team_name %} + {% if study_group.team %}
    {% trans "Team" %}
    - {% if team_page_slug %} - + {% if study_group.team.page_slug %} + {% else %} -
    {{ team_name }}
    +
    {{ study_group.team.name }}
    {% endif %} {% endif %} - {% if study_group.facilitator.profile.city %} + {% if facilitator.user.profile.city %}
    {% trans "City" %}
    -
    {{ study_group.facilitator.profile.city }}
    +
    {{ facilitator.user.profile.city }}
    {% endif %}
    + {% endfor %}
    diff --git a/templates/studygroups/studygroup_form.html b/templates/studygroups/studygroup_form.html index 286d18c4e..94a0f9676 100644 --- a/templates/studygroups/studygroup_form.html +++ b/templates/studygroups/studygroup_form.html @@ -25,15 +25,21 @@ {% if object.pk %} {% endif %} diff --git a/templates/studygroups/studygroup_react.html b/templates/studygroups/studygroup_react.html deleted file mode 100644 index a51112f6c..000000000 --- a/templates/studygroups/studygroup_react.html +++ /dev/null @@ -1,23 +0,0 @@ -{% extends 'base.html' %} -{% load render_bundle from webpack_loader %} -{% load static %} -{% load i18n %} - -{% block content %} -
    -
    -
    -
    - - -{% endblock %} - -{% block scripts %} - -{% render_bundle 'common' %} -{% render_bundle 'learning-circle-create' %} -{% endblock %} diff --git a/templates/studygroups/view_study_group.html b/templates/studygroups/view_study_group.html index 0a1e9373f..31da453d5 100644 --- a/templates/studygroups/view_study_group.html +++ b/templates/studygroups/view_study_group.html @@ -26,7 +26,7 @@

    {% if study_group.draft %}[DRAFT] {% endif %}{{study_group.name}}

    - {% blocktrans with first_name=study_group.facilitator.first_name %}Facilitated by {{first_name}}{% endblocktrans %} + Facilitated by {{study_group.facilitators_display}}
    {% blocktrans with link=study_group.course.link provider=study_group.course.provider %}Course materials provided by {{ provider }}{% endblocktrans %}