Skip to content
Permalink
Browse files

Bumping version to v2.7.0

  • Loading branch information...
pbanaszkiewicz committed Apr 5, 2019
2 parents 3418c51 + dab7311 commit 11d7fcf32ce08784d73d571f8af9976e67497ad2
Showing with 913 additions and 341 deletions.
  1. +1 −0 amy/api/serializers.py
  2. +46 −19 amy/api/views.py
  3. +6 −0 amy/extforms/tests/test_training_request_form.py
  4. +23 −1 amy/extforms/tests/test_workshop_request_form.py
  5. +15 −4 amy/extforms/views.py
  6. +42 −7 amy/extrequests/views.py
  7. +9 −9 amy/reports/tests/test_issues.py
  8. +1 −1 amy/reports/views.py
  9. +4 −4 amy/static/amy_utils.js
  10. +1 −0 amy/static/css/amy.css
  11. +4 −3 amy/templates/base.html
  12. +1 −0 amy/templates/includes/event_details_table.html
  13. +10 −0 amy/templates/mailing/training_request.html
  14. +7 −1 amy/templates/mailing/training_request.txt
  15. +12 −22 amy/templates/mailing/workshoprequest.html
  16. +13 −11 amy/templates/mailing/workshoprequest.txt
  17. +119 −0 amy/templates/mailing/workshoprequest_admin.html
  18. +33 −0 amy/templates/mailing/workshoprequest_admin.txt
  19. +87 −43 amy/templates/reports/all_activity_over_time.html
  20. +2 −2 amy/templates/workshops/events_merge.html
  21. +2 −0 amy/templates/workshops/workshop_staff.html
  22. +2 −2 amy/trainings/filters.py
  23. +1 −1 amy/workshops/__init__.py
  24. +56 −15 amy/workshops/base_views.py
  25. +9 −0 amy/workshops/filters.py
  26. +10 −5 amy/workshops/forms.py
  27. +168 −142 amy/workshops/management/commands/fake_database.py
  28. +23 −0 amy/workshops/migrations/0182_auto_20190317_1044.py
  29. +51 −15 amy/workshops/models.py
  30. +5 −3 amy/workshops/tests/test_bulk_add.py
  31. +91 −17 amy/workshops/tests/test_event.py
  32. +20 −8 amy/workshops/tests/test_workshop_staff_searching.py
  33. +6 −2 amy/workshops/views.py
  34. +8 −1 config/settings.py
  35. +23 −0 docs/releases/v2.7.0.md
  36. +1 −1 package.json
  37. +1 −2 requirements.txt
@@ -233,6 +233,7 @@ class EventSerializer(serializers.ModelSerializer):
)
assigned_to = serializers.HyperlinkedRelatedField(
read_only=True, view_name='api:person-detail')
attendance = serializers.IntegerField()

class Meta:
model = Event
@@ -296,7 +296,7 @@ class PublishedEvents(ListAPIView):
paginator = None # disable pagination
serializer_class = ExportEventSerializer
filterset_class = EventFilter
queryset = Event.objects.published_events()
queryset = Event.objects.published_events().attendance()


