);
}
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 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)." %}
{% 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 %}
{% 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 %}
- {% 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 %}
{{ 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 %}
- {% 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.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 %}