class TrainingRequests(ListAPIView):
@@ -337,7 +337,7 @@ class ReportsViewSet(ViewSet):
are missing, because we want to still have the power and simplicity of
a router."""
permission_classes = (IsAuthenticated, IsAdmin)
event_queryset = Event.objects.past_events().order_by('start')
event_queryset = Event.objects.past_events().attendance().order_by('start')
award_queryset = Award.objects.order_by('awarded')

renderer_classes = (BrowsableAPIRenderer, JSONRenderer, CSVRenderer,
@@ -542,40 +542,55 @@ def get_all_activity_over_time(self, start, end):
events_qs = Event.objects.filter(start__gte=start, start__lte=end)
swc_tag = Tag.objects.get(name='SWC')
dc_tag = Tag.objects.get(name='DC')
lc_tag = Tag.objects.get(name='LC')
wise_tag = Tag.objects.get(name='WiSE')
TTT_tag = Tag.objects.get(name='TTT')
self_organized_host = Organization.objects.get(domain='self-organized')

# count workshops: SWC, DC, total (SWC and/or DC), self-organized,
# count workshops: SWC, DC, LC, total (SWC, DC and LC), self-organized,
# WiSE, TTT
swc_workshops = events_qs.filter(tags=swc_tag)
dc_workshops = events_qs.filter(tags=dc_tag)
swc_dc_workshops = events_qs.filter(tags__in=[swc_tag, dc_tag]).count()
lc_workshops = events_qs.filter(tags=lc_tag)
total_workshops = events_qs.filter(
tags__in=[swc_tag, dc_tag, lc_tag]).count()
wise_workshops = events_qs.filter(tags=wise_tag).count()
ttt_workshops = events_qs.filter(tags=TTT_tag).count()
self_organized_workshops = events_qs \
.filter(administrator=self_organized_host).count()

# total and unique instructors for both SWC and DC workshops
# total and unique instructors for SWC, DC, LC workshops
swc_total_instr = Person.objects \
.filter(task__event__in=swc_workshops,
task__role__name='instructor')
swc_unique_instr = swc_total_instr.distinct().count()
swc_total_instr = swc_total_instr.count()

dc_total_instr = Person.objects \
.filter(task__event__in=dc_workshops,
task__role__name='instructor')
dc_unique_instr = dc_total_instr.distinct().count()
dc_total_instr = dc_total_instr.count()

# total learners for both SWC and DC workshops
swc_total_learners = swc_workshops.aggregate(count=Sum('attendance'))
swc_total_learners = swc_total_learners['count']
dc_total_learners = dc_workshops.aggregate(count=Sum('attendance'))
dc_total_learners = dc_total_learners['count']
lc_total_instr = Person.objects \
.filter(task__event__in=lc_workshops,
task__role__name='instructor')
lc_unique_instr = lc_total_instr.distinct().count()
lc_total_instr = lc_total_instr.count()

# total learners for SWC, DC, LC workshops
swc_total_learners = swc_workshops.attendance().aggregate(
learners_total=Sum('attendance')
)['learners_total']
dc_total_learners = dc_workshops.attendance().aggregate(
learners_total=Sum('attendance')
)['learners_total']
lc_total_learners = lc_workshops.attendance().aggregate(
learners_total=Sum('attendance')
)['learners_total']

# Workshops missing any of this data.
missing_attendance = events_qs.filter(attendance=None) \
missing_attendance = events_qs.attendance().filter(attendance=None) \
.values_list('slug', flat=True)
missing_instructors = events_qs.annotate(
instructors=Sum(
@@ -593,18 +608,23 @@ def get_all_activity_over_time(self, start, end):
'workshops': {
'SWC': swc_workshops.count(),
'DC': dc_workshops.count(),
'LC': lc_workshops.count(),

# This dictionary is traversed in a template where we cannot
# write "{{ data.workshops.SWC,DC }}", because commas are
# disallowed in templates. Therefore, we include
# swc_dc_workshops twice, under two different keys:
# - 'SWC,DC' - for backward compatibility,
# - 'SWC_or_DC' - so that you can access it in a template.
'SWC,DC': swc_dc_workshops,
'SWC_or_DC': swc_dc_workshops,
# total_workshops under different keys:
# - 'SWC,DC' and 'SWC_or_DC' - for backward compatibility,
# - 'carpentries' - new name for SWC/DC/LC collective
'SWC,DC': total_workshops,
'SWC_or_DC': total_workshops,
'carpentries': total_workshops,

'WiSE': wise_workshops,
'TTT': ttt_workshops,

# We include self_organized_workshops twice, under two
# different keys, for the same reason as swc_dc_workshops.
# different keys, for the same reason as total_workshops.
'self-organized': self_organized_workshops,
'self_organized': self_organized_workshops,
},
@@ -617,10 +637,15 @@ def get_all_activity_over_time(self, start, end):
'total': dc_total_instr,
'unique': dc_unique_instr,
},
'LC': {
'total': lc_total_instr,
'unique': lc_unique_instr,
},
},
'learners': {
'SWC': swc_total_learners,
'DC': dc_total_learners,
'LC': lc_total_learners,
},
'missing': {
'attendance': missing_attendance,
@@ -740,8 +765,10 @@ class OrganizationViewSet(viewsets.ReadOnlyModelViewSet):
class EventViewSet(viewsets.ReadOnlyModelViewSet):
"""List many events or retrieve only one."""
permission_classes = (IsAuthenticated, IsAdmin)
queryset = Event.objects.all().select_related('host', 'administrator') \
.prefetch_related('tags')
queryset = Event.objects \
.select_related('host', 'administrator') \
.prefetch_related('tags') \
.attendance()
serializer_class = EventSerializer
lookup_field = 'slug'
pagination_class = StandardResultsSetPagination
@@ -1,6 +1,8 @@
from django.core import mail
from django.urls import reverse

from extforms.forms import TrainingRequestForm
from extforms.views import TrainingRequestCreate
from workshops.models import Role, TrainingRequest
from workshops.tests.base import TestBase

@@ -59,6 +61,10 @@ def test_request_added(self):
self.assertEqual(len(mail.outbox), 1)
msg = mail.outbox[0]
self.assertEqual(msg.to, [email])
self.assertEqual(msg.subject,
TrainingRequestCreate.autoresponder_subject)
# with open('email.eml', 'wb') as f:
# f.write(msg.message().as_bytes())


class GroupNameFieldTestsBase(TestBase):
@@ -1,4 +1,5 @@
from django.core import mail
from django.conf import settings
from django.urls import reverse

from extforms.forms import WorkshopRequestExternalForm
@@ -81,10 +82,31 @@ def test_request_added(self):
self.assertIn('Thank you for requesting a workshop', content)
self.assertEqual(WorkshopRequest.objects.all().count(), 1)
self.assertEqual(WorkshopRequest.objects.all()[0].state, 'p')
self.assertEqual(len(mail.outbox), 1)

# 1 email for autoresponder, 1 email for admins
self.assertEqual(len(mail.outbox), 2)

# save the email messages for test debuggig
# with open('email0.eml', 'wb') as f:
# f.write(mail.outbox[0].message().as_bytes())
# with open('email1.eml', 'wb') as f:
# f.write(mail.outbox[1].message().as_bytes())

# before tests, check if the template invalid string exists
self.assertTrue(settings.TEMPLATES[0]['OPTIONS']['string_if_invalid'])

# test autoresponder email
msg = mail.outbox[0]
self.assertEqual(msg.subject, 'Workshop request confirmation')
self.assertEqual(msg.recipients(), ['hpotter@magic.gov'])
self.assertNotIn(settings.TEMPLATES[0]['OPTIONS']['string_if_invalid'],
msg.body)
# test email for admins
msg = mail.outbox[1]
self.assertEqual(
msg.subject,
'New workshop request: Ministry of Magic, 03-04 November, 2018',
)
self.assertEqual(msg.recipients(), ['admin-uk@carpentries.org'])
self.assertNotIn(settings.TEMPLATES[0]['OPTIONS']['string_if_invalid'],
msg.body)
@@ -35,8 +35,10 @@ class TrainingRequestCreate(
form_class = TrainingRequestForm
template_name = 'forms/trainingrequest.html'
success_url = reverse_lazy('training_request_confirm')
email_subject = 'Thank you for your application'
email_body_template = 'mailing/training_request.txt'
autoresponder_subject = 'Thank you for your application'
autoresponder_body_template_txt = 'mailing/training_request.txt'
autoresponder_body_template_html = 'mailing/training_request.html'
autoresponder_form_field = 'email'

def get_success_message(self, *args, **kwargs):
"""Don't display a success message."""
@@ -65,6 +67,7 @@ def get_context_data(self, **kwargs):
class WorkshopRequestCreate(
LoginNotRequiredMixin,
EmailSendMixin,
AutoresponderMixin,
AMYCreateView,
):
model = WorkshopRequest
@@ -74,6 +77,14 @@ class WorkshopRequestCreate(
success_url = reverse_lazy('workshop_request_confirm')
email_fail_silently = False

autoresponder_subject = 'Workshop request confirmation'
autoresponder_body_template_txt = 'mailing/workshoprequest.txt'
autoresponder_body_template_html = 'mailing/workshoprequest.html'
autoresponder_form_field = 'email'

def autoresponder_email_context(self, form):
return dict(object=self.object)

def get_success_message(self, *args, **kwargs):
"""Don't display a success message."""
return ''
@@ -108,15 +119,15 @@ def get_body(self):
link_domain = 'https://{}'.format(get_current_site(self.request))

body_txt = get_template(
'mailing/workshoprequest.txt'
'mailing/workshoprequest_admin.txt'
).render({
'object': self.object,
'link': link,
'link_domain': link_domain,
})

body_html = get_template(
'mailing/workshoprequest.html'
'mailing/workshoprequest_admin.html'
).render({
'object': self.object,
'link': link,
@@ -21,6 +21,8 @@
)
from django.shortcuts import redirect, render, get_object_or_404
from django.urls import reverse
from django_countries import countries
from requests.exceptions import HTTPError, RequestException

from extrequests.filters import (
TrainingRequestFilter,
@@ -56,6 +58,7 @@
Task,
Role,
Person,
Language,
)
from workshops.util import (
OnlyForAdminsMixin,
@@ -69,6 +72,9 @@
merge_objects,
failed_to_delete,
InternalError,
fetch_event_metadata,
parse_metadata_from_event_website,
WrongWorkshopURL,
)


@@ -135,26 +141,55 @@ class WorkshopRequestSetState(OnlyForAdminsMixin, ChangeRequestStateView):
raise_exception=True)
def workshoprequest_accept_event(request, request_id):
"""Accept event request by creating a new event."""
workshoprequest = get_object_or_404(WorkshopRequest, state='p',
pk=request_id)
form = EventCreateForm()
wr = get_object_or_404(WorkshopRequest, state='p', pk=request_id)

if request.method == 'POST':
form = EventCreateForm(request.POST)

if form.is_valid():
event = form.save()

workshoprequest.state = 'a'
workshoprequest.event = event
workshoprequest.save()
wr.state = 'a'
wr.event = event
wr.save()
return redirect(reverse('event_details',
args=[event.slug]))
else:
messages.error(request, 'Fix errors below.')

else:
# non-POST request
form = EventCreateForm()

# perhaps this WorkshopRequest has URL and we could pre-fill
# the form?
if wr.organization_type == 'self' and wr.self_organized_github:

try:
url = wr.self_organized_github.strip()
metadata = fetch_event_metadata(url)
data = parse_metadata_from_event_website(metadata)

if 'language' in data:
lang = data['language'].lower()
data['language'] = Language.objects.get(subtag=lang)

if 'instructors' in data or 'helpers' in data:
instructors = data.get('instructors') or ['none']
helpers = data.get('helpers') or ['none']
data['comment'] = "Instructors: {}\n\nHelpers: {}" \
.format(','.join(instructors), ','.join(helpers))

form = EventCreateForm(initial=data)

except (AttributeError, KeyError, ValueError, HTTPError,
RequestException, WrongWorkshopURL, Language.DoesNotExist):
# ignore errors
messages.warning(request, "Cannot automatically fill the form "
"from provided workshop URL.")

context = {
'object': workshoprequest,
'object': wr,
'form': form,
}
return render(request, 'requests/workshoprequest_accept_event.html',
Oops, something went wrong.

0 comments on commit 11d7fcf

Please sign in to comment.
You can’t perform that action at this time.