From d9264e25e2d5b0770583151c564278c90baf5e2f Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 22 Jun 2022 11:59:41 +0200 Subject: [PATCH 001/112] Add TeamSlots --- bluebottle/activities/admin.py | 5 +- .../migrations/0058_auto_20220622_1050.py | 18 ++ bluebottle/time_based/admin.py | 160 +++++++++++++----- .../migrations/0069_auto_20220622_0930.py | 45 +++++ bluebottle/time_based/models.py | 69 +++++--- bluebottle/time_based/states.py | 6 + 6 files changed, 240 insertions(+), 63 deletions(-) create mode 100644 bluebottle/activities/migrations/0058_auto_20220622_1050.py create mode 100644 bluebottle/time_based/migrations/0069_auto_20220622_0930.py diff --git a/bluebottle/activities/admin.py b/bluebottle/activities/admin.py index 6272e8648d..3d82eb7ca4 100644 --- a/bluebottle/activities/admin.py +++ b/bluebottle/activities/admin.py @@ -682,9 +682,10 @@ class TeamAdmin(StateMachineAdmin): def get_inline_instances(self, request, obj=None): self.inlines = [] if isinstance(obj.activity, PeriodActivity): - from bluebottle.time_based.admin import PeriodParticipantAdminInline + from bluebottle.time_based.admin import PeriodParticipantAdminInline, TeamSlotInline self.inlines = [ - PeriodParticipantAdminInline + PeriodParticipantAdminInline, + TeamSlotInline ] return super(TeamAdmin, self).get_inline_instances(request, obj) diff --git a/bluebottle/activities/migrations/0058_auto_20220622_1050.py b/bluebottle/activities/migrations/0058_auto_20220622_1050.py new file mode 100644 index 0000000000..574db89232 --- /dev/null +++ b/bluebottle/activities/migrations/0058_auto_20220622_1050.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.24 on 2022-06-22 08:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('activities', '0057_auto_20220608_1513'), + ] + + operations = [ + migrations.AlterField( + model_name='activity', + name='team_activity', + field=models.CharField(blank=True, choices=[('teams', 'Teams'), ('individuals', 'Individuals')], default='individuals', help_text='Is this activity open for individuals or can only teams sign up?', max_length=100, verbose_name='participation'), + ), + ] diff --git a/bluebottle/time_based/admin.py b/bluebottle/time_based/admin.py index 5300970e61..4454617498 100644 --- a/bluebottle/time_based/admin.py +++ b/bluebottle/time_based/admin.py @@ -28,7 +28,7 @@ from bluebottle.notifications.admin import MessageAdminInline from bluebottle.time_based.models import ( DateActivity, PeriodActivity, DateParticipant, PeriodParticipant, Participant, TimeContribution, DateActivitySlot, - SlotParticipant, Skill + SlotParticipant, Skill, PeriodActivitySlot, TeamSlot ) from bluebottle.time_based.states import SlotParticipantStateMachine from bluebottle.time_based.utils import nth_weekday, duplicate_slot @@ -251,6 +251,44 @@ def timezone(self, obj): timezone.short_description = _('Timezone') +class TeamSlotForm(ModelForm): + + class Meta: + model = TeamSlot + exclude = [] + + def full_clean(self): + data = super(TeamSlotForm, self).full_clean() + if not self.instance.activity_id and self.instance.team_id: + self.instance.activity = self.instance.team.activity + return data + + +class TeamSlotInline(admin.StackedInline): + model = TeamSlot + + form = TeamSlotForm + verbose_name = _('Time slot') + verbose_name_plural = _('Time slot') + + ordering = ['-start'] + readonly_fields = ['link', 'timezone', ] + raw_id_fields = ['location'] + fields = [ + 'start', + 'timezone', + 'is_online', + 'location' + ] + + def timezone(self, obj): + if not obj.is_online and obj.location: + return obj.location.timezone + else: + return str(obj.start.astimezone(get_current_timezone()).tzinfo) + timezone.short_description = _('Timezone') + + @admin.register(DateActivity) class DateActivityAdmin(TimeBasedAdmin): base_model = DateActivity @@ -391,7 +429,7 @@ def smart_status(self, obj): class SlotAdmin(StateMachineAdmin): - inlines = [SlotParticipantInline] + raw_id_fields = ['activity', 'location'] formfield_overrides = { models.DurationField: { @@ -411,6 +449,38 @@ class SlotAdmin(StateMachineAdmin): }, } + def activity_link(self, obj): + url = reverse( + 'admin:time_based_{}_change'.format(obj.activity._meta.model_name), + args=(obj.activity.id,) + ) + return format_html('{}', url, obj.activity) + activity_link.short_description = _('Activity') + + def get_form(self, request, obj=None, **kwargs): + if obj and not obj.is_online and obj.location: + local_start = obj.start.astimezone(timezone(obj.location.timezone)) + platform_start = obj.start.astimezone(get_current_timezone()) + offset = local_start.utcoffset() - platform_start.utcoffset() + + if offset.total_seconds() != 0: + timezone_text = _( + 'Local time in "{location}" is {local_time}. ' + 'This is {offset} hours {relation} compared to the ' + 'standard platform timezone ({current_timezone}).' + ).format( + location=obj.location, + local_time=defaultfilters.time(local_start), + offset=abs(offset.total_seconds() / 3600.0), + relation=_('later') if offset.total_seconds() > 0 else _('earlier'), + current_timezone=get_current_timezone() + ) + + help_texts = {'start': timezone_text} + kwargs.update({'help_texts': help_texts}) + + return super(SlotAdmin, self).get_form(request, obj, **kwargs) + def duration_string(self, obj): duration = get_human_readable_duration(str(obj.duration)).lower() return duration @@ -440,7 +510,13 @@ def valid(self, obj): 'updated', 'valid' ] - detail_fields = [] + detail_fields = [ + 'activity', + 'is_online', + 'location', + 'location_hint', + 'online_meeting_url' + ] status_fields = [ 'status', 'states', @@ -579,8 +655,7 @@ def __init__(self, slot, data=None, *args, **kwargs): @admin.register(DateActivitySlot) class DateSlotAdmin(SlotAdmin): model = DateActivitySlot - - raw_id_fields = ['activity', 'location'] + inlines = [SlotParticipantInline] def lookup_allowed(self, lookup, value): if lookup == 'activity__slot_selection__exact': @@ -597,11 +672,6 @@ def lookup_allowed(self, lookup, value): RequiredSlotFilter, ] - def activity_link(self, obj): - url = reverse('admin:time_based_dateactivity_change', args=(obj.activity.id,)) - return format_html('{}', url, obj.activity) - activity_link.short_description = _('Activity') - def attendee_limit(self, obj): return obj.capacity or obj.activity.capacity @@ -615,40 +685,11 @@ def required(self, obj): return _('Required') required.short_description = _('Required') - def get_form(self, request, obj=None, **kwargs): - if obj and not obj.is_online and obj.location: - local_start = obj.start.astimezone(timezone(obj.location.timezone)) - platform_start = obj.start.astimezone(get_current_timezone()) - offset = local_start.utcoffset() - platform_start.utcoffset() - - if offset.total_seconds() != 0: - timezone_text = _( - 'Local time in "{location}" is {local_time}. ' - 'This is {offset} hours {relation} compared to the ' - 'standard platform timezone ({current_timezone}).' - ).format( - location=obj.location, - local_time=defaultfilters.time(local_start), - offset=abs(offset.total_seconds() / 3600.0), - relation=_('later') if offset.total_seconds() > 0 else _('earlier'), - current_timezone=get_current_timezone() - ) - - help_texts = {'start': timezone_text} - kwargs.update({'help_texts': help_texts}) - - return super(DateSlotAdmin, self).get_form(request, obj, **kwargs) - detail_fields = SlotAdmin.detail_fields + [ - 'activity', 'title', 'capacity', 'start', 'duration', - 'is_online', - 'location', - 'location_hint', - 'online_meeting_url' ] def get_urls(self): @@ -688,6 +729,47 @@ def duplicate_slot(self, request, pk, *args, **kwargs): ) +@admin.register(PeriodActivitySlot) +class PeriodSlotAdmin(SlotAdmin): + model = PeriodActivitySlot + + date_hierarchy = 'start' + list_display = [ + '__str__', 'start', 'activity_link', + ] + list_filter = [ + 'status', + SlotTimeFilter, + RequiredSlotFilter, + ] + + def participants(self, obj): + return obj.accepted_participants.count() + participants.short_description = _('Accepted participants') + + +@admin.register(TeamSlot) +class TeamSlotAdmin(SlotAdmin): + model = TeamSlot + raw_id_fields = SlotAdmin.raw_id_fields + ['team'] + date_hierarchy = 'start' + list_display = [ + '__str__', 'start', 'activity_link', + ] + list_filter = [ + 'status', + SlotTimeFilter, + RequiredSlotFilter, + ] + detail_fields = SlotAdmin.detail_fields + [ + 'team', + ] + + def participants(self, obj): + return obj.accepted_participants.count() + participants.short_description = _('Accepted participants') + + class TimeContributionInlineAdmin(admin.TabularInline): model = TimeContribution extra = 0 diff --git a/bluebottle/time_based/migrations/0069_auto_20220622_0930.py b/bluebottle/time_based/migrations/0069_auto_20220622_0930.py new file mode 100644 index 0000000000..09064cf022 --- /dev/null +++ b/bluebottle/time_based/migrations/0069_auto_20220622_0930.py @@ -0,0 +1,45 @@ +# Generated by Django 2.2.24 on 2022-06-22 07:30 + +from django.db import migrations, models +import django.db.models.deletion +import parler.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('geo', '0030_merge_20211026_1137'), + ('time_based', '0068_merge_20220608_1149'), + ] + + operations = [ + migrations.AlterModelOptions( + name='timebasedactivity', + options={'base_manager_name': 'objects'}, + ), + migrations.AddField( + model_name='periodactivityslot', + name='is_online', + field=models.NullBooleanField(choices=[(None, 'Not set yet'), (True, 'Yes, anywhere/online'), (False, 'No, enter a location')], default=None, verbose_name='is online'), + ), + migrations.AddField( + model_name='periodactivityslot', + name='location', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='geo.Geolocation', verbose_name='location'), + ), + migrations.AddField( + model_name='periodactivityslot', + name='location_hint', + field=models.TextField(blank=True, null=True, verbose_name='location hint'), + ), + migrations.AddField( + model_name='periodactivityslot', + name='online_meeting_url', + field=models.TextField(blank=True, default='', verbose_name='online meeting link'), + ), + migrations.AlterField( + model_name='skilltranslation', + name='master', + field=parler.fields.TranslationsForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translations', to='time_based.Skill'), + ), + ] diff --git a/bluebottle/time_based/models.py b/bluebottle/time_based/models.py index 23fa70c9f8..e748a9a46c 100644 --- a/bluebottle/time_based/models.py +++ b/bluebottle/time_based/models.py @@ -9,7 +9,7 @@ from parler.models import TranslatableModel, TranslatedFields from timezonefinder import TimezoneFinder -from bluebottle.activities.models import Activity, Contributor, Contribution +from bluebottle.activities.models import Activity, Contributor, Contribution, Team from bluebottle.files.fields import PrivateDocumentField from bluebottle.fsm.triggers import TriggerMixin from bluebottle.geo.models import Geolocation @@ -227,6 +227,26 @@ class ActivitySlot(TriggerMixin, AnonymizationMixin, ValidatedModelMixin, models null=True, blank=True) capacity = models.PositiveIntegerField(_('attendee limit'), null=True, blank=True) + is_online = models.NullBooleanField( + _('is online'), + choices=DateActivity.ONLINE_CHOICES, + null=True, default=None + ) + + online_meeting_url = models.TextField( + _('online meeting link'), + blank=True, default='' + ) + + location = models.ForeignKey( + Geolocation, + verbose_name=_('location'), + null=True, blank=True, + on_delete=models.SET_NULL + ) + + location_hint = models.TextField(_('location hint'), null=True, blank=True) + @property def uid(self): return '{}-{}-{}'.format(connection.tenant.client_name, 'dateactivityslot', self.pk) @@ -298,25 +318,6 @@ class DateActivitySlot(ActivitySlot): start = models.DateTimeField(_('start date and time'), null=True, blank=True) duration = models.DurationField(_('duration'), null=True, blank=True) - is_online = models.NullBooleanField( - _('is online'), - choices=DateActivity.ONLINE_CHOICES, - null=True, default=None - ) - - online_meeting_url = models.TextField( - _('online meeting link'), - blank=True, default='' - ) - - location = models.ForeignKey( - Geolocation, - verbose_name=_('location'), - null=True, blank=True, - on_delete=models.SET_NULL - ) - - location_hint = models.TextField(_('location hint'), null=True, blank=True) @property def required_fields(self): @@ -492,8 +493,8 @@ class PeriodActivitySlot(ActivitySlot): end = models.DateTimeField(_('end date and time'), null=True, blank=True) class Meta: - verbose_name = _('slot') - verbose_name_plural = _('slots') + verbose_name = _('period activity slot') + verbose_name_plural = _('period activity slots') permissions = ( ('api_read_periodactivityslot', 'Can view over a period activity slots through the API'), ('api_add_periodactivityslot', 'Can add over a period activity slots through the API'), @@ -507,6 +508,30 @@ class Meta: ) +class TeamSlot(ActivitySlot): + activity = models.ForeignKey(PeriodActivity, related_name='team_slots', on_delete=models.CASCADE) + start = models.DateTimeField(_('start date and time'), null=True, blank=True) + team = models.OneToOneField(Team, related_name='slot', on_delete=models.CASCADE) + + class Meta: + verbose_name = _('team slot') + verbose_name_plural = _('team slots') + permissions = ( + ('api_read_teamslot', 'Can view over a team slots through the API'), + ('api_add_teamslot', 'Can add over a team slots through the API'), + ('api_change_teamslot', 'Can change over a team slots through the API'), + ('api_delete_teamslot', 'Can delete over a team slots through the API'), + + ('api_read_own_teamslot', 'Can view own over a team slots through the API'), + ('api_add_own_teamslot', 'Can add own over a team slots through the API'), + ('api_change_own_teamslot', 'Can change own over a team slots through the API'), + ('api_delete_own_teamslot', 'Can delete own over a team slots through the API'), + ) + + def __str__(self): + return str(_('Time slot for {}')).format(self.team) + + class Participant(Contributor): @property diff --git a/bluebottle/time_based/states.py b/bluebottle/time_based/states.py index a0d9cc1743..d71b886b2e 100644 --- a/bluebottle/time_based/states.py +++ b/bluebottle/time_based/states.py @@ -6,6 +6,7 @@ from bluebottle.time_based.models import ( DateActivity, PeriodActivity, DateParticipant, PeriodParticipant, TimeContribution, DateActivitySlot, PeriodActivitySlot, SlotParticipant, + TeamSlot, ) from bluebottle.fsm.state import ( register, State, Transition, EmptyState, AllStates, ModelStateMachine @@ -282,6 +283,11 @@ class PeriodActivitySlotStateMachine(ActivitySlotStateMachine): pass +@register(TeamSlot) +class TeamSlotStateMachine(ActivitySlotStateMachine): + pass + + class ParticipantStateMachine(ContributorStateMachine): new = State( _('pending'), From 716f23c84621a3d84a091ad670242e8cfc3d98b8 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 22 Jun 2022 12:00:06 +0200 Subject: [PATCH 002/112] Hide delete button on subsequent tabs --- .../static/admin/js/dashboard.js | 83 ++++++++++++------- 1 file changed, 52 insertions(+), 31 deletions(-) diff --git a/bluebottle/bluebottle_dashboard/static/admin/js/dashboard.js b/bluebottle/bluebottle_dashboard/static/admin/js/dashboard.js index 1369982dcd..0f250befe0 100644 --- a/bluebottle/bluebottle_dashboard/static/admin/js/dashboard.js +++ b/bluebottle/bluebottle_dashboard/static/admin/js/dashboard.js @@ -1,42 +1,63 @@ function removeRedundantTabs() { - var t = 0; - if (django && django.jQuery) { - django.jQuery('.changeform-tabs-item:contains("General")').each(function(index, tab){ - t++; - if (t > 1) { - tab.remove(); - } - }); - } + var t = 0; + if (django && django.jQuery) { + django + .jQuery('.changeform-tabs-item:contains("General")') + .each(function(index, tab) { + t++; + if (t > 1) { + tab.remove(); + } + }); + } } function addHashToInlinePaginator() { - // Make sure nested inline paginator links to the same inline tab - jQuery('.paginator a').each(function(index, btn){ - if (btn.href) { - btn.href = btn.href.split('#')[0] - btn.href += document.location.hash; - } - }); + // Make sure nested inline paginator links to the same inline tab + jQuery(".paginator a").each(function(index, btn) { + if (btn.href) { + btn.href = btn.href.split("#")[0]; + btn.href += document.location.hash; + } + }); } function replaceInlineActivityAddButton() { - django.jQuery('#activities-group .add-row a').unbind(); - django.jQuery('#activities-group .add-row a').click(function(e) { - e.preventDefault(); - var path = document.location.pathname; - path = path.replace('initiatives/initiative/', 'activities/activity/add/?initiative='); - path = path.replace('/change/', ''); - document.location.href = path - }); + django.jQuery("#activities-group .add-row a").unbind(); + django.jQuery("#activities-group .add-row a").click(function(e) { + e.preventDefault(); + var path = document.location.pathname; + path = path.replace( + "initiatives/initiative/", + "activities/activity/add/?initiative=" + ); + path = path.replace("/change/", ""); + document.location.href = path; + }); +} + +function toggleDeleteButton() { + if (window.location.hash === "#/tab/module_0/") { + django.jQuery(".deletelink").show(); + } else { + django.jQuery(".deletelink").hide(); + } +} + +function hideDeleteButton() { + toggleDeleteButton(); + django.jQuery(window).on("hashchange", function(e) { + toggleDeleteButton(); + }); } window.onload = function() { - if (!django.jQuery && jQuery) { - django.jQuery = jQuery; - } - replaceInlineActivityAddButton(); - removeRedundantTabs(); - addHashToInlinePaginator(); - window.onhashchange = addHashToInlinePaginator; + if (!django.jQuery && jQuery) { + django.jQuery = jQuery; + } + replaceInlineActivityAddButton(); + removeRedundantTabs(); + addHashToInlinePaginator(); + hideDeleteButton(); + window.onhashchange = addHashToInlinePaginator; }; From cfb5243847398b0be2f66ff539001fe7b652e215 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 22 Jun 2022 12:16:58 +0200 Subject: [PATCH 003/112] Tweak admin --- bluebottle/activities/admin.py | 12 ++++++++++-- .../static/admin/js/dashboard.js | 6 +++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/bluebottle/activities/admin.py b/bluebottle/activities/admin.py index 3d82eb7ca4..3d217e7509 100644 --- a/bluebottle/activities/admin.py +++ b/bluebottle/activities/admin.py @@ -230,10 +230,11 @@ def __init__(self, *args, **kwargs): class TeamInline(admin.TabularInline): model = Team raw_id_fields = ('owner',) - readonly_fields = ('team_link', 'created', 'status') + readonly_fields = ('team_link', 'slot_link', 'created', 'status') fields = readonly_fields + ('owner',) extra = 0 + ordering = ['slot__start'] def team_link(self, obj): return format_html( @@ -241,9 +242,16 @@ def team_link(self, obj): reverse('admin:activities_team_change', args=(obj.id,)), obj ) - team_link.short_description = _('Edit') + def slot_link(self, obj): + return format_html( + '{}', + reverse('admin:activities_team_change', args=(obj.id,)), + obj.slot.start + ) + slot_link.short_description = _('Time slot') + class ActivityChildAdmin(PolymorphicChildModelAdmin, StateMachineAdmin): base_model = Activity diff --git a/bluebottle/bluebottle_dashboard/static/admin/js/dashboard.js b/bluebottle/bluebottle_dashboard/static/admin/js/dashboard.js index 0f250befe0..d3dd38f771 100644 --- a/bluebottle/bluebottle_dashboard/static/admin/js/dashboard.js +++ b/bluebottle/bluebottle_dashboard/static/admin/js/dashboard.js @@ -37,10 +37,10 @@ function replaceInlineActivityAddButton() { } function toggleDeleteButton() { - if (window.location.hash === "#/tab/module_0/") { - django.jQuery(".deletelink").show(); - } else { + if (window.location.hash.startsWith("#/tab/inline")) { django.jQuery(".deletelink").hide(); + } else { + django.jQuery(".deletelink").show(); } } From c6b450d1cbe11c474122bb1279dbe1046dc2caea Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 22 Jun 2022 13:40:38 +0200 Subject: [PATCH 004/112] Improve admin and add migrations --- bluebottle/activities/admin.py | 17 ++++++- .../migrations/0070_auto_20220622_1050.py | 47 +++++++++++++++++++ .../migrations/0071_auto_20220622_1132.py | 19 ++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 bluebottle/time_based/migrations/0070_auto_20220622_1050.py create mode 100644 bluebottle/time_based/migrations/0071_auto_20220622_1132.py diff --git a/bluebottle/activities/admin.py b/bluebottle/activities/admin.py index 3d217e7509..7b5137403b 100644 --- a/bluebottle/activities/admin.py +++ b/bluebottle/activities/admin.py @@ -1,3 +1,5 @@ +from pytz import timezone + from django import forms from django.conf.urls import url from django.contrib import admin @@ -245,10 +247,23 @@ def team_link(self, obj): team_link.short_description = _('Edit') def slot_link(self, obj): + if getattr(obj, 'slot', None): + if obj.slot.location: + return format_html( + '{}', + reverse('admin:activities_team_change', args=(obj.id,)), + obj.slot.start.astimezone(timezone(obj.slot.location.timezone)).strftime('%c') + ) + else: + return format_html( + '{}', + reverse('admin:activities_team_change', args=(obj.id,)), + obj.slot.start.strftime('%c') + ) return format_html( '{}', reverse('admin:activities_team_change', args=(obj.id,)), - obj.slot.start + _('Add time slot') ) slot_link.short_description = _('Time slot') diff --git a/bluebottle/time_based/migrations/0070_auto_20220622_1050.py b/bluebottle/time_based/migrations/0070_auto_20220622_1050.py new file mode 100644 index 0000000000..178d799d18 --- /dev/null +++ b/bluebottle/time_based/migrations/0070_auto_20220622_1050.py @@ -0,0 +1,47 @@ +# Generated by Django 2.2.24 on 2022-06-22 08:50 + +import bluebottle.fsm.triggers +import bluebottle.utils.models +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('activities', '0058_auto_20220622_1050'), + ('geo', '0030_merge_20211026_1137'), + ('time_based', '0069_auto_20220622_0930'), + ] + + operations = [ + migrations.AlterModelOptions( + name='periodactivityslot', + options={'permissions': (('api_read_periodactivityslot', 'Can view over a period activity slots through the API'), ('api_add_periodactivityslot', 'Can add over a period activity slots through the API'), ('api_change_periodactivityslot', 'Can change over a period activity slots through the API'), ('api_delete_periodactivityslot', 'Can delete over a period activity slots through the API'), ('api_read_own_periodactivityslot', 'Can view own over a period activity slots through the API'), ('api_add_own_periodactivityslot', 'Can add own over a period activity slots through the API'), ('api_change_own_periodactivityslot', 'Can change own over a period activity slots through the API'), ('api_delete_own_periodactivityslot', 'Can delete own over a period activity slots through the API')), 'verbose_name': 'period activity slot', 'verbose_name_plural': 'period activity slots'}, + ), + migrations.CreateModel( + name='TeamSlot', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(default=django.utils.timezone.now)), + ('updated', models.DateTimeField(auto_now=True)), + ('status', models.CharField(max_length=40)), + ('title', models.CharField(blank=True, max_length=255, null=True, verbose_name='title')), + ('capacity', models.PositiveIntegerField(blank=True, null=True, verbose_name='attendee limit')), + ('is_online', models.NullBooleanField(choices=[(None, 'Not set yet'), (True, 'Yes, anywhere/online'), (False, 'No, enter a location')], default=None, verbose_name='is online')), + ('online_meeting_url', models.TextField(blank=True, default='', verbose_name='online meeting link')), + ('location_hint', models.TextField(blank=True, null=True, verbose_name='location hint')), + ('start', models.DateTimeField(blank=True, null=True, verbose_name='start date and time')), + ('activity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='team_slots', to='time_based.PeriodActivity')), + ('location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='geo.Geolocation', verbose_name='location')), + ('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='slot', to='activities.Team')), + ], + options={ + 'verbose_name': 'team slot', + 'verbose_name_plural': 'team slots', + 'permissions': (('api_read_teamslot', 'Can view over a team slots through the API'), ('api_add_teamslot', 'Can add over a team slots through the API'), ('api_change_teamslot', 'Can change over a team slots through the API'), ('api_delete_teamslot', 'Can delete over a team slots through the API'), ('api_read_own_teamslot', 'Can view own over a team slots through the API'), ('api_add_own_teamslot', 'Can add own over a team slots through the API'), ('api_change_own_teamslot', 'Can change own over a team slots through the API'), ('api_delete_own_teamslot', 'Can delete own over a team slots through the API')), + }, + bases=(bluebottle.fsm.triggers.TriggerMixin, bluebottle.utils.models.AnonymizationMixin, bluebottle.utils.models.ValidatedModelMixin, models.Model), + ), + ] diff --git a/bluebottle/time_based/migrations/0071_auto_20220622_1132.py b/bluebottle/time_based/migrations/0071_auto_20220622_1132.py new file mode 100644 index 0000000000..fca5177093 --- /dev/null +++ b/bluebottle/time_based/migrations/0071_auto_20220622_1132.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.24 on 2022-06-22 09:32 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('time_based', '0070_auto_20220622_1050'), + ] + + operations = [ + migrations.AlterField( + model_name='teamslot', + name='team', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='slot', to='activities.Team'), + ), + ] From 664ca3bc342f8454d355fb1009c20a36e08db199 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 22 Jun 2022 16:26:07 +0200 Subject: [PATCH 005/112] Brush up serializers --- bluebottle/activities/utils.py | 9 ++++-- bluebottle/activities/views.py | 5 +-- bluebottle/time_based/models.py | 3 ++ bluebottle/time_based/serializers.py | 48 +++++++++++++++++++++------- 4 files changed, 48 insertions(+), 17 deletions(-) diff --git a/bluebottle/activities/utils.py b/bluebottle/activities/utils.py index f13fdca6d3..fddf91778e 100644 --- a/bluebottle/activities/utils.py +++ b/bluebottle/activities/utils.py @@ -25,7 +25,7 @@ from bluebottle.initiatives.models import InitiativePlatformSettings from bluebottle.members.models import Member from bluebottle.segments.models import Segment -from bluebottle.time_based.models import TimeContribution, PeriodParticipant +from bluebottle.time_based.models import TimeContribution, PeriodParticipant, TeamSlot from bluebottle.time_based.states import ParticipantStateMachine from bluebottle.utils.exchange_rates import convert from bluebottle.utils.fields import FSMField, ValidationErrorsField, RequiredErrorsField @@ -51,6 +51,7 @@ class TeamSerializer(ModelSerializer): permission=CanExportTeamParticipantsPermission, read_only=True ) + slot = ResourceRelatedField(queryset=TeamSlot.objects) def get_members(self, instance): user = self.context['request'].user @@ -74,7 +75,7 @@ def get_members(self, instance): class Meta(object): model = Team - fields = ('owner', 'members', 'activity') + fields = ('owner', 'members', 'activity', 'slot') meta_fields = ( 'status', 'transitions', @@ -85,12 +86,16 @@ class Meta(object): class JSONAPIMeta(object): included_resources = [ 'owner', + 'slot', + 'slot.location' ] resource_name = 'activities/teams' included_serializers = { 'owner': 'bluebottle.initiatives.serializers.MemberSerializer', + 'slot': 'bluebottle.time_based.serializers.TeamSlotSerializer', + 'slot.location': 'bluebottle.geo.serializers.GeolocationSerializer', } diff --git a/bluebottle/activities/views.py b/bluebottle/activities/views.py index e134f69fb7..6a8bd0e06e 100644 --- a/bluebottle/activities/views.py +++ b/bluebottle/activities/views.py @@ -181,10 +181,7 @@ def get_queryset(self, *args, **kwargs): queryset = self.queryset.filter( status='open' ) - - return queryset.filter( - activity_id=self.kwargs['activity_id'] - ) + return queryset.filter(activity_id=self.kwargs['activity_id']) class TeamTransitionList(TransitionList): diff --git a/bluebottle/time_based/models.py b/bluebottle/time_based/models.py index e748a9a46c..d93c8117ce 100644 --- a/bluebottle/time_based/models.py +++ b/bluebottle/time_based/models.py @@ -531,6 +531,9 @@ class Meta: def __str__(self): return str(_('Time slot for {}')).format(self.team) + class JSONAPIMeta: + resource_name = 'activities/time-based/team-slots' + class Participant(Contributor): diff --git a/bluebottle/time_based/serializers.py b/bluebottle/time_based/serializers.py index 32c65eb51f..ed3afbbcdc 100644 --- a/bluebottle/time_based/serializers.py +++ b/bluebottle/time_based/serializers.py @@ -24,7 +24,7 @@ from bluebottle.time_based.models import ( TimeBasedActivity, DateActivity, PeriodActivity, DateParticipant, PeriodParticipant, TimeContribution, DateActivitySlot, - SlotParticipant, Skill + SlotParticipant, Skill, TeamSlot ) from bluebottle.time_based.permissions import ParticipantDocumentPermission, CanExportParticipantsPermission from bluebottle.time_based.states import ParticipantStateMachine @@ -86,14 +86,17 @@ class ActivitySlotSerializer(ModelSerializer): permissions = ResourcePermissionField('date-slot-detail', view_args=('pk',)) transitions = AvailableTransitionsField(source='states') status = FSMField(read_only=True) + location = ResourceRelatedField(read_only=True) class Meta: fields = ( 'id', 'activity', 'start', - 'duration', 'transitions', + 'is_online', + 'location_hint', + 'online_meeting_url', ) meta_fields = ( 'status', @@ -107,9 +110,15 @@ class Meta: class JSONAPIMeta(object): included_resources = [ - 'activity', 'location' + 'activity', + 'location', ] + included_serializers = { + 'location': 'bluebottle.geo.serializers.GeolocationSerializer', + 'activity': 'bluebottle.time_based.serializers.DateActivitySerializer', + } + class DateActivitySlotSerializer(ActivitySlotSerializer): @@ -169,20 +178,37 @@ class Meta(ActivitySlotSerializer.Meta): 'links', 'duration', 'capacity', - 'is_online', - 'location', - 'location_hint', - 'online_meeting_url', 'participants', ) class JSONAPIMeta(ActivitySlotSerializer.JSONAPIMeta): resource_name = 'activities/time-based/date-slots' - included_serializers = { - 'location': 'bluebottle.geo.serializers.GeolocationSerializer', - 'activity': 'bluebottle.time_based.serializers.DateActivitySerializer', - } + +class TeamSlotSerializer(ActivitySlotSerializer): + errors = ValidationErrorsField() + required = RequiredErrorsField() + # team = ResourceRelatedField(read_only=True) + + class Meta(ActivitySlotSerializer.Meta): + model = TeamSlot + fields = ActivitySlotSerializer.Meta.fields + ( + 'start', + + ) + + class JSONAPIMeta(ActivitySlotSerializer.JSONAPIMeta): + resource_name = 'activities/time-based/team-slots' + included_resources = ActivitySlotSerializer.JSONAPIMeta.included_resources + [ + 'team', + ] + + included_serializers = dict( + ActivitySlotSerializer.included_serializers, + **{ + 'team': 'bluebottle.activities.utils.TeamSerializer', + } + ) class DateActivitySlotInfoMixin(): From 0e97b6db606b78b6f398655b52586f867e63e084 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 22 Jun 2022 16:41:44 +0200 Subject: [PATCH 006/112] More fixing of serializers --- bluebottle/activities/views.py | 2 +- bluebottle/time_based/serializers.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/bluebottle/activities/views.py b/bluebottle/activities/views.py index 6a8bd0e06e..0351698401 100644 --- a/bluebottle/activities/views.py +++ b/bluebottle/activities/views.py @@ -170,7 +170,7 @@ def get_queryset(self, *args, **kwargs): Q(activity__initiative__activity_managers=self.request.user) | Q(activity__owner=self.request.user) | Q(owner=self.request.user) | - Q(status='open') + Q(activity__status='open') ).annotate( current_user=ExpressionWrapper( Q(members__user=self.request.user), diff --git a/bluebottle/time_based/serializers.py b/bluebottle/time_based/serializers.py index ed3afbbcdc..b5bbe3b8b3 100644 --- a/bluebottle/time_based/serializers.py +++ b/bluebottle/time_based/serializers.py @@ -97,6 +97,7 @@ class Meta: 'is_online', 'location_hint', 'online_meeting_url', + 'location' ) meta_fields = ( 'status', @@ -188,7 +189,6 @@ class JSONAPIMeta(ActivitySlotSerializer.JSONAPIMeta): class TeamSlotSerializer(ActivitySlotSerializer): errors = ValidationErrorsField() required = RequiredErrorsField() - # team = ResourceRelatedField(read_only=True) class Meta(ActivitySlotSerializer.Meta): model = TeamSlot @@ -197,10 +197,12 @@ class Meta(ActivitySlotSerializer.Meta): ) - class JSONAPIMeta(ActivitySlotSerializer.JSONAPIMeta): + class JSONAPIMeta(object): resource_name = 'activities/time-based/team-slots' - included_resources = ActivitySlotSerializer.JSONAPIMeta.included_resources + [ + included_resources = [ + 'activity' 'team', + 'location' ] included_serializers = dict( From 0067133efef4593e39cf6c41b95f44f563eb42f1 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Thu, 23 Jun 2022 08:25:17 +0200 Subject: [PATCH 007/112] Only open teams --- bluebottle/activities/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bluebottle/activities/views.py b/bluebottle/activities/views.py index 0351698401..6a8bd0e06e 100644 --- a/bluebottle/activities/views.py +++ b/bluebottle/activities/views.py @@ -170,7 +170,7 @@ def get_queryset(self, *args, **kwargs): Q(activity__initiative__activity_managers=self.request.user) | Q(activity__owner=self.request.user) | Q(owner=self.request.user) | - Q(activity__status='open') + Q(status='open') ).annotate( current_user=ExpressionWrapper( Q(members__user=self.request.user), From f234ba187ca701c236979e508f0b9dc8930ef029 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Thu, 23 Jun 2022 08:55:26 +0200 Subject: [PATCH 008/112] Fix teams overview --- bluebottle/activities/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bluebottle/activities/views.py b/bluebottle/activities/views.py index 6a8bd0e06e..f56a8873b3 100644 --- a/bluebottle/activities/views.py +++ b/bluebottle/activities/views.py @@ -176,7 +176,7 @@ def get_queryset(self, *args, **kwargs): Q(members__user=self.request.user), output_field=BooleanField() ) - ).order_by('-current_user', '-id') + ).distinct().order_by('-current_user', '-id') else: queryset = self.queryset.filter( status='open' From a576c11e964551f0d58dc58cf4d397cb4d02a7ec Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Thu, 23 Jun 2022 12:11:33 +0200 Subject: [PATCH 009/112] Add duration --- bluebottle/time_based/admin.py | 11 +++++ .../migrations/0072_teamslot_duration.py | 18 +++++++++ bluebottle/time_based/models.py | 40 +++++++++++++++++++ bluebottle/time_based/serializers.py | 2 +- 4 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 bluebottle/time_based/migrations/0072_teamslot_duration.py diff --git a/bluebottle/time_based/admin.py b/bluebottle/time_based/admin.py index 4454617498..813470a39b 100644 --- a/bluebottle/time_based/admin.py +++ b/bluebottle/time_based/admin.py @@ -271,11 +271,22 @@ class TeamSlotInline(admin.StackedInline): verbose_name = _('Time slot') verbose_name_plural = _('Time slot') + formfield_overrides = { + models.DurationField: { + 'widget': TimeDurationWidget( + show_days=False, + show_hours=True, + show_minutes=True, + show_seconds=False) + }, + } + ordering = ['-start'] readonly_fields = ['link', 'timezone', ] raw_id_fields = ['location'] fields = [ 'start', + 'duration', 'timezone', 'is_online', 'location' diff --git a/bluebottle/time_based/migrations/0072_teamslot_duration.py b/bluebottle/time_based/migrations/0072_teamslot_duration.py new file mode 100644 index 0000000000..c45a654bd0 --- /dev/null +++ b/bluebottle/time_based/migrations/0072_teamslot_duration.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.24 on 2022-06-23 10:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('time_based', '0071_auto_20220622_1132'), + ] + + operations = [ + migrations.AddField( + model_name='teamslot', + name='duration', + field=models.DurationField(blank=True, null=True, verbose_name='duration'), + ), + ] diff --git a/bluebottle/time_based/models.py b/bluebottle/time_based/models.py index d93c8117ce..a57b8cf309 100644 --- a/bluebottle/time_based/models.py +++ b/bluebottle/time_based/models.py @@ -511,8 +511,48 @@ class Meta: class TeamSlot(ActivitySlot): activity = models.ForeignKey(PeriodActivity, related_name='team_slots', on_delete=models.CASCADE) start = models.DateTimeField(_('start date and time'), null=True, blank=True) + duration = models.DurationField(_('duration'), null=True, blank=True) team = models.OneToOneField(Team, related_name='slot', on_delete=models.CASCADE) + @property + def required_fields(self): + fields = super().required_fields + [ + 'start', + 'duration', + 'is_online', + ] + + if not self.is_online: + fields.append('location') + return fields + + @property + def end(self): + if self.start and self.duration: + return self.start + self.duration + + @property + def sequence(self): + ids = list(self.activity.slots.values_list('id', flat=True)) + if len(ids) and self.id and self.id in ids: + return ids.index(self.id) + 1 + return '-' + + @property + def local_timezone(self): + if self.location and self.location.position: + tz_name = tf.timezone_at( + lng=self.location.position.x, + lat=self.location.position.y + ) + return pytz.timezone(tz_name) + + @property + def utc_offset(self): + tz = self.local_timezone or timezone.get_current_timezone() + if self.start and tz: + return self.start.astimezone(tz).utcoffset().total_seconds() / 60 + class Meta: verbose_name = _('team slot') verbose_name_plural = _('team slots') diff --git a/bluebottle/time_based/serializers.py b/bluebottle/time_based/serializers.py index b5bbe3b8b3..36136c01be 100644 --- a/bluebottle/time_based/serializers.py +++ b/bluebottle/time_based/serializers.py @@ -194,7 +194,7 @@ class Meta(ActivitySlotSerializer.Meta): model = TeamSlot fields = ActivitySlotSerializer.Meta.fields + ( 'start', - + 'duration', ) class JSONAPIMeta(object): From 4cf3ef95b5a4531ea29c717e836832598355628a Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Fri, 24 Jun 2022 09:41:05 +0200 Subject: [PATCH 010/112] Add tests for loading teams --- bluebottle/test/utils.py | 11 ++++++++-- bluebottle/time_based/tests/test_api.py | 29 ++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/bluebottle/test/utils.py b/bluebottle/test/utils.py index a99b401798..6f91038c7a 100644 --- a/bluebottle/test/utils.py +++ b/bluebottle/test/utils.py @@ -407,6 +407,13 @@ def get_included(self, relationship): if {'type': included['type'], 'id': included['id']} in relations ] + def getRelatedLink(self, relation, data=None): + """ + Get the link to a relationship + """ + data = data or self.response.json()['data'] + return data['relationships'][relation]['links']['related'] + def assertRelationship(self, relation, models=None, data=None): """ Assert that a resource with `relation` is linked in the response @@ -418,7 +425,6 @@ def assertRelationship(self, relation, models=None, data=None): self.assertRelationship(relation, models, resource) else: self.assertTrue(relation in data['relationships']) - if models: relation_data = data['relationships'][relation]['data'] if not isinstance(relation_data, (tuple, list)): @@ -433,7 +439,8 @@ def assertRelationship(self, relation, models=None, data=None): def assertNoRelationship(self, relation): self.assertFalse(relation in self.response.json()['data']['relationships']) - def assertObjectList(self, data, models=None): + def assertObjectList(self, data=None, models=None): + data = data or self.response.json()['data'] if models: ids = [resource['id'] for resource in data] for model in models: diff --git a/bluebottle/time_based/tests/test_api.py b/bluebottle/time_based/tests/test_api.py index 0c5ce699b8..5513b2d514 100644 --- a/bluebottle/time_based/tests/test_api.py +++ b/bluebottle/time_based/tests/test_api.py @@ -24,6 +24,7 @@ ) from bluebottle.test.utils import BluebottleTestCase, JSONAPITestClient, get_first_included_by_type from bluebottle.time_based.models import SlotParticipant, Skill, PeriodActivity +from bluebottle.time_based.serializers import PeriodActivitySerializer from bluebottle.time_based.tests.factories import ( DateActivityFactory, PeriodActivityFactory, DateParticipantFactory, PeriodParticipantFactory, @@ -1048,7 +1049,6 @@ def test_matching_location_location_too_far(self): self.assertEqual(data['meta']['matching-properties']['location'], False) def test_get_owner_export_teams_enabled(self): - initiative_settings = InitiativePlatformSettings.load() initiative_settings.enable_participant_exports = True initiative_settings.team_activities = True @@ -1079,6 +1079,33 @@ def test_get_owner_export_teams_enabled(self): ) +class TeamsActivityAPIViewTestCase(APITestCase): + + def setUp(self): + super().setUp() + self.serializer = PeriodActivitySerializer + self.manager = BlueBottleUserFactory.create() + self.activity = PeriodActivityFactory.create( + team_activity='teams', + owner=self.manager + ) + self.team_captain = PeriodParticipantFactory.create(activity=self.activity) + self.team = self.team_captain.team + + PeriodParticipantFactory.create_batch( + 3, activity=self.activity, team=self.team + ) + self.activity_url = reverse('period-detail', args=(self.activity.pk,)) + + def test_activity_has_teams(self): + self.response = self.client.get(self.activity_url, user=self.activity.owner) + self.assertStatus(status.HTTP_200_OK) + teams_url = self.getRelatedLink('teams') + self.response = self.client.get(teams_url, user=self.activity.owner) + self.assertStatus(status.HTTP_200_OK) + self.assertObjectList(models=[self.team]) + + class TimeBasedTransitionAPIViewTestCase(): def setUp(self): super().setUp() From 8ebd6ca6840b12a718b6c8aed9fb0381d6466610 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Fri, 24 Jun 2022 12:42:21 +0200 Subject: [PATCH 011/112] Add test for creating teamslot --- bluebottle/time_based/serializers.py | 1 + bluebottle/time_based/tests/factories.py | 12 +++++++++- bluebottle/time_based/tests/test_api.py | 29 ++++++++++++++++++++---- bluebottle/time_based/urls/api.py | 10 +++++++- bluebottle/time_based/views.py | 24 ++++++++++++++++++-- 5 files changed, 68 insertions(+), 8 deletions(-) diff --git a/bluebottle/time_based/serializers.py b/bluebottle/time_based/serializers.py index 36136c01be..b8153d0273 100644 --- a/bluebottle/time_based/serializers.py +++ b/bluebottle/time_based/serializers.py @@ -193,6 +193,7 @@ class TeamSlotSerializer(ActivitySlotSerializer): class Meta(ActivitySlotSerializer.Meta): model = TeamSlot fields = ActivitySlotSerializer.Meta.fields + ( + 'team', 'start', 'duration', ) diff --git a/bluebottle/time_based/tests/factories.py b/bluebottle/time_based/tests/factories.py index f5114c2901..81bfe5ee6f 100644 --- a/bluebottle/time_based/tests/factories.py +++ b/bluebottle/time_based/tests/factories.py @@ -9,7 +9,7 @@ from bluebottle.test.factory_models.geo import GeolocationFactory from bluebottle.time_based.models import ( DateActivity, PeriodActivity, - DateParticipant, PeriodParticipant, TimeContribution, DateActivitySlot, SlotParticipant, Skill + DateParticipant, PeriodParticipant, TimeContribution, DateActivitySlot, SlotParticipant, Skill, TeamSlot ) @@ -116,3 +116,13 @@ class Meta(object): slot = factory.SubFactory(DateActivitySlotFactory) participant = factory.SubFactory(DateParticipantFactory) + + +class TeamSlotFactory(factory.DjangoModelFactory): + class Meta(object): + model = TeamSlot + + is_online = False + location = factory.SubFactory(GeolocationFactory) + start = now() + timedelta(weeks=4) + duration = timedelta(hours=2) diff --git a/bluebottle/time_based/tests/test_api.py b/bluebottle/time_based/tests/test_api.py index 5513b2d514..bb4c769224 100644 --- a/bluebottle/time_based/tests/test_api.py +++ b/bluebottle/time_based/tests/test_api.py @@ -24,11 +24,11 @@ ) from bluebottle.test.utils import BluebottleTestCase, JSONAPITestClient, get_first_included_by_type from bluebottle.time_based.models import SlotParticipant, Skill, PeriodActivity -from bluebottle.time_based.serializers import PeriodActivitySerializer +from bluebottle.time_based.serializers import TeamSlotSerializer from bluebottle.time_based.tests.factories import ( DateActivityFactory, PeriodActivityFactory, DateParticipantFactory, PeriodParticipantFactory, - DateActivitySlotFactory, SlotParticipantFactory, SkillFactory + DateActivitySlotFactory, SlotParticipantFactory, SkillFactory, TeamSlotFactory ) @@ -1079,11 +1079,10 @@ def test_get_owner_export_teams_enabled(self): ) -class TeamsActivityAPIViewTestCase(APITestCase): +class TeamSlotAPIViewTestCase(APITestCase): def setUp(self): super().setUp() - self.serializer = PeriodActivitySerializer self.manager = BlueBottleUserFactory.create() self.activity = PeriodActivityFactory.create( team_activity='teams', @@ -1097,6 +1096,24 @@ def setUp(self): ) self.activity_url = reverse('period-detail', args=(self.activity.pk,)) + self.url = reverse('team-slot-list') + self.serializer = TeamSlotSerializer + self.factory = TeamSlotFactory + + self.defaults = { + 'activity': self.activity, + 'team': self.team, + 'start': (now() + timedelta(days=2)).replace(hour=11, minute=0, second=0.0), + 'duration': '2:00:00', + } + + self.fields = [ + 'activity', + 'team', + 'start', + 'duration' + ] + def test_activity_has_teams(self): self.response = self.client.get(self.activity_url, user=self.activity.owner) self.assertStatus(status.HTTP_200_OK) @@ -1105,6 +1122,10 @@ def test_activity_has_teams(self): self.assertStatus(status.HTTP_200_OK) self.assertObjectList(models=[self.team]) + def test_create_team_slot(self): + self.perform_create(user=self.manager) + self.assertStatus(status.HTTP_201_CREATED) + class TimeBasedTransitionAPIViewTestCase(): def setUp(self): diff --git a/bluebottle/time_based/urls/api.py b/bluebottle/time_based/urls/api.py index cfae2ec14f..e7265814f6 100644 --- a/bluebottle/time_based/urls/api.py +++ b/bluebottle/time_based/urls/api.py @@ -14,7 +14,7 @@ SlotParticipantListView, SlotParticipantDetailView, SlotParticipantTransitionList, DateActivityIcalView, ActivitySlotIcalView, DateParticipantExportView, PeriodParticipantExportView, SlotRelatedParticipantList, SkillList, SkillDetail, - RelatedSlotParticipantListView + RelatedSlotParticipantListView, TeamSlotListView, TeamSlotDetailView ) urlpatterns = [ @@ -62,6 +62,14 @@ PeriodActivityRelatedParticipantList.as_view(), name='period-participants'), + url(r'^/team/slots$', + TeamSlotListView.as_view(), + name='team-slot-list'), + + url(r'^/team/slots/(?P\d+)$', + TeamSlotDetailView.as_view(), + name='team-slot-detail'), + url(r'^/date/transitions$', DateTransitionList.as_view(), name='date-transition-list'), diff --git a/bluebottle/time_based/views.py b/bluebottle/time_based/views.py index 5483132ce0..399561b97d 100644 --- a/bluebottle/time_based/views.py +++ b/bluebottle/time_based/views.py @@ -23,7 +23,7 @@ DateActivity, PeriodActivity, DateParticipant, PeriodParticipant, TimeContribution, - DateActivitySlot, SlotParticipant, Skill + DateActivitySlot, SlotParticipant, Skill, TeamSlot ) from bluebottle.time_based.permissions import ( SlotParticipantPermission, DateSlotActivityStatusPermission @@ -41,7 +41,7 @@ TimeContributionSerializer, DateActivitySlotSerializer, SlotParticipantSerializer, - SlotParticipantTransitionSerializer, SkillSerializer + SlotParticipantTransitionSerializer, SkillSerializer, TeamSlotSerializer ) from bluebottle.transitions.views import TransitionList from bluebottle.utils.admin import prep_field @@ -202,6 +202,26 @@ class DateSlotDetailView(JsonApiViewMixin, RetrieveUpdateDestroyAPIView): serializer_class = DateActivitySlotSerializer +class TeamSlotListView(DateSlotListView): + related_permission_classes = { + 'activity': [ + ActivityStatusPermission, + OneOf(ResourcePermission, ActivityOwnerPermission), + DeleteActivityPermission + ] + } + + permission_classes = [TenantConditionalOpenClose, DateSlotActivityStatusPermission, ] + queryset = TeamSlot.objects.all() + serializer_class = TeamSlotSerializer + + +class TeamSlotDetailView(DateSlotDetailView): + permission_classes = [DateSlotActivityStatusPermission, ] + queryset = TeamSlot.objects.all() + serializer_class = TeamSlotSerializer + + class DateActivityRelatedParticipantList(RelatedContributorListView): queryset = DateParticipant.objects.prefetch_related( 'user', 'slot_participants', 'slot_participants__slot' From 6f1a1aad7b530c093c361279fbddc9f2fcb91de5 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Fri, 24 Jun 2022 13:29:27 +0200 Subject: [PATCH 012/112] Complete test --- bluebottle/geo/models.py | 2 +- bluebottle/time_based/serializers.py | 4 +++- bluebottle/time_based/tests/test_api.py | 26 ++++++++++++++++++++++--- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/bluebottle/geo/models.py b/bluebottle/geo/models.py index 8fe480210e..af4fe2f1ce 100644 --- a/bluebottle/geo/models.py +++ b/bluebottle/geo/models.py @@ -237,7 +237,7 @@ class Geolocation(models.Model): anonymized = False class JSONAPIMeta(object): - resource_name = 'geo-locations' + resource_name = 'geolocations' def __str__(self): if self.locality: diff --git a/bluebottle/time_based/serializers.py b/bluebottle/time_based/serializers.py index b8153d0273..ee3dc7c921 100644 --- a/bluebottle/time_based/serializers.py +++ b/bluebottle/time_based/serializers.py @@ -21,6 +21,7 @@ from bluebottle.bluebottle_drf2.serializers import PrivateFileSerializer from bluebottle.files.serializers import PrivateDocumentSerializer, PrivateDocumentField from bluebottle.fsm.serializers import TransitionSerializer, AvailableTransitionsField +from bluebottle.geo.models import Geolocation from bluebottle.time_based.models import ( TimeBasedActivity, DateActivity, PeriodActivity, DateParticipant, PeriodParticipant, TimeContribution, DateActivitySlot, @@ -86,7 +87,7 @@ class ActivitySlotSerializer(ModelSerializer): permissions = ResourcePermissionField('date-slot-detail', view_args=('pk',)) transitions = AvailableTransitionsField(source='states') status = FSMField(read_only=True) - location = ResourceRelatedField(read_only=True) + location = ResourceRelatedField(queryset=Geolocation.objects, required=False, allow_null=True) class Meta: fields = ( @@ -196,6 +197,7 @@ class Meta(ActivitySlotSerializer.Meta): 'team', 'start', 'duration', + 'location' ) class JSONAPIMeta(object): diff --git a/bluebottle/time_based/tests/test_api.py b/bluebottle/time_based/tests/test_api.py index bb4c769224..ef87282c32 100644 --- a/bluebottle/time_based/tests/test_api.py +++ b/bluebottle/time_based/tests/test_api.py @@ -17,7 +17,7 @@ from bluebottle.members.models import MemberPlatformSettings from bluebottle.segments.tests.factories import SegmentTypeFactory, SegmentFactory from bluebottle.test.factory_models.accounts import BlueBottleUserFactory -from bluebottle.test.factory_models.geo import LocationFactory, PlaceFactory +from bluebottle.test.factory_models.geo import LocationFactory, PlaceFactory, GeolocationFactory from bluebottle.test.factory_models.projects import ThemeFactory from bluebottle.test.utils import ( APITestCase @@ -1103,15 +1103,21 @@ def setUp(self): self.defaults = { 'activity': self.activity, 'team': self.team, - 'start': (now() + timedelta(days=2)).replace(hour=11, minute=0, second=0.0), + 'start': (now() + timedelta(days=2)).replace(hour=11, minute=0, second=0, microsecond=0), 'duration': '2:00:00', + 'location': None, + 'is_online': True, + 'location_hint': None } self.fields = [ 'activity', 'team', 'start', - 'duration' + 'duration', + 'location', + 'is_online', + 'location_hint' ] def test_activity_has_teams(self): @@ -1126,6 +1132,20 @@ def test_create_team_slot(self): self.perform_create(user=self.manager) self.assertStatus(status.HTTP_201_CREATED) + def test_update_team_slot(self): + self.perform_create(user=self.manager) + self.assertStatus(status.HTTP_201_CREATED) + self.url = reverse('team-slot-detail', args=(self.model.id,)) + location = GeolocationFactory.create() + to_change = { + 'is_online': False, + 'location_hint': 'Ring top bell', + 'location': location + } + self.perform_update(to_change=to_change, user=self.manager) + self.assertEqual(self.model.location_hint, 'Ring top bell') + self.assertEqual(self.model.location, location) + class TimeBasedTransitionAPIViewTestCase(): def setUp(self): From ecee68c3ee68771c54288cef2c957a59008bd5cc Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Mon, 27 Jun 2022 12:54:19 +0200 Subject: [PATCH 013/112] Fix trigger --- bluebottle/activities/triggers.py | 21 ++++++++++++++++++-- bluebottle/time_based/tests/test_triggers.py | 2 ++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/bluebottle/activities/triggers.py b/bluebottle/activities/triggers.py index 3a31d6aa0c..b9192fa21a 100644 --- a/bluebottle/activities/triggers.py +++ b/bluebottle/activities/triggers.py @@ -225,6 +225,15 @@ def needs_review(effect): return hasattr(effect.instance.activity, 'review') and effect.instance.activity.review +def is_not_user(effect): + """ + User is not the participant + """ + if 'user' in effect.options: + return effect.instance.user != effect.options['user'] + return True + + @register(Team) class TeamTriggers(TriggerManager): triggers = [ @@ -241,8 +250,16 @@ class TeamTriggers(TriggerManager): ), TransitionEffect( TeamStateMachine.accept, - conditions=[automatically_accept] - ) + conditions=[ + automatically_accept + ] + ), + TransitionEffect( + TeamStateMachine.accept, + conditions=[ + is_not_user + ] + ), ] ), diff --git a/bluebottle/time_based/tests/test_triggers.py b/bluebottle/time_based/tests/test_triggers.py index 203ea972ca..bb0c4ab7a9 100644 --- a/bluebottle/time_based/tests/test_triggers.py +++ b/bluebottle/time_based/tests/test_triggers.py @@ -954,6 +954,8 @@ def test_initial_added_through_admin_team(self): participant.save() self.assertTrue(participant.team) self.assertEqual(participant.team.owner, participant.user) + participant.team.refresh_from_db() + self.assertEqual(participant.team.status, 'open') def test_initiate_team_invite(self): self.activity.team_activity = Activity.TeamActivityChoices.teams From db051a90f7b6924285ff7b0d92e6b6e4c9ed7fe5 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Tue, 28 Jun 2022 12:42:14 +0200 Subject: [PATCH 014/112] Fix some serializers and such --- bluebottle/activities/effects.py | 6 +- bluebottle/activities/triggers.py | 17 +-- bluebottle/fsm/factory.py | 35 ++++++ bluebottle/time_based/tests/factories.py | 6 +- bluebottle/time_based/tests/test_triggers.py | 125 ++++++++++++------- bluebottle/time_based/triggers.py | 33 ++--- 6 files changed, 138 insertions(+), 84 deletions(-) create mode 100644 bluebottle/fsm/factory.py diff --git a/bluebottle/activities/effects.py b/bluebottle/activities/effects.py index 19e3057281..1e1ad64283 100644 --- a/bluebottle/activities/effects.py +++ b/bluebottle/activities/effects.py @@ -128,19 +128,19 @@ def is_valid(self): ) def pre_save(self, effects): - self.transitioned_conributions = [] + self.transitioned_contributions = [] for contribution in self.contributions: effect = TransitionEffect(self.transition)(contribution) if effect.is_valid: - self.transitioned_conributions.append(contribution) + self.transitioned_contributions.append(contribution) effect.pre_save(effects=effects) effects.append(effect) contribution.execute_triggers(effects=effects) def post_save(self): - for contribution in self.transitioned_conributions: + for contribution in self.transitioned_contributions: try: contribution.contributor.refresh_from_db() contribution.save() diff --git a/bluebottle/activities/triggers.py b/bluebottle/activities/triggers.py index b9192fa21a..8132184a00 100644 --- a/bluebottle/activities/triggers.py +++ b/bluebottle/activities/triggers.py @@ -213,7 +213,7 @@ def contributor_is_active(contribution): def automatically_accept(effect): """ - automatically accept participants + automatically accept team """ return not hasattr(effect.instance.activity, 'review') or not effect.instance.activity.review @@ -225,15 +225,6 @@ def needs_review(effect): return hasattr(effect.instance.activity, 'review') and effect.instance.activity.review -def is_not_user(effect): - """ - User is not the participant - """ - if 'user' in effect.options: - return effect.instance.user != effect.options['user'] - return True - - @register(Team) class TeamTriggers(TriggerManager): triggers = [ @@ -254,12 +245,6 @@ class TeamTriggers(TriggerManager): automatically_accept ] ), - TransitionEffect( - TeamStateMachine.accept, - conditions=[ - is_not_user - ] - ), ] ), diff --git a/bluebottle/fsm/factory.py b/bluebottle/fsm/factory.py new file mode 100644 index 0000000000..7b1dc9466b --- /dev/null +++ b/bluebottle/fsm/factory.py @@ -0,0 +1,35 @@ +from factory import DjangoModelFactory + + +class FSMModelFactory(DjangoModelFactory): + + @classmethod + def create(cls, as_user=None, as_relation=None, **kwargs): + if as_user: + model = super(FSMModelFactory, cls).build(**kwargs) + model.execute_triggers(user=as_user) + model.save() + return model + if as_relation: + model = super(FSMModelFactory, cls).build(**kwargs) + model.execute_triggers(user=getattr(model, as_relation)) + model.save() + return model + + return super(FSMModelFactory, cls).create(**kwargs) + + @classmethod + def create_batch(cls, size, as_user=None, as_relation=None, **kwargs): + if as_user: + batch = super(FSMModelFactory, cls).build_batch(size, **kwargs) + for model in batch: + model.execute_triggers(user=as_user) + model.save() + return batch + if as_relation: + batch = super(FSMModelFactory, cls).build_batch(size, **kwargs) + for model in batch: + model.execute_triggers(user=getattr(model, as_relation)) + model.save() + return batch + return super(FSMModelFactory, cls).create_batch(size, **kwargs) diff --git a/bluebottle/time_based/tests/factories.py b/bluebottle/time_based/tests/factories.py index 81bfe5ee6f..049e8f33e9 100644 --- a/bluebottle/time_based/tests/factories.py +++ b/bluebottle/time_based/tests/factories.py @@ -1,6 +1,8 @@ from datetime import timedelta, date import factory.fuzzy + +from bluebottle.fsm.factory import FSMModelFactory from bluebottle.utils.models import Language from django.utils.timezone import now @@ -82,7 +84,7 @@ class Meta: start = (now() + timedelta(weeks=2)).date() -class DateParticipantFactory(factory.DjangoModelFactory): +class DateParticipantFactory(FSMModelFactory): class Meta(object): model = DateParticipant @@ -90,7 +92,7 @@ class Meta(object): user = factory.SubFactory(BlueBottleUserFactory) -class PeriodParticipantFactory(factory.DjangoModelFactory): +class PeriodParticipantFactory(FSMModelFactory): class Meta(object): model = PeriodParticipant diff --git a/bluebottle/time_based/tests/test_triggers.py b/bluebottle/time_based/tests/test_triggers.py index bb0c4ab7a9..5ed8d729aa 100644 --- a/bluebottle/time_based/tests/test_triggers.py +++ b/bluebottle/time_based/tests/test_triggers.py @@ -912,13 +912,11 @@ def setUp(self): def test_initial_added_through_admin(self): mail.outbox = [] - participant = self.participant_factory.build( + participant = self.participant_factory.create( activity=self.review_activity, - user=BlueBottleUserFactory.create() + user=BlueBottleUserFactory.create(), + as_user=self.admin_user ) - participant.execute_triggers(user=self.admin_user, send_messages=True) - participant.save() - self.assertEqual(participant.status, 'accepted') self.assertEqual(len(mail.outbox), 2) @@ -946,15 +944,14 @@ def test_initial_added_through_admin_team(self): self.review_activity.team_activity = Activity.TeamActivityChoices.teams self.review_activity.save() - participant = self.participant_factory.build( + participant = self.participant_factory.create( activity=self.review_activity, - user=BlueBottleUserFactory.create() + user=BlueBottleUserFactory.create(), + as_user=self.admin_user ) - participant.execute_triggers(user=self.admin_user, send_messages=True) - participant.save() self.assertTrue(participant.team) self.assertEqual(participant.team.owner, participant.user) - participant.team.refresh_from_db() + self.assertEqual(participant.status, 'accepted') self.assertEqual(participant.team.status, 'open') def test_initiate_team_invite(self): @@ -980,10 +977,13 @@ def test_initiate_team_invite_review(self): self.activity.review = True self.activity.save() + capt = BlueBottleUserFactory.create() team_captain = self.participant_factory.create( activity=self.activity, - user=BlueBottleUserFactory.create() + user=capt, + as_user=capt ) + team_captain.states.accept(save=True) mail.outbox = [] @@ -1001,34 +1001,38 @@ def test_initiate_team_invite_review_after_signup(self): self.activity.review = True self.activity.save() + capt = BlueBottleUserFactory.create() + team_captain = self.participant_factory.create( activity=self.activity, - user=BlueBottleUserFactory.create() + user=capt, + as_user=capt ) mail.outbox = [] + user = BlueBottleUserFactory.create() participant = self.participant_factory.create( activity=self.activity, accepted_invite=team_captain.invite, - user=BlueBottleUserFactory.create() + user=user, + as_user=user ) + self.assertEqual(participant.team, team_captain.team) 'New team member' in [message.subject for message in mail.outbox] - self.assertEqual(participant.status, 'new') team_captain.states.accept(save=True) - participant.refresh_from_db() + self.assertEqual(team_captain.status, 'accepted') self.assertEqual(participant.status, 'accepted') def test_initial_removed_through_admin(self): mail.outbox = [] - participant = self.participant_factory.build( + participant = self.participant_factory.create( activity=self.review_activity, - user=BlueBottleUserFactory.create() + user=BlueBottleUserFactory.create(), + as_user=self.admin_user ) - participant.execute_triggers(user=self.admin_user, send_messages=True) - participant.save() mail.outbox = [] participant.states.remove() participant.execute_triggers(user=self.admin_user, send_messages=True) @@ -1048,8 +1052,11 @@ def test_initial_removed_through_admin(self): ) def test_accept(self): + user = BlueBottleUserFactory.create() participant = self.participant_factory.create( - activity=self.review_activity + activity=self.review_activity, + user=user, + as_user=user ) mail.outbox = [] @@ -1077,8 +1084,11 @@ def test_accept_team(self): self.review_activity.team_activity = Activity.TeamActivityChoices.teams self.review_activity.save() + user = BlueBottleUserFactory.create() participant = self.participant_factory.create( - activity=self.review_activity + activity=self.review_activity, + user=user, + as_user=user ) participant.states.accept(save=True) @@ -1087,12 +1097,12 @@ def test_accept_team(self): def test_initial_review(self): mail.outbox = [] - participant = self.participant_factory.build( + user = BlueBottleUserFactory.create() + participant = self.participant_factory.create( activity=self.review_activity, - user=BlueBottleUserFactory.create() + user=user, + as_user=user ) - participant.execute_triggers(user=participant.user, send_messages=True) - participant.save() self.assertEqual(participant.status, 'new') self.assertEqual(len(mail.outbox), 2) @@ -1129,12 +1139,12 @@ def test_initial_team_created(self): def test_initial_no_review(self): mail.outbox = [] - participant = self.participant_factory.build( + user = BlueBottleUserFactory.create() + participant = self.participant_factory.create( activity=self.activity, - user=BlueBottleUserFactory.create() + user=user, + as_user=user ) - participant.execute_triggers(user=participant.user, send_messages=True) - participant.save() self.assertEqual(participant.status, 'accepted') self.assertEqual(len(mail.outbox), 2) @@ -1161,15 +1171,13 @@ def test_initial_no_review(self): def test_initial_no_review_team(self): self.activity.team_activity = Activity.TeamActivityChoices.teams self.activity.save() - - participant = self.participant_factory.build( + user = BlueBottleUserFactory.create() + participant = self.participant_factory.create( activity=self.activity, - user=BlueBottleUserFactory.create() + user=user, + as_user=user ) - participant.execute_triggers(user=participant.user, send_messages=True) - participant.save() - self.assertTrue(participant.team) self.assertEqual(participant.team.owner, participant.user) @@ -1178,7 +1186,6 @@ def test_no_review_fill(self): self.activity.capacity, activity=self.activity ) self.activity.refresh_from_db() - self.assertEqual(self.activity.status, 'full') def test_no_review_fill_cancel(self): @@ -1193,16 +1200,20 @@ def test_no_review_fill_cancel(self): self.assertEqual(self.activity.status, 'cancelled') def test_review_fill(self): - participants = self.participant_factory.build_batch( - self.review_activity.capacity, activity=self.review_activity + participants = self.participant_factory.create_batch( + self.review_activity.capacity, + activity=self.review_activity, + user=BlueBottleUserFactory.create(), + as_relation='user' ) self.review_activity.refresh_from_db() self.assertEqual(self.activity.status, 'open') for participant in participants: - participant.user.save() - participant.execute_triggers(user=participant.user, send_messages=True) + user = participant.user + user.save() + participant.execute_triggers(user=user, send_messages=True) participant.save() participant.states.accept(save=True) @@ -1290,9 +1301,19 @@ def test_remove_team(self): ) def test_reject(self): - self.participants = self.participant_factory.create_batch( - self.activity.capacity, activity=self.review_activity - ) + users = BlueBottleUserFactory.create_batch(self.activity.capacity) + self.participants = [] + for user in users: + participant = self.participant_factory.build( + user=user, + activity=self.review_activity, + ) + participant.execute_triggers(user=user) + participant.save() + + self.participants.append( + participant + ) mail.outbox = [] participant = self.participants[0] @@ -1330,7 +1351,9 @@ def test_reaccept(self): def test_withdraw(self): self.participants = self.participant_factory.create_batch( - self.activity.capacity, activity=self.activity + self.activity.capacity, + activity=self.activity, + user=BlueBottleUserFactory.create() ) self.activity.refresh_from_db() @@ -1697,8 +1720,11 @@ def test_team_join(self): self.activity.team_activity = Activity.TeamActivityChoices.teams self.activity.save() mail.outbox = [] + user = BlueBottleUserFactory.create() participant = self.participant_factory.create( - activity=self.activity + activity=self.activity, + user=user, + as_user=user ) self.assertEqual(len(mail.outbox), 2) self.assertEqual( @@ -1722,7 +1748,9 @@ def test_team_join(self): def test_apply(self): mail.outbox = [] participant = self.participant_factory.create( - activity=self.review_activity + activity=self.review_activity, + user=BlueBottleUserFactory.create(), + as_relation='user' ) self.assertEqual(len(mail.outbox), 2) self.assertEqual( @@ -1774,12 +1802,13 @@ def test_team_accept(self): self.review_activity.save() participant = self.participant_factory.create( - activity=self.review_activity + activity=self.review_activity, + user=BlueBottleUserFactory.create(), + as_relation='user' ) mail.outbox = [] participant.states.accept(save=True) - self.assertEqual(participant.status, 'accepted') self.assertEqual(len(mail.outbox), 1) self.assertEqual( diff --git a/bluebottle/time_based/triggers.py b/bluebottle/time_based/triggers.py index 229098795f..a5b12af4f4 100644 --- a/bluebottle/time_based/triggers.py +++ b/bluebottle/time_based/triggers.py @@ -8,7 +8,7 @@ ActivityExpiredNotification, ActivityRejectedNotification, ActivityCancelledNotification, ActivityRestoredNotification, ParticipantWithdrewConfirmationNotification, - TeamMemberAddedMessage, TeamMemberWithdrewMessage, TeamMemberRemovedMessage + TeamMemberWithdrewMessage, TeamMemberRemovedMessage ) from bluebottle.activities.states import OrganizerStateMachine, TeamStateMachine from bluebottle.activities.triggers import ( @@ -865,13 +865,17 @@ def user_is_not_team_captain(effect): return not effect.instance.team_id or effect.instance.team.owner != effect.options['user'] +def always_false(effect): + return False + + def is_not_user(effect): """ User is not the participant """ if 'user' in effect.options: return effect.instance.user != effect.options['user'] - return False + return True def is_user(effect): @@ -880,7 +884,7 @@ def is_user(effect): """ if 'user' in effect.options: return effect.instance.user == effect.options['user'] - return True + return False def is_owner(effect): @@ -968,10 +972,6 @@ def is_team_activity(effect): return effect.instance.activity.team_activity == 'teams' -def is_added_to_team(effect): - """Contributor is part of a team""" - - def team_is_open(effect): """Team status is open, or there is no team""" return ( @@ -1028,12 +1028,6 @@ class ParticipantTriggers(ContributorTriggers): is_user ] ), - NotificationEffect( - TeamMemberAddedMessage, - conditions=[ - is_added_to_team - ] - ), TransitionEffect( ParticipantStateMachine.add, conditions=[ @@ -1141,11 +1135,17 @@ class ParticipantTriggers(ContributorTriggers): 'finished_contributions', TimeContributionStateMachine.succeed, ), - RelatedTransitionEffect( 'preparation_contributions', TimeContributionStateMachine.succeed, ), + RelatedTransitionEffect( + 'team', + TeamStateMachine.accept, + conditions=[ + has_team + ] + ), ] ), @@ -1156,6 +1156,7 @@ class ParticipantTriggers(ContributorTriggers): TeamParticipantAddedNotification, conditions=[ is_team_activity, + not_team_captain, is_not_user, has_team ] @@ -1176,7 +1177,9 @@ class ParticipantTriggers(ContributorTriggers): RelatedTransitionEffect( 'team', TeamStateMachine.accept, - conditions=[has_team] + conditions=[ + has_team + ] ), NotificationEffect( ParticipantJoinedNotification, From 5569242d81f07d282c730af3bfd07890f53b1bb8 Mon Sep 17 00:00:00 2001 From: Ernst Odolphi Date: Tue, 28 Jun 2022 17:16:12 +0200 Subject: [PATCH 015/112] Add endpoint that allows filtering teams on slots --- bluebottle/activities/urls/api.py | 8 ++++---- bluebottle/activities/views.py | 25 ++++++++++++++++++++----- bluebottle/time_based/serializers.py | 10 ---------- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/bluebottle/activities/urls/api.py b/bluebottle/activities/urls/api.py index 20598049f4..da5ff4ac27 100644 --- a/bluebottle/activities/urls/api.py +++ b/bluebottle/activities/urls/api.py @@ -4,7 +4,7 @@ ActivityList, ActivityDetail, ActivityTransitionList, ContributorList, RelatedActivityImageList, RelatedActivityImageContent, ActivityImage, - RelatedTeamList, TeamTransitionList, TeamMembersList, + TeamList, TeamTransitionList, TeamMembersList, InviteDetailView, TeamMembersExportView ) @@ -49,9 +49,9 @@ ), url( - r'^/(?P\d+)/teams/$', - RelatedTeamList.as_view(), - name='related-activity-team' + r'^/teams/$', + TeamList.as_view(), + name='teams-list' ), url( diff --git a/bluebottle/activities/views.py b/bluebottle/activities/views.py index 4a2bb9422a..e3a84e980a 100644 --- a/bluebottle/activities/views.py +++ b/bluebottle/activities/views.py @@ -1,6 +1,7 @@ from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.aggregates import BoolOr from django.db.models import Sum, Q, ExpressionWrapper, BooleanField, Case, When, Value, Count +from django.utils import timezone from rest_framework.permissions import IsAuthenticated from rest_framework_json_api.views import AutoPrefetchMixin @@ -158,17 +159,31 @@ class ActivityTransitionList(TransitionList): queryset = Activity.objects.all() -class RelatedTeamList(JsonApiViewMixin, ListAPIView): +class TeamList(JsonApiViewMixin, ListAPIView): queryset = Team.objects.all() serializer_class = TeamSerializer pemrission_classes = [OneOf(ResourcePermission, ActivityOwnerPermission), ] def get_queryset(self, *args, **kwargs): - queryset = super(RelatedTeamList, self).get_queryset(*args, **kwargs) - queryset = queryset.filter( - activity_id=self.kwargs['activity_id'] - ) + queryset = super(TeamList, self).get_queryset(*args, **kwargs) + + activity_id = self.request.query_params.get('activity_id') + if activity_id: + queryset = queryset.filter( + activity_id=activity_id + ) + + has_slot = self.request.query_params.get('filter[has_slot]') + if has_slot is not None: + queryset = queryset.filter(slot__start__isnull=True) + + start = self.request.query_params.get('filter[start]') + if start == 'future': + queryset = queryset.filter(slot__start__gt=timezone.now()) + elif start == 'passed': + queryset = queryset.filter(slot__start__lt=timezone.now()) + if self.request.user.is_authenticated: queryset = queryset.filter( Q(activity__initiative__activity_managers=self.request.user) | diff --git a/bluebottle/time_based/serializers.py b/bluebottle/time_based/serializers.py index ee3dc7c921..489d0acda6 100644 --- a/bluebottle/time_based/serializers.py +++ b/bluebottle/time_based/serializers.py @@ -13,7 +13,6 @@ ) from rest_framework_json_api.serializers import PolymorphicModelSerializer, ModelSerializer -from bluebottle.activities.models import Team from bluebottle.activities.utils import ( BaseActivitySerializer, BaseActivityListSerializer, BaseContributorSerializer, BaseContributionSerializer @@ -38,14 +37,6 @@ class TimeBasedBaseSerializer(BaseActivitySerializer): review = serializers.BooleanField(required=False) is_online = serializers.BooleanField(required=False, allow_null=True) - teams = SerializerMethodHyperlinkedRelatedField( - model=Team, - many=True, - related_link_view_name='related-activity-team', - related_link_url_kwarg='activity_id' - - ) - class Meta(BaseActivitySerializer.Meta): fields = BaseActivitySerializer.Meta.fields + ( 'capacity', @@ -53,7 +44,6 @@ class Meta(BaseActivitySerializer.Meta): 'expertise', 'review', 'contributors', - 'teams', 'my_contributor' ) From e871240e7812896f9b2d0f677a86e65dabd8ac29 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 29 Jun 2022 08:33:01 +0200 Subject: [PATCH 016/112] Force current user in triggers... :-( --- bluebottle/deeds/tests/test_periodic_tasks.py | 2 +- bluebottle/fsm/triggers.py | 2 ++ bluebottle/time_based/permissions.py | 17 +++++++++++++++++ bluebottle/time_based/serializers.py | 11 +++++------ bluebottle/time_based/tests/test_api.py | 9 ++++++--- bluebottle/time_based/views.py | 6 +++--- 6 files changed, 34 insertions(+), 13 deletions(-) diff --git a/bluebottle/deeds/tests/test_periodic_tasks.py b/bluebottle/deeds/tests/test_periodic_tasks.py index 16cdc4c388..d4bd6825fc 100644 --- a/bluebottle/deeds/tests/test_periodic_tasks.py +++ b/bluebottle/deeds/tests/test_periodic_tasks.py @@ -111,7 +111,7 @@ def test_reminder(self): self.run_tasks(self.activity.start - timedelta(days=1)) self.assertEqual(len(mail.outbox), 2) - self.assertEqual(mail.outbox[0].to[0], self.activity.owner.email) + self.assertEqual(mail.outbox[1].to[0], self.activity.owner.email) mail.outbox = [] self.assertEqual(len(mail.outbox), 0) self.run_tasks(self.activity.start - timedelta(days=1)) diff --git a/bluebottle/fsm/triggers.py b/bluebottle/fsm/triggers.py index 690664362f..99da3aed43 100644 --- a/bluebottle/fsm/triggers.py +++ b/bluebottle/fsm/triggers.py @@ -5,6 +5,7 @@ from builtins import zip from builtins import object from django.utils.translation import gettext_lazy as _ +from django_tools.middlewares.ThreadLocal import get_current_user from future.utils import python_2_unicode_compatible @@ -185,6 +186,7 @@ def _check_model_changed_triggers(self): self._triggers.append(BoundTrigger(self, trigger)) def execute_triggers(self, effects=None, **options): + options['user'] = getattr(options, 'user', get_current_user()) if hasattr(self, '_state_machines'): for machine_name in self._state_machines: machine = getattr(self, machine_name) diff --git a/bluebottle/time_based/permissions.py b/bluebottle/time_based/permissions.py index 7d886ebf01..df28c4a75a 100644 --- a/bluebottle/time_based/permissions.py +++ b/bluebottle/time_based/permissions.py @@ -31,6 +31,23 @@ def has_object_permission(self, request, view, obj): ) +class TeamSlotActivityStatusPermission(BasePermission): + def has_object_action_permission(self, action, user, obj): + return ( + action not in ('POST', 'DELETE') or + obj.team.activity.status in ['draft', 'needs_work', 'submitted'] + ) + + def has_action_permission(self, action, user, model_cls): + return True + + def has_object_permission(self, request, view, obj): + return ( + request.method not in ('POST', 'DELETE') or + obj.team.activity.status in ['draft', 'needs_work', 'submitted'] + ) + + class ParticipantDocumentPermission(permissions.DjangoModelPermissions): def has_object_permission(self, request, view, obj): diff --git a/bluebottle/time_based/serializers.py b/bluebottle/time_based/serializers.py index ee3dc7c921..2c48c95394 100644 --- a/bluebottle/time_based/serializers.py +++ b/bluebottle/time_based/serializers.py @@ -208,12 +208,11 @@ class JSONAPIMeta(object): 'location' ] - included_serializers = dict( - ActivitySlotSerializer.included_serializers, - **{ - 'team': 'bluebottle.activities.utils.TeamSerializer', - } - ) + included_serializers = { + 'team': 'bluebottle.activities.utils.TeamSerializer', + 'location': 'bluebottle.geo.serializers.GeolocationSerializer', + 'activity': 'bluebottle.time_based.serializers.PeriodActivitySerializer', + } class DateActivitySlotInfoMixin(): diff --git a/bluebottle/time_based/tests/test_api.py b/bluebottle/time_based/tests/test_api.py index ef87282c32..7e5b429081 100644 --- a/bluebottle/time_based/tests/test_api.py +++ b/bluebottle/time_based/tests/test_api.py @@ -2088,9 +2088,12 @@ def setUp(self): super().setUp() self.client = JSONAPITestClient() self.user = BlueBottleUserFactory() + self.another_user = BlueBottleUserFactory() self.activity = self.factory.create(review=True) self.participant = self.participant_factory.create( - activity=self.activity + activity=self.activity, + user=self.user, + as_user=self.user ) self.url = reverse(self.url_name) @@ -2116,7 +2119,7 @@ def test_withdraw_by_user(self): response = self.client.post( self.url, json.dumps(self.data), - user=self.participant.user + user=self.user ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) @@ -2135,7 +2138,7 @@ def test_withdraw_by_other_user(self): response = self.client.post( self.url, json.dumps(self.data), - user=self.user + user=self.another_user ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) diff --git a/bluebottle/time_based/views.py b/bluebottle/time_based/views.py index 399561b97d..1f0824ecc3 100644 --- a/bluebottle/time_based/views.py +++ b/bluebottle/time_based/views.py @@ -26,7 +26,7 @@ DateActivitySlot, SlotParticipant, Skill, TeamSlot ) from bluebottle.time_based.permissions import ( - SlotParticipantPermission, DateSlotActivityStatusPermission + SlotParticipantPermission, DateSlotActivityStatusPermission, TeamSlotActivityStatusPermission ) from bluebottle.time_based.serializers import ( DateActivitySerializer, @@ -211,13 +211,13 @@ class TeamSlotListView(DateSlotListView): ] } - permission_classes = [TenantConditionalOpenClose, DateSlotActivityStatusPermission, ] + permission_classes = [TenantConditionalOpenClose, TeamSlotActivityStatusPermission, ] queryset = TeamSlot.objects.all() serializer_class = TeamSlotSerializer class TeamSlotDetailView(DateSlotDetailView): - permission_classes = [DateSlotActivityStatusPermission, ] + permission_classes = [TeamSlotActivityStatusPermission, ] queryset = TeamSlot.objects.all() serializer_class = TeamSlotSerializer From f776ce0b49f17eb9ac60fa332ff39780d7b53e0e Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 29 Jun 2022 09:01:03 +0200 Subject: [PATCH 017/112] Fix more tests --- bluebottle/fsm/triggers.py | 3 +- bluebottle/time_based/tests/test_triggers.py | 29 ++++++++++---------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/bluebottle/fsm/triggers.py b/bluebottle/fsm/triggers.py index 99da3aed43..e06be9e6eb 100644 --- a/bluebottle/fsm/triggers.py +++ b/bluebottle/fsm/triggers.py @@ -186,7 +186,8 @@ def _check_model_changed_triggers(self): self._triggers.append(BoundTrigger(self, trigger)) def execute_triggers(self, effects=None, **options): - options['user'] = getattr(options, 'user', get_current_user()) + if 'user' not in options and get_current_user(): + options['user'] = get_current_user() if hasattr(self, '_state_machines'): for machine_name in self._state_machines: machine = getattr(self, machine_name) diff --git a/bluebottle/time_based/tests/test_triggers.py b/bluebottle/time_based/tests/test_triggers.py index 5ed8d729aa..66445fa7ae 100644 --- a/bluebottle/time_based/tests/test_triggers.py +++ b/bluebottle/time_based/tests/test_triggers.py @@ -1518,12 +1518,11 @@ def test_join_all(self): self.activity.save() user = BlueBottleUserFactory.create() - participant = self.participant_factory.build( + self.participant_factory.create( activity=self.activity, - user=user + user=user, + as_user=user ) - participant.execute_triggers(user=user, send_messages=True) - participant.save() time.sleep(4) @@ -1550,12 +1549,11 @@ def test_join_free(self): mail.outbox = [] user = BlueBottleUserFactory.create() - participant = self.participant_factory.build( + participant = self.participant_factory.create( activity=self.activity, - user=user + user=user, + as_user=user ) - participant.execute_triggers(user=user, send_messages=True) - participant.save() self.slot_participants = [ SlotParticipantFactory.create(slot=slot, participant=participant) @@ -1584,12 +1582,11 @@ def test_join_free_review(self): mail.outbox = [] user = BlueBottleUserFactory.create() - participant = self.participant_factory.build( + participant = self.participant_factory.create( activity=self.activity, - user=user + user=user, + as_user=user ) - participant.execute_triggers(user=user, send_messages=True) - participant.save() self.slot_participants = [ SlotParticipantFactory.create(slot=slot, participant=participant) @@ -1695,7 +1692,9 @@ def test_initial_added_with_team_through_admin(self): def test_join(self): mail.outbox = [] participant = self.participant_factory.create( - activity=self.activity + activity=self.activity, + user=BlueBottleUserFactory.create(), + as_relation='user' ) self.assertEqual(len(mail.outbox), 2) self.assertEqual( @@ -1776,7 +1775,9 @@ def test_team_apply(self): self.review_activity.save() mail.outbox = [] participant = self.participant_factory.create( - activity=self.review_activity + activity=self.review_activity, + user=BlueBottleUserFactory.create(), + as_relation='user' ) self.assertEqual(len(mail.outbox), 2) self.assertEqual( From 9fe9df6d1c0fdcde3ed4435aaf0536a3e670093f Mon Sep 17 00:00:00 2001 From: Ernst Odolphi Date: Wed, 29 Jun 2022 10:42:41 +0200 Subject: [PATCH 018/112] include team slot in response --- bluebottle/time_based/serializers.py | 4 +++- bluebottle/time_based/tests/test_api.py | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/bluebottle/time_based/serializers.py b/bluebottle/time_based/serializers.py index ee3dc7c921..a5c00aee86 100644 --- a/bluebottle/time_based/serializers.py +++ b/bluebottle/time_based/serializers.py @@ -783,7 +783,8 @@ class JSONAPIMeta(ParticipantSerializer.JSONAPIMeta): resource_name = 'contributors/time-based/period-participants' included_resources = ParticipantSerializer.JSONAPIMeta.included_resources + [ 'contributions', - 'team' + 'team', + 'team.slot' ] included_serializers = dict( @@ -791,6 +792,7 @@ class JSONAPIMeta(ParticipantSerializer.JSONAPIMeta): **{ 'document': 'bluebottle.time_based.serializers.PeriodParticipantDocumentSerializer', 'contributions': 'bluebottle.time_based.serializers.TimeContributionSerializer', + 'team.slot': 'bluebottle.time_based.serializers.TeamSlotSerializer', } ) diff --git a/bluebottle/time_based/tests/test_api.py b/bluebottle/time_based/tests/test_api.py index ef87282c32..c0977e6320 100644 --- a/bluebottle/time_based/tests/test_api.py +++ b/bluebottle/time_based/tests/test_api.py @@ -30,6 +30,7 @@ DateParticipantFactory, PeriodParticipantFactory, DateActivitySlotFactory, SlotParticipantFactory, SkillFactory, TeamSlotFactory ) +from bluebottle.activities.tests.factories import TeamFactory class TimeBasedListAPIViewTestCase(): @@ -2359,11 +2360,21 @@ class RelatedPeriodParticipantAPIViewTestCase(RelatedParticipantsAPIViewTestCase participant_factory = PeriodParticipantFactory def test_get_owner(self): + self.participants[2].team = TeamFactory.create(activity=self.activity) + self.participants[2].save() + TeamSlotFactory.create(team=self.participants[2].team, activity=self.activity) + super().test_get_owner() included_contributions = self.included_by_type(self.response, 'contributions/time-contributions') self.assertEqual(len(included_contributions), 8) + included_teams = self.included_by_type(self.response, 'activities/teams') + self.assertEqual(len(included_teams), 1) + + included_team_slots = self.included_by_type(self.response, 'activities/time-based/team-slots') + self.assertEqual(len(included_team_slots), 1) + class SlotParticipantListAPIViewTestCase(BluebottleTestCase): def setUp(self): From 9804dd000e937a483b96057f850331f89a13cd63 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 29 Jun 2022 11:44:43 +0200 Subject: [PATCH 019/112] Fix accepting team --- bluebottle/activities/effects.py | 2 +- bluebottle/activities/triggers.py | 9 +++++++-- bluebottle/time_based/tests/test_triggers.py | 6 +++--- bluebottle/time_based/triggers.py | 7 ------- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/bluebottle/activities/effects.py b/bluebottle/activities/effects.py index 1e1ad64283..5bcc6af512 100644 --- a/bluebottle/activities/effects.py +++ b/bluebottle/activities/effects.py @@ -82,7 +82,7 @@ def post_save(self, **kwargs): if not self.instance.team: self.instance.team = Team.objects.create( owner=self.instance.user, - activity=self.instance.activity + activity=self.instance.activity, ) self.instance.save() diff --git a/bluebottle/activities/triggers.py b/bluebottle/activities/triggers.py index 8132184a00..7d26669072 100644 --- a/bluebottle/activities/triggers.py +++ b/bluebottle/activities/triggers.py @@ -215,7 +215,12 @@ def automatically_accept(effect): """ automatically accept team """ - return not hasattr(effect.instance.activity, 'review') or not effect.instance.activity.review + captain = effect.instance.activity.participants.filter(user=effect.instance.owner).first() + return ( + not hasattr(effect.instance.activity, 'review') or + not effect.instance.activity.review or + captain.status == 'accepted' + ) def needs_review(effect): @@ -244,7 +249,7 @@ class TeamTriggers(TriggerManager): conditions=[ automatically_accept ] - ), + ) ] ), diff --git a/bluebottle/time_based/tests/test_triggers.py b/bluebottle/time_based/tests/test_triggers.py index 66445fa7ae..ab38560984 100644 --- a/bluebottle/time_based/tests/test_triggers.py +++ b/bluebottle/time_based/tests/test_triggers.py @@ -993,7 +993,6 @@ def test_initiate_team_invite_review(self): user=BlueBottleUserFactory.create() ) self.assertEqual(participant.team, team_captain.team) - 'New team member' in [message.subject for message in mail.outbox] self.assertEqual(participant.status, 'accepted') def test_initiate_team_invite_review_after_signup(self): @@ -1019,10 +1018,11 @@ def test_initiate_team_invite_review_after_signup(self): ) self.assertEqual(participant.team, team_captain.team) - 'New team member' in [message.subject for message in mail.outbox] - team_captain.states.accept(save=True) + self.assertEqual(team_captain.status, 'accepted') + self.assertEqual(team_captain.team.status, 'open') + participant.refresh_from_db() self.assertEqual(participant.status, 'accepted') def test_initial_removed_through_admin(self): diff --git a/bluebottle/time_based/triggers.py b/bluebottle/time_based/triggers.py index a5b12af4f4..74eb3e34c5 100644 --- a/bluebottle/time_based/triggers.py +++ b/bluebottle/time_based/triggers.py @@ -1139,13 +1139,6 @@ class ParticipantTriggers(ContributorTriggers): 'preparation_contributions', TimeContributionStateMachine.succeed, ), - RelatedTransitionEffect( - 'team', - TeamStateMachine.accept, - conditions=[ - has_team - ] - ), ] ), From 1bc67de51c128b71a79e2b597bf9fc38a908f128 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 29 Jun 2022 13:04:59 +0200 Subject: [PATCH 020/112] Last tests... pretty please... --- bluebottle/activities/triggers.py | 6 ++++-- bluebottle/time_based/triggers.py | 4 ---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/bluebottle/activities/triggers.py b/bluebottle/activities/triggers.py index 7d26669072..91b8a35317 100644 --- a/bluebottle/activities/triggers.py +++ b/bluebottle/activities/triggers.py @@ -215,11 +215,13 @@ def automatically_accept(effect): """ automatically accept team """ - captain = effect.instance.activity.participants.filter(user=effect.instance.owner).first() + captain = effect.instance.activity\ + .contributors.not_instance_of(Organizer)\ + .filter(user=effect.instance.owner).first() return ( not hasattr(effect.instance.activity, 'review') or not effect.instance.activity.review or - captain.status == 'accepted' + (captain and captain.status == 'accepted') ) diff --git a/bluebottle/time_based/triggers.py b/bluebottle/time_based/triggers.py index 74eb3e34c5..50ec42be6e 100644 --- a/bluebottle/time_based/triggers.py +++ b/bluebottle/time_based/triggers.py @@ -865,10 +865,6 @@ def user_is_not_team_captain(effect): return not effect.instance.team_id or effect.instance.team.owner != effect.options['user'] -def always_false(effect): - return False - - def is_not_user(effect): """ User is not the participant From bfd0d807c66ecd64e76a53cc7e6c0acf3b60abba Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 29 Jun 2022 14:08:40 +0200 Subject: [PATCH 021/112] Make sure we do'n screw up participants --- bluebottle/time_based/admin.py | 12 +----------- bluebottle/time_based/tests/test_admin.py | 8 ++------ 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/bluebottle/time_based/admin.py b/bluebottle/time_based/admin.py index 813470a39b..455080cffd 100644 --- a/bluebottle/time_based/admin.py +++ b/bluebottle/time_based/admin.py @@ -40,24 +40,14 @@ class BaseParticipantAdminInline(TabularInlinePaginated): model = Participant per_page = 20 readonly_fields = ('contributor_date', 'motivation', 'document', 'edit', - 'created', 'transition_date', 'status', 'disabled') + 'created', 'transition_date', 'status') raw_id_fields = ('user', 'document') extra = 0 ordering = ['-created'] - def get_fields(self, request, obj=None): - if self.can_edit(obj): - return super().get_fields(request, obj) - else: - return ['disabled'] - def get_template(self): pass - def disabled(self, obj): - return format_html('{}', obj) - disabled.short_description = _('First complete and submit the activity before managing participants.') - def can_edit(self, obj): return obj and obj.id and obj.status in ['open', 'succeeded', 'full', 'submitted'] diff --git a/bluebottle/time_based/tests/test_admin.py b/bluebottle/time_based/tests/test_admin.py index d777de8699..50a1395122 100644 --- a/bluebottle/time_based/tests/test_admin.py +++ b/bluebottle/time_based/tests/test_admin.py @@ -183,10 +183,6 @@ def test_add_participants(self): DateParticipantFactory.create(activity=activity) url = reverse('admin:time_based_dateactivity_change', args=(activity.pk,)) page = self.app.get(url) - self.assertFalse( - 'First complete and submit the activity before managing participants.' in - page.text - ) self.assertTrue( 'Add another Participant' in page.text @@ -194,8 +190,8 @@ def test_add_participants(self): activity.status = 'rejected' activity.save() page = self.app.get(url) - self.assertTrue( - 'First complete and submit the activity before managing participants.' in + self.assertFalse( + 'Add another Participant' in page.text ) From 8c192a56974f38b69d0180c28ce538645208e892 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 29 Jun 2022 14:24:44 +0200 Subject: [PATCH 022/112] Fix tests --- bluebottle/activities/tests/test_api.py | 2 +- bluebottle/activities/urls/api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bluebottle/activities/tests/test_api.py b/bluebottle/activities/tests/test_api.py index ddb842244f..c876298315 100644 --- a/bluebottle/activities/tests/test_api.py +++ b/bluebottle/activities/tests/test_api.py @@ -1744,7 +1744,7 @@ def setUp(self): PeriodParticipantFactory.create(activity=self.activity, team=team, user=team.owner) PeriodParticipantFactory.create(activity=self.activity, team=team) - self.url = reverse('related-activity-team', args=(self.activity.pk, )) + self.url = "{}?activity_id={}".format(reverse('team-list'), self.activity.pk) settings = InitiativePlatformSettings.objects.get() settings.team_activities = True diff --git a/bluebottle/activities/urls/api.py b/bluebottle/activities/urls/api.py index da5ff4ac27..e3051d69e2 100644 --- a/bluebottle/activities/urls/api.py +++ b/bluebottle/activities/urls/api.py @@ -51,7 +51,7 @@ url( r'^/teams/$', TeamList.as_view(), - name='teams-list' + name='team-list' ), url( From 83d1a8d61609a3befbf14a5b0d5364956d6bd14b Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 29 Jun 2022 14:27:49 +0200 Subject: [PATCH 023/112] Remove obsolete test --- bluebottle/activities/tests/test_api.py | 2 +- bluebottle/time_based/tests/test_api.py | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/bluebottle/activities/tests/test_api.py b/bluebottle/activities/tests/test_api.py index c876298315..be5014edce 100644 --- a/bluebottle/activities/tests/test_api.py +++ b/bluebottle/activities/tests/test_api.py @@ -1991,7 +1991,7 @@ def setUp(self): activity=self.activity, ) - self.url = reverse('related-activity-team', args=(self.activity.pk, )) + self.url = "{}?activity_id={}".format(reverse('team-list'), self.activity.pk) @property def export_url(self): diff --git a/bluebottle/time_based/tests/test_api.py b/bluebottle/time_based/tests/test_api.py index c0977e6320..7b5d3de0a2 100644 --- a/bluebottle/time_based/tests/test_api.py +++ b/bluebottle/time_based/tests/test_api.py @@ -1121,14 +1121,6 @@ def setUp(self): 'location_hint' ] - def test_activity_has_teams(self): - self.response = self.client.get(self.activity_url, user=self.activity.owner) - self.assertStatus(status.HTTP_200_OK) - teams_url = self.getRelatedLink('teams') - self.response = self.client.get(teams_url, user=self.activity.owner) - self.assertStatus(status.HTTP_200_OK) - self.assertObjectList(models=[self.team]) - def test_create_team_slot(self): self.perform_create(user=self.manager) self.assertStatus(status.HTTP_201_CREATED) From f5cfd66cede635be26f7b750fdfdf4b6828b9944 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 29 Jun 2022 14:40:02 +0200 Subject: [PATCH 024/112] Fix serializer --- bluebottle/time_based/serializers.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/bluebottle/time_based/serializers.py b/bluebottle/time_based/serializers.py index 05cafa76e8..4b94bc278f 100644 --- a/bluebottle/time_based/serializers.py +++ b/bluebottle/time_based/serializers.py @@ -44,7 +44,7 @@ class Meta(BaseActivitySerializer.Meta): 'expertise', 'review', 'contributors', - 'my_contributor' + 'my_contributor', ) class JSONAPIMeta(BaseActivitySerializer.JSONAPIMeta): @@ -198,12 +198,11 @@ class JSONAPIMeta(object): 'location' ] - included_serializers = dict( - ActivitySlotSerializer.included_serializers, - **{ - 'team': 'bluebottle.activities.utils.TeamSerializer', - } - ) + included_serializers = { + 'team': 'bluebottle.activities.utils.TeamSerializer', + 'location': 'bluebottle.geo.serializers.GeolocationSerializer', + 'activity': 'bluebottle.time_based.serializers.DateActivitySerializer', + } class DateActivitySlotInfoMixin(): From 6979cee0273b0f62b9945ca96748470176ca57aa Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 29 Jun 2022 14:44:40 +0200 Subject: [PATCH 025/112] Fix some serializer/view for teams --- bluebottle/time_based/permissions.py | 17 +++++++++++++++++ bluebottle/time_based/serializers.py | 2 +- bluebottle/time_based/views.py | 6 +++--- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/bluebottle/time_based/permissions.py b/bluebottle/time_based/permissions.py index 7d886ebf01..df28c4a75a 100644 --- a/bluebottle/time_based/permissions.py +++ b/bluebottle/time_based/permissions.py @@ -31,6 +31,23 @@ def has_object_permission(self, request, view, obj): ) +class TeamSlotActivityStatusPermission(BasePermission): + def has_object_action_permission(self, action, user, obj): + return ( + action not in ('POST', 'DELETE') or + obj.team.activity.status in ['draft', 'needs_work', 'submitted'] + ) + + def has_action_permission(self, action, user, model_cls): + return True + + def has_object_permission(self, request, view, obj): + return ( + request.method not in ('POST', 'DELETE') or + obj.team.activity.status in ['draft', 'needs_work', 'submitted'] + ) + + class ParticipantDocumentPermission(permissions.DjangoModelPermissions): def has_object_permission(self, request, view, obj): diff --git a/bluebottle/time_based/serializers.py b/bluebottle/time_based/serializers.py index 4b94bc278f..b120f9df4a 100644 --- a/bluebottle/time_based/serializers.py +++ b/bluebottle/time_based/serializers.py @@ -201,7 +201,7 @@ class JSONAPIMeta(object): included_serializers = { 'team': 'bluebottle.activities.utils.TeamSerializer', 'location': 'bluebottle.geo.serializers.GeolocationSerializer', - 'activity': 'bluebottle.time_based.serializers.DateActivitySerializer', + 'activity': 'bluebottle.time_based.serializers.PeriodActivitySerializer', } diff --git a/bluebottle/time_based/views.py b/bluebottle/time_based/views.py index 399561b97d..1f0824ecc3 100644 --- a/bluebottle/time_based/views.py +++ b/bluebottle/time_based/views.py @@ -26,7 +26,7 @@ DateActivitySlot, SlotParticipant, Skill, TeamSlot ) from bluebottle.time_based.permissions import ( - SlotParticipantPermission, DateSlotActivityStatusPermission + SlotParticipantPermission, DateSlotActivityStatusPermission, TeamSlotActivityStatusPermission ) from bluebottle.time_based.serializers import ( DateActivitySerializer, @@ -211,13 +211,13 @@ class TeamSlotListView(DateSlotListView): ] } - permission_classes = [TenantConditionalOpenClose, DateSlotActivityStatusPermission, ] + permission_classes = [TenantConditionalOpenClose, TeamSlotActivityStatusPermission, ] queryset = TeamSlot.objects.all() serializer_class = TeamSlotSerializer class TeamSlotDetailView(DateSlotDetailView): - permission_classes = [DateSlotActivityStatusPermission, ] + permission_classes = [TeamSlotActivityStatusPermission, ] queryset = TeamSlot.objects.all() serializer_class = TeamSlotSerializer From 83262fc5363e7dbfae082ec4f75831d4ea813102 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 29 Jun 2022 14:46:01 +0200 Subject: [PATCH 026/112] Don't show unscheduled teams as past --- bluebottle/activities/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bluebottle/activities/views.py b/bluebottle/activities/views.py index e3a84e980a..c75290c081 100644 --- a/bluebottle/activities/views.py +++ b/bluebottle/activities/views.py @@ -182,7 +182,7 @@ def get_queryset(self, *args, **kwargs): if start == 'future': queryset = queryset.filter(slot__start__gt=timezone.now()) elif start == 'passed': - queryset = queryset.filter(slot__start__lt=timezone.now()) + queryset = queryset.filter(slot__start__lt=timezone.now()).exclude(slot__start__isnull=True) if self.request.user.is_authenticated: queryset = queryset.filter( From 74e332da771e63cbb6bfa7e5f47b2eb5796bfdb3 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 29 Jun 2022 15:00:35 +0200 Subject: [PATCH 027/112] Fix team filter --- bluebottle/activities/tests/test_api.py | 4 ++-- bluebottle/activities/views.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bluebottle/activities/tests/test_api.py b/bluebottle/activities/tests/test_api.py index be5014edce..5bc5c296b7 100644 --- a/bluebottle/activities/tests/test_api.py +++ b/bluebottle/activities/tests/test_api.py @@ -1744,7 +1744,7 @@ def setUp(self): PeriodParticipantFactory.create(activity=self.activity, team=team, user=team.owner) PeriodParticipantFactory.create(activity=self.activity, team=team) - self.url = "{}?activity_id={}".format(reverse('team-list'), self.activity.pk) + self.url = "{}?filter[activity_id]={}".format(reverse('team-list'), self.activity.pk) settings = InitiativePlatformSettings.objects.get() settings.team_activities = True @@ -1991,7 +1991,7 @@ def setUp(self): activity=self.activity, ) - self.url = "{}?activity_id={}".format(reverse('team-list'), self.activity.pk) + self.url = "{}?filter[activity_id]={}".format(reverse('team-list'), self.activity.pk) @property def export_url(self): diff --git a/bluebottle/activities/views.py b/bluebottle/activities/views.py index c75290c081..1d67d4b60c 100644 --- a/bluebottle/activities/views.py +++ b/bluebottle/activities/views.py @@ -168,7 +168,7 @@ class TeamList(JsonApiViewMixin, ListAPIView): def get_queryset(self, *args, **kwargs): queryset = super(TeamList, self).get_queryset(*args, **kwargs) - activity_id = self.request.query_params.get('activity_id') + activity_id = self.request.query_params.get('filter[activity_id]') if activity_id: queryset = queryset.filter( activity_id=activity_id From 8316148c4cb816ca59d56208b68857f1960ecb13 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 29 Jun 2022 15:10:35 +0200 Subject: [PATCH 028/112] Fix permissions --- bluebottle/time_based/permissions.py | 17 ----------------- bluebottle/time_based/views.py | 6 +++--- 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/bluebottle/time_based/permissions.py b/bluebottle/time_based/permissions.py index df28c4a75a..7d886ebf01 100644 --- a/bluebottle/time_based/permissions.py +++ b/bluebottle/time_based/permissions.py @@ -31,23 +31,6 @@ def has_object_permission(self, request, view, obj): ) -class TeamSlotActivityStatusPermission(BasePermission): - def has_object_action_permission(self, action, user, obj): - return ( - action not in ('POST', 'DELETE') or - obj.team.activity.status in ['draft', 'needs_work', 'submitted'] - ) - - def has_action_permission(self, action, user, model_cls): - return True - - def has_object_permission(self, request, view, obj): - return ( - request.method not in ('POST', 'DELETE') or - obj.team.activity.status in ['draft', 'needs_work', 'submitted'] - ) - - class ParticipantDocumentPermission(permissions.DjangoModelPermissions): def has_object_permission(self, request, view, obj): diff --git a/bluebottle/time_based/views.py b/bluebottle/time_based/views.py index 1f0824ecc3..b484622a3c 100644 --- a/bluebottle/time_based/views.py +++ b/bluebottle/time_based/views.py @@ -26,7 +26,7 @@ DateActivitySlot, SlotParticipant, Skill, TeamSlot ) from bluebottle.time_based.permissions import ( - SlotParticipantPermission, DateSlotActivityStatusPermission, TeamSlotActivityStatusPermission + SlotParticipantPermission, DateSlotActivityStatusPermission ) from bluebottle.time_based.serializers import ( DateActivitySerializer, @@ -211,13 +211,13 @@ class TeamSlotListView(DateSlotListView): ] } - permission_classes = [TenantConditionalOpenClose, TeamSlotActivityStatusPermission, ] + permission_classes = [TenantConditionalOpenClose] queryset = TeamSlot.objects.all() serializer_class = TeamSlotSerializer class TeamSlotDetailView(DateSlotDetailView): - permission_classes = [TeamSlotActivityStatusPermission, ] + permission_classes = [TenantConditionalOpenClose] queryset = TeamSlot.objects.all() serializer_class = TeamSlotSerializer From 506e7a523d708091c82a882e4cae862046a46852 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 29 Jun 2022 15:39:50 +0200 Subject: [PATCH 029/112] Fix filter --- bluebottle/activities/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bluebottle/activities/views.py b/bluebottle/activities/views.py index 1d67d4b60c..f57b93ad2e 100644 --- a/bluebottle/activities/views.py +++ b/bluebottle/activities/views.py @@ -175,7 +175,7 @@ def get_queryset(self, *args, **kwargs): ) has_slot = self.request.query_params.get('filter[has_slot]') - if has_slot is not None: + if has_slot == 'false': queryset = queryset.filter(slot__start__isnull=True) start = self.request.query_params.get('filter[start]') From 6a7ebcbf3350c913cc798fe3c88e0e6e1e48bce1 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 29 Jun 2022 16:19:23 +0200 Subject: [PATCH 030/112] Automatically set activity based on team --- bluebottle/time_based/serializers.py | 1 + bluebottle/time_based/views.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/bluebottle/time_based/serializers.py b/bluebottle/time_based/serializers.py index b120f9df4a..03a5365c91 100644 --- a/bluebottle/time_based/serializers.py +++ b/bluebottle/time_based/serializers.py @@ -180,6 +180,7 @@ class JSONAPIMeta(ActivitySlotSerializer.JSONAPIMeta): class TeamSlotSerializer(ActivitySlotSerializer): errors = ValidationErrorsField() required = RequiredErrorsField() + activity = ResourceRelatedField(read_only=True) class Meta(ActivitySlotSerializer.Meta): model = TeamSlot diff --git a/bluebottle/time_based/views.py b/bluebottle/time_based/views.py index b484622a3c..7745ff57f8 100644 --- a/bluebottle/time_based/views.py +++ b/bluebottle/time_based/views.py @@ -188,6 +188,9 @@ def filter_queryset(self, queryset): queryset = DateActivitySlot.objects.all() serializer_class = DateActivitySlotSerializer + def perform_create(self, serializer): + serializer.save(activity=serializer.validated_data['team'].activity) + class DateSlotDetailView(JsonApiViewMixin, RetrieveUpdateDestroyAPIView): related_permission_classes = { From 0edf0d124cb4b694b86ef7ec9441b499b08bfd5b Mon Sep 17 00:00:00 2001 From: Ernst Odolphi Date: Fri, 1 Jul 2022 13:30:01 +0200 Subject: [PATCH 031/112] Add tests for filtering, add back teams link to activity --- bluebottle/activities/tests/test_api.py | 39 +++++++++++++++++++++++-- bluebottle/activities/views.py | 1 + bluebottle/test/utils.py | 20 +++++++++++-- bluebottle/time_based/serializers.py | 12 ++++++++ bluebottle/time_based/tests/test_api.py | 5 ++++ 5 files changed, 72 insertions(+), 5 deletions(-) diff --git a/bluebottle/activities/tests/test_api.py b/bluebottle/activities/tests/test_api.py index 5bc5c296b7..1c1d1dcd88 100644 --- a/bluebottle/activities/tests/test_api.py +++ b/bluebottle/activities/tests/test_api.py @@ -26,7 +26,7 @@ from bluebottle.time_based.serializers import PeriodParticipantSerializer from bluebottle.time_based.tests.factories import ( DateActivityFactory, PeriodActivityFactory, DateParticipantFactory, PeriodParticipantFactory, - DateActivitySlotFactory, SkillFactory + DateActivitySlotFactory, SkillFactory, TeamSlotFactory ) from bluebottle.initiatives.tests.factories import InitiativeFactory from bluebottle.initiatives.models import InitiativePlatformSettings @@ -1726,7 +1726,7 @@ def test_participants_over_max_age(self): ) -class RelatedTeamListViewAPITestCase(APITestCase): +class TeamListViewAPITestCase(APITestCase): serializer = TeamSerializer def setUp(self): @@ -1737,6 +1737,14 @@ def setUp(self): self.approved_teams = TeamFactory.create_batch(5, activity=self.activity) for team in self.approved_teams: PeriodParticipantFactory.create(activity=self.activity, team=team, user=team.owner) + + for team in self.approved_teams[:2]: + TeamSlotFactory.create(activity=self.activity, team=team, start=now() + timedelta(days=5)) + + TeamSlotFactory.create( + activity=self.activity, team=self.approved_teams[2], start=now() - timedelta(days=5) + ) + self.cancelled_teams = TeamFactory.create_batch( 5, activity=self.activity, status='cancelled' ) @@ -1744,7 +1752,7 @@ def setUp(self): PeriodParticipantFactory.create(activity=self.activity, team=team, user=team.owner) PeriodParticipantFactory.create(activity=self.activity, team=team) - self.url = "{}?filter[activity_id]={}".format(reverse('team-list'), self.activity.pk) + self.url = f"{reverse('team-list')}?activity_id={self.activity.pk}" settings = InitiativePlatformSettings.objects.get() settings.team_activities = True @@ -1771,6 +1779,31 @@ def test_get_activity_owner(self): 'We should have a unique list of team ids' ) + def test_get_filtered_has_slot(self): + self.perform_get(user=self.activity.owner, query={'filter[has_slot]': 'false'}) + + self.assertStatus(status.HTTP_200_OK) + for resource in self.response.json()['data']: + self.assertIsNone(resource['relationships']['slot']['data']) + + def test_get_filtered_future(self): + self.perform_get(user=self.activity.owner, query={'filter[start]': 'future'}) + + self.assertStatus(status.HTTP_200_OK) + for resource in self.response.json()['data']: + self.assertTrue( + resource['id'] in [str(team.pk) for team in self.approved_teams[:2]] + ) + + def test_get_filtered_passed(self): + self.perform_get(user=self.activity.owner, query={'filter[start]': 'future'}) + + self.assertStatus(status.HTTP_200_OK) + for resource in self.response.json()['data']: + self.assertEqual( + resource['id'], str(self.approved_teams[2].pk) + ) + def test_get_cancelled_team_captain(self): team = self.cancelled_teams[0] self.perform_get(user=team.owner) diff --git a/bluebottle/activities/views.py b/bluebottle/activities/views.py index f57b93ad2e..0e2cd445a9 100644 --- a/bluebottle/activities/views.py +++ b/bluebottle/activities/views.py @@ -179,6 +179,7 @@ def get_queryset(self, *args, **kwargs): queryset = queryset.filter(slot__start__isnull=True) start = self.request.query_params.get('filter[start]') + if start == 'future': queryset = queryset.filter(slot__start__gt=timezone.now()) elif start == 'passed': diff --git a/bluebottle/test/utils.py b/bluebottle/test/utils.py index 6f91038c7a..6b158c727b 100644 --- a/bluebottle/test/utils.py +++ b/bluebottle/test/utils.py @@ -3,6 +3,9 @@ from builtins import str from contextlib import contextmanager from importlib import import_module +from urllib.parse import ( + urlencode, urlparse, parse_qsl, ParseResult +) from bs4 import BeautifulSoup from django.conf import settings @@ -206,15 +209,28 @@ def setUp(self): self.user = BlueBottleUserFactory.create() self.client = JSONAPITestClient() - def perform_get(self, user=None): + def perform_get(self, user=None, query=None): """ Perform a get request and save the result in `self.response` If `user` is None, perform an anoymous request """ + + if query: + parsed_url = urlparse(self.url) + current_query = dict(parse_qsl(parsed_url.query)) + current_query.update(query) + + url = ParseResult( + parsed_url.scheme, parsed_url.netloc, parsed_url.path, + parsed_url.params, urlencode(query, doseq=True), parsed_url.fragment + ).geturl() + else: + url = self.url + self.user = user self.response = self.client.get( - self.url, + url, user=user ) diff --git a/bluebottle/time_based/serializers.py b/bluebottle/time_based/serializers.py index 03a5365c91..4ab80dbc3b 100644 --- a/bluebottle/time_based/serializers.py +++ b/bluebottle/time_based/serializers.py @@ -13,6 +13,7 @@ ) from rest_framework_json_api.serializers import PolymorphicModelSerializer, ModelSerializer +from bluebottle.activities.models import Team from bluebottle.activities.utils import ( BaseActivitySerializer, BaseActivityListSerializer, BaseContributorSerializer, BaseContributionSerializer @@ -33,10 +34,20 @@ from bluebottle.utils.utils import reverse_signed +class TeamsField(HyperlinkedRelatedField): + def __init__(self, many=True, read_only=True, *args, **kwargs): + super().__init__(Team, many=many, read_only=read_only, *args, **kwargs) + + def get_url(self, name, view_name, kwargs, request): + return f"{self.reverse('team-list')}?activity_id={kwargs['pk']}" + + class TimeBasedBaseSerializer(BaseActivitySerializer): review = serializers.BooleanField(required=False) is_online = serializers.BooleanField(required=False, allow_null=True) + teams = TeamsField() + class Meta(BaseActivitySerializer.Meta): fields = BaseActivitySerializer.Meta.fields + ( 'capacity', @@ -45,6 +56,7 @@ class Meta(BaseActivitySerializer.Meta): 'review', 'contributors', 'my_contributor', + 'teams' ) class JSONAPIMeta(BaseActivitySerializer.JSONAPIMeta): diff --git a/bluebottle/time_based/tests/test_api.py b/bluebottle/time_based/tests/test_api.py index 7b5d3de0a2..83a5fd17d2 100644 --- a/bluebottle/time_based/tests/test_api.py +++ b/bluebottle/time_based/tests/test_api.py @@ -951,6 +951,11 @@ def test_get_open(self): in self.data['meta']['transitions'] ) + self.assertEqual( + self.data['relationships']['teams']['links']['self'], + f"{reverse('teams-list')}?activity_id={self.activity.pk}" + ) + def test_get_open_with_participant(self): self.activity.duration_period = 'weeks' self.activity.save() From 7939801a126b6e5ced9912e874516ebbb70e201d Mon Sep 17 00:00:00 2001 From: Ernst Odolphi Date: Fri, 1 Jul 2022 13:38:13 +0200 Subject: [PATCH 032/112] Add permissions in migration --- bluebottle/activities/tests/test_api.py | 2 +- .../migrations/0073_auto_20220701_1330.py | 49 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 bluebottle/time_based/migrations/0073_auto_20220701_1330.py diff --git a/bluebottle/activities/tests/test_api.py b/bluebottle/activities/tests/test_api.py index 1c1d1dcd88..b1e790b322 100644 --- a/bluebottle/activities/tests/test_api.py +++ b/bluebottle/activities/tests/test_api.py @@ -1796,7 +1796,7 @@ def test_get_filtered_future(self): ) def test_get_filtered_passed(self): - self.perform_get(user=self.activity.owner, query={'filter[start]': 'future'}) + self.perform_get(user=self.activity.owner, query={'filter[start]': 'passed'}) self.assertStatus(status.HTTP_200_OK) for resource in self.response.json()['data']: diff --git a/bluebottle/time_based/migrations/0073_auto_20220701_1330.py b/bluebottle/time_based/migrations/0073_auto_20220701_1330.py new file mode 100644 index 0000000000..b618de7d55 --- /dev/null +++ b/bluebottle/time_based/migrations/0073_auto_20220701_1330.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.17 on 2021-01-14 09:56 +from __future__ import unicode_literals + +from django.db import migrations, connection + +from bluebottle.utils.utils import update_group_permissions + +from bluebottle.clients import properties +from bluebottle.clients.models import Client +from bluebottle.clients.utils import LocalTenant + + +def add_group_permissions(apps, schema_editor): + tenant = Client.objects.get(schema_name=connection.tenant.schema_name) + with LocalTenant(tenant): + group_perms = { + 'Staff': { + 'perms': ( + 'add_teamslot', 'change_teamslot', 'delete_teamslot', + ) + }, + 'Anonymous': { + 'perms': ( + 'api_read_teamslot', + ) if not properties.CLOSED_SITE else () + }, + 'Authenticated': { + 'perms': ( + 'api_read_teamslot', 'api_add_own_teamslot', + 'api_change_own_teamslot', 'api_delete_own_teamslot', + ) + } + } + + update_group_permissions('time_based', group_perms, apps) + +class Migration(migrations.Migration): + + dependencies = [ + ('time_based', '0072_teamslot_duration'), + ] + + operations = [ + migrations.RunPython( + add_group_permissions, + migrations.RunPython.noop + ) + ] From f83e93984fc2dc3affd174aac8721656f3f0b53a Mon Sep 17 00:00:00 2001 From: Ernst Odolphi Date: Mon, 4 Jul 2022 11:36:29 +0200 Subject: [PATCH 033/112] Small tweak for TeamSlotSerializer --- bluebottle/time_based/serializers.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/bluebottle/time_based/serializers.py b/bluebottle/time_based/serializers.py index 4ab80dbc3b..f31543ee78 100644 --- a/bluebottle/time_based/serializers.py +++ b/bluebottle/time_based/serializers.py @@ -211,11 +211,13 @@ class JSONAPIMeta(object): 'location' ] - included_serializers = { - 'team': 'bluebottle.activities.utils.TeamSerializer', - 'location': 'bluebottle.geo.serializers.GeolocationSerializer', - 'activity': 'bluebottle.time_based.serializers.PeriodActivitySerializer', - } + included_serializers = dict( + ActivitySlotSerializer.included_serializers, + **{ + 'team': 'bluebottle.activities.utils.TeamSerializer', + 'activity': 'bluebottle.time_based.serializers.PeriodActivitySerializer', + } + ) class DateActivitySlotInfoMixin(): From 202dbcc0191bdd441f0c1117518f19fd49e464ea Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Mon, 4 Jul 2022 13:43:12 +0200 Subject: [PATCH 034/112] Fix filtering teams --- bluebottle/activities/views.py | 24 ++++++++++++++++++------ bluebottle/time_based/views.py | 4 +++- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/bluebottle/activities/views.py b/bluebottle/activities/views.py index f57b93ad2e..8ab5e7c03d 100644 --- a/bluebottle/activities/views.py +++ b/bluebottle/activities/views.py @@ -175,14 +175,19 @@ def get_queryset(self, *args, **kwargs): ) has_slot = self.request.query_params.get('filter[has_slot]') + start = self.request.query_params.get('filter[start]') if has_slot == 'false': queryset = queryset.filter(slot__start__isnull=True) - - start = self.request.query_params.get('filter[start]') - if start == 'future': - queryset = queryset.filter(slot__start__gt=timezone.now()) + elif start == 'future': + queryset = queryset.filter( + slot__start__gt=timezone.now() + ).order_by('start') elif start == 'passed': - queryset = queryset.filter(slot__start__lt=timezone.now()).exclude(slot__start__isnull=True) + queryset = queryset.filter( + slot__start__lt=timezone.now() + ).exclude( + slot__start__isnull=True + ).order_by('-start') if self.request.user.is_authenticated: queryset = queryset.filter( @@ -205,7 +210,14 @@ def get_queryset(self, *args, **kwargs): ) ) ) - ).distinct().order_by('-current_user', '-id') + ).distinct().order_by('-current_user') + if has_slot == 'false': + queryset = queryset.order_by('-current_user', 'id') + elif start == 'future': + queryset = queryset.order_by('-current_user', 'slot__start') + elif start == 'passed': + queryset = queryset.order_by('-current_user', '-slot__start') + else: queryset = self.queryset.filter( status='open' diff --git a/bluebottle/time_based/views.py b/bluebottle/time_based/views.py index 7745ff57f8..4472e33d34 100644 --- a/bluebottle/time_based/views.py +++ b/bluebottle/time_based/views.py @@ -170,7 +170,9 @@ def filter_queryset(self, queryset): start = self.request.GET.get('start') try: - queryset = queryset.filter(start__gte=dateutil.parser.parse(start).astimezone(tz)) + queryset = queryset.filter( + start__gte=dateutil.parser.parse(start).astimezone(tz) + ) except (ValueError, TypeError): pass From 81717b0427cf06d3dbd367758309762267c9ce4e Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Tue, 5 Jul 2022 09:50:06 +0200 Subject: [PATCH 035/112] Fix permission --- bluebottle/time_based/tests/test_api.py | 2 +- bluebottle/time_based/views.py | 20 ++++++++++---------- bluebottle/utils/views.py | 7 +++---- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/bluebottle/time_based/tests/test_api.py b/bluebottle/time_based/tests/test_api.py index 0aa3e9969d..94563678bf 100644 --- a/bluebottle/time_based/tests/test_api.py +++ b/bluebottle/time_based/tests/test_api.py @@ -953,7 +953,7 @@ def test_get_open(self): self.assertEqual( self.data['relationships']['teams']['links']['self'], - f"{reverse('teams-list')}?activity_id={self.activity.pk}" + f"{reverse('team-list')}?activity_id={self.activity.pk}" ) def test_get_open_with_participant(self): diff --git a/bluebottle/time_based/views.py b/bluebottle/time_based/views.py index d3d06bb85c..0107fe5917 100644 --- a/bluebottle/time_based/views.py +++ b/bluebottle/time_based/views.py @@ -190,15 +190,6 @@ def filter_queryset(self, queryset): queryset = DateActivitySlot.objects.all() serializer_class = DateActivitySlotSerializer - def perform_create(self, serializer): - self.check_object_permissions( - self.request, - serializer.Meta.model(**serializer.validated_data) - ) - if 'team' in serializer.validated_data: - serializer.save(activity=serializer.validated_data['team'].activity) - serializer.save() - class DateSlotDetailView(JsonApiViewMixin, RetrieveUpdateDestroyAPIView): related_permission_classes = { @@ -215,7 +206,7 @@ class DateSlotDetailView(JsonApiViewMixin, RetrieveUpdateDestroyAPIView): class TeamSlotListView(DateSlotListView): related_permission_classes = { - 'activity': [ + 'team.activity': [ ActivityStatusPermission, OneOf(ResourcePermission, ActivityOwnerPermission), DeleteActivityPermission @@ -226,6 +217,15 @@ class TeamSlotListView(DateSlotListView): queryset = TeamSlot.objects.all() serializer_class = TeamSlotSerializer + def perform_create(self, serializer): + self.check_object_permissions( + self.request, + serializer.Meta.model(**serializer.validated_data) + ) + if 'team' in serializer.validated_data: + serializer.save(activity=serializer.validated_data['team'].activity) + serializer.save() + class TeamSlotDetailView(DateSlotDetailView): permission_classes = [TenantConditionalOpenClose] diff --git a/bluebottle/utils/views.py b/bluebottle/utils/views.py index 484272a001..df21048f8f 100644 --- a/bluebottle/utils/views.py +++ b/bluebottle/utils/views.py @@ -1,12 +1,11 @@ import mimetypes import os from io import BytesIO - -import xlsxwriter +from operator import attrgetter import icalendar - import magic +import xlsxwriter from django.core.paginator import Paginator from django.core.signing import TimestampSigner, BadSignature from django.db.models import Case, When, IntegerField @@ -129,7 +128,7 @@ def check_related_object_permissions(self, request, obj): Raises an appropriate exception if the request is not permitted. """ for related, permissions in list(self.related_permission_classes.items()): - related_obj = getattr(obj, related) + related_obj = attrgetter(related)(obj) for permission in permissions: if not permission().has_object_permission(request, None, related_obj): self.permission_denied( From 54730b54c21f6bd074dd96865681d8813e15f7f0 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 6 Jul 2022 20:37:14 +0200 Subject: [PATCH 036/112] Add specific team member serializer --- bluebottle/activities/utils.py | 60 +++++++++++----------------- bluebottle/activities/views.py | 4 +- bluebottle/time_based/serializers.py | 37 ++++++++++++++++- 3 files changed, 61 insertions(+), 40 deletions(-) diff --git a/bluebottle/activities/utils.py b/bluebottle/activities/utils.py index fddf91778e..4e0424b31f 100644 --- a/bluebottle/activities/utils.py +++ b/bluebottle/activities/utils.py @@ -9,7 +9,8 @@ from rest_framework import serializers from rest_framework.fields import SerializerMethodField from rest_framework_json_api.relations import ( - ResourceRelatedField, SerializerMethodHyperlinkedRelatedField, SerializerMethodResourceRelatedField + ResourceRelatedField, SerializerMethodResourceRelatedField, + HyperlinkedRelatedField ) from rest_framework_json_api.serializers import ModelSerializer @@ -17,6 +18,7 @@ Activity, Contributor, Contribution, Organizer, EffortContribution, Team, Invite ) from bluebottle.activities.permissions import CanExportTeamParticipantsPermission +from bluebottle.bluebottle_drf2.serializers import PrivateFileSerializer from bluebottle.clients import properties from bluebottle.collect.models import CollectContribution from bluebottle.fsm.serializers import AvailableTransitionsField @@ -25,20 +27,18 @@ from bluebottle.initiatives.models import InitiativePlatformSettings from bluebottle.members.models import Member from bluebottle.segments.models import Segment -from bluebottle.time_based.models import TimeContribution, PeriodParticipant, TeamSlot -from bluebottle.time_based.states import ParticipantStateMachine +from bluebottle.time_based.models import TimeContribution, TeamSlot from bluebottle.utils.exchange_rates import convert from bluebottle.utils.fields import FSMField, ValidationErrorsField, RequiredErrorsField from bluebottle.utils.serializers import ResourcePermissionField, AnonymizedResourceRelatedField -from bluebottle.bluebottle_drf2.serializers import PrivateFileSerializer class TeamSerializer(ModelSerializer): status = FSMField(read_only=True) transitions = AvailableTransitionsField(source='states') - members = SerializerMethodHyperlinkedRelatedField( - model=Contributor, + members = HyperlinkedRelatedField( + read_only=True, many=True, related_link_view_name='team-members', related_link_url_kwarg='team_id' @@ -46,41 +46,22 @@ class TeamSerializer(ModelSerializer): participants_export_url = PrivateFileSerializer( 'team-members-export', - url_args=('pk', ), + url_args=('pk',), filename='participants.csv', permission=CanExportTeamParticipantsPermission, read_only=True ) slot = ResourceRelatedField(queryset=TeamSlot.objects) - def get_members(self, instance): - user = self.context['request'].user - return [ - contributor for contributor in instance.members.all() if ( - isinstance(contributor, PeriodParticipant) and ( - contributor.status in [ - ParticipantStateMachine.new.value, - ParticipantStateMachine.accepted.value, - ParticipantStateMachine.succeeded.value - ] or - user in ( - instance.owner, - instance.activity.owner, - instance.activity.initiative.owner, - contributor.user - ) - ) - ) - ] - class Meta(object): model = Team - fields = ('owner', 'members', 'activity', 'slot') + fields = ('owner', 'activity', 'slot', 'members') meta_fields = ( 'status', 'transitions', 'created', 'participants_export_url', + ) class JSONAPIMeta(object): @@ -416,7 +397,7 @@ class BaseContributorListSerializer(ModelSerializer): class Meta(object): model = Contributor fields = ('user', 'activity', 'status', 'created', 'updated', 'accepted_invite', 'invite') - meta_fields = ('created', 'updated', ) + meta_fields = ('created', 'updated',) class JSONAPIMeta(object): included_resources = [ @@ -444,8 +425,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if ( - isinstance(self.instance, Iterable) or - ( + isinstance(self.instance, Iterable) or ( self.instance and ( self.instance.accepted_invite or self.instance.user != self.context['request'].user @@ -456,8 +436,15 @@ def __init__(self, *args, **kwargs): class Meta(object): model = Contributor - fields = ('user', 'activity', 'status', 'team', 'accepted_invite', 'invite',) - meta_fields = ('transitions', 'created', 'updated', ) + fields = ( + 'user', + 'activity', + 'status', + 'team', + 'accepted_invite', + 'invite', + ) + meta_fields = ('transitions', 'created', 'updated',) class JSONAPIMeta(object): included_resources = [ @@ -475,15 +462,14 @@ class BaseContributionSerializer(ModelSerializer): class Meta(object): model = Contribution - fields = ('value', 'status', ) - meta_fields = ('created', ) + fields = ('value', 'status',) + meta_fields = ('created',) class JSONAPIMeta(object): resource_name = 'contributors' def get_stats_for_activities(activities): - ids = activities.values_list('id', flat=True) default_currency = properties.DEFAULT_CURRENCY @@ -590,7 +576,7 @@ def get_team(self, obj): class Meta(object): model = Invite - fields = ('id', 'team', ) + fields = ('id', 'team',) class JSONAPIMeta(object): included_resources = [ diff --git a/bluebottle/activities/views.py b/bluebottle/activities/views.py index cf19f26ca6..894d93de03 100644 --- a/bluebottle/activities/views.py +++ b/bluebottle/activities/views.py @@ -24,7 +24,7 @@ from bluebottle.funding.models import Donor from bluebottle.members.models import MemberPlatformSettings from bluebottle.time_based.models import DateParticipant, PeriodParticipant -from bluebottle.time_based.serializers import PeriodParticipantSerializer +from bluebottle.time_based.serializers import TeamMemberSerializer from bluebottle.transitions.views import TransitionList from bluebottle.utils.permissions import ( OneOf, ResourcePermission, ResourceOwnerPermission, TenantConditionalOpenClose @@ -260,7 +260,7 @@ def get_queryset(self): team_id=self.kwargs['team_id'] ) - serializer_class = PeriodParticipantSerializer + serializer_class = TeamMemberSerializer class InviteDetailView(JsonApiViewMixin, RetrieveAPIView): diff --git a/bluebottle/time_based/serializers.py b/bluebottle/time_based/serializers.py index 731d881656..70ad88014d 100644 --- a/bluebottle/time_based/serializers.py +++ b/bluebottle/time_based/serializers.py @@ -710,12 +710,47 @@ class Meta(BaseContributorSerializer.Meta): class JSONAPIMeta(BaseContributorSerializer.JSONAPIMeta): resource_name = 'contributors/time-based/participants' - included_resources = BaseContributorSerializer.JSONAPIMeta.included_resources + [ + included_resources = [ + 'user', + 'invite', 'document', 'team' ] +class TeamMemberSerializer(ParticipantSerializer): + + class Meta(ParticipantSerializer.Meta): + model = PeriodParticipant + fields = ( + 'user', + 'status', + 'team', + 'activity', + 'accepted_invite', + 'invite', + 'team' + ) + + class JSONAPIMeta(ParticipantSerializer.JSONAPIMeta): + resource_name = 'contributors/time-based/period-participants' + included_resources = ParticipantSerializer.JSONAPIMeta.included_resources + [ + 'contributions', + 'team', + 'team.slot' + ] + + included_serializers = dict( + ParticipantSerializer.included_serializers, + **{ + 'document': 'bluebottle.time_based.serializers.PeriodParticipantDocumentSerializer', + 'contributions': 'bluebottle.time_based.serializers.TimeContributionSerializer', + 'team.slot': 'bluebottle.time_based.serializers.TeamSlotSerializer', + 'activity': 'bluebottle.time_based.serializers.PeriodActivitySerializer', + } + ) + + class DateParticipantSerializer(ParticipantSerializer): slots = ResourceRelatedField( source='slot_participants', From d4db7ec3ae4de00599fff9e0b0700fd4b9617d2d Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Thu, 7 Jul 2022 11:25:02 +0200 Subject: [PATCH 037/112] Add filter for teams in review --- bluebottle/activities/views.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bluebottle/activities/views.py b/bluebottle/activities/views.py index 894d93de03..5a974c3c88 100644 --- a/bluebottle/activities/views.py +++ b/bluebottle/activities/views.py @@ -163,7 +163,7 @@ class TeamList(JsonApiViewMixin, ListAPIView): queryset = Team.objects.all() serializer_class = TeamSerializer - pemrission_classes = [OneOf(ResourcePermission, ActivityOwnerPermission), ] + permission_classes = [OneOf(ResourcePermission, ActivityOwnerPermission), ] def get_queryset(self, *args, **kwargs): queryset = super(TeamList, self).get_queryset(*args, **kwargs) @@ -176,7 +176,10 @@ def get_queryset(self, *args, **kwargs): has_slot = self.request.query_params.get('filter[has_slot]') start = self.request.query_params.get('filter[start]') - if has_slot == 'false': + status = self.request.query_params.get('filter[status]') + if status: + queryset = queryset.filter(status=status) + elif has_slot == 'false': queryset = queryset.filter(slot__start__isnull=True) elif start == 'future': queryset = queryset.filter( From faddf4088c698efe27c27649cfa7647a952d4e6a Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Thu, 7 Jul 2022 13:31:26 +0200 Subject: [PATCH 038/112] Fix serializer --- bluebottle/time_based/serializers.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bluebottle/time_based/serializers.py b/bluebottle/time_based/serializers.py index 70ad88014d..bc25e84a9e 100644 --- a/bluebottle/time_based/serializers.py +++ b/bluebottle/time_based/serializers.py @@ -718,9 +718,9 @@ class JSONAPIMeta(BaseContributorSerializer.JSONAPIMeta): ] -class TeamMemberSerializer(ParticipantSerializer): +class TeamMemberSerializer(BaseContributorSerializer): - class Meta(ParticipantSerializer.Meta): + class Meta(BaseContributorSerializer.Meta): model = PeriodParticipant fields = ( 'user', @@ -732,16 +732,16 @@ class Meta(ParticipantSerializer.Meta): 'team' ) - class JSONAPIMeta(ParticipantSerializer.JSONAPIMeta): + class JSONAPIMeta(BaseContributorSerializer.JSONAPIMeta): resource_name = 'contributors/time-based/period-participants' - included_resources = ParticipantSerializer.JSONAPIMeta.included_resources + [ + included_resources = BaseContributorSerializer.JSONAPIMeta.included_resources + [ 'contributions', 'team', 'team.slot' ] included_serializers = dict( - ParticipantSerializer.included_serializers, + BaseContributorSerializer.included_serializers, **{ 'document': 'bluebottle.time_based.serializers.PeriodParticipantDocumentSerializer', 'contributions': 'bluebottle.time_based.serializers.TimeContributionSerializer', From 2c4525ee0fe90b7b44091c6b2dd417274403e819 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Fri, 8 Jul 2022 13:28:30 +0200 Subject: [PATCH 039/112] Remove unuseful check --- bluebottle/time_based/tests/test_admin.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/bluebottle/time_based/tests/test_admin.py b/bluebottle/time_based/tests/test_admin.py index 50a1395122..d39ccbbbd5 100644 --- a/bluebottle/time_based/tests/test_admin.py +++ b/bluebottle/time_based/tests/test_admin.py @@ -179,7 +179,10 @@ def test_add_slot_participants(self): self.assertEqual(len(participant.slot_participants.all()), 3) def test_add_participants(self): - activity = DateActivityFactory.create(initiative=self.initiative, status='open') + activity = DateActivityFactory.create( + initiative=self.initiative, + status='open' + ) DateParticipantFactory.create(activity=activity) url = reverse('admin:time_based_dateactivity_change', args=(activity.pk,)) page = self.app.get(url) @@ -187,13 +190,6 @@ def test_add_participants(self): 'Add another Participant' in page.text ) - activity.status = 'rejected' - activity.save() - page = self.app.get(url) - self.assertFalse( - 'Add another Participant' in - page.text - ) class DateParticipantAdminTestCase(BluebottleAdminTestCase): From a841c37dc76fd89097cb001110d0b17dddf3257f Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Mon, 11 Jul 2022 09:49:13 +0200 Subject: [PATCH 040/112] Show team activity label in activity list overview --- bluebottle/activities/admin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bluebottle/activities/admin.py b/bluebottle/activities/admin.py index 7b5137403b..36be9f87ce 100644 --- a/bluebottle/activities/admin.py +++ b/bluebottle/activities/admin.py @@ -612,6 +612,8 @@ def link(self, obj): ordering = ('-created',) def type(self, obj): + if obj.team_activity == 'teams': + return _('Team activity') return obj.get_real_instance_class()._meta.verbose_name From 4552b0aa1464d31af3869ab69f3b713213dc8a42 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Mon, 11 Jul 2022 09:56:54 +0200 Subject: [PATCH 041/112] Remove unused code --- bluebottle/time_based/admin.py | 3 --- bluebottle/time_based/models.py | 29 +---------------------------- 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/bluebottle/time_based/admin.py b/bluebottle/time_based/admin.py index 455080cffd..f474f1c47a 100644 --- a/bluebottle/time_based/admin.py +++ b/bluebottle/time_based/admin.py @@ -273,13 +273,10 @@ class TeamSlotInline(admin.StackedInline): ordering = ['-start'] readonly_fields = ['link', 'timezone', ] - raw_id_fields = ['location'] fields = [ 'start', 'duration', 'timezone', - 'is_online', - 'location' ] def timezone(self, obj): diff --git a/bluebottle/time_based/models.py b/bluebottle/time_based/models.py index a57b8cf309..c1a881ec6e 100644 --- a/bluebottle/time_based/models.py +++ b/bluebottle/time_based/models.py @@ -225,6 +225,7 @@ class ActivitySlot(TriggerMixin, AnonymizationMixin, ValidatedModelMixin, models _('title'), max_length=255, null=True, blank=True) + capacity = models.PositiveIntegerField(_('attendee limit'), null=True, blank=True) is_online = models.NullBooleanField( @@ -519,40 +520,12 @@ def required_fields(self): fields = super().required_fields + [ 'start', 'duration', - 'is_online', ] if not self.is_online: fields.append('location') return fields - @property - def end(self): - if self.start and self.duration: - return self.start + self.duration - - @property - def sequence(self): - ids = list(self.activity.slots.values_list('id', flat=True)) - if len(ids) and self.id and self.id in ids: - return ids.index(self.id) + 1 - return '-' - - @property - def local_timezone(self): - if self.location and self.location.position: - tz_name = tf.timezone_at( - lng=self.location.position.x, - lat=self.location.position.y - ) - return pytz.timezone(tz_name) - - @property - def utc_offset(self): - tz = self.local_timezone or timezone.get_current_timezone() - if self.start and tz: - return self.start.astimezone(tz).utcoffset().total_seconds() / 60 - class Meta: verbose_name = _('team slot') verbose_name_plural = _('team slots') From 635cd51f9d01eb25a13d95b14caa033a9a0e5036 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Mon, 11 Jul 2022 10:26:05 +0200 Subject: [PATCH 042/112] Add test --- bluebottle/activities/tests/test_api.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/bluebottle/activities/tests/test_api.py b/bluebottle/activities/tests/test_api.py index b1e790b322..ce50d17a1e 100644 --- a/bluebottle/activities/tests/test_api.py +++ b/bluebottle/activities/tests/test_api.py @@ -1779,6 +1779,16 @@ def test_get_activity_owner(self): 'We should have a unique list of team ids' ) + def test_get_filtered_status(self): + new_teams = TeamFactory.create_batch(2, activity=self.activity, status='new') + self.perform_get(user=self.activity.owner, query={'filter[status]': 'new'}) + + self.assertStatus(status.HTTP_200_OK) + for resource in self.response.json()['data']: + self.assertTrue( + resource['id'] in [str(team.pk) for team in new_teams] + ) + def test_get_filtered_has_slot(self): self.perform_get(user=self.activity.owner, query={'filter[has_slot]': 'false'}) From 84d6bc0666c0e710e76074ce0b27d7dc66a5370f Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Mon, 11 Jul 2022 10:30:13 +0200 Subject: [PATCH 043/112] Clear obsolete migration --- .../migrations/0091_project_to_initiatives.py | 425 ------------------ 1 file changed, 425 deletions(-) diff --git a/bluebottle/projects/migrations/0091_project_to_initiatives.py b/bluebottle/projects/migrations/0091_project_to_initiatives.py index f49c0c385d..d89ecec14d 100644 --- a/bluebottle/projects/migrations/0091_project_to_initiatives.py +++ b/bluebottle/projects/migrations/0091_project_to_initiatives.py @@ -10,430 +10,6 @@ from bluebottle.clients import properties - -def map_status(status): - mapping = { - 'plan-new': 'draft', - 'plan-submitted': 'submitted', - 'plan-needs-work': 'needs_work', - 'voting': 'approved', - 'voting-done': 'approved', - 'campaign': 'approved', - 'to-be-continued': 'approved', - 'done-complete': 'approved', - 'done-incomplete': 'approved', - 'closed': 'closed', - 'refunded': 'approved', - } - return mapping[status.slug] - - -def map_funding_status(status): - mapping = { - 'plan-new': 'in_review', - 'plan-submitted': 'in_review', - 'plan-needs-work': 'in_review', - 'voting': 'open', - 'voting-done': 'open', - 'campaign': 'open', - 'to-be-continued': 'open', - 'done-complete': 'succeeded', - 'done-incomplete': 'succeeded', - 'closed': 'closed', - 'refunded': 'refunded', - } - return mapping[status.slug] - - -def map_funding_review_status(status): - mapping = { - 'plan-new': 'draft', - 'plan-submitted': 'submitted', - 'plan-needs-work': 'needs_work', - 'voting': 'approved', - 'voting-done': 'approved', - 'campaign': 'approved', - 'to-be-continued': 'approved', - 'done-complete': 'approved', - 'done-incomplete': 'approved', - 'closed': 'approved', - 'refunded': 'approved', - } - return mapping[status.slug] - - -def truncate(number, limit): - return old_div(int(number * pow(10, limit)), 10) ^ pow(10, limit) - - -def set_currencies(apps, provider, name): - PaymentCurrency = apps.get_model('funding', 'PaymentCurrency') - defaults = properties.DONATION_AMOUNTS - for method in properties.PAYMENT_METHODS: - if method['provider'] == name: - for cur in method['currencies']: - val = method['currencies'][cur] - PaymentCurrency.objects.get_or_create( - provider=provider, - code=cur, - defaults={ - 'min_amount': getattr(val, 'min_amount', 5.0), - 'default1': defaults[cur][0], - 'default2': defaults[cur][1], - 'default3': defaults[cur][2], - 'default4': defaults[cur][3], - } - ) - - -def migrate_payment_providers(apps): - - PledgePaymentProvider = apps.get_model('funding_pledge', 'PledgePaymentProvider') - StripePaymentProvider = apps.get_model('funding_stripe', 'StripePaymentProvider') - FlutterwavePaymentProvider = apps.get_model('funding_flutterwave', 'FlutterwavePaymentProvider') - VitepayPaymentProvider = apps.get_model('funding_vitepay', 'VitepayPaymentProvider') - LipishaPaymentProvider = apps.get_model('funding_lipisha', 'LipishaPaymentProvider') - - Client = apps.get_model('clients', 'Client') - ContentType = apps.get_model('contenttypes', 'ContentType') - - tenant = Client.objects.get(schema_name=connection.tenant.schema_name) - properties.set_tenant(tenant) - - for provider in properties.MERCHANT_ACCOUNTS: - pp = None - if provider['merchant'] == 'stripe': - content_type = ContentType.objects.get_for_model(StripePaymentProvider) - pp = StripePaymentProvider.objects.create( - polymorphic_ctype=content_type, - ) - for payment_methods in properties.PAYMENT_METHODS: - if payment_methods['id'] == 'stripe-creditcard': - pp.credit_card = True - elif payment_methods['id'] == 'stripe-ideal': - pp.ideal = True - elif payment_methods['id'] == 'stripe-directdebit': - pp.direct_debit = True - elif payment_methods['id'] == 'stripe-bancontact': - pp.bancontact = True - pp.save() - set_currencies(apps, pp, 'stripe') - - elif provider['merchant'] == 'vitepay': - content_type = ContentType.objects.get_for_model(VitepayPaymentProvider) - pp = VitepayPaymentProvider.objects.create( - polymorphic_ctype=content_type, - api_secret=provider['api_secret'], - api_key=provider['api_key'], - api_url=provider['api_url'], - prefix='new' - ) - set_currencies(apps, pp, 'vitepay') - elif provider['merchant'] == 'lipisha': - content_type = ContentType.objects.get_for_model(LipishaPaymentProvider) - pp = LipishaPaymentProvider.objects.create( - polymorphic_ctype=content_type, - api_key=provider['api_key'], - api_signature=provider['api_signature'], - paybill=provider['business_number'], - prefix='new' - ) - set_currencies(apps, pp, 'lipisha') - elif provider['merchant'] == 'flutterwave': - content_type = ContentType.objects.get_for_model(FlutterwavePaymentProvider) - pp = FlutterwavePaymentProvider.objects.create( - polymorphic_ctype=content_type, - pub_key=provider['pub_key'], - sec_key=provider['sec_key'], - prefix='new' - ) - set_currencies(apps, pp, 'flutterwave') - elif provider['merchant'] == 'pledge': - content_type = ContentType.objects.get_for_model(PledgePaymentProvider) - pp = PledgePaymentProvider.objects.create( - polymorphic_ctype=content_type, - ) - set_currencies(apps, pp, 'pledge') - - -def migrate_projects(apps, schema_editor): - migrate_payment_providers(apps) - - Project = apps.get_model('projects', 'Project') - Initiative = apps.get_model('initiatives', 'Initiative') - Funding = apps.get_model('funding', 'Funding') - Geolocation = apps.get_model('geo', 'Geolocation') - Country = apps.get_model('geo', 'Country') - Image = apps.get_model('files', 'Image') - Client = apps.get_model('clients', 'Client') - OrganizationContact = apps.get_model('organizations', 'OrganizationContact') - StripePayoutAccount = apps.get_model('funding_stripe', 'StripePayoutAccount') - ExternalAccount = apps.get_model('funding_stripe', 'ExternalAccount') - PlainPayoutAccount = apps.get_model('funding', 'PlainPayoutAccount') - PayoutAccount = apps.get_model('funding', 'PayoutAccount') - BudgetLine = apps.get_model('funding', 'BudgetLine') - Reward = apps.get_model('funding', 'Reward') - Fundraiser = apps.get_model('funding', 'Fundraiser') - OldPayoutAccount = apps.get_model('payouts', 'PayoutAccount') - FlutterwaveBankAccount = apps.get_model('funding_flutterwave', 'FlutterwaveBankAccount') - VitepayBankAccount = apps.get_model('funding_vitepay', 'VitepayBankAccount') - LipishaBankAccount = apps.get_model('funding_lipisha', 'LipishaBankAccount') - PledgeBankAccount = apps.get_model('funding_pledge', 'PledgeBankAccount') - - Wallpost = apps.get_model('wallposts', 'Wallpost') - - ContentType = apps.get_model('contenttypes', 'ContentType') - - # Clean-up previous migrations of projects to initiatives - Initiative.objects.all().delete() - - tenant = Client.objects.get(schema_name=connection.tenant.schema_name) - properties.set_tenant(tenant) - - stripe_bank_ct = ContentType.objects.get_for_model(ExternalAccount) - stripe_account_ct = ContentType.objects.get_for_model(StripePayoutAccount) - plain_account_ct = ContentType.objects.get_for_model(PlainPayoutAccount) - flutterwave_ct = ContentType.objects.get_for_model(FlutterwaveBankAccount) - lipisha_ct = ContentType.objects.get_for_model(LipishaBankAccount) - vitepay_ct = ContentType.objects.get_for_model(VitepayBankAccount) - pledge_ct = ContentType.objects.get_for_model(PledgeBankAccount) - funding_ct = ContentType.objects.get_for_model(Funding) - project_ct = ContentType.objects.get_for_model(Project) - initiative_ct = ContentType.objects.get_for_model(Initiative) - - for project in Project.objects.select_related('payout_account', 'projectlocation').iterator(): - if hasattr(project, 'projectlocation') and project.projectlocation.country: - if project.projectlocation.latitude and project.projectlocation.longitude: - point = Point( - float(project.projectlocation.longitude), - float(project.projectlocation.latitude) - ) - else: - point = Point(0, 0) - - country = project.country - - if not country and project.projectlocation.country and Country.objects.filter( - translations__name=project.projectlocation.country - ).count(): - country = Country.objects.filter( - translations__name__contains=project.projectlocation.country - ).first() - - if not country: - country = Country.objects.filter(alpha2_code=properties.DEFAULT_COUNTRY_CODE).first() - - if country: - place = Geolocation.objects.create( - street=project.projectlocation.street, - postal_code=project.projectlocation.postal_code, - country=country, - locality=project.projectlocation.city, - position=point - ) - else: - place = None - else: - if project.country: - place = Geolocation.objects.create( - country=project.country, - position=Point(0, 0) - ) - else: - place = None - - initiative = Initiative.objects.create( - title=project.title, - slug=project.slug, - pitch=project.pitch or '', - story=project.story or '', - theme_id=project.theme_id, - video_url=project.video_url, - place=place, - location_id=project.location_id, - owner_id=project.owner_id, - reviewer_id=project.reviewer_id, - activity_manager_id=project.task_manager_id, - promoter_id=project.promoter_id, - status=map_status(project.status) - - ) - if project.image: - try: - image = Image.objects.create(owner=project.owner) - image.file.save(project.image.name, project.image.file, save=True) - initiative.image = image - except IOError: - pass - - if project.organization: - initiative.organization = project.organization - - contact = OrganizationContact.objects.filter(organization=project.organization).first() - if contact: - initiative.organization_contact = contact - - initiative.created = project.created - initiative.categories = project.categories.all() - initiative.save() - - # Create Funding event if there are donations - if project.project_type in ['both', 'funding'] \ - or project.donation_set.count() \ - or project.amount_asked.amount: - account = None - if project.payout_account: - try: - stripe_account = project.payout_account.stripepayoutaccount - if stripe_account.account_id: - payout_account = StripePayoutAccount.objects.create( - polymorphic_ctype=stripe_account_ct, - account_id=stripe_account.account_id, - owner=stripe_account.user, - # country=stripe_account.country.alpha2_code - ) - account = ExternalAccount.objects.create( - polymorphic_ctype=stripe_bank_ct, - connect_account=payout_account, - # account_id=stripe_account.bank_details.account - ) - except OldPayoutAccount.DoesNotExist: - plain_account = project.payout_account.plainpayoutaccount - payout_account = PlainPayoutAccount.objects.create( - polymorphic_ctype=plain_account_ct, - owner=plain_account.user, - reviewed=plain_account.reviewed - ) - - if str(project.amount_asked.currency) == 'NGN': - country = None - if plain_account.account_bank_country: - country = plain_account.account_bank_country.alpha2_code - account = FlutterwaveBankAccount.objects.create( - polymorphic_ctype=flutterwave_ct, - connect_account=payout_account, - account_holder_name=plain_account.account_holder_name, - bank_country_code=country, - account_number=plain_account.account_number - ) - elif str(project.amount_asked.currency) == 'KES': - account = LipishaBankAccount.objects.create( - polymorphic_ctype=lipisha_ct, - connect_account=payout_account, - account_number=plain_account.account_number, - account_name=plain_account.account_holder_name, - address=plain_account.account_holder_address - ) - elif str(project.amount_asked.currency) == 'XOF': - account = VitepayBankAccount.objects.create( - polymorphic_ctype=vitepay_ct, - connect_account=payout_account, - account_name=plain_account.account_holder_name, - ) - else: - account = PledgeBankAccount.objects.create( - polymorphic_ctype=pledge_ct, - connect_account=payout_account, - account_holder_name=plain_account.account_holder_name, - account_holder_address=plain_account.account_holder_address, - account_holder_postal_code=plain_account.account_holder_postal_code, - account_holder_city=plain_account.account_holder_city, - account_holder_country_id=plain_account.account_holder_country_id, - account_number=plain_account.account_number, - account_details=plain_account.account_details, - account_bank_country_id=plain_account.account_bank_country_id, - ) - - funding = Funding.objects.create( - # Common activity fields - polymorphic_ctype=funding_ct, # This does not get set automatically in migrations - initiative=initiative, - owner_id=project.owner_id, - highlight=project.is_campaign, - created=project.created, - updated=project.updated, - status=map_funding_status(project.status), - review_status=map_funding_review_status(project.status), - title=project.title, - slug=project.slug, - description=project.pitch or '', - transition_date=project.campaign_ended, - - # Funding specific fields - deadline=project.deadline, - target=project.amount_asked, - amount_matching=project.amount_extra, - country=project.country, - bank_account=account - ) - project.funding_id = funding.id - project.save() - - Wallpost.objects.filter(content_type=project_ct, object_id=project.id).\ - update(content_type=funding_ct, object_id=funding.id) - - for budget_line in project.projectbudgetline_set.all(): - new_budget_line = BudgetLine.objects.create( - activity=funding, - amount=budget_line.amount, - description=budget_line.description, - created=budget_line.created, - updated=budget_line.updated - ) - - fundraiser_ct = ContentType.objects.get_for_model(Initiative) - old_fundraiser_ct = ContentType.objects.get_for_model(Project) - - for fundraiser in project.fundraiser_set.all(): - new_fundraiser = Fundraiser.objects.create( - owner_id=fundraiser.owner_id, - activity=funding, - title=fundraiser.title, - description=fundraiser.description, - amount=fundraiser.amount, - deadline=fundraiser.deadline, - created=fundraiser.created, - updated=fundraiser.updated - ) - new_fundraiser.save() - if fundraiser.image: - try: - image = Image.objects.create(owner=fundraiser.owner) - image.file.save(fundraiser.image.name, fundraiser.image.file, save=True) - initiative.image = image - except IOError: - pass - Wallpost.objects.filter(content_type=old_fundraiser_ct, object_id=fundraiser.id). \ - update(content_type=fundraiser_ct, object_id=new_fundraiser.id) - - for reward in project.reward_set.all(): - new_reward = Reward.objects.create( - activity=funding, - amount=reward.amount, - description=reward.description, - title=reward.title, - limit=reward.limit, - created=reward.created, - updated=reward.updated - ) - reward.new_reward_id = new_reward.id - reward.save() - else: - Wallpost.objects.filter(content_type=project_ct, object_id=project.id).\ - update(content_type=initiative_ct, object_id=initiative.id) - - -def wipe_initiatives(apps, schema_editor): - - Initiative = apps.get_model('initiatives', 'Initiative') - Funding = apps.get_model('funding', 'Funding') - PaymentProvider = apps.get_model('funding', 'PaymentProvider') - - PaymentProvider.objects.all().delete() - Initiative.objects.all().delete() - Funding.objects.all().delete() - - class Migration(migrations.Migration): dependencies = [ @@ -456,5 +32,4 @@ class Migration(migrations.Migration): name='funding_id', field=models.IntegerField(null=True), ), - migrations.RunPython(migrate_projects, wipe_initiatives) ] From 2f8968192b882541f4a33af026b35b5c6bc8a647 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Mon, 11 Jul 2022 10:32:11 +0200 Subject: [PATCH 044/112] Clear more migrations --- .../0042_migrate_tasks_to_activities.py | 214 --------------- .../migrations/0036_auto_20201125_1258.py | 248 ------------------ 2 files changed, 462 deletions(-) diff --git a/bluebottle/tasks/migrations/0042_migrate_tasks_to_activities.py b/bluebottle/tasks/migrations/0042_migrate_tasks_to_activities.py index b7046bae37..fd07de5ea8 100644 --- a/bluebottle/tasks/migrations/0042_migrate_tasks_to_activities.py +++ b/bluebottle/tasks/migrations/0042_migrate_tasks_to_activities.py @@ -12,219 +12,6 @@ from bluebottle.clients import properties - -def map_event_status(task): - - mapping = { - 'open': 'open', - 'full': 'full', - 'running': 'open', - 'realised': 'succeeded', - 'realized': 'succeeded', - 'in progress': 'open', - 'closed': 'closed' - } - status = mapping[task.status] - if task.project.status in ['plan-new', 'draft', 'submitted']: - status = 'draft' - - return status - - -def map_participant_status(member): - mapping = { - 'applied': 'new', - 'accepted': 'new', - 'rejected': 'rejected', - 'stopped': 'closed', - 'withdrew': 'withdrawn', - 'realized': 'succeeded', - 'absent': 'no_show' - } - status = mapping[member.status] - return status - - -def map_applicant_status(member): - mapping = { - 'applied': 'new', - 'accepted': 'accepted', - 'rejected': 'rejected', - 'stopped': 'closed', - 'withdrew': 'withdrawn', - 'realized': 'succeeded', - 'absent': 'no_show' - } - status = mapping[member.status] - return status - - -def migrate_tasks(apps, schema_editor): - Client = apps.get_model('clients', 'Client') - tenant = Client.objects.get(schema_name=connection.tenant.schema_name) - properties.set_tenant(tenant) - - Task = apps.get_model('tasks', 'Task') - - Activity = apps.get_model('activities', 'Activity') - Event = apps.get_model('events', 'Event') - Assignment = apps.get_model('assignments', 'Assignment') - - Contribution = apps.get_model('activities', 'Contribution') - Participant = apps.get_model('events', 'Participant') - Applicant = apps.get_model('assignments', 'Applicant') - - Initiative = apps.get_model('initiatives', 'Initiative') - ContentType = apps.get_model('contenttypes', 'ContentType') - Wallpost = apps.get_model('wallposts', 'Wallpost') - - Place = apps.get_model('geo', 'Place') - Geolocation = apps.get_model('geo', 'Geolocation') - # Clean-up previous migrations of projects to initiatives - Event.objects.all().delete() - Assignment.objects.all().delete() - - event_ctype = ContentType.objects.get_for_model(Event) - participant_ctype= ContentType.objects.get_for_model(Participant) - assignment_ctype = ContentType.objects.get_for_model(Assignment) - applicant_ctype= ContentType.objects.get_for_model(Applicant) - task_ctype= ContentType.objects.get_for_model(Task) - - def get_location(task): - place = Place.objects.filter( - content_type=task_ctype, - object_id=task.pk - ).first() - if place: - return Geolocation.objects.create( - street_number=place.street_number, - street=place.street, - postal_code=place.postal_code, - locality=place.locality, - province=place.province, - position=Point(float(place.position.longitude), float(place.position.latitude)), - country_id=place.country_id - ) - else: - return None - - for task in Task.objects.select_related('project').prefetch_related('members').iterator(): - if task.type == 'event' and (not task.skill_id or not task.skill.expertise): - initiative = Initiative.objects.get(slug=task.project.slug) - - geolocation = get_location(task) - status = map_event_status(task) - - event = Event.objects.create( - # activity fields - polymorphic_ctype=event_ctype, - initiative=initiative, - title=task.title, - slug=slugify(task.title), - description=task.description, - review_status=initiative.status, - status=status, - owner_id=task.author_id, - - # event fields - capacity=task.people_needed, - automatically_accept=bool(task.accepting == 'automatic'), - is_online=bool(not task.location), - location=geolocation, - location_hint=task.location, - start_date=task.deadline.date(), - start_time=task.deadline.time(), - duration=task.time_needed, - transition_date=task.deadline - ) - task.activity_id = event.pk - task.save() - - event.created = task.created - event.updated = task.updated - event.polymorphic_ctype = event_ctype - event.save() - - for task_member in task.members.all(): - status = map_participant_status(task_member) - - participant = Participant.objects.create( - activity=event, - user=task_member.member, - status=status, - time_spent=task_member.time_spent, - transition_date=task.updated - ) - participant.created = task.created - participant.updated = task.updated - participant.polymorphic_ctype = participant_ctype - participant.save() - - old_ct = ContentType.objects.get_for_model(Task) - Wallpost.objects.filter(content_type=old_ct, object_id=task.id).\ - update(content_type=event_ctype, object_id=event.id) - - else: - initiative = Initiative.objects.get(slug=task.project.slug) - geolocation = get_location(task) - - status = map_event_status(task) - end_date_type = 'deadline' - if task.type == 'event': - end_date_type = 'on_date' - - assignment = Assignment.objects.create( - # activity fields - polymorphic_ctype=assignment_ctype, - initiative=initiative, - title=task.title, - slug=slugify(task.title), - description=task.description, - status=status, - review_status=initiative.status, - owner=task.author, - - # assignment fields - end_date_type=end_date_type, - registration_deadline=task.deadline_to_apply.date(), - end_date=task.deadline.date(), - capacity=task.people_needed, - is_online=bool(not task.location), - location=geolocation, - duration=task.time_needed, - expertise=task.skill, - transition_date=task.deadline - ) - - task.activity_id = assignment.pk - task.save() - - assignment.created = task.created - assignment.updated = task.updated - assignment.polymorphic_ctype = assignment_ctype - assignment.save() - - for task_member in task.members.all(): - status = map_applicant_status(task_member) - - applicant = Applicant.objects.create( - activity=assignment, - user=task_member.member, - status=status, - time_spent=task_member.time_spent, - motivation=task_member.motivation, - transition_date=task.updated - ) - applicant.created = task.created - applicant.updated = task.updated - applicant.polymorphic_ctype = applicant_ctype - applicant.save() - - old_ct = ContentType.objects.get_for_model(Task) - Wallpost.objects.filter(content_type=old_ct, object_id=task.id). \ - update(content_type=assignment_ctype, object_id=assignment.id) - - class Migration(migrations.Migration): dependencies = [ @@ -240,5 +27,4 @@ class Migration(migrations.Migration): name='activity_id', field=models.IntegerField(null=True), ), - migrations.RunPython(migrate_tasks, migrations.RunPython.noop) ] diff --git a/bluebottle/time_based/migrations/0036_auto_20201125_1258.py b/bluebottle/time_based/migrations/0036_auto_20201125_1258.py index 48c24224c5..f106ecf48e 100644 --- a/bluebottle/time_based/migrations/0036_auto_20201125_1258.py +++ b/bluebottle/time_based/migrations/0036_auto_20201125_1258.py @@ -8,252 +8,6 @@ from django.db import migrations, transaction, connection -def insert(table, fields, values): - with connection.cursor() as cursor: - query = 'INSERT into {} ({}) VALUES ({})'.format( - table, - ", ".join('"{}"'.format(field) for field in fields), - ','.join('%s' for field in fields) - ) - actual_values = [] - - for value in values: - actual_values.append( - [value[field] for field in fields ] - ) - - cursor.executemany(query, actual_values) - - -def migrate_activities(apps, schema_editor): - Event = apps.get_model('events', 'Event') - Assignment = apps.get_model('assignments', 'Assignment') - DateActivity = apps.get_model('time_based', 'DateActivity') - PeriodActivity = apps.get_model('time_based', 'PeriodActivity') - ContentType = apps.get_model('contenttypes', 'ContentType') - - time_based_fields = ( - 'activity_ptr_id', 'capacity', 'is_online', 'location_hint', - 'registration_deadline', 'location_id', 'review', 'expertise_id', - ) - date_fields = ( - 'timebasedactivity_ptr_id', 'start', 'duration', 'online_meeting_url', - ) - period_fields = ( - 'timebasedactivity_ptr_id', 'duration', 'duration_period', 'deadline' - ) - - date_activities = [] - period_activities = [] - - for event in Event.objects.values( - 'capacity', 'start', 'duration', - 'is_online', 'location_id', 'location_hint', - 'online_meeting_url', 'registration_deadline', - 'activity_ptr_id' - ): - event['timebasedactivity_ptr_id'] = event['activity_ptr_id'] - event['review'] = False - event['expertise_id'] = None - if event['duration'] is not None: - event['duration'] = timedelta(hours=event['duration']) - - date_activities.append(event) - - for assignment in Assignment.objects.values( - 'capacity', 'end_date_type', 'date', 'duration', - 'is_online', 'location_id', - 'online_meeting_url', 'registration_deadline', - 'activity_ptr_id', 'expertise_id' - ): - assignment['timebasedactivity_ptr_id'] = assignment['activity_ptr_id'] - assignment['review'] = True - assignment['duration_period'] = 'overall' - assignment['location_hint'] = '' - assignment['online_meeting_url'] = '' - - if assignment['duration'] is not None: - assignment['duration'] = timedelta(hours=assignment['duration']) - - if assignment['end_date_type'] == 'on_date': - assignment['start'] = assignment.pop('date') - date_activities.append(assignment) - else: - if assignment['date']: - assignment['deadline'] = assignment.pop('date').date() - else: - assignment['deadline'] = None - - period_activities.append(assignment) - - - insert( - 'time_based_timebasedactivity', - time_based_fields, - date_activities + period_activities - ) - - insert( - 'time_based_dateactivity', - date_fields, - date_activities - ) - insert( - 'time_based_periodactivity', - period_fields, - period_activities - ) - - Event.objects.update( - polymorphic_ctype_id=ContentType.objects.get_for_model(DateActivity) - ) - - Assignment.objects.filter(end_date_type='on_date').update( - polymorphic_ctype_id=ContentType.objects.get_for_model(DateActivity) - ) - - Assignment.objects.exclude(end_date_type='on_date').update( - polymorphic_ctype_id=ContentType.objects.get_for_model(PeriodActivity) - ) - - -def migrate_contributors(apps, schema_editor): - Participant = apps.get_model('events', 'Participant') - Applicant = apps.get_model('assignments', 'Applicant') - DateParticipant = apps.get_model('time_based', 'DateParticipant') - PeriodParticipant = apps.get_model('time_based', 'PeriodParticipant') - Contribution = apps.get_model('activities', 'Contribution') - TimeContribution = apps.get_model('time_based', 'TimeContribution') - - ContentType = apps.get_model('contenttypes', 'ContentType') - - date_participant_fields = ( - 'contributor_ptr_id', - ) - period_participant_fields = ( - 'contributor_ptr_id', 'motivation', 'document_id' - ) - contribution_fields = ( - 'contributor_id', 'status', 'created', 'polymorphic_ctype_id' - ) - time_contribution_fields = ( - 'contribution_ptr_id', 'start', 'end', 'value' - ) - - date_participants = [] - period_participants = [] - time_contributions = [] - contributions = [] - time_contributions_ctype = ContentType.objects.get_for_model(TimeContribution).pk - - - for participant in Participant.objects.values( - 'contributor_ptr_id', 'time_spent', 'activity__event__start', 'activity__event__duration', - 'status', 'created', 'contributor_date' - ): - duration = participant['activity__event__duration'] - date_participants.append({'contributor_ptr_id': participant['contributor_ptr_id']}) - - if participant['status'] in ('accepted', 'active'): - status = 'new' - elif participant['status'] in ('withdrawn', 'failed', 'rejected', 'no_show'): - status = 'failed' - else: - status = participant['status'] - - contributions.append({ - 'status': status, - 'contributor_id': participant['contributor_ptr_id'], - 'created': participant['created'], - 'polymorphic_ctype_id': time_contributions_ctype - }) - time_contributions.append({ - 'contributor_id': participant['contributor_ptr_id'], - 'start': participant['contributor_date'], - 'end': None, - 'value': timedelta(hours=participant['time_spent'] or 0) - }) - - for applicant in Applicant.objects.values( - 'contributor_ptr_id', 'time_spent', 'motivation', 'document_id', 'activity__assignment__date', - 'activity__assignment__end_date_type', 'created', 'status', 'contributor_date' - ): - participant = { - 'contributor_ptr_id': applicant['contributor_ptr_id'], - 'motivation': applicant['motivation'], - 'document_id': applicant['document_id'], - } - if applicant['activity__assignment__end_date_type'] == 'on_date': - date_participants.append(participant) - else: - period_participants.append(participant) - - if applicant['status'] in ('accepted', 'active'): - status = 'new' - elif applicant['status'] in ('withdrawn', 'failed', 'rejected', 'no_show'): - status = 'failed' - else: - status = applicant['status'] - - contributions.append({ - 'status': status, - 'contributor_id': applicant['contributor_ptr_id'], - 'created': applicant['created'], - 'polymorphic_ctype_id': time_contributions_ctype - }) - time_contributions.append({ - 'contributor_id': applicant['contributor_ptr_id'], - 'start': applicant['contributor_date'], - 'end': None, - 'value': timedelta(hours=applicant['time_spent'] or 0) - }) - - insert('time_based_dateparticipant', date_participant_fields, date_participants) - insert('time_based_periodparticipant', period_participant_fields, period_participants) - insert('activities_contribution', contribution_fields, contributions) - - contributions = dict(Contribution.objects.values_list('contributor_id', 'id')) - for time_contribution in time_contributions: - time_contribution['contribution_ptr_id'] = contributions[time_contribution['contributor_id']] - - insert('time_based_timecontribution', time_contribution_fields, time_contributions) - - Participant.objects.update( - polymorphic_ctype_id=ContentType.objects.get_for_model(DateParticipant) - ) - - Applicant.objects.filter(activity__assignment__end_date_type='on_date').update( - polymorphic_ctype_id=ContentType.objects.get_for_model(DateParticipant) - ) - - Applicant.objects.exclude(activity__assignment__end_date_type='on_date').update( - polymorphic_ctype_id=ContentType.objects.get_for_model(PeriodParticipant) - ) - - DateParticipant.objects.filter( - status__in=('succeeded', 'active', ) - ).update( - status='accepted' - ) - - PeriodParticipant.objects.filter( - status__in=('succeeded', 'active', ) - ).update( - status='accepted' - ) - - DateParticipant.objects.filter( - status__in=('failed', ) - ).update( - status='rejected' - ) - - PeriodParticipant.objects.filter( - status__in=('failed', ) - ).update( - status='rejected' - ) - class Migration(migrations.Migration): dependencies = [ @@ -263,6 +17,4 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RunPython(migrate_activities, migrations.RunPython.noop), - migrations.RunPython(migrate_contributors, migrations.RunPython.noop), ] From 54c92bba5ee574500ebbea5e3de4a53056b1478b Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Mon, 11 Jul 2022 13:06:51 +0200 Subject: [PATCH 045/112] Try to omit migrations --- coverage.rc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coverage.rc b/coverage.rc index ddeec64ed6..61eca85d75 100644 --- a/coverage.rc +++ b/coverage.rc @@ -1,3 +1,3 @@ [run] include = apps* -omit = **/migrations/** +omit = ../*migrations* From 284789e6c19bbb741ee339fc488db22f095e91a9 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Mon, 11 Jul 2022 14:47:19 +0200 Subject: [PATCH 046/112] Ignore --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c9118ddf68..ebe22c7cc1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -47,4 +47,4 @@ notifications: secure: TOveMBh9HePYKWuGTrWF+hTXzxGZvbVsa3KU0sB1yv6qkcixb5/ggvmkTeRddYEd/zyWyMenicFsrXVBgsP0SmbNgke6kq5+EN0U5oJWse998lvCVCpwmJQMdwDHvYsOtbFEOppQrbRK4vmH8qibx3x2YVg+u+61ePHvWYF9z6U= after_success: - bash post_travis.sh -- python -m coverage combine; python -m coverage report --omit **/migrations/**; coveralls +- python -m coverage combine; python -m coverage report --omit "**/migrations/**"; coveralls From 79f0301394dd8b5d7da33732842b6e81ee6e6fd2 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Mon, 11 Jul 2022 17:11:15 +0200 Subject: [PATCH 047/112] Add message to team slot --- bluebottle/activities/models.py | 4 ++ bluebottle/activities/utils.py | 2 +- bluebottle/time_based/messages.py | 27 +++++++++++++ bluebottle/time_based/models.py | 9 +++++ .../mails/messages/changed_team_date.html | 39 +++++++++++++++++++ bluebottle/time_based/triggers.py | 36 ++++++++++++++++- 6 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 bluebottle/time_based/templates/mails/messages/changed_team_date.html diff --git a/bluebottle/activities/models.py b/bluebottle/activities/models.py index c37d1f5d47..3eaf7dd691 100644 --- a/bluebottle/activities/models.py +++ b/bluebottle/activities/models.py @@ -290,6 +290,10 @@ class Team(TriggerMixin, models.Model): 'members.Member', related_name='teams', null=True, on_delete=models.SET_NULL ) + @property + def accepted_participants(self): + return self.members.filter(status='accepted') + class Meta(object): ordering = ('-created',) verbose_name = _("Team") diff --git a/bluebottle/activities/utils.py b/bluebottle/activities/utils.py index 4e0424b31f..ffed7fbdc4 100644 --- a/bluebottle/activities/utils.py +++ b/bluebottle/activities/utils.py @@ -55,7 +55,7 @@ class TeamSerializer(ModelSerializer): class Meta(object): model = Team - fields = ('owner', 'activity', 'slot', 'members') + fields = ('owner', 'slot', 'members') meta_fields = ( 'status', 'transitions', diff --git a/bluebottle/time_based/messages.py b/bluebottle/time_based/messages.py index efef5ea3c6..9a58d55b23 100644 --- a/bluebottle/time_based/messages.py +++ b/bluebottle/time_based/messages.py @@ -232,6 +232,33 @@ def get_recipients(self): ] +class TeamSlotChangedNotification(TransitionMessage): + """ + Notification when slot details (date, time or location) changed for a team activity + """ + subject = pgettext('email', 'The details of the team activity "{title}" have changed') + template = 'messages/changed_team_date' + context = { + 'title': 'activity.title', + 'team_name': 'team', + 'start': 'start', + 'duration': 'duration', + 'end': 'end', + } + + @property + def action_link(self): + return self.obj.activity.get_absolute_url() + + action_title = pgettext('email', 'View activity') + + def get_recipients(self): + """team members""" + return [ + participant.user for participant in self.obj.team.accepted_participants + ] + + class ActivitySucceededManuallyNotification(TransitionMessage): """ The activity was set to succeeded manually diff --git a/bluebottle/time_based/models.py b/bluebottle/time_based/models.py index c1a881ec6e..d13d5eb0a7 100644 --- a/bluebottle/time_based/models.py +++ b/bluebottle/time_based/models.py @@ -515,6 +515,11 @@ class TeamSlot(ActivitySlot): duration = models.DurationField(_('duration'), null=True, blank=True) team = models.OneToOneField(Team, related_name='slot', on_delete=models.CASCADE) + @property + def end(self): + if self.start and self.duration: + return self.start + self.duration + @property def required_fields(self): fields = super().required_fields + [ @@ -547,6 +552,10 @@ def __str__(self): class JSONAPIMeta: resource_name = 'activities/time-based/team-slots' + @property + def accepted_participants(self): + return self.team.members.filter(status='accepted') + class Participant(Contributor): diff --git a/bluebottle/time_based/templates/mails/messages/changed_team_date.html b/bluebottle/time_based/templates/mails/messages/changed_team_date.html new file mode 100644 index 0000000000..637e17abbd --- /dev/null +++ b/bluebottle/time_based/templates/mails/messages/changed_team_date.html @@ -0,0 +1,39 @@ +{% extends "mails/messages/activity_base.html" %} +{% load i18n %} +{% block message %} +

+{% blocktrans context 'email' %} +Some details of the team activity you are a part of have changed: +{% endblocktrans %} +

+

+{{ team_name }} +

+

+{{ title }} +

+ +{% if duration %} + {% include 'mails/messages/partial/period.html' %} +{% else %} + {% include 'mails/messages/partial/slots.html' %} +{% endif %} +

+

+{% blocktrans context 'email' %} +Please view your team from the activity page to see the changes. +{% endblocktrans %} +

+

+{% endblock %} + +{% block end_message %} +

+ + {% blocktrans context 'email' %} + If you are unable to participate, please withdraw via the activity page so that others can take your place. + {% endblocktrans %} + +

+{% endblock %} + diff --git a/bluebottle/time_based/triggers.py b/bluebottle/time_based/triggers.py index 50ec42be6e..296fbf9044 100644 --- a/bluebottle/time_based/triggers.py +++ b/bluebottle/time_based/triggers.py @@ -43,12 +43,13 @@ ParticipantWithdrewNotification, ParticipantAddedOwnerNotification, TeamParticipantAddedNotification, ParticipantRemovedOwnerNotification, ParticipantJoinedNotification, TeamParticipantJoinedNotification, - ParticipantAppliedNotification, TeamParticipantAppliedNotification, SlotCancelledNotification + ParticipantAppliedNotification, TeamParticipantAppliedNotification, SlotCancelledNotification, + TeamSlotChangedNotification ) from bluebottle.time_based.models import ( DateActivity, PeriodActivity, DateParticipant, PeriodParticipant, TimeContribution, DateActivitySlot, - PeriodActivitySlot, SlotParticipant + PeriodActivitySlot, SlotParticipant, TeamSlot ) from bluebottle.time_based.states import ( TimeBasedStateMachine, DateStateMachine, PeriodStateMachine, ActivitySlotStateMachine, @@ -721,6 +722,37 @@ class DateActivitySlotTriggers(ActivitySlotTriggers): ] +def has_future_date(effect): + """ + team slot has a date set + """ + return effect.instance.start and effect.instance.start > now() + + +@register(TeamSlot) +class TeamSlotTriggers(TriggerManager): + triggers = [ + TransitionTrigger( + ActivitySlotStateMachine.initiate, + effects=[ + NotificationEffect( + TeamSlotChangedNotification, + conditions=[has_future_date] + ) + ] + ), + ModelChangedTrigger( + 'start', + effects=[ + NotificationEffect( + TeamSlotChangedNotification, + conditions=[has_future_date] + ) + ] + ), + ] + + @register(PeriodActivity) class PeriodActivityTriggers(TimeBasedTriggers): triggers = TimeBasedTriggers.triggers + [ From 1d8ea535061f38649b57b842c047013e128a71bf Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Tue, 12 Jul 2022 10:57:07 +0200 Subject: [PATCH 048/112] Add tests --- bluebottle/time_based/states.py | 9 +++- .../time_based/tests/test_notifications.py | 42 ++++++++++++++++- bluebottle/time_based/tests/test_triggers.py | 47 +++++++++++++++++-- bluebottle/time_based/triggers.py | 4 +- 4 files changed, 93 insertions(+), 9 deletions(-) diff --git a/bluebottle/time_based/states.py b/bluebottle/time_based/states.py index d71b886b2e..23bcbbe24c 100644 --- a/bluebottle/time_based/states.py +++ b/bluebottle/time_based/states.py @@ -285,7 +285,14 @@ class PeriodActivitySlotStateMachine(ActivitySlotStateMachine): @register(TeamSlot) class TeamSlotStateMachine(ActivitySlotStateMachine): - pass + initiate = Transition( + EmptyState(), + ActivitySlotStateMachine.open, + name=_('Initiate'), + description=_( + 'The slot was created.' + ), + ) class ParticipantStateMachine(ContributorStateMachine): diff --git a/bluebottle/time_based/tests/test_notifications.py b/bluebottle/time_based/tests/test_notifications.py index e53331dfaf..4b2d050955 100644 --- a/bluebottle/time_based/tests/test_notifications.py +++ b/bluebottle/time_based/tests/test_notifications.py @@ -1,3 +1,7 @@ +from datetime import timedelta + +from django.utils.timezone import now + from bluebottle.activities.messages import ActivityRejectedNotification, ActivityCancelledNotification, \ ActivitySucceededNotification, ActivityRestoredNotification, ActivityExpiredNotification from bluebottle.activities.tests.factories import TeamFactory @@ -7,10 +11,11 @@ ParticipantRemovedNotification, TeamParticipantRemovedNotification, ParticipantFinishedNotification, ParticipantWithdrewNotification, NewParticipantNotification, ParticipantAddedOwnerNotification, ParticipantRemovedOwnerNotification, ParticipantJoinedNotification, ParticipantAppliedNotification, - SlotCancelledNotification, ParticipantAddedNotification, TeamParticipantAddedNotification + SlotCancelledNotification, ParticipantAddedNotification, TeamParticipantAddedNotification, + TeamSlotChangedNotification ) from bluebottle.time_based.tests.factories import DateActivityFactory, DateParticipantFactory, \ - DateActivitySlotFactory, PeriodActivityFactory, PeriodParticipantFactory + DateActivitySlotFactory, PeriodActivityFactory, PeriodParticipantFactory, TeamSlotFactory class DateActivityNotificationTestCase(NotificationTestCase): @@ -257,3 +262,36 @@ def test_new_participant_notification(self): self.assertSubject('A slot for your activity "Save the world!" has been cancelled') self.assertActionLink(self.activity.get_absolute_url()) self.assertActionTitle('Open your activity') + + +class TeamSlotNotificationTestCase(NotificationTestCase): + + def setUp(self): + self.supporter = BlueBottleUserFactory.create( + first_name='Frans', + last_name='Beckenbauer' + ) + self.activity = PeriodActivityFactory.create( + title="Save the world!", + team_activity='teams' + ) + + self.participant = PeriodParticipantFactory.create( + activity=self.activity, + user=self.supporter + ) + + self.obj = TeamSlotFactory.create( + activity=self.activity, + team=self.participant.team, + start=(now() + timedelta(days=3)).replace(hour=20, minute=0, second=0, microsecond=0), + duration=timedelta(hours=1) + ) + + def test_slot_created(self): + self.message_class = TeamSlotChangedNotification + self.create() + self.assertRecipients([self.supporter]) + self.assertSubject('The details of the team activity "Save the world!" have changed') + self.assertActionLink(self.activity.get_absolute_url()) + self.assertActionTitle('View activity') diff --git a/bluebottle/time_based/tests/test_triggers.py b/bluebottle/time_based/tests/test_triggers.py index ab38560984..b2bb90f475 100644 --- a/bluebottle/time_based/tests/test_triggers.py +++ b/bluebottle/time_based/tests/test_triggers.py @@ -1,5 +1,4 @@ -import time -from datetime import timedelta, date +from datetime import timedelta, date, time import mock from django.core import mail @@ -17,12 +16,12 @@ ParticipantJoinedNotification, ParticipantChangedNotification, ParticipantAppliedNotification, ParticipantRemovedNotification, ParticipantRemovedOwnerNotification, NewParticipantNotification, TeamParticipantJoinedNotification, ParticipantAddedNotification, - ParticipantAddedOwnerNotification + ParticipantAddedOwnerNotification, TeamSlotChangedNotification ) from bluebottle.time_based.tests.factories import ( DateActivityFactory, PeriodActivityFactory, DateParticipantFactory, PeriodParticipantFactory, - DateActivitySlotFactory, SlotParticipantFactory + DateActivitySlotFactory, SlotParticipantFactory, TeamSlotFactory ) @@ -2242,3 +2241,43 @@ def test_refill_slot_remove(self): self.test_unfill_slot_remove() self.slot_part.states.accept(save=True) self.assertStatus(self.slot2, 'full') + + +class TeamSlotTriggerTestCase(TriggerTestCase): + + def setUp(self): + super().setUp() + self.user = BlueBottleUserFactory() + self.initiative = InitiativeFactory(owner=self.user) + + self.activity = PeriodActivityFactory.create( + initiative=self.initiative, + team_activity='teams', + status='approved', + review=False) + self.participant = PeriodParticipantFactory.create( + user=self.user, + activity=self.activity + ) + + def assertStatus(self, obj, status): + obj.refresh_from_db() + self.assertEqual(obj.status, status) + + def test_set_date(self): + self.assertTrue(self.participant.team) + start = now() + timedelta(days=4) + self.model = TeamSlotFactory.build( + team=self.participant.team, + activity=self.activity, + start=start, + duration=timedelta(hours=2) + ) + with self.execute(): + self.assertNotificationEffect(TeamSlotChangedNotification) + self.assertEqual(self.model.status, 'open') + + self.model.start = now() + timedelta(days=1) + with self.execute(): + self.assertNotificationEffect(TeamSlotChangedNotification) + self.assertEqual(self.model.status, 'open') diff --git a/bluebottle/time_based/triggers.py b/bluebottle/time_based/triggers.py index 296fbf9044..9699b6d9f8 100644 --- a/bluebottle/time_based/triggers.py +++ b/bluebottle/time_based/triggers.py @@ -54,7 +54,7 @@ from bluebottle.time_based.states import ( TimeBasedStateMachine, DateStateMachine, PeriodStateMachine, ActivitySlotStateMachine, ParticipantStateMachine, TimeContributionStateMachine, SlotParticipantStateMachine, - PeriodParticipantStateMachine + PeriodParticipantStateMachine, TeamSlotStateMachine ) @@ -733,7 +733,7 @@ def has_future_date(effect): class TeamSlotTriggers(TriggerManager): triggers = [ TransitionTrigger( - ActivitySlotStateMachine.initiate, + TeamSlotStateMachine.initiate, effects=[ NotificationEffect( TeamSlotChangedNotification, From 399f2cb522a301bcf1528d2840584307bf7e24ee Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Tue, 12 Jul 2022 13:22:27 +0200 Subject: [PATCH 049/112] Add more triggers --- bluebottle/activities/states.py | 30 +++++++ bluebottle/time_based/messages.py | 45 +++++++++- bluebottle/time_based/periodic_tasks.py | 46 +++++++++- bluebottle/time_based/states.py | 2 +- bluebottle/time_based/tasks.py | 14 ++- .../mails/messages/reminder_team_slot.html | 32 +++++++ .../time_based/tests/test_periodic_tasks.py | 87 ++++++++++++++++++- bluebottle/time_based/triggers.py | 20 ++++- 8 files changed, 264 insertions(+), 12 deletions(-) create mode 100644 bluebottle/time_based/templates/mails/messages/reminder_team_slot.html diff --git a/bluebottle/activities/states.py b/bluebottle/activities/states.py index 94677b6f28..9b32e0330a 100644 --- a/bluebottle/activities/states.py +++ b/bluebottle/activities/states.py @@ -373,6 +373,18 @@ class TeamStateMachine(ModelStateMachine): _('The team is cancelled. Contributors can no longer register') ) + running = State( + _('running'), + 'running', + _('The team is currently running the activity.') + ) + + finished = State( + _('finished'), + 'finished', + _('The team has completed the activity.') + ) + def is_team_captain(self, user): return user == self.instance.owner @@ -444,3 +456,21 @@ def is_activity_owner(self, user): name=_('accept'), description=_('The team is reopened. Contributors can apply again') ) + + start = Transition( + [open, finished], + running, + name=_("Start"), + description=_( + "The slot is currently taking place." + ) + ) + finish = Transition( + [open, running], + finished, + name=_("Finish"), + description=_( + "The slot has ended. " + "Triggered when slot has ended." + ) + ) diff --git a/bluebottle/time_based/messages.py b/bluebottle/time_based/messages.py index 9a58d55b23..466c007b2b 100644 --- a/bluebottle/time_based/messages.py +++ b/bluebottle/time_based/messages.py @@ -54,7 +54,7 @@ def get_context(self, recipient): if isinstance(participant, DateParticipant): slots = [] for slot_participant in participant.slot_participants.filter( - status='registered' + status='registered' ): slots.append(get_slot_info(slot_participant.slot)) @@ -186,6 +186,43 @@ def get_recipients(self): ] +class ReminderTeamSlotNotification(TransitionMessage): + """ + Reminder notification for a team activity slot + """ + subject = pgettext('email', 'The team activity "{title}" will take place in a few days!') + template = 'messages/reminder_team_slot' + send_once = True + + context = { + 'title': 'activity.title', + 'team_name': 'team', + 'start': 'start', + 'duration': 'duration', + 'end': 'end', + } + + def already_send(self, recipient): + return Message.objects.filter( + template=self.get_template(), + recipient=recipient, + content_type=get_content_type_for_model(self.obj), + object_id=self.obj.id + ).count() > 0 + + @property + def action_link(self): + return self.obj.activity.get_absolute_url() + + action_title = pgettext('email', 'View activity') + + def get_recipients(self): + """participants that signed up""" + return [ + participant.user for participant in self.obj.team.accepted_participants + ] + + class ChangedSingleDateNotification(TimeBasedInfoMixin, TransitionMessage): """ Notification when slot details (date, time or location) changed for a single date activity @@ -473,9 +510,9 @@ def get_recipients(self): participant = DateParticipant.objects.get(pk=self.obj.participant.pk) if ( - participant.status == 'withdrawn' or - joined_message.is_delayed or - changed_message.is_delayed or applied_message.is_delayed + participant.status == 'withdrawn' or + joined_message.is_delayed or + changed_message.is_delayed or applied_message.is_delayed ): return [] diff --git a/bluebottle/time_based/periodic_tasks.py b/bluebottle/time_based/periodic_tasks.py index 90e688850f..27088fbe6d 100644 --- a/bluebottle/time_based/periodic_tasks.py +++ b/bluebottle/time_based/periodic_tasks.py @@ -8,12 +8,12 @@ from bluebottle.fsm.periodic_tasks import ModelPeriodicTask from bluebottle.notifications.effects import NotificationEffect from bluebottle.time_based.effects import CreatePeriodTimeContributionEffect -from bluebottle.time_based.messages import ReminderSlotNotification +from bluebottle.time_based.messages import ReminderSlotNotification, ReminderTeamSlotNotification from bluebottle.time_based.models import ( - DateActivity, PeriodActivity, PeriodParticipant, TimeContribution, DateActivitySlot + DateActivity, PeriodActivity, PeriodParticipant, TimeContribution, DateActivitySlot, TeamSlot ) from bluebottle.time_based.states import ( - TimeBasedStateMachine, TimeContributionStateMachine, ActivitySlotStateMachine + TimeBasedStateMachine, TimeContributionStateMachine, ActivitySlotStateMachine, TeamSlotStateMachine ) from bluebottle.time_based.triggers import has_participants, has_no_participants @@ -153,6 +153,40 @@ def __str__(self): return str(_("Send a reminder five days before the activity slot.")) +class TeamSlotReminderTask(ModelPeriodicTask): + + def get_queryset(self): + return TeamSlot.objects.filter( + start__lte=timezone.now() + timedelta(days=5), + start__gt=timezone.now(), + status__in=['open', 'full'], + activity__status__in=['open', 'full'] + ) + + effects = [ + NotificationEffect( + ReminderTeamSlotNotification, + ), + ] + + def __str__(self): + return str(_("Send a reminder five days before the team activity slot.")) + + +class TeamSlotStartedTask(SlotStartedTask): + + effects = [ + TransitionEffect(TeamSlotStateMachine.start), + ] + + +class TeamSlotFinishedTask(SlotFinishedTask): + + effects = [ + TransitionEffect(TeamSlotStateMachine.finish), + ] + + DateActivity.periodic_tasks = [ TimeBasedActivityRegistrationDeadlinePassedTask, ] @@ -163,6 +197,12 @@ def __str__(self): SlotFinishedTask, ] +TeamSlot.periodic_tasks = [ + TeamSlotReminderTask, + TeamSlotStartedTask, + TeamSlotFinishedTask, +] + PeriodActivity.periodic_tasks = [ PeriodActivityFinishedTask, TimeBasedActivityRegistrationDeadlinePassedTask diff --git a/bluebottle/time_based/states.py b/bluebottle/time_based/states.py index 23bcbbe24c..0cfcdc4d90 100644 --- a/bluebottle/time_based/states.py +++ b/bluebottle/time_based/states.py @@ -245,7 +245,7 @@ class ActivitySlotStateMachine(ModelStateMachine): ) start = Transition( - [open, finished], + [open, finished, full], running, name=_("Start"), description=_( diff --git a/bluebottle/time_based/tasks.py b/bluebottle/time_based/tasks.py index e03e3e3fbf..6fd3bc40c6 100644 --- a/bluebottle/time_based/tasks.py +++ b/bluebottle/time_based/tasks.py @@ -7,7 +7,7 @@ from bluebottle.clients.utils import LocalTenant from bluebottle.time_based.models import ( DateActivity, PeriodActivity, - DateParticipant, PeriodParticipant, TimeContribution, DateActivitySlot + DateParticipant, PeriodParticipant, TimeContribution, DateActivitySlot, TeamSlot ) logger = logging.getLogger('bluebottle') @@ -39,6 +39,18 @@ def with_a_deadline_tasks(): task.execute() +@periodic_task( + run_every=(crontab(minute='*/15')), + name="team_slot_tasks", + ignore_result=True +) +def team_slot_tasks(): + for tenant in Client.objects.all(): + with LocalTenant(tenant, clear_tenant=True): + for task in TeamSlot.get_periodic_tasks(): + task.execute() + + @periodic_task( run_every=(crontab(minute='*/15')), name="date_participant_tasks", diff --git a/bluebottle/time_based/templates/mails/messages/reminder_team_slot.html b/bluebottle/time_based/templates/mails/messages/reminder_team_slot.html new file mode 100644 index 0000000000..47a4c17866 --- /dev/null +++ b/bluebottle/time_based/templates/mails/messages/reminder_team_slot.html @@ -0,0 +1,32 @@ +{% extends "mails/messages/activity_base.html" %} +{% load i18n %} +{% block message %} +

+{% blocktrans context 'email' %} +The team activity is just a few days away! +{% endblocktrans %} +

+

+{{ team_name }} +

+

+{{ title }} +

+

+{% include 'mails/messages/partial/period.html' %} +

+ +

+{% blocktrans context 'email' %} +Please view your team from the activity page. +{% endblocktrans %} +

+

+ +{% blocktrans context 'email' %} +If you are unable to participate, please withdraw via the activity page so that others can take your place. +{% endblocktrans %} + +

+ +{% endblock %} diff --git a/bluebottle/time_based/tests/test_periodic_tasks.py b/bluebottle/time_based/tests/test_periodic_tasks.py index 7a81ac8897..4a4bf741fd 100644 --- a/bluebottle/time_based/tests/test_periodic_tasks.py +++ b/bluebottle/time_based/tests/test_periodic_tasks.py @@ -21,12 +21,12 @@ from bluebottle.test.utils import BluebottleTestCase from bluebottle.time_based.tasks import ( date_activity_tasks, with_a_deadline_tasks, - period_participant_tasks, time_contribution_tasks + period_participant_tasks, time_contribution_tasks, team_slot_tasks ) from bluebottle.time_based.tests.factories import ( DateActivityFactory, PeriodActivityFactory, DateParticipantFactory, PeriodParticipantFactory, DateActivitySlotFactory, - SlotParticipantFactory + SlotParticipantFactory, TeamSlotFactory ) @@ -843,3 +843,86 @@ def test_contribution_value_is_succeeded(self): len(self.participant.contributions.filter(status='new')), 1 ) + + +class TeamSlotPeriodicTasksTest(BluebottleTestCase): + + def setUp(self): + self.activity = PeriodActivityFactory.create( + team_activity='teams', + status='open' + ) + self.participant = PeriodParticipantFactory.create( + activity=self.activity + ) + self.slot = TeamSlotFactory.create( + activity=self.activity, + team=self.participant.team, + start=datetime.combine((now() + timedelta(days=10)).date(), time(11, 30, tzinfo=UTC)), + duration=timedelta(hours=3) + ) + + def run_task(self, when): + with mock.patch.object(timezone, 'now', return_value=when): + with mock.patch('bluebottle.time_based.periodic_tasks.date') as mock_date: + mock_date.today.return_value = when.date() + mock_date.side_effect = lambda *args, **kw: date(*args, **kw) + team_slot_tasks() + + @property + def nigh(self): + return timezone.get_current_timezone().localize( + datetime( + self.slot.start.year, + self.slot.start.month, + self.slot.start.day + ) - timedelta(days=4) + ) + + @property + def current(self): + return self.slot.start + timedelta(hours=1) + + @property + def after(self): + return self.slot.start + timedelta(days=2) + + @property + def before(self): + return timezone.get_current_timezone().localize( + datetime( + self.activity.registration_deadline.year, + self.activity.registration_deadline.month, + self.activity.registration_deadline.day + ) - timedelta(days=1) + ) + + def assertStatus(self, obj, status): + obj.refresh_from_db() + return self.assertEqual(obj.status, status) + + def test_reminder_team_slot(self): + mail.outbox = [] + self.run_task(self.nigh) + self.assertEqual(len(mail.outbox), 1) + self.assertEqual( + mail.outbox[0].subject, + 'The team activity "{}" will take place in a few days!'.format(self.activity.title) + ) + self.assertTrue('The team activity is just a few days away!' in mail.outbox[0].body) + + mail.outbox = [] + self.run_task(self.nigh) + self.assertEqual(len(mail.outbox), 0, "Reminder mail should not be send again.") + + def test_start_team_slot(self): + mail.outbox = [] + self.run_task(self.current) + self.assertStatus(self.slot, 'running') + self.assertStatus(self.slot.team, 'running') + + def test_finish_team_slot(self): + mail.outbox = [] + self.run_task(self.after) + self.assertStatus(self.slot, 'finished') + self.assertStatus(self.slot.team, 'finished') diff --git a/bluebottle/time_based/triggers.py b/bluebottle/time_based/triggers.py index 9699b6d9f8..31002e9375 100644 --- a/bluebottle/time_based/triggers.py +++ b/bluebottle/time_based/triggers.py @@ -741,13 +741,31 @@ class TeamSlotTriggers(TriggerManager): ) ] ), + TransitionTrigger( + TeamSlotStateMachine.start, + effects=[ + RelatedTransitionEffect( + 'team', + TeamStateMachine.start + ) + ] + ), + TransitionTrigger( + TeamSlotStateMachine.finish, + effects=[ + RelatedTransitionEffect( + 'team', + TeamStateMachine.finish + ) + ] + ), ModelChangedTrigger( 'start', effects=[ NotificationEffect( TeamSlotChangedNotification, conditions=[has_future_date] - ) + ), ] ), ] From 37abd2a0ca7e1073642bf2cf6c2a8b3ef9f6117d Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Tue, 12 Jul 2022 13:32:45 +0200 Subject: [PATCH 050/112] Fix test import --- bluebottle/time_based/tests/test_triggers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bluebottle/time_based/tests/test_triggers.py b/bluebottle/time_based/tests/test_triggers.py index b2bb90f475..d068a45f23 100644 --- a/bluebottle/time_based/tests/test_triggers.py +++ b/bluebottle/time_based/tests/test_triggers.py @@ -1,4 +1,5 @@ -from datetime import timedelta, date, time +import time +from datetime import timedelta, date import mock from django.core import mail From 25ab335fd75907af3ca4946bf2e83e49e961c874 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Tue, 12 Jul 2022 14:16:35 +0200 Subject: [PATCH 051/112] Change team/slot status when date changes --- bluebottle/activities/states.py | 2 +- bluebottle/test/test_runner.py | 1 + bluebottle/time_based/tests/test_triggers.py | 21 +++++++++++++++ bluebottle/time_based/triggers.py | 28 ++++++++++++++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/bluebottle/activities/states.py b/bluebottle/activities/states.py index 9b32e0330a..c8a9462f79 100644 --- a/bluebottle/activities/states.py +++ b/bluebottle/activities/states.py @@ -449,7 +449,7 @@ def is_activity_owner(self, user): ) reopen = Transition( - cancelled, + [cancelled, running, finished], open, automatic=False, permission=is_activity_owner, diff --git a/bluebottle/test/test_runner.py b/bluebottle/test/test_runner.py index 555407fe06..02ad07d88e 100644 --- a/bluebottle/test/test_runner.py +++ b/bluebottle/test/test_runner.py @@ -11,6 +11,7 @@ class MultiTenantRunner(DiscoverSlowestTestsRunner, InitProjectDataMixin): def setup_databases(self, *args, **kwargs): + parallel = self.parallel self.parallel = 0 result = super(MultiTenantRunner, self).setup_databases(**kwargs) diff --git a/bluebottle/time_based/tests/test_triggers.py b/bluebottle/time_based/tests/test_triggers.py index d068a45f23..ec96696318 100644 --- a/bluebottle/time_based/tests/test_triggers.py +++ b/bluebottle/time_based/tests/test_triggers.py @@ -2282,3 +2282,24 @@ def test_set_date(self): with self.execute(): self.assertNotificationEffect(TeamSlotChangedNotification) self.assertEqual(self.model.status, 'open') + + def test_change_date(self): + self.assertTrue(self.participant.team) + start = now() + timedelta(days=4) + self.model = TeamSlotFactory.build( + team=self.participant.team, + activity=self.activity, + start=start, + duration=timedelta(hours=2) + ) + self.model.start = now() - timedelta(days=1) + with self.execute(): + self.assertNoNotificationEffect(TeamSlotChangedNotification) + self.assertEqual(self.model.status, 'finished') + self.assertEqual(self.model.team.status, 'finished') + + self.model.start = now() + timedelta(days=3) + with self.execute(): + self.assertNotificationEffect(TeamSlotChangedNotification) + self.assertEqual(self.model.status, 'open') + self.assertEqual(self.model.team.status, 'open') diff --git a/bluebottle/time_based/triggers.py b/bluebottle/time_based/triggers.py index 31002e9375..7152c25305 100644 --- a/bluebottle/time_based/triggers.py +++ b/bluebottle/time_based/triggers.py @@ -759,6 +759,15 @@ class TeamSlotTriggers(TriggerManager): ) ] ), + TransitionTrigger( + TeamSlotStateMachine.reschedule, + effects=[ + RelatedTransitionEffect( + 'team', + TeamStateMachine.reopen + ) + ] + ), ModelChangedTrigger( 'start', effects=[ @@ -766,6 +775,25 @@ class TeamSlotTriggers(TriggerManager): TeamSlotChangedNotification, conditions=[has_future_date] ), + TransitionEffect( + TeamSlotStateMachine.reschedule, + conditions=[ + slot_is_not_started + ] + ), + TransitionEffect( + TeamSlotStateMachine.finish, + conditions=[ + slot_is_finished + ] + ), + TransitionEffect( + TeamSlotStateMachine.start, + conditions=[ + slot_is_not_finished, + slot_is_started + ] + ), ] ), ] From a8cc32f90c080e3afa45e9cf943aca0bcb209cbe Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Tue, 12 Jul 2022 14:23:52 +0200 Subject: [PATCH 052/112] Fix tests --- bluebottle/activities/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bluebottle/activities/utils.py b/bluebottle/activities/utils.py index ffed7fbdc4..4e0424b31f 100644 --- a/bluebottle/activities/utils.py +++ b/bluebottle/activities/utils.py @@ -55,7 +55,7 @@ class TeamSerializer(ModelSerializer): class Meta(object): model = Team - fields = ('owner', 'slot', 'members') + fields = ('owner', 'activity', 'slot', 'members') meta_fields = ( 'status', 'transitions', From a592dfd1cbf9bbde67478a5c81eea01a32732cef Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 13 Jul 2022 08:46:31 +0200 Subject: [PATCH 053/112] Add is_complete prop to team --- bluebottle/activities/admin.py | 2 +- bluebottle/time_based/admin.py | 3 ++- bluebottle/time_based/models.py | 4 ++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/bluebottle/activities/admin.py b/bluebottle/activities/admin.py index 36be9f87ce..d3b5b92a5f 100644 --- a/bluebottle/activities/admin.py +++ b/bluebottle/activities/admin.py @@ -700,7 +700,7 @@ class PaginationFormSet(PaginationFormSetBase, formset_class): class TeamAdmin(StateMachineAdmin): raw_id_fields = ['owner', 'activity'] readonly_fields = ['created', 'activity_link', 'invite_link'] - fields = ['activity', 'invite_link', 'created', 'owner', 'states'] + fields = ['activity', 'invite_link', 'created', 'owner', 'status', 'states'] superadmin_fields = ['force_status'] list_display = ['__str__', 'activity_link', 'status'] diff --git a/bluebottle/time_based/admin.py b/bluebottle/time_based/admin.py index f474f1c47a..a7bb93e346 100644 --- a/bluebottle/time_based/admin.py +++ b/bluebottle/time_based/admin.py @@ -272,11 +272,12 @@ class TeamSlotInline(admin.StackedInline): } ordering = ['-start'] - readonly_fields = ['link', 'timezone', ] + readonly_fields = ['link', 'timezone', 'status'] fields = [ 'start', 'duration', 'timezone', + 'status', ] def timezone(self, obj): diff --git a/bluebottle/time_based/models.py b/bluebottle/time_based/models.py index d13d5eb0a7..e862b558fb 100644 --- a/bluebottle/time_based/models.py +++ b/bluebottle/time_based/models.py @@ -531,6 +531,10 @@ def required_fields(self): fields.append('location') return fields + @property + def is_complete(self): + return self.start and self.duration + class Meta: verbose_name = _('team slot') verbose_name_plural = _('team slots') From 936ac36f708b6586d021989fa11bfa5d66064d4c Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 13 Jul 2022 09:28:45 +0200 Subject: [PATCH 054/112] Make team changed trigger less greedy --- bluebottle/activities/messages.py | 2 +- bluebottle/time_based/triggers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bluebottle/activities/messages.py b/bluebottle/activities/messages.py index e44f655426..26a6ec028c 100644 --- a/bluebottle/activities/messages.py +++ b/bluebottle/activities/messages.py @@ -286,7 +286,7 @@ def action_link(self): action_title = pgettext('email', 'View activity') def get_recipients(self): - """acitvity mananager""" + """activity mananager""" return [self.obj.activity.owner] diff --git a/bluebottle/time_based/triggers.py b/bluebottle/time_based/triggers.py index 7152c25305..023fc6e171 100644 --- a/bluebottle/time_based/triggers.py +++ b/bluebottle/time_based/triggers.py @@ -1217,7 +1217,7 @@ class ParticipantTriggers(ContributorTriggers): ), ModelChangedTrigger( - 'team', + 'team_id', effects=[ NotificationEffect( TeamParticipantAddedNotification, From f4d6e9e472a484a7c7fc7483893a577c0bdaa7bd Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 13 Jul 2022 12:57:36 +0200 Subject: [PATCH 055/112] No activity on team member serializer --- bluebottle/activities/utils.py | 2 +- bluebottle/activities/views.py | 7 +------ bluebottle/time_based/serializers.py | 2 -- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/bluebottle/activities/utils.py b/bluebottle/activities/utils.py index 4e0424b31f..6e308f0726 100644 --- a/bluebottle/activities/utils.py +++ b/bluebottle/activities/utils.py @@ -68,7 +68,7 @@ class JSONAPIMeta(object): included_resources = [ 'owner', 'slot', - 'slot.location' + 'slot.location', ] resource_name = 'activities/teams' diff --git a/bluebottle/activities/views.py b/bluebottle/activities/views.py index 5a974c3c88..2443ac66b0 100644 --- a/bluebottle/activities/views.py +++ b/bluebottle/activities/views.py @@ -242,17 +242,12 @@ class TeamMembersList(JsonApiViewMixin, ListAPIView): def get_queryset(self): if self.request.user.is_authenticated: - queryset = self.queryset.order_by('-current_user', '-id').filter( + queryset = self.queryset.order_by('-id').filter( Q(user=self.request.user) | Q(team__owner=self.request.user) | Q(team__activity__owner=self.request.user) | Q(team__activity__initiative__activity_managers=self.request.user) | Q(status='accepted') - ).annotate( - current_user=ExpressionWrapper( - Q(user=self.request.user), - output_field=BooleanField() - ) ) else: queryset = self.queryset.filter( diff --git a/bluebottle/time_based/serializers.py b/bluebottle/time_based/serializers.py index bc25e84a9e..eb59c3869e 100644 --- a/bluebottle/time_based/serializers.py +++ b/bluebottle/time_based/serializers.py @@ -726,7 +726,6 @@ class Meta(BaseContributorSerializer.Meta): 'user', 'status', 'team', - 'activity', 'accepted_invite', 'invite', 'team' @@ -746,7 +745,6 @@ class JSONAPIMeta(BaseContributorSerializer.JSONAPIMeta): 'document': 'bluebottle.time_based.serializers.PeriodParticipantDocumentSerializer', 'contributions': 'bluebottle.time_based.serializers.TimeContributionSerializer', 'team.slot': 'bluebottle.time_based.serializers.TeamSlotSerializer', - 'activity': 'bluebottle.time_based.serializers.PeriodActivitySerializer', } ) From f5a7fbc902162f108bfc24eec9aa9108c0880ed0 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 13 Jul 2022 14:33:50 +0200 Subject: [PATCH 056/112] Fix tests --- bluebottle/activities/tests/test_api.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/bluebottle/activities/tests/test_api.py b/bluebottle/activities/tests/test_api.py index ce50d17a1e..4fbff17045 100644 --- a/bluebottle/activities/tests/test_api.py +++ b/bluebottle/activities/tests/test_api.py @@ -2139,7 +2139,6 @@ def test_get_activity_owner(self): self.assertStatus(status.HTTP_200_OK) self.assertTotal(len(self.accepted_members) + len(self.withdrawn_members) + 1) - self.assertRelationship('activity', [self.activity]) self.assertRelationship('user') self.assertAttribute('status') @@ -2151,7 +2150,6 @@ def test_get_team_captain(self): self.assertStatus(status.HTTP_200_OK) self.assertTotal(len(self.accepted_members) + len(self.withdrawn_members) + 1) self.assertObjectList(self.accepted_members + self.withdrawn_members + [self.team_captain]) - self.assertRelationship('activity', [self.activity]) self.assertRelationship('user') self.assertAttribute('status') @@ -2169,7 +2167,6 @@ def test_get_team_member(self): self.assertTotal(len(self.accepted_members) + 1) self.assertObjectList(self.accepted_members + [self.team_captain]) - self.assertRelationship('activity', [self.activity]) self.assertRelationship('user') self.assertAttribute('status') @@ -2182,7 +2179,6 @@ def test_get_other_user(self): self.assertTotal(len(self.accepted_members) + 1) self.assertObjectList(self.accepted_members + [self.team_captain]) - self.assertRelationship('activity', [self.activity]) self.assertRelationship('user') self.assertAttribute('status') From 13458b8d57826db6c9e3be57763fa4a4e527513a Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Thu, 14 Jul 2022 10:00:51 +0200 Subject: [PATCH 057/112] Fix some team related serializers --- bluebottle/activities/utils.py | 2 +- bluebottle/time_based/admin.py | 3 ++ bluebottle/time_based/serializers.py | 55 +++++++++++++++------------- 3 files changed, 34 insertions(+), 26 deletions(-) diff --git a/bluebottle/activities/utils.py b/bluebottle/activities/utils.py index 6e308f0726..295701fcd6 100644 --- a/bluebottle/activities/utils.py +++ b/bluebottle/activities/utils.py @@ -214,7 +214,7 @@ def get_contributor_count(self, instance): ).count() def get_team_count(self, instance): - return instance.teams.filter(status='open').count() + return instance.teams.filter(status__in=['open', 'finished']).count() class Meta(object): model = Activity diff --git a/bluebottle/time_based/admin.py b/bluebottle/time_based/admin.py index f474f1c47a..1ca793690a 100644 --- a/bluebottle/time_based/admin.py +++ b/bluebottle/time_based/admin.py @@ -260,6 +260,7 @@ class TeamSlotInline(admin.StackedInline): form = TeamSlotForm verbose_name = _('Time slot') verbose_name_plural = _('Time slot') + raw_id_fields = ('location', ) formfield_overrides = { models.DurationField: { @@ -277,6 +278,8 @@ class TeamSlotInline(admin.StackedInline): 'start', 'duration', 'timezone', + 'location', + 'is_online' ] def timezone(self, obj): diff --git a/bluebottle/time_based/serializers.py b/bluebottle/time_based/serializers.py index eb59c3869e..132eaa7c5a 100644 --- a/bluebottle/time_based/serializers.py +++ b/bluebottle/time_based/serializers.py @@ -39,7 +39,8 @@ def __init__(self, many=True, read_only=True, *args, **kwargs): super().__init__(Team, many=many, read_only=read_only, *args, **kwargs) def get_url(self, name, view_name, kwargs, request): - return f"{self.reverse('team-list')}?activity_id={kwargs['pk']}" + if self.parent.instance.team_activity == 'teams': + return f"{self.reverse('team-list')}?filter[activity_id]={kwargs['pk']}" class TimeBasedBaseSerializer(BaseActivitySerializer): @@ -193,6 +194,16 @@ class TeamSlotSerializer(ActivitySlotSerializer): errors = ValidationErrorsField() required = RequiredErrorsField() activity = ResourceRelatedField(read_only=True) + links = serializers.SerializerMethodField() + + def get_links(self, instance): + if instance.start and instance.duration: + return { + 'ical': reverse_signed('slot-ical', args=(instance.pk, )), + 'google': instance.google_calendar_link, + } + else: + return {} class Meta(ActivitySlotSerializer.Meta): model = TeamSlot @@ -200,7 +211,8 @@ class Meta(ActivitySlotSerializer.Meta): 'team', 'start', 'duration', - 'location' + 'location', + 'links' ) class JSONAPIMeta(object): @@ -424,6 +436,16 @@ class JSONAPIMeta(TimeBasedBaseSerializer.JSONAPIMeta): ) +class ParticipantsField(HyperlinkedRelatedField): + def __init__(self, many=True, read_only=True, *args, **kwargs): + super().__init__(Team, many=many, read_only=read_only, *args, **kwargs) + + def get_url(self, name, view_name, kwargs, request): + + if self.parent.instance.team_activity != 'teams': + return f"{view_name}?activity_id={kwargs['pk']}" + + class PeriodActivitySerializer(TimeBasedBaseSerializer): permissions = ResourcePermissionField('period-detail', view_args=('pk',)) @@ -433,28 +455,7 @@ class PeriodActivitySerializer(TimeBasedBaseSerializer): source='get_my_contributor' ) - contributors = SerializerMethodHyperlinkedRelatedField( - model=PeriodParticipant, - many=True, - related_link_view_name='period-participants', - related_link_url_kwarg='activity_id' - - ) - - def get_contributors(self, instance): - user = self.context['request'].user - return [ - contributor for contributor in instance.contributors.all() if ( - isinstance(contributor, PeriodParticipant) and ( - contributor.status in [ - ParticipantStateMachine.new.value, - ParticipantStateMachine.accepted.value, - ParticipantStateMachine.succeeded.value - ] or - user in (instance.owner, instance.initiative.owner, contributor.user) - ) - ) - ] + contributors = ParticipantsField(related_link_view_name='period-participant') participants_export_url = PrivateFileSerializer( 'period-participant-export', @@ -488,13 +489,17 @@ class JSONAPIMeta(TimeBasedBaseSerializer.JSONAPIMeta): resource_name = 'activities/time-based/periods' included_resources = TimeBasedBaseSerializer.JSONAPIMeta.included_resources + [ 'location', + 'my_contributor.team', + 'my_contributor.team.slot', ] included_serializers = dict( TimeBasedBaseSerializer.included_serializers, **{ 'location': 'bluebottle.geo.serializers.GeolocationSerializer', - 'my_contributor': 'bluebottle.time_based.serializers.PeriodParticipantSerializer' + 'my_contributor': 'bluebottle.time_based.serializers.PeriodParticipantSerializer', + 'my_contributor.team': 'bluebottle.activities.utils.TeamSerializer', + 'my_contributor.team.slot': 'bluebottle.time_based.serializers.TeamSlotSerializer', } ) From 0741dc5102ce1429f79d24bc0b16feb10552d95c Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Fri, 15 Jul 2022 10:52:08 +0200 Subject: [PATCH 058/112] Fix some views --- bluebottle/activities/views.py | 7 +++- bluebottle/time_based/admin.py | 2 +- bluebottle/time_based/serializers.py | 7 ++-- bluebottle/time_based/tests/test_api.py | 6 ++- bluebottle/time_based/views.py | 50 ++++++++++++++++++++++++- 5 files changed, 64 insertions(+), 8 deletions(-) diff --git a/bluebottle/activities/views.py b/bluebottle/activities/views.py index 2443ac66b0..5a974c3c88 100644 --- a/bluebottle/activities/views.py +++ b/bluebottle/activities/views.py @@ -242,12 +242,17 @@ class TeamMembersList(JsonApiViewMixin, ListAPIView): def get_queryset(self): if self.request.user.is_authenticated: - queryset = self.queryset.order_by('-id').filter( + queryset = self.queryset.order_by('-current_user', '-id').filter( Q(user=self.request.user) | Q(team__owner=self.request.user) | Q(team__activity__owner=self.request.user) | Q(team__activity__initiative__activity_managers=self.request.user) | Q(status='accepted') + ).annotate( + current_user=ExpressionWrapper( + Q(user=self.request.user), + output_field=BooleanField() + ) ) else: queryset = self.queryset.filter( diff --git a/bluebottle/time_based/admin.py b/bluebottle/time_based/admin.py index a20e74bc0d..b0c6b87fdd 100644 --- a/bluebottle/time_based/admin.py +++ b/bluebottle/time_based/admin.py @@ -279,7 +279,7 @@ class TeamSlotInline(admin.StackedInline): 'duration', 'timezone', 'location', - 'is_online' + 'is_online', 'status', ] diff --git a/bluebottle/time_based/serializers.py b/bluebottle/time_based/serializers.py index 132eaa7c5a..44987baeb3 100644 --- a/bluebottle/time_based/serializers.py +++ b/bluebottle/time_based/serializers.py @@ -2,6 +2,7 @@ import dateutil from django.db.models.functions import Trunc +from django.urls import reverse from django.utils.timezone import now, get_current_timezone from rest_framework import serializers from rest_framework.exceptions import ValidationError @@ -441,9 +442,9 @@ def __init__(self, many=True, read_only=True, *args, **kwargs): super().__init__(Team, many=many, read_only=read_only, *args, **kwargs) def get_url(self, name, view_name, kwargs, request): - if self.parent.instance.team_activity != 'teams': - return f"{view_name}?activity_id={kwargs['pk']}" + url = reverse(self.related_link_view_name) + return f"{url}?activity_id={kwargs['pk']}" class PeriodActivitySerializer(TimeBasedBaseSerializer): @@ -455,7 +456,7 @@ class PeriodActivitySerializer(TimeBasedBaseSerializer): source='get_my_contributor' ) - contributors = ParticipantsField(related_link_view_name='period-participant') + contributors = ParticipantsField(related_link_view_name='period-participant-list') participants_export_url = PrivateFileSerializer( 'period-participant-export', diff --git a/bluebottle/time_based/tests/test_api.py b/bluebottle/time_based/tests/test_api.py index 94563678bf..a819353b68 100644 --- a/bluebottle/time_based/tests/test_api.py +++ b/bluebottle/time_based/tests/test_api.py @@ -548,7 +548,6 @@ def test_get_contributors(self): self.response_data['relationships']['contributors']['links']['related'], user=self.activity.owner ) - self.response_data = response.json()['data'] self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -944,6 +943,9 @@ def setUp(self): }) def test_get_open(self): + self.activity.team_activity = 'teams' + self.activity.save() + super().test_get_open() self.assertFalse( @@ -953,7 +955,7 @@ def test_get_open(self): self.assertEqual( self.data['relationships']['teams']['links']['self'], - f"{reverse('team-list')}?activity_id={self.activity.pk}" + f"{reverse('team-list')}?filter[activity_id]={self.activity.pk}" ) def test_get_open_with_participant(self): diff --git a/bluebottle/time_based/views.py b/bluebottle/time_based/views.py index 0107fe5917..2500b0295c 100644 --- a/bluebottle/time_based/views.py +++ b/bluebottle/time_based/views.py @@ -2,12 +2,13 @@ import dateutil import icalendar -from django.db.models import Q +from django.db.models import Q, ExpressionWrapper, BooleanField from django.http import HttpResponse from django.utils.timezone import utc, get_current_timezone from django.utils.translation import gettext_lazy as _ from rest_framework.exceptions import ValidationError +from bluebottle.activities.models import Activity from bluebottle.activities.permissions import ( ActivityOwnerPermission, ActivityTypePermission, ActivityStatusPermission, ContributorPermission, ContributionPermission, DeleteActivityPermission, @@ -323,6 +324,53 @@ def perform_create(self, serializer): serializer.save(user=self.request.user) + def get_serializer_context(self, **kwargs): + context = super().get_serializer_context(**kwargs) + context['display_member_names'] = MemberPlatformSettings.objects.get().display_member_names + + if 'activity_id' in kwargs: + activity = Activity.objects.get(pk=self.kwargs['activity_id']) + context['owners'] = [activity.owner] + list(activity.initiative.activity_managers.all()) + + if self.request.user and self.request.user.is_authenticated and ( + self.request.user in context['owners'] or + self.request.user.is_staff or + self.request.user.is_superuser + ): + context['display_member_names'] = 'full_name' + else: + if self.request.user and self.request.user.is_authenticated and ( + self.request.user.is_staff or + self.request.user.is_superuser + ): + context['display_member_names'] = 'full_name' + + return context + + def get_queryset(self): + if self.request.user.is_authenticated: + queryset = self.queryset.filter( + Q(user=self.request.user) | + Q(activity__owner=self.request.user) | + Q(activity__initiative__activity_manager=self.request.user) | + Q(status__in=('accepted', 'succeeded',)) + ).annotate( + current_user=ExpressionWrapper( + Q(user=self.request.user if self.request.user.is_authenticated else None), + output_field=BooleanField() + ) + ).order_by('-current_user', '-id') + else: + queryset = self.queryset.filter( + status__in=('accepted', 'succeeded',) + ) + + if 'activity_id' in self.kwargs: + queryset = queryset.filter( + activity_id=self.kwargs['activity_id'] + ) + return queryset + class DateParticipantList(ParticipantList): queryset = DateParticipant.objects.all() From 9db59572fc2876ebd8346dbad757df8d84a9cd29 Mon Sep 17 00:00:00 2001 From: Ernst Odolphi Date: Mon, 18 Jul 2022 15:33:07 +0200 Subject: [PATCH 059/112] Add Teams sheet when activity is a team activity. Also make it possible to override the XlS generation method in ExportView --- bluebottle/activities/models.py | 4 +++ bluebottle/time_based/tests/test_api.py | 44 +++++++++++++++++++++++-- bluebottle/time_based/views.py | 23 +++++++++++++ bluebottle/utils/views.py | 14 +++++--- 4 files changed, 77 insertions(+), 8 deletions(-) diff --git a/bluebottle/activities/models.py b/bluebottle/activities/models.py index 3eaf7dd691..82170c29da 100644 --- a/bluebottle/activities/models.py +++ b/bluebottle/activities/models.py @@ -294,6 +294,10 @@ class Team(TriggerMixin, models.Model): def accepted_participants(self): return self.members.filter(status='accepted') + @property + def accepted_participants_count(self): + return len(self.accepted_participants) + class Meta(object): ordering = ('-created',) verbose_name = _("Team") diff --git a/bluebottle/time_based/tests/test_api.py b/bluebottle/time_based/tests/test_api.py index a819353b68..7bb05549d5 100644 --- a/bluebottle/time_based/tests/test_api.py +++ b/bluebottle/time_based/tests/test_api.py @@ -426,7 +426,10 @@ def test_get_owner_export_enabled(self): export_url = data['attributes']['participants-export-url']['url'] export_response = self.client.get(export_url) - sheet = load_workbook(filename=BytesIO(export_response.content)).get_active_sheet() + workbook = load_workbook(filename=BytesIO(export_response.content)) + self.assertEqual(len(workbook.worksheets), 1) + + sheet = workbook.get_active_sheet() if isinstance(self.activity, PeriodActivity): self.assertEqual( @@ -1069,18 +1072,53 @@ def test_get_owner_export_teams_enabled(self): self.participant_factory.create_batch( 3, activity=self.activity, accepted_invite=team_captain.invite ) + team_captain.team.slot = TeamSlotFactory.create( + team=team_captain.team, activity=self.activity + ) + + # create another team + other_team_captain = self.participant_factory.create(activity=self.activity) response = self.client.get(self.url, user=self.activity.owner) self.assertEqual(response.status_code, status.HTTP_200_OK) data = response.json()['data'] export_url = data['attributes']['participants-export-url']['url'] export_response = self.client.get(export_url) - sheet = load_workbook(filename=BytesIO(export_response.content)).get_active_sheet() + workbook = load_workbook(filename=BytesIO(export_response.content)) + + self.assertEqual(len(workbook.worksheets), 2) + + sheet = workbook.worksheets[0] self.assertEqual( - tuple(sheet.values)[0], + tuple(sheet)[0], ('Email', 'Name', 'Motivation', 'Registration Date', 'Status', 'Team', 'Team Captain') ) + teams_sheet = workbook.worksheets[1] + + self.assertEqual( + tuple(teams_sheet.values)[0], + ('Name', 'Owner', 'ID', 'Status', '# Accepted Participants', 'Start', 'duration') + ) + self.assertEqual( + tuple(teams_sheet.values)[1], + ( + other_team_captain.team.name, other_team_captain.user.full_name, + other_team_captain.team.pk, other_team_captain.team.status, + 1, None, None + ) + ) + self.assertEqual( + tuple(teams_sheet.values)[2], + ( + team_captain.team.name, team_captain.user.full_name, + team_captain.team.pk, team_captain.team.status, + team_captain.team.accepted_participants_count, + team_captain.team.slot.start.strftime('%d-%m-%y %H:%M'), + team_captain.team.slot.duration.seconds / (60 * 60 * 24), + ) + ) + wrong_signature_response = self.client.get(export_url + '111') self.assertEqual( wrong_signature_response.status_code, 404 diff --git a/bluebottle/time_based/views.py b/bluebottle/time_based/views.py index 2500b0295c..d942d47b31 100644 --- a/bluebottle/time_based/views.py +++ b/bluebottle/time_based/views.py @@ -650,6 +650,29 @@ def get_instances(self): PeriodParticipant ).prefetch_related('user__segments') + def write_data(self, workbook): + """ Create extra tab with team info""" + super().write_data(workbook) + if self.get_object().team_activity == 'teams': + worksheet = workbook.add_worksheet('Teams') + + fields = [ + ('name', 'Name'), + ('owner__full_name', 'Owner'), + ('id', 'ID'), + ('status', 'Status'), + ('accepted_participants_count', '# Accepted Participants'), + ('slot__start', 'Start'), + ('slot__duration', 'duration'), + ] + + worksheet.write_row(0, 0, [field[1] for field in fields]) + + for index, team in enumerate(self.get_object().teams.all()): + row = [prep_field(self.request, team, field[0]) for field in fields] + + worksheet.write_row(index + 1, 0, row) + class SkillPagination(JsonApiPagination): page_size = 100 diff --git a/bluebottle/utils/views.py b/bluebottle/utils/views.py index df21048f8f..a0178abe07 100644 --- a/bluebottle/utils/views.py +++ b/bluebottle/utils/views.py @@ -363,21 +363,25 @@ def get_data(self): def get_instances(self): raise NotImplementedError() - def get(self, request, *args, **kwargs): - output = BytesIO() - - workbook = xlsxwriter.Workbook(output, {'remove_timezone': True}) - worksheet = workbook.add_worksheet() + def write_data(self, workbook): + worksheet = workbook.add_worksheet(str(self.get_object())[:30]) worksheet.write_row(0, 0, [field[1] for field in self.get_fields()]) for (index, row) in enumerate(self.get_data()): worksheet.write_row(index + 1, 0, row) + def get(self, request, *args, **kwargs): + output = BytesIO() + + workbook = xlsxwriter.Workbook(output, {'remove_timezone': True}) + self.write_data(workbook) workbook.close() + output.seek(0) response = HttpResponse(output.read()) + response['Content-Disposition'] = f'attachment; filename="{self.get_filename()}"' response['Content-Type'] = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' From 4783a1c35a2db3b9d812d0472ebb1f01bd588abc Mon Sep 17 00:00:00 2001 From: Ernst Odolphi Date: Wed, 20 Jul 2022 10:10:29 +0200 Subject: [PATCH 060/112] Fix duration in excel exports --- bluebottle/time_based/tests/test_api.py | 4 ++-- bluebottle/utils/admin.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/bluebottle/time_based/tests/test_api.py b/bluebottle/time_based/tests/test_api.py index 7bb05549d5..50c994b126 100644 --- a/bluebottle/time_based/tests/test_api.py +++ b/bluebottle/time_based/tests/test_api.py @@ -1090,7 +1090,7 @@ def test_get_owner_export_teams_enabled(self): sheet = workbook.worksheets[0] self.assertEqual( - tuple(sheet)[0], + tuple(sheet.values)[0], ('Email', 'Name', 'Motivation', 'Registration Date', 'Status', 'Team', 'Team Captain') ) @@ -1115,7 +1115,7 @@ def test_get_owner_export_teams_enabled(self): team_captain.team.pk, team_captain.team.status, team_captain.team.accepted_participants_count, team_captain.team.slot.start.strftime('%d-%m-%y %H:%M'), - team_captain.team.slot.duration.seconds / (60 * 60 * 24), + team_captain.team.slot.duration.seconds / (60 * 60), ) ) diff --git a/bluebottle/utils/admin.py b/bluebottle/utils/admin.py index 878e76735d..d624368d00 100644 --- a/bluebottle/utils/admin.py +++ b/bluebottle/utils/admin.py @@ -57,6 +57,9 @@ def prep_field(request, obj, field, manyToManySep=';'): if isinstance(attr, datetime.datetime): attr = attr.strftime('%d-%m-%y %H:%M') + if isinstance(attr, datetime.timedelta): + attr = attr.seconds / (60 * 60) + output = attr() if callable(attr) else attr if isinstance(output, (list, tuple, QuerySet)): From 11599ebddc08eb55de69026dda1cf72ae2751dc4 Mon Sep 17 00:00:00 2001 From: Ernst Odolphi Date: Wed, 20 Jul 2022 12:07:14 +0200 Subject: [PATCH 061/112] Remove location from required fields for team slots --- bluebottle/time_based/models.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/bluebottle/time_based/models.py b/bluebottle/time_based/models.py index e862b558fb..650de0ae64 100644 --- a/bluebottle/time_based/models.py +++ b/bluebottle/time_based/models.py @@ -522,15 +522,11 @@ def end(self): @property def required_fields(self): - fields = super().required_fields + [ + return super().required_fields + [ 'start', 'duration', ] - if not self.is_online: - fields.append('location') - return fields - @property def is_complete(self): return self.start and self.duration From f021d056ae0cdcbe9affdab9d943c894497fcd05 Mon Sep 17 00:00:00 2001 From: Ernst Odolphi Date: Wed, 20 Jul 2022 13:50:34 +0200 Subject: [PATCH 062/112] Use normal validation for team slots --- .../migrations/0074_auto_20220720_1349.py | 23 +++++++++++++++++++ bluebottle/time_based/models.py | 11 ++------- bluebottle/time_based/serializers.py | 9 ++++++-- bluebottle/time_based/tests/test_api.py | 18 +++++++++++++++ 4 files changed, 50 insertions(+), 11 deletions(-) create mode 100644 bluebottle/time_based/migrations/0074_auto_20220720_1349.py diff --git a/bluebottle/time_based/migrations/0074_auto_20220720_1349.py b/bluebottle/time_based/migrations/0074_auto_20220720_1349.py new file mode 100644 index 0000000000..915f256f94 --- /dev/null +++ b/bluebottle/time_based/migrations/0074_auto_20220720_1349.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.24 on 2022-07-20 11:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('time_based', '0073_auto_20220701_1330'), + ] + + operations = [ + migrations.AlterField( + model_name='teamslot', + name='duration', + field=models.DurationField(verbose_name='duration'), + ), + migrations.AlterField( + model_name='teamslot', + name='start', + field=models.DateTimeField(verbose_name='start date and time'), + ), + ] diff --git a/bluebottle/time_based/models.py b/bluebottle/time_based/models.py index 650de0ae64..166efb8e64 100644 --- a/bluebottle/time_based/models.py +++ b/bluebottle/time_based/models.py @@ -511,8 +511,8 @@ class Meta: class TeamSlot(ActivitySlot): activity = models.ForeignKey(PeriodActivity, related_name='team_slots', on_delete=models.CASCADE) - start = models.DateTimeField(_('start date and time'), null=True, blank=True) - duration = models.DurationField(_('duration'), null=True, blank=True) + start = models.DateTimeField(_('start date and time')) + duration = models.DurationField(_('duration')) team = models.OneToOneField(Team, related_name='slot', on_delete=models.CASCADE) @property @@ -520,13 +520,6 @@ def end(self): if self.start and self.duration: return self.start + self.duration - @property - def required_fields(self): - return super().required_fields + [ - 'start', - 'duration', - ] - @property def is_complete(self): return self.start and self.duration diff --git a/bluebottle/time_based/serializers.py b/bluebottle/time_based/serializers.py index 44987baeb3..d3505909a0 100644 --- a/bluebottle/time_based/serializers.py +++ b/bluebottle/time_based/serializers.py @@ -192,8 +192,6 @@ class JSONAPIMeta(ActivitySlotSerializer.JSONAPIMeta): class TeamSlotSerializer(ActivitySlotSerializer): - errors = ValidationErrorsField() - required = RequiredErrorsField() activity = ResourceRelatedField(read_only=True) links = serializers.SerializerMethodField() @@ -215,6 +213,13 @@ class Meta(ActivitySlotSerializer.Meta): 'location', 'links' ) + meta_fields = ( + 'status', + 'permissions', + 'transitions', + 'created', + 'updated', + ) class JSONAPIMeta(object): resource_name = 'activities/time-based/team-slots' diff --git a/bluebottle/time_based/tests/test_api.py b/bluebottle/time_based/tests/test_api.py index a819353b68..8d9c1156d9 100644 --- a/bluebottle/time_based/tests/test_api.py +++ b/bluebottle/time_based/tests/test_api.py @@ -1132,6 +1132,24 @@ def test_create_team_slot(self): self.perform_create(user=self.manager) self.assertStatus(status.HTTP_201_CREATED) + def test_create_team_slot_missing_start(self): + self.defaults['start'] = None + self.perform_create(user=self.manager) + self.assertStatus(status.HTTP_400_BAD_REQUEST) + self.assertEqual( + self.response.json()['errors'][0]['source']['pointer'], + '/data/attributes/start' + ) + + def test_create_team_slot_missing_duration(self): + self.defaults['duration'] = None + self.perform_create(user=self.manager) + self.assertStatus(status.HTTP_400_BAD_REQUEST) + self.assertEqual( + self.response.json()['errors'][0]['source']['pointer'], + '/data/attributes/duration' + ) + def test_update_team_slot(self): self.perform_create(user=self.manager) self.assertStatus(status.HTTP_201_CREATED) From bd8b5642d8a03aaf3e2974046d6e9c80fff1bf7f Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 27 Jul 2022 09:10:26 +0300 Subject: [PATCH 063/112] Escape invalid chars for sheet names --- bluebottle/utils/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bluebottle/utils/views.py b/bluebottle/utils/views.py index a0178abe07..20d63b94c2 100644 --- a/bluebottle/utils/views.py +++ b/bluebottle/utils/views.py @@ -32,6 +32,7 @@ from bluebottle.utils.permissions import ResourcePermission from .models import Language from .serializers import LanguageSerializer +import re mime = magic.Magic(mime=True) @@ -364,7 +365,8 @@ def get_instances(self): raise NotImplementedError() def write_data(self, workbook): - worksheet = workbook.add_worksheet(str(self.get_object())[:30]) + title = re.sub("[\[\]\\:*?/]", '', str(self.get_object())[:30]) + worksheet = workbook.add_worksheet(title) worksheet.write_row(0, 0, [field[1] for field in self.get_fields()]) From 09e1194fe2eed212256a80eb7983596f4e23b700 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Thu, 28 Jul 2022 15:45:51 +0300 Subject: [PATCH 064/112] Tweak serializers --- bluebottle/activities/utils.py | 2 +- bluebottle/time_based/serializers.py | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/bluebottle/activities/utils.py b/bluebottle/activities/utils.py index 295701fcd6..e56f20b8f8 100644 --- a/bluebottle/activities/utils.py +++ b/bluebottle/activities/utils.py @@ -55,7 +55,7 @@ class TeamSerializer(ModelSerializer): class Meta(object): model = Team - fields = ('owner', 'activity', 'slot', 'members') + fields = ('owner', 'slot', 'members') meta_fields = ( 'status', 'transitions', diff --git a/bluebottle/time_based/serializers.py b/bluebottle/time_based/serializers.py index 07a3b5ed10..e8120088c5 100644 --- a/bluebottle/time_based/serializers.py +++ b/bluebottle/time_based/serializers.py @@ -47,8 +47,6 @@ class TimeBasedBaseSerializer(BaseActivitySerializer): review = serializers.BooleanField(required=False) is_online = serializers.BooleanField(required=False, allow_null=True) - teams = TeamsField() - class Meta(BaseActivitySerializer.Meta): fields = BaseActivitySerializer.Meta.fields + ( 'capacity', @@ -57,7 +55,6 @@ class Meta(BaseActivitySerializer.Meta): 'review', 'contributors', 'my_contributor', - 'teams' ) class JSONAPIMeta(BaseActivitySerializer.JSONAPIMeta): @@ -742,7 +739,8 @@ class Meta(BaseContributorSerializer.Meta): 'team', 'accepted_invite', 'invite', - 'team' + 'team', + 'activity' ) class JSONAPIMeta(BaseContributorSerializer.JSONAPIMeta): @@ -750,7 +748,7 @@ class JSONAPIMeta(BaseContributorSerializer.JSONAPIMeta): included_resources = BaseContributorSerializer.JSONAPIMeta.included_resources + [ 'contributions', 'team', - 'team.slot' + 'team.slot', ] included_serializers = dict( From 80fde43200dbb71e3962d2663d67c22334314326 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Thu, 28 Jul 2022 16:16:08 +0300 Subject: [PATCH 065/112] Another tweak --- bluebottle/activities/serializers.py | 1 + bluebottle/time_based/serializers.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/bluebottle/activities/serializers.py b/bluebottle/activities/serializers.py index 2e69f6ea73..5d476aff73 100644 --- a/bluebottle/activities/serializers.py +++ b/bluebottle/activities/serializers.py @@ -71,6 +71,7 @@ class Meta(object): ) class JSONAPIMeta(object): + resource_name = 'activities/activity' included_resources = [ 'owner', 'initiative', diff --git a/bluebottle/time_based/serializers.py b/bluebottle/time_based/serializers.py index 07a3b5ed10..64c1864c10 100644 --- a/bluebottle/time_based/serializers.py +++ b/bluebottle/time_based/serializers.py @@ -742,7 +742,8 @@ class Meta(BaseContributorSerializer.Meta): 'team', 'accepted_invite', 'invite', - 'team' + 'team', + 'activity' ) class JSONAPIMeta(BaseContributorSerializer.JSONAPIMeta): From 4c3b826844934f609aa2b78f0b36622600d72462 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Thu, 28 Jul 2022 16:24:40 +0300 Subject: [PATCH 066/112] Right json type --- bluebottle/activities/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bluebottle/activities/serializers.py b/bluebottle/activities/serializers.py index 5d476aff73..62e92c8f4f 100644 --- a/bluebottle/activities/serializers.py +++ b/bluebottle/activities/serializers.py @@ -71,7 +71,7 @@ class Meta(object): ) class JSONAPIMeta(object): - resource_name = 'activities/activity' + resource_name = 'activity' included_resources = [ 'owner', 'initiative', From efc4d3dd81a5a7ca2a73e683f1369647e76955e4 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Thu, 28 Jul 2022 17:08:27 +0300 Subject: [PATCH 067/112] Add right serializer for activity --- bluebottle/activities/serializers.py | 3 +-- bluebottle/activities/utils.py | 2 -- bluebottle/time_based/serializers.py | 5 +++++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/bluebottle/activities/serializers.py b/bluebottle/activities/serializers.py index 62e92c8f4f..aacb7f27d9 100644 --- a/bluebottle/activities/serializers.py +++ b/bluebottle/activities/serializers.py @@ -70,8 +70,7 @@ class Meta(object): 'matching_properties', ) - class JSONAPIMeta(object): - resource_name = 'activity' + class JSONAPIMeta: included_resources = [ 'owner', 'initiative', diff --git a/bluebottle/activities/utils.py b/bluebottle/activities/utils.py index e56f20b8f8..2ae5924ac9 100644 --- a/bluebottle/activities/utils.py +++ b/bluebottle/activities/utils.py @@ -270,7 +270,6 @@ class JSONAPIMeta(object): 'segments', 'segments.segment_type' ] - resource_name = 'activities' class BaseActivityListSerializer(ModelSerializer): @@ -340,7 +339,6 @@ class JSONAPIMeta(object): 'goals', 'goals.type', ] - resource_name = 'activities' class BaseTinyActivitySerializer(ModelSerializer): diff --git a/bluebottle/time_based/serializers.py b/bluebottle/time_based/serializers.py index e8120088c5..eea255e2e0 100644 --- a/bluebottle/time_based/serializers.py +++ b/bluebottle/time_based/serializers.py @@ -731,6 +731,11 @@ class JSONAPIMeta(BaseContributorSerializer.JSONAPIMeta): class TeamMemberSerializer(BaseContributorSerializer): + activity = PolymorphicResourceRelatedField( + TimeBasedActivitySerializer, + queryset=TimeBasedActivity.objects.all() + ) + class Meta(BaseContributorSerializer.Meta): model = PeriodParticipant fields = ( From aaafddbc94c77430136018763ef0868ba4b096e4 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Fri, 29 Jul 2022 07:11:36 +0300 Subject: [PATCH 068/112] Fix tests --- bluebottle/activities/tests/test_api.py | 5 ----- bluebottle/time_based/serializers.py | 5 ----- bluebottle/time_based/tests/test_api.py | 5 ----- 3 files changed, 15 deletions(-) diff --git a/bluebottle/activities/tests/test_api.py b/bluebottle/activities/tests/test_api.py index ef1f36f175..4b8877557d 100644 --- a/bluebottle/activities/tests/test_api.py +++ b/bluebottle/activities/tests/test_api.py @@ -1768,7 +1768,6 @@ def test_get_activity_owner(self): self.assertStatus(status.HTTP_200_OK) self.assertTotal(len(self.approved_teams) + len(self.cancelled_teams)) self.assertObjectList(self.approved_teams) - self.assertRelationship('activity', [self.activity]) self.assertRelationship('owner') self.assertMeta('status') @@ -1825,7 +1824,6 @@ def test_get_cancelled_team_captain(self): self.assertTotal(len(self.approved_teams) + 1) self.assertObjectList(self.approved_teams + [team]) - self.assertRelationship('activity', [self.activity]) self.assertRelationship('owner') self.assertEqual( @@ -1840,7 +1838,6 @@ def test_get_team_captain(self): self.assertStatus(status.HTTP_200_OK) self.assertTotal(len(self.approved_teams)) self.assertObjectList(self.approved_teams) - self.assertRelationship('activity', [self.activity]) self.assertRelationship('owner') self.assertEqual( @@ -1860,7 +1857,6 @@ def test_get_anonymous(self): self.assertStatus(status.HTTP_200_OK) self.assertTotal(len(self.approved_teams)) self.assertObjectList(self.approved_teams) - self.assertRelationship('activity', [self.activity]) self.assertRelationship('owner') for resource in self.response.json()['data']: self.assertTrue(resource['meta']['participants-export-url'] is None) @@ -1881,7 +1877,6 @@ def test_other_user_anonymous(self): self.assertStatus(status.HTTP_200_OK) self.assertTotal(len(self.approved_teams)) self.assertObjectList(self.approved_teams) - self.assertRelationship('activity', [self.activity]) self.assertRelationship('owner') def test_get_anonymous_closed_site(self): diff --git a/bluebottle/time_based/serializers.py b/bluebottle/time_based/serializers.py index eea255e2e0..d7cd19e3b5 100644 --- a/bluebottle/time_based/serializers.py +++ b/bluebottle/time_based/serializers.py @@ -686,11 +686,6 @@ class ParticipantSerializer(BaseContributorSerializer): motivation = serializers.CharField(required=False, allow_null=True, allow_blank=True) document = PrivateDocumentField(required=False, allow_null=True, permissions=[ParticipantDocumentPermission]) - activity = PolymorphicResourceRelatedField( - TimeBasedActivitySerializer, - queryset=TimeBasedActivity.objects.all() - ) - def to_representation(self, instance): result = super().to_representation(instance) diff --git a/bluebottle/time_based/tests/test_api.py b/bluebottle/time_based/tests/test_api.py index 0a3f576cfe..89871ef4ee 100644 --- a/bluebottle/time_based/tests/test_api.py +++ b/bluebottle/time_based/tests/test_api.py @@ -955,11 +955,6 @@ def test_get_open(self): in self.data['meta']['transitions'] ) - self.assertEqual( - self.data['relationships']['teams']['links']['self'], - f"{reverse('team-list')}?filter[activity_id]={self.activity.pk}" - ) - def test_get_open_with_participant(self): self.activity.duration_period = 'weeks' self.activity.save() From cfff264826f20b247c97e053c8ca5eb8f8480a4f Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Fri, 29 Jul 2022 07:24:16 +0300 Subject: [PATCH 069/112] No unreviewed teams on unscheduled api response --- bluebottle/activities/tests/test_api.py | 2 +- bluebottle/activities/views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bluebottle/activities/tests/test_api.py b/bluebottle/activities/tests/test_api.py index ef1f36f175..9b9449ff39 100644 --- a/bluebottle/activities/tests/test_api.py +++ b/bluebottle/activities/tests/test_api.py @@ -1792,7 +1792,7 @@ def test_get_filtered_status(self): resource['id'] in [str(team.pk) for team in new_teams] ) - def test_get_filtered_has_slot(self): + def test_get_filtered_has_no_slot(self): self.perform_get(user=self.activity.owner, query={'filter[has_slot]': 'false'}) self.assertStatus(status.HTTP_200_OK) diff --git a/bluebottle/activities/views.py b/bluebottle/activities/views.py index 5a974c3c88..8785aa9065 100644 --- a/bluebottle/activities/views.py +++ b/bluebottle/activities/views.py @@ -180,7 +180,7 @@ def get_queryset(self, *args, **kwargs): if status: queryset = queryset.filter(status=status) elif has_slot == 'false': - queryset = queryset.filter(slot__start__isnull=True) + queryset = queryset.filter(slot__start__isnull=True).exclude(status='new') elif start == 'future': queryset = queryset.filter( slot__start__gt=timezone.now() From d49c5533fb8303feba465dd38d4d52b2c8f48272 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Thu, 4 Aug 2022 12:02:10 +0200 Subject: [PATCH 070/112] Add location to mycontributor slot --- bluebottle/time_based/serializers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bluebottle/time_based/serializers.py b/bluebottle/time_based/serializers.py index d7cd19e3b5..3025973bd9 100644 --- a/bluebottle/time_based/serializers.py +++ b/bluebottle/time_based/serializers.py @@ -423,6 +423,7 @@ class JSONAPIMeta(TimeBasedBaseSerializer.JSONAPIMeta): included_resources = TimeBasedBaseSerializer.JSONAPIMeta.included_resources + [ 'my_contributor', 'my_contributor.user', + 'my_contributor.location', 'my_contributor.slots', 'my_contributor.slots.slot', ] @@ -497,6 +498,7 @@ class JSONAPIMeta(TimeBasedBaseSerializer.JSONAPIMeta): 'location', 'my_contributor.team', 'my_contributor.team.slot', + 'my_contributor.team.slot.location', ] included_serializers = dict( @@ -506,6 +508,7 @@ class JSONAPIMeta(TimeBasedBaseSerializer.JSONAPIMeta): 'my_contributor': 'bluebottle.time_based.serializers.PeriodParticipantSerializer', 'my_contributor.team': 'bluebottle.activities.utils.TeamSerializer', 'my_contributor.team.slot': 'bluebottle.time_based.serializers.TeamSlotSerializer', + 'my_contributor.team.slot.location': 'bluebottle.geo.serializers.GeolocationSerializer', } ) From 52d4911a90c6822ed4d2ea4a81d89ecfbacf4b41 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Thu, 4 Aug 2022 12:12:43 +0200 Subject: [PATCH 071/112] Don't show cancelled adn withdrawn teams --- bluebottle/activities/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bluebottle/activities/views.py b/bluebottle/activities/views.py index 8785aa9065..3433f3af31 100644 --- a/bluebottle/activities/views.py +++ b/bluebottle/activities/views.py @@ -180,7 +180,9 @@ def get_queryset(self, *args, **kwargs): if status: queryset = queryset.filter(status=status) elif has_slot == 'false': - queryset = queryset.filter(slot__start__isnull=True).exclude(status='new') + queryset = queryset.filter(slot__start__isnull=True).exclude( + status_in=['new', 'withdrawn', 'cancelled'] + ) elif start == 'future': queryset = queryset.filter( slot__start__gt=timezone.now() From 9e15e40c6f4689469b9f79d19d9b81695f235e83 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Thu, 4 Aug 2022 12:16:11 +0200 Subject: [PATCH 072/112] Delete participants with team --- .../migrations/0059_auto_20220804_1214.py | 19 +++++++++++++++++++ bluebottle/activities/models.py | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 bluebottle/activities/migrations/0059_auto_20220804_1214.py diff --git a/bluebottle/activities/migrations/0059_auto_20220804_1214.py b/bluebottle/activities/migrations/0059_auto_20220804_1214.py new file mode 100644 index 0000000000..07a346a629 --- /dev/null +++ b/bluebottle/activities/migrations/0059_auto_20220804_1214.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.24 on 2022-08-04 10:14 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('activities', '0058_auto_20220622_1050'), + ] + + operations = [ + migrations.AlterField( + model_name='contributor', + name='team', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='members', to='activities.Team', verbose_name='team'), + ), + ] diff --git a/bluebottle/activities/models.py b/bluebottle/activities/models.py index ac2516c1d1..cfb8aca2dc 100644 --- a/bluebottle/activities/models.py +++ b/bluebottle/activities/models.py @@ -183,7 +183,7 @@ class Contributor(TriggerMixin, AnonymizationMixin, PolymorphicModel): team = models.ForeignKey( 'activities.Team', verbose_name=_('team'), - null=True, blank=True, related_name='members', on_delete=models.SET_NULL + null=True, blank=True, related_name='members', on_delete=models.CASCADE ) user = models.ForeignKey( 'members.Member', verbose_name=_('user'), From 5d368acb2eb00a2253985436546c22fb3b247dac Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Thu, 4 Aug 2022 12:44:48 +0200 Subject: [PATCH 073/112] Don't notify act manager on team member withdrawal --- bluebottle/activities/messages.py | 2 +- bluebottle/time_based/tests/test_triggers.py | 23 ++++++++++++++++++-- bluebottle/time_based/triggers.py | 20 ++++++++++++++--- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/bluebottle/activities/messages.py b/bluebottle/activities/messages.py index 26a6ec028c..9490a91b49 100644 --- a/bluebottle/activities/messages.py +++ b/bluebottle/activities/messages.py @@ -391,7 +391,7 @@ def get_recipients(self): class TeamMemberWithdrewMessage(ActivityNotification): - subject = pgettext('email', "Withdrawal for '{title}'") + subject = pgettext('email', 'A participant has withdrawn from your team for "{title}"') template = 'messages/team_member_withdrew' context = { diff --git a/bluebottle/time_based/tests/test_triggers.py b/bluebottle/time_based/tests/test_triggers.py index 4c290bacbd..0dc3c74a73 100644 --- a/bluebottle/time_based/tests/test_triggers.py +++ b/bluebottle/time_based/tests/test_triggers.py @@ -7,7 +7,8 @@ from django.utils.timezone import now, get_current_timezone from tenant_extras.utils import TenantLanguage -from bluebottle.activities.messages import TeamMemberRemovedMessage +from bluebottle.activities.messages import TeamMemberRemovedMessage, ParticipantWithdrewConfirmationNotification, \ + TeamMemberWithdrewMessage from bluebottle.activities.models import Organizer, Activity from bluebottle.activities.tests.factories import TeamFactory from bluebottle.initiatives.tests.factories import InitiativeFactory, InitiativePlatformSettingsFactory @@ -17,7 +18,7 @@ ParticipantJoinedNotification, ParticipantChangedNotification, ParticipantAppliedNotification, ParticipantRemovedNotification, ParticipantRemovedOwnerNotification, NewParticipantNotification, TeamParticipantJoinedNotification, ParticipantAddedNotification, - ParticipantAddedOwnerNotification, TeamSlotChangedNotification + ParticipantAddedOwnerNotification, TeamSlotChangedNotification, ParticipantWithdrewNotification ) from bluebottle.time_based.tests.factories import ( DateActivityFactory, PeriodActivityFactory, @@ -1930,6 +1931,24 @@ def test_remove_participant(self): self.assertNotificationEffect(ParticipantRemovedNotification) self.assertNotificationEffect(ParticipantRemovedOwnerNotification) + def test_withdraw_team_participant(self): + self.activity.team_activity = 'teams' + captain = BlueBottleUserFactory.create() + team = TeamFactory.create( + owner=captain, + activity=self.activity + ) + self.model = self.participant_factory.create( + activity=self.activity, + team=team, + status='accepted' + ) + self.model.states.withdraw() + with self.execute(): + self.assertNoNotificationEffect(ParticipantWithdrewNotification) + self.assertNotificationEffect(TeamMemberWithdrewMessage) + self.assertNotificationEffect(ParticipantWithdrewConfirmationNotification) + def test_remove_team_participant(self): self.activity.team_activity = 'teams' self.activity.save() diff --git a/bluebottle/time_based/triggers.py b/bluebottle/time_based/triggers.py index 1d2798b0ea..9ed58ec694 100644 --- a/bluebottle/time_based/triggers.py +++ b/bluebottle/time_based/triggers.py @@ -1371,9 +1371,23 @@ class ParticipantTriggers(ContributorTriggers): TimeContributionStateMachine.fail, ), UnFollowActivityEffect, - NotificationEffect(ParticipantWithdrewNotification), - NotificationEffect(ParticipantWithdrewConfirmationNotification), - NotificationEffect(TeamMemberWithdrewMessage), + NotificationEffect( + ParticipantWithdrewNotification, + conditions=[ + is_not_team_activity + ] + ), + NotificationEffect( + ParticipantWithdrewConfirmationNotification + ), + NotificationEffect( + TeamMemberWithdrewMessage, + conditions=[ + is_team_activity, + not_team_captain + ] + + ), ] ), From 69643a223d62bcb7b733cffa5f2b2efc5481df6b Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Thu, 4 Aug 2022 15:50:10 +0200 Subject: [PATCH 074/112] Reject team when captain is rejected --- bluebottle/activities/states.py | 5 ++- bluebottle/time_based/tests/test_triggers.py | 43 +++++++++++++++++++- bluebottle/time_based/triggers.py | 19 ++++++++- 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/bluebottle/activities/states.py b/bluebottle/activities/states.py index c8a9462f79..765611436a 100644 --- a/bluebottle/activities/states.py +++ b/bluebottle/activities/states.py @@ -440,7 +440,10 @@ def is_activity_owner(self, user): ) cancel = Transition( - open, + [ + open, + new + ], cancelled, automatic=False, permission=is_activity_owner, diff --git a/bluebottle/time_based/tests/test_triggers.py b/bluebottle/time_based/tests/test_triggers.py index 4c290bacbd..168ab42a36 100644 --- a/bluebottle/time_based/tests/test_triggers.py +++ b/bluebottle/time_based/tests/test_triggers.py @@ -7,7 +7,8 @@ from django.utils.timezone import now, get_current_timezone from tenant_extras.utils import TenantLanguage -from bluebottle.activities.messages import TeamMemberRemovedMessage +from bluebottle.activities.messages import TeamMemberRemovedMessage, TeamCancelledTeamCaptainMessage, \ + TeamCancelledMessage from bluebottle.activities.models import Organizer, Activity from bluebottle.activities.tests.factories import TeamFactory from bluebottle.initiatives.tests.factories import InitiativeFactory, InitiativePlatformSettingsFactory @@ -17,7 +18,7 @@ ParticipantJoinedNotification, ParticipantChangedNotification, ParticipantAppliedNotification, ParticipantRemovedNotification, ParticipantRemovedOwnerNotification, NewParticipantNotification, TeamParticipantJoinedNotification, ParticipantAddedNotification, - ParticipantAddedOwnerNotification, TeamSlotChangedNotification + ParticipantAddedOwnerNotification, TeamSlotChangedNotification, ParticipantRejectedNotification ) from bluebottle.time_based.tests.factories import ( DateActivityFactory, PeriodActivityFactory, @@ -2307,3 +2308,41 @@ def test_change_date(self): self.assertNotificationEffect(TeamSlotChangedNotification) self.assertEqual(self.model.status, 'open') self.assertEqual(self.model.team.status, 'open') + + +class TeamReviewTriggerTestCase(TriggerTestCase): + + def setUp(self): + super().setUp() + self.initiator = BlueBottleUserFactory() + self.user = BlueBottleUserFactory() + self.initiative = InitiativeFactory(owner=self.initiator) + + self.activity = PeriodActivityFactory.create( + initiative=self.initiative, + team_activity='teams', + status='approved', + review=True + ) + self.model = PeriodParticipantFactory.create( + user=self.user, + activity=self.activity, + as_relation='user' + ) + + def assertStatus(self, obj, status): + obj.refresh_from_db() + self.assertEqual(obj.status, status) + + def test_reject(self): + self.assertTrue(self.model.team) + self.assertEqual( + self.model.team.owner, + self.user + ) + self.model.states.reject() + + with self.execute(): + self.assertNoNotificationEffect(ParticipantRejectedNotification) + self.assertNoNotificationEffect(TeamCancelledMessage) + self.assertNotificationEffect(TeamCancelledTeamCaptainMessage) diff --git a/bluebottle/time_based/triggers.py b/bluebottle/time_based/triggers.py index 1d2798b0ea..e44834199c 100644 --- a/bluebottle/time_based/triggers.py +++ b/bluebottle/time_based/triggers.py @@ -940,6 +940,13 @@ def not_team_captain(effect): return not effect.instance.team_id or effect.instance.team.owner != effect.instance.user +def is_team_captain(effect): + """ + is the team captain + """ + return effect.instance.team_id and effect.instance.team.owner == effect.instance.user + + def user_is_not_team_captain(effect): """ current user is not team captain @@ -1307,7 +1314,17 @@ class ParticipantTriggers(ContributorTriggers): ParticipantStateMachine.reject, effects=[ NotificationEffect( - ParticipantRejectedNotification + ParticipantRejectedNotification, + conditions=[ + not_team_captain + ] + ), + RelatedTransitionEffect( + 'team', + TeamStateMachine.cancel, + conditions=[ + is_team_captain + ] ), RelatedTransitionEffect( 'activity', From 10ede6cd8a4d6f99285fba1617b95eb1ae50da6b Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Thu, 4 Aug 2022 16:08:50 +0200 Subject: [PATCH 075/112] Fix and extend tests --- .../activities/tests/test_notifications.py | 2 +- bluebottle/time_based/tests/test_triggers.py | 67 ++++++++++++++++--- 2 files changed, 59 insertions(+), 10 deletions(-) diff --git a/bluebottle/activities/tests/test_notifications.py b/bluebottle/activities/tests/test_notifications.py index cfb13b88ff..c231fe21e5 100644 --- a/bluebottle/activities/tests/test_notifications.py +++ b/bluebottle/activities/tests/test_notifications.py @@ -220,7 +220,7 @@ def test_team_member_withdrew_notification(self): self.message_class = TeamMemberWithdrewMessage self.create() self.assertRecipients([self.captain]) - self.assertSubject("Withdrawal for 'Save the world!'") + self.assertSubjec('A participant has withdrawn from your team for "Save the world!"') self.assertHtmlBodyContains( f"{self.obj.user.full_name} has withdrawn from your team for the activity ‘Save the world!’." ) diff --git a/bluebottle/time_based/tests/test_triggers.py b/bluebottle/time_based/tests/test_triggers.py index 0dc3c74a73..002db9df08 100644 --- a/bluebottle/time_based/tests/test_triggers.py +++ b/bluebottle/time_based/tests/test_triggers.py @@ -1397,12 +1397,12 @@ def test_withdraw_team(self): user=BlueBottleUserFactory.create() ) - mail.outbox = [] participant = self.participant_factory.create( activity=self.activity, accepted_invite=team_captain.invite, user=BlueBottleUserFactory.create() ) + mail.outbox = [] participant.states.withdraw(save=True) self.activity.refresh_from_db() @@ -1417,11 +1417,7 @@ def test_withdraw_team(self): f'You have withdrawn from the activity "{self.activity.title}"' in subjects ) self.assertTrue( - f'A participant has withdrawn from your activity "{self.activity.title}"' in subjects - ) - - self.assertTrue( - f"Withdrawal for '{self.activity.title}'" in subjects + f'A participant has withdrawn from your team for "{self.activity.title}"' in subjects ) def test_reapply(self): @@ -1439,9 +1435,38 @@ def test_reapply(self): ) self.assertTrue(self.activity.followers.filter(user=self.participants[0].user).exists()) - def test_reapply_cancelled_team(self): - self.activity.team_activity = Activity.TeamActivityChoices.teams - self.test_withdraw() + def test_reapply_cancelled(self): + self.participants = self.participant_factory.create_batch( + self.activity.capacity, + activity=self.activity, + user=BlueBottleUserFactory.create() + ) + self.activity.refresh_from_db() + + self.assertEqual(self.activity.status, 'full') + mail.outbox = [] + + self.participants[0].states.withdraw(save=True) + + self.activity.refresh_from_db() + self.assertEqual(self.activity.status, 'open') + + self.assertEqual( + self.participants[0].contributions. + exclude(timecontribution__contribution_type='preparation').get().status, + 'failed' + ) + + self.assertFalse(self.activity.followers.filter(user=self.participants[0].user).exists()) + + subjects = [mail.subject for mail in mail.outbox] + self.assertTrue( + f'You have withdrawn from the activity "{self.activity.title}"' in subjects + ) + self.assertTrue( + f'A participant has withdrawn from your team for "{self.activity.title}"' in subjects + ) + self.participants[0].team.states.cancel(save=True) self.assertEqual( @@ -1462,6 +1487,30 @@ def test_reapply_cancelled_team(self): ) self.assertTrue(self.activity.followers.filter(user=self.participants[0].user).exists()) + def test_withdraw_from_team(self): + self.activity.team_activity = Activity.TeamActivityChoices.teams + self.captain = self.participant_factory.create( + activity=self.activity, + user=BlueBottleUserFactory.create() + ) + self.participant = self.participant_factory.create( + activity=self.activity, + user=BlueBottleUserFactory.create(), + team=self.captain.team + ) + + mail.outbox = [] + + self.participant.states.withdraw(save=True) + + subjects = [mail.subject for mail in mail.outbox] + self.assertTrue( + f'You have withdrawn from the activity "{self.activity.title}"' in subjects + ) + self.assertTrue( + f'A participant has withdrawn from your team for "{self.activity.title}"' in subjects + ) + class DateParticipantTriggerTestCase(ParticipantTriggerTestCase, BluebottleTestCase): factory = DateActivityFactory From 3515058e7943763150110872f7776278e62d8538 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Thu, 4 Aug 2022 16:13:03 +0200 Subject: [PATCH 076/112] Fix test --- bluebottle/time_based/tests/test_triggers.py | 22 +------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/bluebottle/time_based/tests/test_triggers.py b/bluebottle/time_based/tests/test_triggers.py index 002db9df08..98517ca0d7 100644 --- a/bluebottle/time_based/tests/test_triggers.py +++ b/bluebottle/time_based/tests/test_triggers.py @@ -1464,28 +1464,8 @@ def test_reapply_cancelled(self): f'You have withdrawn from the activity "{self.activity.title}"' in subjects ) self.assertTrue( - f'A participant has withdrawn from your team for "{self.activity.title}"' in subjects - ) - - self.participants[0].team.states.cancel(save=True) - - self.assertEqual( - self.participants[0].contributions. - exclude(timecontribution__contribution_type='preparation').get().status, - 'failed' - ) - - self.participants[0].states.reapply(save=True) - - self.activity.refresh_from_db() - - self.assertEqual(self.activity.status, 'full') - self.assertEqual( - self.participants[0].contributions. - exclude(timecontribution__contribution_type='preparation').get().status, - 'failed' + f'A participant has withdrawn from your activity "{self.activity.title}"' in subjects ) - self.assertTrue(self.activity.followers.filter(user=self.participants[0].user).exists()) def test_withdraw_from_team(self): self.activity.team_activity = Activity.TeamActivityChoices.teams From 3deeefa70542b2289af1f3e6eed671ae09946cd3 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Fri, 5 Aug 2022 08:48:29 +0200 Subject: [PATCH 077/112] Fix typo in test --- bluebottle/activities/tests/test_notifications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bluebottle/activities/tests/test_notifications.py b/bluebottle/activities/tests/test_notifications.py index c231fe21e5..6ab302645e 100644 --- a/bluebottle/activities/tests/test_notifications.py +++ b/bluebottle/activities/tests/test_notifications.py @@ -220,7 +220,7 @@ def test_team_member_withdrew_notification(self): self.message_class = TeamMemberWithdrewMessage self.create() self.assertRecipients([self.captain]) - self.assertSubjec('A participant has withdrawn from your team for "Save the world!"') + self.assertSubject('A participant has withdrawn from your team for "Save the world!"') self.assertHtmlBodyContains( f"{self.obj.user.full_name} has withdrawn from your team for the activity ‘Save the world!’." ) From bfd6c1064fd5268c9257107a7c18ed5b5a9b32ef Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Fri, 5 Aug 2022 09:00:33 +0200 Subject: [PATCH 078/112] Change email for team joined + test --- .../messages/team_participant_joined.html | 9 +++------ .../time_based/tests/test_notifications.py | 20 ++++++++++++++++++- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/bluebottle/time_based/templates/mails/messages/team_participant_joined.html b/bluebottle/time_based/templates/mails/messages/team_participant_joined.html index eb77bb62b0..74a2a53d68 100644 --- a/bluebottle/time_based/templates/mails/messages/team_participant_joined.html +++ b/bluebottle/time_based/templates/mails/messages/team_participant_joined.html @@ -12,12 +12,9 @@ {{ title }}

- {% if duration %} - {% include 'mails/messages/partial/period.html' %} - {% else %} - {% include 'mails/messages/partial/slots.html' %} - - {% endif %} +

+ The activity manager will be in touch to confirm details such as time, date and location. You can start inviting team members, or wait until the details have been set. +

diff --git a/bluebottle/time_based/tests/test_notifications.py b/bluebottle/time_based/tests/test_notifications.py index 4b2d050955..7c35874bf4 100644 --- a/bluebottle/time_based/tests/test_notifications.py +++ b/bluebottle/time_based/tests/test_notifications.py @@ -12,7 +12,7 @@ ParticipantWithdrewNotification, NewParticipantNotification, ParticipantAddedOwnerNotification, ParticipantRemovedOwnerNotification, ParticipantJoinedNotification, ParticipantAppliedNotification, SlotCancelledNotification, ParticipantAddedNotification, TeamParticipantAddedNotification, - TeamSlotChangedNotification + TeamSlotChangedNotification, TeamParticipantJoinedNotification ) from bluebottle.time_based.tests.factories import DateActivityFactory, DateParticipantFactory, \ DateActivitySlotFactory, PeriodActivityFactory, PeriodParticipantFactory, TeamSlotFactory @@ -232,6 +232,24 @@ def test_participant_joined_notification(self): 'Go to the activity page to see the times in your own timezone and add them to your calendar.' ) + def test_team_joined_notification(self): + self.activity.team_activity = 'teams' + self.activity.save() + self.obj.team = TeamFactory.create() + self.obj.save() + self.message_class = TeamParticipantJoinedNotification + self.create() + self.assertRecipients([self.supporter]) + self.assertSubject('You have registered your team for "Save the world!"') + self.assertActionLink(self.activity.get_absolute_url()) + self.assertActionTitle('View activity') + self.assertBodyNotContains( + 'Go to the activity page to see the times in your own timezone and add them to your calendar.' + ) + self.assertBodyContains( + 'The activity manager will be in touch to confirm details' + ) + def test_new_participant_notification(self): self.message_class = ParticipantAppliedNotification self.create() From c3e0729734f5c2ece604c3a50da4f75826c3469e Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Fri, 5 Aug 2022 15:46:26 +0200 Subject: [PATCH 079/112] Fix transitions around team withdrawal --- bluebottle/activities/tests/test_triggers.py | 60 ++++++++++++++ bluebottle/activities/triggers.py | 83 +++++++++++++++----- bluebottle/test/utils.py | 4 + bluebottle/time_based/models.py | 10 ++- bluebottle/time_based/triggers.py | 63 +++++++++++---- 5 files changed, 183 insertions(+), 37 deletions(-) diff --git a/bluebottle/activities/tests/test_triggers.py b/bluebottle/activities/tests/test_triggers.py index 34c69c3a21..96816096bd 100644 --- a/bluebottle/activities/tests/test_triggers.py +++ b/bluebottle/activities/tests/test_triggers.py @@ -123,6 +123,66 @@ def test_withdrawn(self): for contribution in self.participant.contributions.all(): self.assertEqual(contribution.status, TimeContributionStateMachine.failed.value) + def test_fill_team_activity(self): + self.activity.capacity = 2 + self.activity.team_activity = 'teams' + self.activity.save() + + captain = PeriodParticipantFactory.create( + activity=self.activity, + user=BlueBottleUserFactory.create(), + as_relation='user', + ) + participant = PeriodParticipantFactory.create( + activity=self.activity, + user=BlueBottleUserFactory.create(), + as_relation='user', + team=captain.team + ) + self.activity.refresh_from_db() + self.assertEqual( + self.activity.status, + 'open' + ) + PeriodParticipantFactory.create( + activity=self.activity, + user=BlueBottleUserFactory.create(), + as_relation='user', + ) + self.assertEqual( + self.activity.status, + 'full' + ) + + captain.states.withdraw(save=True) + self.assertEqual( + captain.team.status, + 'open', + ) + self.assertEqual( + self.activity.status, + 'full', + ) + self.assertEqual( + participant.status, + 'accepted', + ) + self.model = captain.team + self.model.states.withdraw(save=True) + + self.assertStatus( + captain.team, + 'withdrawn', + ) + self.assertStatus( + participant, + 'rejected', + ) + self.assertStatus( + self.activity, + 'open', + ) + def test_reapply(self): self.create() diff --git a/bluebottle/activities/triggers.py b/bluebottle/activities/triggers.py index 10f97e3875..5989c2841b 100644 --- a/bluebottle/activities/triggers.py +++ b/bluebottle/activities/triggers.py @@ -1,27 +1,23 @@ -from bluebottle.activities.models import Organizer, EffortContribution, Team -from bluebottle.fsm.triggers import ( - TriggerManager, TransitionTrigger, ModelDeletedTrigger, register -) -from bluebottle.fsm.effects import TransitionEffect, RelatedTransitionEffect -from bluebottle.notifications.effects import NotificationEffect - -from bluebottle.activities.states import ( - ActivityStateMachine, OrganizerStateMachine, ContributionStateMachine, - EffortContributionStateMachine, TeamStateMachine -) from bluebottle.activities.effects import ( CreateOrganizer, CreateOrganizerContribution, SetContributionDateEffect, TeamContributionTransitionEffect, ResetTeamParticipantsEffect ) - from bluebottle.activities.messages import ( - TeamAddedMessage, TeamCancelledMessage, TeamReopenedMessage, TeamAcceptedMessage, TeamAppliedMessage, - TeamWithdrawnMessage, TeamWithdrawnActivityOwnerMessage, TeamCancelledTeamCaptainMessage, - TeamReappliedMessage + TeamAddedMessage, TeamReopenedMessage, TeamAcceptedMessage, TeamAppliedMessage, + TeamWithdrawnMessage, TeamWithdrawnActivityOwnerMessage, TeamReappliedMessage +) +from bluebottle.activities.models import Organizer, EffortContribution, Team +from bluebottle.activities.states import ( + ActivityStateMachine, OrganizerStateMachine, ContributionStateMachine, + EffortContributionStateMachine, TeamStateMachine +) +from bluebottle.fsm.effects import TransitionEffect, RelatedTransitionEffect +from bluebottle.fsm.triggers import ( + TriggerManager, TransitionTrigger, ModelDeletedTrigger, register ) - -from bluebottle.time_based.states import ParticipantStateMachine from bluebottle.impact.effects import UpdateImpactGoalEffect +from bluebottle.notifications.effects import NotificationEffect +from bluebottle.time_based.states import ParticipantStateMachine, TimeBasedStateMachine def initiative_is_approved(effect): @@ -232,6 +228,28 @@ def needs_review(effect): return hasattr(effect.instance.activity, 'review') and effect.instance.activity.review +def team_activity_will_be_full(effect): + """ + the activity is full + """ + activity = effect.instance.activity + accepted_teams = activity.teams.filter(status__in=['open', 'running', 'finished']).count() + 1 + return ( + activity.capacity and + activity.capacity <= accepted_teams + ) + + +def team_activity_will_not_be_full(effect): + """ + the activity is full + """ + activity = effect.instance.activity + accepted_teams = activity.teams.filter(status__in=['open', 'running', 'finished']).count() - 1 + + return not activity.capacity or activity.capacity > accepted_teams + + @register(Team) class TeamTriggers(TriggerManager): triggers = [ @@ -266,22 +284,45 @@ class TeamTriggers(TriggerManager): 'members', ParticipantStateMachine.accept, conditions=[needs_review] - ) + ), + RelatedTransitionEffect( + 'activity', + TimeBasedStateMachine.lock, + conditions=[team_activity_will_be_full] + ), ] ), TransitionTrigger( TeamStateMachine.cancel, effects=[ - TeamContributionTransitionEffect(ContributionStateMachine.fail), - NotificationEffect(TeamCancelledMessage), - NotificationEffect(TeamCancelledTeamCaptainMessage) + # RelatedTransitionEffect( + # 'members', + # ParticipantStateMachine.remove + # ), + RelatedTransitionEffect( + 'activity', + TimeBasedStateMachine.reopen, + conditions=[team_activity_will_not_be_full] + ), + # TeamContributionTransitionEffect(ContributionStateMachine.fail), + # NotificationEffect(TeamCancelledMessage), + # NotificationEffect(TeamCancelledTeamCaptainMessage) ] ), TransitionTrigger( TeamStateMachine.withdraw, effects=[ + RelatedTransitionEffect( + 'members', + ParticipantStateMachine.remove + ), + RelatedTransitionEffect( + 'activity', + TimeBasedStateMachine.reopen, + conditions=[team_activity_will_not_be_full] + ), TeamContributionTransitionEffect(ContributionStateMachine.fail), NotificationEffect(TeamWithdrawnMessage), NotificationEffect(TeamWithdrawnActivityOwnerMessage) diff --git a/bluebottle/test/utils.py b/bluebottle/test/utils.py index 6b158c727b..304a54c3b2 100644 --- a/bluebottle/test/utils.py +++ b/bluebottle/test/utils.py @@ -687,6 +687,10 @@ def _hasEffect(self, effect_cls, model=None): if effect == effect_cls(model): return effect + def assertStatus(self, obj, status): + obj.refresh_from_db() + return self.assertEqual(obj.status, status) + def assertTransitionEffect(self, transition, model=None): if not self._hasTransitionEffect(transition, model): self.fail('Transition effect "{}" not triggered'.format(transition)) diff --git a/bluebottle/time_based/models.py b/bluebottle/time_based/models.py index 166efb8e64..826d49f6b5 100644 --- a/bluebottle/time_based/models.py +++ b/bluebottle/time_based/models.py @@ -33,7 +33,10 @@ class TimeBasedActivity(Activity): (True, 'Yes, anywhere/online'), (False, 'No, enter a location') ) - capacity = models.PositiveIntegerField(_('attendee limit'), null=True, blank=True) + capacity = models.PositiveIntegerField( + _('attendee limit'), + help_text=_('Number of participants or teams that can join'), + null=True, blank=True) old_is_online = models.NullBooleanField( _('is online'), @@ -64,7 +67,10 @@ class TimeBasedActivity(Activity): on_delete=models.SET_NULL ) - review = models.NullBooleanField(_('review participants'), null=True, default=None) + review = models.NullBooleanField( + _('review participants'), + help_text=_('Activity manager accepts or rejects participants or teams'), + null=True, default=None) preparation = models.DurationField( _('Preparation time'), diff --git a/bluebottle/time_based/triggers.py b/bluebottle/time_based/triggers.py index 1d2798b0ea..f44c2068b9 100644 --- a/bluebottle/time_based/triggers.py +++ b/bluebottle/time_based/triggers.py @@ -62,6 +62,13 @@ def is_full(effect): """ the activity is full """ + if effect.instance.team_activity == 'teams': + accepted_teams = effect.instance.teams.filter(status__in=['open', 'running', 'finished']).count() + return ( + effect.instance.capacity and + effect.instance.capacity <= accepted_teams + ) + if ( isinstance(effect.instance, DateActivity) and effect.instance.slots.count() > 1 and @@ -86,8 +93,15 @@ def is_not_full(effect): """ the activity is not full """ + if effect.instance.team_activity == 'teams': + accepted_teams = effect.instance.teams.filter(status__in=['open', 'running', 'finished']).count() + return ( + not effect.instance.capacity or + effect.instance.capacity > accepted_teams + ) + return ( - effect.instance.capacity and + not effect.instance.capacity or effect.instance.capacity > len(effect.instance.accepted_participants) ) @@ -190,14 +204,20 @@ class TimeBasedTriggers(ActivityTriggers): ModelChangedTrigger( 'capacity', effects=[ - TransitionEffect(TimeBasedStateMachine.reopen, conditions=[ - is_not_full, - registration_deadline_is_not_passed - ]), - TransitionEffect(TimeBasedStateMachine.lock, conditions=[ - is_full, - registration_deadline_is_not_passed - ]), + TransitionEffect( + TimeBasedStateMachine.reopen, + conditions=[ + is_not_full, + registration_deadline_is_not_passed + ] + ), + TransitionEffect( + TimeBasedStateMachine.lock, + conditions=[ + is_full, + registration_deadline_is_not_passed + ] + ), ] ), @@ -990,6 +1010,12 @@ def activity_will_be_full(effect): the activity is full """ activity = effect.instance.activity + if activity.team_activity == 'teams': + accepted_teams = activity.teams.filter(status__in=['open', 'running', 'finished']).count() + return ( + activity.capacity and + activity.capacity <= accepted_teams + ) if ( isinstance(activity, DateActivity) and @@ -1009,8 +1035,15 @@ def activity_will_not_be_full(effect): the activity is full """ activity = effect.instance.activity + if activity.team_activity == 'teams': + accepted_teams = activity.teams.filter(status__in=['open', 'running', 'finished']).count() + return ( + not activity.capacity or + activity.capacity > accepted_teams + ) + return ( - activity.capacity and + not activity.capacity or activity.capacity >= len(activity.accepted_participants) ) @@ -1197,9 +1230,10 @@ class ParticipantTriggers(ContributorTriggers): RelatedTransitionEffect( 'activity', TimeBasedStateMachine.lock, - conditions=[activity_will_be_full] + conditions=[ + activity_will_be_full + ] ), - RelatedTransitionEffect( 'activity', TimeBasedStateMachine.succeed, @@ -1278,7 +1312,9 @@ class ParticipantTriggers(ContributorTriggers): RelatedTransitionEffect( 'activity', TimeBasedStateMachine.lock, - conditions=[activity_will_be_full] + conditions=[ + activity_will_be_full + ] ), RelatedTransitionEffect( 'activity', @@ -1376,7 +1412,6 @@ class ParticipantTriggers(ContributorTriggers): NotificationEffect(TeamMemberWithdrewMessage), ] ), - ] From 18b4a7dde13e1ff7bb879c3e6f033a4bf1127147 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Mon, 8 Aug 2022 09:38:39 +0200 Subject: [PATCH 080/112] Fix test and get keepdb from settings --- bluebottle/activities/triggers.py | 7 +++++-- bluebottle/test/test_runner.py | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/bluebottle/activities/triggers.py b/bluebottle/activities/triggers.py index 5989c2841b..406baad4d7 100644 --- a/bluebottle/activities/triggers.py +++ b/bluebottle/activities/triggers.py @@ -234,7 +234,7 @@ def team_activity_will_be_full(effect): """ activity = effect.instance.activity accepted_teams = activity.teams.filter(status__in=['open', 'running', 'finished']).count() + 1 - return ( + return not hasattr(activity, 'capacity') or ( activity.capacity and activity.capacity <= accepted_teams ) @@ -247,7 +247,10 @@ def team_activity_will_not_be_full(effect): activity = effect.instance.activity accepted_teams = activity.teams.filter(status__in=['open', 'running', 'finished']).count() - 1 - return not activity.capacity or activity.capacity > accepted_teams + return ( + not getattr(activity, 'capacity', False) or + activity.capacity > accepted_teams + ) @register(Team) diff --git a/bluebottle/test/test_runner.py b/bluebottle/test/test_runner.py index 02ad07d88e..994cb3f7bb 100644 --- a/bluebottle/test/test_runner.py +++ b/bluebottle/test/test_runner.py @@ -1,6 +1,7 @@ import locale from builtins import range +from django.conf import settings from django.db import connection, IntegrityError from django_slowtests.testrunner import DiscoverSlowestTestsRunner from djmoney.contrib.exchange.models import Rate, ExchangeBackend @@ -11,7 +12,7 @@ class MultiTenantRunner(DiscoverSlowestTestsRunner, InitProjectDataMixin): def setup_databases(self, *args, **kwargs): - + self.keepdb = getattr(kwargs, 'keepdb', getattr(settings, 'KEEPDB', False)) parallel = self.parallel self.parallel = 0 result = super(MultiTenantRunner, self).setup_databases(**kwargs) From 72c123a9a96999721432e4a00a42795cf1d860be Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Mon, 8 Aug 2022 10:00:34 +0200 Subject: [PATCH 081/112] Change settings --- bluebottle/test/test_runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bluebottle/test/test_runner.py b/bluebottle/test/test_runner.py index 994cb3f7bb..14c4b78f4c 100644 --- a/bluebottle/test/test_runner.py +++ b/bluebottle/test/test_runner.py @@ -12,7 +12,7 @@ class MultiTenantRunner(DiscoverSlowestTestsRunner, InitProjectDataMixin): def setup_databases(self, *args, **kwargs): - self.keepdb = getattr(kwargs, 'keepdb', getattr(settings, 'KEEPDB', False)) + self.keepdb = getattr(settings, 'KEEPDB', self.keepdb) parallel = self.parallel self.parallel = 0 result = super(MultiTenantRunner, self).setup_databases(**kwargs) From 446c10920d78e12903a6e5bf0cd2b9e41f09a488 Mon Sep 17 00:00:00 2001 From: Ernst Odolphi Date: Mon, 8 Aug 2022 10:58:01 +0200 Subject: [PATCH 082/112] Fix filter on status --- bluebottle/activities/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bluebottle/activities/views.py b/bluebottle/activities/views.py index 3433f3af31..26c04fac19 100644 --- a/bluebottle/activities/views.py +++ b/bluebottle/activities/views.py @@ -181,7 +181,7 @@ def get_queryset(self, *args, **kwargs): queryset = queryset.filter(status=status) elif has_slot == 'false': queryset = queryset.filter(slot__start__isnull=True).exclude( - status_in=['new', 'withdrawn', 'cancelled'] + status__in=['new', 'withdrawn', 'cancelled'] ) elif start == 'future': queryset = queryset.filter( From 8a2303b3d1e8fa32d0e8493fad2b190cb80b627c Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Mon, 8 Aug 2022 11:36:48 +0200 Subject: [PATCH 083/112] Fix some triggers --- bluebottle/time_based/triggers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bluebottle/time_based/triggers.py b/bluebottle/time_based/triggers.py index f44c2068b9..47231427a5 100644 --- a/bluebottle/time_based/triggers.py +++ b/bluebottle/time_based/triggers.py @@ -62,7 +62,7 @@ def is_full(effect): """ the activity is full """ - if effect.instance.team_activity == 'teams': + if getattr(effect.instance, 'team_activity', None) == 'teams': accepted_teams = effect.instance.teams.filter(status__in=['open', 'running', 'finished']).count() return ( effect.instance.capacity and @@ -93,7 +93,7 @@ def is_not_full(effect): """ the activity is not full """ - if effect.instance.team_activity == 'teams': + if getattr(effect.instance, 'team_activity', None) == 'teams': accepted_teams = effect.instance.teams.filter(status__in=['open', 'running', 'finished']).count() return ( not effect.instance.capacity or From 8d6a16ec23c305681bff4dcc7b2b3dda3cc00c76 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Mon, 8 Aug 2022 11:52:10 +0200 Subject: [PATCH 084/112] Fix statusses differently --- bluebottle/activities/tests/test_triggers.py | 4 --- bluebottle/activities/triggers.py | 27 ++++++++++---------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/bluebottle/activities/tests/test_triggers.py b/bluebottle/activities/tests/test_triggers.py index 96816096bd..150d71cb66 100644 --- a/bluebottle/activities/tests/test_triggers.py +++ b/bluebottle/activities/tests/test_triggers.py @@ -174,10 +174,6 @@ def test_fill_team_activity(self): captain.team, 'withdrawn', ) - self.assertStatus( - participant, - 'rejected', - ) self.assertStatus( self.activity, 'open', diff --git a/bluebottle/activities/triggers.py b/bluebottle/activities/triggers.py index 406baad4d7..2c1a2d5a3d 100644 --- a/bluebottle/activities/triggers.py +++ b/bluebottle/activities/triggers.py @@ -4,7 +4,8 @@ ) from bluebottle.activities.messages import ( TeamAddedMessage, TeamReopenedMessage, TeamAcceptedMessage, TeamAppliedMessage, - TeamWithdrawnMessage, TeamWithdrawnActivityOwnerMessage, TeamReappliedMessage + TeamWithdrawnMessage, TeamWithdrawnActivityOwnerMessage, TeamReappliedMessage, TeamCancelledMessage, + TeamCancelledTeamCaptainMessage ) from bluebottle.activities.models import Organizer, EffortContribution, Team from bluebottle.activities.states import ( @@ -299,28 +300,20 @@ class TeamTriggers(TriggerManager): TransitionTrigger( TeamStateMachine.cancel, effects=[ - # RelatedTransitionEffect( - # 'members', - # ParticipantStateMachine.remove - # ), RelatedTransitionEffect( 'activity', TimeBasedStateMachine.reopen, conditions=[team_activity_will_not_be_full] ), - # TeamContributionTransitionEffect(ContributionStateMachine.fail), - # NotificationEffect(TeamCancelledMessage), - # NotificationEffect(TeamCancelledTeamCaptainMessage) + TeamContributionTransitionEffect(ContributionStateMachine.fail), + NotificationEffect(TeamCancelledMessage), + NotificationEffect(TeamCancelledTeamCaptainMessage) ] ), TransitionTrigger( TeamStateMachine.withdraw, effects=[ - RelatedTransitionEffect( - 'members', - ParticipantStateMachine.remove - ), RelatedTransitionEffect( 'activity', TimeBasedStateMachine.reopen, @@ -338,7 +331,10 @@ class TeamTriggers(TriggerManager): NotificationEffect(TeamReopenedMessage), TeamContributionTransitionEffect( ContributionStateMachine.reset, - contribution_conditions=[activity_is_active, contributor_is_active] + contribution_conditions=[ + activity_is_active, + contributor_is_active + ] ), ] @@ -349,7 +345,10 @@ class TeamTriggers(TriggerManager): effects=[ TeamContributionTransitionEffect( ContributionStateMachine.reset, - contribution_conditions=[activity_is_active, contributor_is_active] + contribution_conditions=[ + activity_is_active, + contributor_is_active + ] ), NotificationEffect(TeamReappliedMessage), NotificationEffect(TeamAddedMessage) From e7715156e2524c9f7ca7c8b45e274301df271513 Mon Sep 17 00:00:00 2001 From: pieter Rees Date: Mon, 8 Aug 2022 14:49:07 +0200 Subject: [PATCH 085/112] changed one email --- .../activities/templates/mails/messages/team_added.html | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bluebottle/activities/templates/mails/messages/team_added.html b/bluebottle/activities/templates/mails/messages/team_added.html index 5c351f48fe..2dc00dd2e4 100644 --- a/bluebottle/activities/templates/mails/messages/team_added.html +++ b/bluebottle/activities/templates/mails/messages/team_added.html @@ -5,6 +5,13 @@

{% blocktrans context 'email' %}{{team_name}} has joined your activity "{{title}}".{% endblocktrans %}

+

+ {% blocktrans context 'email' %}Next steps{% endblocktrans %} +

+
    +
  1. {% blocktrans context 'email' %}Contact team captain (name of captain) to settle on a date, time and location for when they will participate in this activity.{% endblocktrans %}
  2. +
  3. {% blocktrans context 'email' %}Add this information to the team via the unscheduled team list on the activity page so it is visible for the team members.{% endblocktrans %}
  4. +

{% blocktrans context 'email' %}Please contact them to sort out any details via {{team_captain_email}}.{% endblocktrans %}

From 5a82ba3423b18e9cc18fc7b3ec2600f6589482ac Mon Sep 17 00:00:00 2001 From: pieter Rees Date: Mon, 8 Aug 2022 15:15:12 +0200 Subject: [PATCH 086/112] added team captain name and made the messages complete --- bluebottle/activities/messages.py | 3 ++- .../templates/mails/messages/team_added.html | 7 +++---- .../templates/mails/messages/team_applied.html | 12 ++++++++---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/bluebottle/activities/messages.py b/bluebottle/activities/messages.py index 9490a91b49..7fd95c8fbe 100644 --- a/bluebottle/activities/messages.py +++ b/bluebottle/activities/messages.py @@ -276,7 +276,8 @@ class TeamNotification(ActivityNotification): context = { 'title': 'activity.title', 'team_captain_email': 'owner.email', - 'team_name': 'name' + 'team_name': 'name', + 'team_captain_name': 'owner.full_name' } @property diff --git a/bluebottle/activities/templates/mails/messages/team_added.html b/bluebottle/activities/templates/mails/messages/team_added.html index 2dc00dd2e4..fbb76b8c8d 100644 --- a/bluebottle/activities/templates/mails/messages/team_added.html +++ b/bluebottle/activities/templates/mails/messages/team_added.html @@ -9,10 +9,9 @@ {% blocktrans context 'email' %}Next steps{% endblocktrans %}

    -
  1. {% blocktrans context 'email' %}Contact team captain (name of captain) to settle on a date, time and location for when they will participate in this activity.{% endblocktrans %}
  2. +
  3. {% blocktrans context 'email' %}Contact team captain {{team_captain_name}} to settle on a date, time and location for when they will participate in this activity.{% endblocktrans %}
  4. {% blocktrans context 'email' %}Add this information to the team via the unscheduled team list on the activity page so it is visible for the team members.{% endblocktrans %}
-

- {% blocktrans context 'email' %}Please contact them to sort out any details via {{team_captain_email}}.{% endblocktrans %} -

+

{% blocktrans context 'email' %}Team captain contact details:

+

{{team_captain_email}}

{% endblock %} diff --git a/bluebottle/activities/templates/mails/messages/team_applied.html b/bluebottle/activities/templates/mails/messages/team_applied.html index e9f0a6b812..bfc13f923f 100644 --- a/bluebottle/activities/templates/mails/messages/team_applied.html +++ b/bluebottle/activities/templates/mails/messages/team_applied.html @@ -6,9 +6,13 @@ {% blocktrans context 'email' %}{{team_name}} has applied to your activity "{{title}}".{% endblocktrans %}

- {% blocktrans context 'email' %}Please contact them to sort out any details via {{team_captain_email}}.{% endblocktrans %} -

-

- {% blocktrans context 'email' %}You can accept or reject the team on the activity page.{% endblocktrans %} + {% blocktrans context 'email' %}Next steps{% endblocktrans %}

+
    +
  1. {% blocktrans context 'email' %}Contact team captain {{team_captain_name}} to settle on a date, time and location for when they will participate in this activity.{% endblocktrans %}
  2. +
  3. {% blocktrans context 'email' %}Add this information to the team via the unscheduled team list on the activity page so it is visible for the team members.{% endblocktrans %}
  4. +
  5. {% blocktrans context 'email' %}Accept the team after you have added the details so when the team members are invited to join the activity it includes the time, date and location.{% endblocktrans %}
  6. +
+

{% blocktrans context 'email' %}Team captain contact details:

+

{{team_captain_email}}

{% endblock %} From 95fa0f0966c3b25466751847a64bee40facc9479 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Tue, 9 Aug 2022 10:39:03 +0200 Subject: [PATCH 087/112] Fix succeed flow --- bluebottle/time_based/effects.py | 4 +-- bluebottle/time_based/tests/test_api.py | 36 ++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/bluebottle/time_based/effects.py b/bluebottle/time_based/effects.py index e0917536ce..a59a1445a6 100644 --- a/bluebottle/time_based/effects.py +++ b/bluebottle/time_based/effects.py @@ -1,4 +1,4 @@ -from datetime import datetime, date +from datetime import datetime, date, timedelta from dateutil.relativedelta import relativedelta from django.db.models import F @@ -123,7 +123,7 @@ class SetEndDateEffect(Effect): template = 'admin/set_end_date.html' def pre_save(self, **kwargs): - self.instance.deadline = date.today() + self.instance.deadline = date.today() - timedelta(days=1) class ClearDeadlineEffect(Effect): diff --git a/bluebottle/time_based/tests/test_api.py b/bluebottle/time_based/tests/test_api.py index 89871ef4ee..992b5842ad 100644 --- a/bluebottle/time_based/tests/test_api.py +++ b/bluebottle/time_based/tests/test_api.py @@ -11,6 +11,7 @@ from openpyxl import load_workbook from rest_framework import status +from bluebottle.activities.tests.factories import TeamFactory from bluebottle.files.tests.factories import PrivateDocumentFactory from bluebottle.initiatives.models import InitiativePlatformSettings from bluebottle.initiatives.tests.factories import InitiativeFactory, InitiativePlatformSettingsFactory @@ -30,7 +31,6 @@ DateParticipantFactory, PeriodParticipantFactory, DateActivitySlotFactory, SlotParticipantFactory, SkillFactory, TeamSlotFactory ) -from bluebottle.activities.tests.factories import TeamFactory class TimeBasedListAPIViewTestCase(): @@ -955,6 +955,20 @@ def test_get_open(self): in self.data['meta']['transitions'] ) + def test_owner_succeed_manually(self): + self.initiative = InitiativeFactory.create(status='approved') + self.activity.initiative = self.initiative + self.activity.start = None + self.activity.deadline = None + self.activity.states.submit(save=True) + PeriodParticipantFactory.create(activity=self.activity) + response = self.client.get(self.url, user=self.activity.owner) + self.data = response.json()['data'] + self.assertTrue( + {'name': 'succeed_manually', 'target': 'succeeded', 'available': True} + in self.data['meta']['transitions'] + ) + def test_get_open_with_participant(self): self.activity.duration_period = 'weeks' self.activity.save() @@ -1287,6 +1301,26 @@ class PeriodTransitionAPIViewTestCase(TimeBasedTransitionAPIViewTestCase, Bluebo factory = PeriodActivityFactory participant_factory = PeriodParticipantFactory + def test_succeed_manually(self): + self.activity.start = None + self.activity.deadline = None + self.activity.initiative.states.submit() + self.activity.initiative.states.approve(save=True) + self.activity.states.submit(save=True) + PeriodParticipantFactory.create(activity=self.activity) + + self.data['data']['attributes']['transition'] = 'succeed_manually' + + response = self.client.post( + self.url, + json.dumps(self.data), + user=self.activity.owner + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.activity.refresh_from_db() + self.assertEqual(self.activity.status, 'succeeded') + self.assertIsNotNone(self.activity.deadline) + class DateActivitySlotListAPITestCase(BluebottleTestCase): def setUp(self): From d2fe15d809c2fe1bf86de16f37e4ee060fef1dcc Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Tue, 9 Aug 2022 11:39:33 +0200 Subject: [PATCH 088/112] Fix tests --- bluebottle/time_based/tests/test_triggers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bluebottle/time_based/tests/test_triggers.py b/bluebottle/time_based/tests/test_triggers.py index c50c753eb4..5f21af134b 100644 --- a/bluebottle/time_based/tests/test_triggers.py +++ b/bluebottle/time_based/tests/test_triggers.py @@ -556,7 +556,7 @@ def test_succeed_manually(self): self.activity.refresh_from_db() self.activity.states.succeed_manually(save=True) - self.assertEqual(self.activity.deadline, date.today()) + self.assertEqual(self.activity.deadline, date.today() - timedelta(days=1)) for duration in self.activity.durations: self.assertEqual(duration.status, 'succeeded') @@ -587,7 +587,7 @@ def test_succeed_manually_review_new(self): mail.outbox = [] self.activity.states.succeed_manually(save=True) - self.assertEqual(self.activity.deadline, date.today()) + self.assertEqual(self.activity.deadline, date.today() - timedelta(days=1)) for duration in self.activity.durations: self.assertEqual(duration.status, 'succeeded') From c18a0bb1ab6d9353b9a2608fc1655e4c085df698 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Tue, 9 Aug 2022 15:13:21 +0200 Subject: [PATCH 089/112] Add tests for accept mails --- bluebottle/activities/messages.py | 2 +- .../mails/messages/team_accepted.html | 2 +- bluebottle/time_based/tests/test_api.py | 76 +++++++++++++++++++ 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/bluebottle/activities/messages.py b/bluebottle/activities/messages.py index 9490a91b49..6f7e96a7c8 100644 --- a/bluebottle/activities/messages.py +++ b/bluebottle/activities/messages.py @@ -286,7 +286,7 @@ def action_link(self): action_title = pgettext('email', 'View activity') def get_recipients(self): - """activity mananager""" + """activity manager""" return [self.obj.activity.owner] diff --git a/bluebottle/activities/templates/mails/messages/team_accepted.html b/bluebottle/activities/templates/mails/messages/team_accepted.html index 88a9952d29..816a9aadb1 100644 --- a/bluebottle/activities/templates/mails/messages/team_accepted.html +++ b/bluebottle/activities/templates/mails/messages/team_accepted.html @@ -1,7 +1,7 @@ {% extends "mails/messages/activity_base.html" %} {% load i18n %} -{% block message%} +{% block message %}

{% if custom_message %} {{custom_message|linebreaks}} diff --git a/bluebottle/time_based/tests/test_api.py b/bluebottle/time_based/tests/test_api.py index 89871ef4ee..86c51d4358 100644 --- a/bluebottle/time_based/tests/test_api.py +++ b/bluebottle/time_based/tests/test_api.py @@ -6,6 +6,7 @@ import icalendar from django.contrib.auth.models import Group, Permission from django.contrib.gis.geos import Point +from django.core import mail from django.urls import reverse from django.utils.timezone import now, utc from openpyxl import load_workbook @@ -2132,6 +2133,81 @@ class PeriodParticipantTransitionAPIViewTestCase(ParticipantTransitionAPIViewTes factory = PeriodActivityFactory participant_factory = PeriodParticipantFactory + def test_accept_by_owner(self): + self.participant.status = 'new' + self.participant.save() + self.activity.review = True + self.activity.save() + self.data['data']['attributes']['transition'] = 'accept' + mail.outbox = [] + + response = self.client.post( + self.url, + json.dumps(self.data), + user=self.activity.owner + ) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + data = json.loads(response.content) + self.assertEqual(data['included'][0]['attributes']['status'], 'accepted') + message = mail.outbox[0] + self.assertEqual(message.subject, 'Yea') + + def test_accept_with_custom_message(self): + self.participant.status = 'new' + self.participant.save() + self.activity.review = True + self.activity.save() + self.data['data']['attributes']['transition'] = 'accept' + self.data['data']['attributes']['message'] = 'Great to have you!' + mail.outbox = [] + response = self.client.post( + self.url, + json.dumps(self.data), + user=self.activity.owner + ) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + data = json.loads(response.content) + self.assertEqual(data['included'][0]['attributes']['status'], 'accepted') + message = mail.outbox[0] + self.assertEqual( + message.subject, + f'You have been selected for the activity "{self.activity.title}" 🎉' + ) + self.assertTrue('Great to have you!' in message.body) + + def test_accept_team_with_custom_message(self): + self.activity.team_activity = 'teams' + self.activity.review = True + self.activity.save() + self.participant.team = TeamFactory.create( + activity=self.activity + ) + self.participant.status = 'new' + self.participant.save() + self.data['data']['attributes']['transition'] = 'accept' + self.data['data']['attributes']['message'] = 'Great to have you!' + mail.outbox = [] + response = self.client.post( + self.url, + json.dumps(self.data), + user=self.activity.owner + ) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + data = json.loads(response.content) + self.assertEqual(data['included'][1]['attributes']['status'], 'accepted') + message = mail.outbox[0] + self.assertEqual( + message.subject, + f'Your team has been accepted for "{self.activity.title}"' + ) + self.assertTrue('Great to have you!' in message.body) + class ReviewParticipantTransitionAPIViewTestCase(): def setUp(self): From 60ad22821bca49b709e528e4c5fb43e57cba0b77 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 10 Aug 2022 09:32:50 +0200 Subject: [PATCH 090/112] Fix template --- .../activities/templates/mails/messages/team_applied.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bluebottle/activities/templates/mails/messages/team_applied.html b/bluebottle/activities/templates/mails/messages/team_applied.html index bfc13f923f..71d4fe13e5 100644 --- a/bluebottle/activities/templates/mails/messages/team_applied.html +++ b/bluebottle/activities/templates/mails/messages/team_applied.html @@ -13,6 +13,6 @@

  • {% blocktrans context 'email' %}Add this information to the team via the unscheduled team list on the activity page so it is visible for the team members.{% endblocktrans %}
  • {% blocktrans context 'email' %}Accept the team after you have added the details so when the team members are invited to join the activity it includes the time, date and location.{% endblocktrans %}
  • -

    {% blocktrans context 'email' %}Team captain contact details:

    +

    {% blocktrans context 'email' %}Team captain contact details:{% endblocktrans %}

    {{team_captain_email}}

    {% endblock %} From ed3f46a12b861b46119e96a9d734b5173bd6a4a7 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 10 Aug 2022 11:54:27 +0200 Subject: [PATCH 091/112] Fix team accepted message --- bluebottle/activities/messages.py | 10 ++++++++-- bluebottle/activities/triggers.py | 6 +----- bluebottle/time_based/tests/test_api.py | 8 ++++++-- bluebottle/time_based/triggers.py | 11 +++++++++-- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/bluebottle/activities/messages.py b/bluebottle/activities/messages.py index 6f7e96a7c8..4ff96e8cce 100644 --- a/bluebottle/activities/messages.py +++ b/bluebottle/activities/messages.py @@ -300,13 +300,19 @@ class TeamAppliedMessage(TeamNotification): template = 'messages/team_applied' -class TeamAcceptedMessage(TeamNotification): +class TeamCaptainAcceptedMessage(TeamNotification): subject = pgettext('email', 'Your team has been accepted for "{title}"') template = 'messages/team_accepted' + context = { + 'title': 'activity.title', + 'team_captain_email': 'team.owner.email', + 'team_name': 'team.name' + } + def get_recipients(self): """team captain""" - return [self.obj.owner] + return [self.obj.user] class TeamCancelledMessage(TeamNotification): diff --git a/bluebottle/activities/triggers.py b/bluebottle/activities/triggers.py index 2c1a2d5a3d..0a64e9d61e 100644 --- a/bluebottle/activities/triggers.py +++ b/bluebottle/activities/triggers.py @@ -3,7 +3,7 @@ TeamContributionTransitionEffect, ResetTeamParticipantsEffect ) from bluebottle.activities.messages import ( - TeamAddedMessage, TeamReopenedMessage, TeamAcceptedMessage, TeamAppliedMessage, + TeamAddedMessage, TeamReopenedMessage, TeamAppliedMessage, TeamWithdrawnMessage, TeamWithdrawnActivityOwnerMessage, TeamReappliedMessage, TeamCancelledMessage, TeamCancelledTeamCaptainMessage ) @@ -280,10 +280,6 @@ class TeamTriggers(TriggerManager): TransitionTrigger( TeamStateMachine.accept, effects=[ - NotificationEffect( - TeamAcceptedMessage, - conditions=[needs_review] - ), RelatedTransitionEffect( 'members', ParticipantStateMachine.accept, diff --git a/bluebottle/time_based/tests/test_api.py b/bluebottle/time_based/tests/test_api.py index 86c51d4358..6b1bef27e3 100644 --- a/bluebottle/time_based/tests/test_api.py +++ b/bluebottle/time_based/tests/test_api.py @@ -2152,7 +2152,10 @@ def test_accept_by_owner(self): data = json.loads(response.content) self.assertEqual(data['included'][0]['attributes']['status'], 'accepted') message = mail.outbox[0] - self.assertEqual(message.subject, 'Yea') + self.assertEqual( + message.subject, + f'You have been selected for the activity "{self.activity.title}" 🎉' + ) def test_accept_with_custom_message(self): self.participant.status = 'new' @@ -2184,7 +2187,8 @@ def test_accept_team_with_custom_message(self): self.activity.review = True self.activity.save() self.participant.team = TeamFactory.create( - activity=self.activity + activity=self.activity, + owner=self.participant.user ) self.participant.status = 'new' self.participant.save() diff --git a/bluebottle/time_based/triggers.py b/bluebottle/time_based/triggers.py index ed6e4810e2..6196756ee3 100644 --- a/bluebottle/time_based/triggers.py +++ b/bluebottle/time_based/triggers.py @@ -8,7 +8,7 @@ ActivityExpiredNotification, ActivityRejectedNotification, ActivityCancelledNotification, ActivityRestoredNotification, ParticipantWithdrewConfirmationNotification, - TeamMemberWithdrewMessage, TeamMemberRemovedMessage + TeamMemberWithdrewMessage, TeamMemberRemovedMessage, TeamCaptainAcceptedMessage ) from bluebottle.activities.states import OrganizerStateMachine, TeamStateMachine from bluebottle.activities.triggers import ( @@ -1313,7 +1313,14 @@ class ParticipantTriggers(ContributorTriggers): ParticipantAcceptedNotification, conditions=[ needs_review, - not_team_captain + is_not_team_activity + ] + ), + NotificationEffect( + TeamCaptainAcceptedMessage, + conditions=[ + needs_review, + is_team_captain ] ), RelatedTransitionEffect( From 8dba5c837d690b8ec9914802dab35372fc382619 Mon Sep 17 00:00:00 2001 From: Ernst Odolphi Date: Wed, 10 Aug 2022 11:55:26 +0200 Subject: [PATCH 092/112] Close blocktrans block correctly --- bluebottle/activities/templates/mails/messages/team_added.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bluebottle/activities/templates/mails/messages/team_added.html b/bluebottle/activities/templates/mails/messages/team_added.html index fbb76b8c8d..c7c2b1a624 100644 --- a/bluebottle/activities/templates/mails/messages/team_added.html +++ b/bluebottle/activities/templates/mails/messages/team_added.html @@ -12,6 +12,6 @@
  • {% blocktrans context 'email' %}Contact team captain {{team_captain_name}} to settle on a date, time and location for when they will participate in this activity.{% endblocktrans %}
  • {% blocktrans context 'email' %}Add this information to the team via the unscheduled team list on the activity page so it is visible for the team members.{% endblocktrans %}
  • -

    {% blocktrans context 'email' %}Team captain contact details:

    +

    {% blocktrans context 'email' %}Team captain contact details:{% endblocktrans %}

    {{team_captain_email}}

    {% endblock %} From 3dc7d68756cd197626eb62993f3158c1b32e303a Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 10 Aug 2022 12:12:37 +0200 Subject: [PATCH 093/112] Also fix custom message for team reject --- bluebottle/activities/messages.py | 10 ++- .../messages/team_cancelled_team_captain.html | 6 +- bluebottle/activities/triggers.py | 4 +- bluebottle/time_based/tests/test_api.py | 79 +++++++++++++++++++ bluebottle/time_based/triggers.py | 9 ++- 5 files changed, 101 insertions(+), 7 deletions(-) diff --git a/bluebottle/activities/messages.py b/bluebottle/activities/messages.py index 4ff96e8cce..4b45329941 100644 --- a/bluebottle/activities/messages.py +++ b/bluebottle/activities/messages.py @@ -327,12 +327,18 @@ def get_recipients(self): class TeamCancelledTeamCaptainMessage(TeamNotification): - subject = pgettext('email', "Your team has been rejected for '{title}'") + subject = pgettext('email', 'Your team has been rejected for "{title}"') template = 'messages/team_cancelled_team_captain' + context = { + 'title': 'activity.title', + 'team_captain_email': 'team.owner.email', + 'team_name': 'team.name' + } + def get_recipients(self): """team captain""" - return [self.obj.owner] + return [self.obj.user] class TeamWithdrawnMessage(TeamNotification): diff --git a/bluebottle/activities/templates/mails/messages/team_cancelled_team_captain.html b/bluebottle/activities/templates/mails/messages/team_cancelled_team_captain.html index 24ddad4002..16be1d0c04 100644 --- a/bluebottle/activities/templates/mails/messages/team_cancelled_team_captain.html +++ b/bluebottle/activities/templates/mails/messages/team_cancelled_team_captain.html @@ -3,6 +3,10 @@ {% block message%}

    - {% blocktrans context 'email' %}Unfortunately, your team has been rejected for the activity '{{title}}'.{% endblocktrans %} + {% if custom_message %} + {{custom_message|linebreaks}} + {% else %} + {% blocktrans context 'email' %}Unfortunately, your team has been rejected for the activity '{{title}}'.{% endblocktrans %} + {% endif %}

    {% endblock %} diff --git a/bluebottle/activities/triggers.py b/bluebottle/activities/triggers.py index 0a64e9d61e..b4ca2ed50e 100644 --- a/bluebottle/activities/triggers.py +++ b/bluebottle/activities/triggers.py @@ -4,8 +4,7 @@ ) from bluebottle.activities.messages import ( TeamAddedMessage, TeamReopenedMessage, TeamAppliedMessage, - TeamWithdrawnMessage, TeamWithdrawnActivityOwnerMessage, TeamReappliedMessage, TeamCancelledMessage, - TeamCancelledTeamCaptainMessage + TeamWithdrawnMessage, TeamWithdrawnActivityOwnerMessage, TeamReappliedMessage, TeamCancelledMessage ) from bluebottle.activities.models import Organizer, EffortContribution, Team from bluebottle.activities.states import ( @@ -303,7 +302,6 @@ class TeamTriggers(TriggerManager): ), TeamContributionTransitionEffect(ContributionStateMachine.fail), NotificationEffect(TeamCancelledMessage), - NotificationEffect(TeamCancelledTeamCaptainMessage) ] ), diff --git a/bluebottle/time_based/tests/test_api.py b/bluebottle/time_based/tests/test_api.py index 6b1bef27e3..d92861e784 100644 --- a/bluebottle/time_based/tests/test_api.py +++ b/bluebottle/time_based/tests/test_api.py @@ -2212,6 +2212,85 @@ def test_accept_team_with_custom_message(self): ) self.assertTrue('Great to have you!' in message.body) + def test_reject_by_owner(self): + self.participant.status = 'new' + self.participant.save() + self.activity.review = True + self.activity.save() + self.data['data']['attributes']['transition'] = 'reject' + mail.outbox = [] + + response = self.client.post( + self.url, + json.dumps(self.data), + user=self.activity.owner + ) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + data = json.loads(response.content) + self.assertEqual(data['included'][0]['attributes']['status'], 'rejected') + message = mail.outbox[0] + self.assertEqual( + message.subject, + f'You have not been selected for the activity "{self.activity.title}"' + ) + + def test_reject_with_custom_message(self): + self.participant.status = 'new' + self.participant.save() + self.activity.review = True + self.activity.save() + self.data['data']['attributes']['transition'] = 'reject' + self.data['data']['attributes']['message'] = 'Go away!' + mail.outbox = [] + response = self.client.post( + self.url, + json.dumps(self.data), + user=self.activity.owner + ) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + data = json.loads(response.content) + self.assertEqual(data['included'][0]['attributes']['status'], 'rejected') + message = mail.outbox[0] + self.assertEqual( + message.subject, + f'You have not been selected for the activity "{self.activity.title}"' + ) + self.assertTrue('Go away!' in message.body) + + def test_reject_team_with_custom_message(self): + self.activity.team_activity = 'teams' + self.activity.review = True + self.activity.save() + self.participant.team = TeamFactory.create( + activity=self.activity, + owner=self.participant.user + ) + self.participant.status = 'new' + self.participant.save() + self.data['data']['attributes']['transition'] = 'reject' + self.data['data']['attributes']['message'] = 'Go away!' + mail.outbox = [] + response = self.client.post( + self.url, + json.dumps(self.data), + user=self.activity.owner + ) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + data = json.loads(response.content) + self.assertEqual(data['included'][1]['attributes']['status'], 'rejected') + message = mail.outbox[0] + self.assertEqual( + message.subject, + f'Your team has been rejected for "{self.activity.title}"' + ) + self.assertTrue('Go away!' in message.body) + class ReviewParticipantTransitionAPIViewTestCase(): def setUp(self): diff --git a/bluebottle/time_based/triggers.py b/bluebottle/time_based/triggers.py index 6196756ee3..3ccbae58a5 100644 --- a/bluebottle/time_based/triggers.py +++ b/bluebottle/time_based/triggers.py @@ -8,7 +8,7 @@ ActivityExpiredNotification, ActivityRejectedNotification, ActivityCancelledNotification, ActivityRestoredNotification, ParticipantWithdrewConfirmationNotification, - TeamMemberWithdrewMessage, TeamMemberRemovedMessage, TeamCaptainAcceptedMessage + TeamMemberWithdrewMessage, TeamMemberRemovedMessage, TeamCaptainAcceptedMessage, TeamCancelledTeamCaptainMessage ) from bluebottle.activities.states import OrganizerStateMachine, TeamStateMachine from bluebottle.activities.triggers import ( @@ -1362,6 +1362,13 @@ class ParticipantTriggers(ContributorTriggers): not_team_captain ] ), + NotificationEffect( + TeamCancelledTeamCaptainMessage, + conditions=[ + is_team_captain + ] + ), + RelatedTransitionEffect( 'team', TeamStateMachine.cancel, From 4a7ac1391a73351e23c33949539cb1aecf772846 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 10 Aug 2022 12:16:01 +0200 Subject: [PATCH 094/112] Fix test --- bluebottle/activities/tests/test_notifications.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/bluebottle/activities/tests/test_notifications.py b/bluebottle/activities/tests/test_notifications.py index 6ab302645e..33245a88fa 100644 --- a/bluebottle/activities/tests/test_notifications.py +++ b/bluebottle/activities/tests/test_notifications.py @@ -2,10 +2,10 @@ ActivityRejectedNotification, ActivityCancelledNotification, ActivitySucceededNotification, ActivityRestoredNotification, ActivityExpiredNotification, TeamAddedMessage, - TeamAppliedMessage, TeamAcceptedMessage, TeamCancelledMessage, + TeamAppliedMessage, TeamCancelledMessage, TeamCancelledTeamCaptainMessage, TeamWithdrawnActivityOwnerMessage, TeamWithdrawnMessage, TeamMemberAddedMessage, TeamMemberWithdrewMessage, - TeamMemberRemovedMessage, TeamReappliedMessage + TeamMemberRemovedMessage, TeamReappliedMessage, TeamCaptainAcceptedMessage ) from bluebottle.activities.tests.factories import TeamFactory from bluebottle.test.factory_models.accounts import BlueBottleUserFactory @@ -116,11 +116,16 @@ def test_team_applied_notification(self): self.assertActionTitle('View activity') def test_team_accepted_notification(self): + self.obj = PeriodParticipantFactory.create( + user=self.captain, + activity=self.activity, + team=self.obj + ) self.activity.review = True self.activity.save() - self.message_class = TeamAcceptedMessage + self.message_class = TeamCaptainAcceptedMessage self.create() - self.assertRecipients([self.obj.owner]) + self.assertRecipients([self.obj.user]) self.assertSubject("Your team has been accepted for \"Save the world!\"") self.assertBodyContains('On the activity page you will find the link to invite your team members.') self.assertBodyContains(f"Your team has been accepted for the activity '{self.activity.title}'.") From 108d351ba7dc6796695446bb005a4990049c98ce Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 10 Aug 2022 12:24:35 +0200 Subject: [PATCH 095/112] Fix one more test --- bluebottle/activities/tests/test_notifications.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bluebottle/activities/tests/test_notifications.py b/bluebottle/activities/tests/test_notifications.py index 33245a88fa..6237a0a3c7 100644 --- a/bluebottle/activities/tests/test_notifications.py +++ b/bluebottle/activities/tests/test_notifications.py @@ -145,10 +145,15 @@ def test_team_cancelled_notification(self): self.assertActionTitle('View activity') def test_team_cancelled_team_captain_notification(self): + self.obj = PeriodParticipantFactory.create( + user=self.captain, + activity=self.activity, + team=self.obj + ) self.message_class = TeamCancelledTeamCaptainMessage self.create() - self.assertRecipients([self.obj.owner]) - self.assertSubject("Your team has been rejected for 'Save the world!'") + self.assertRecipients([self.obj.user]) + self.assertSubject('Your team has been rejected for "Save the world!"') self.assertHtmlBodyContains( "Unfortunately, your team has been rejected for the activity 'Save the world!'." ) From 06b39c6a71af01c56cdad2c274259a7a6f68a0d0 Mon Sep 17 00:00:00 2001 From: Ernst Odolphi Date: Wed, 10 Aug 2022 14:24:58 +0200 Subject: [PATCH 096/112] Do not point team ical link to date activity slot ical url --- bluebottle/time_based/serializers.py | 2 +- bluebottle/time_based/tests/test_api.py | 4 ++++ bluebottle/time_based/urls/api.py | 7 ++++++- bluebottle/time_based/views.py | 20 +++++++++++++++----- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/bluebottle/time_based/serializers.py b/bluebottle/time_based/serializers.py index 3025973bd9..e5ee9b1d5b 100644 --- a/bluebottle/time_based/serializers.py +++ b/bluebottle/time_based/serializers.py @@ -194,7 +194,7 @@ class TeamSlotSerializer(ActivitySlotSerializer): def get_links(self, instance): if instance.start and instance.duration: return { - 'ical': reverse_signed('slot-ical', args=(instance.pk, )), + 'ical': reverse_signed('team-ical', args=(instance.pk, )), 'google': instance.google_calendar_link, } else: diff --git a/bluebottle/time_based/tests/test_api.py b/bluebottle/time_based/tests/test_api.py index 992b5842ad..1e03441421 100644 --- a/bluebottle/time_based/tests/test_api.py +++ b/bluebottle/time_based/tests/test_api.py @@ -1178,6 +1178,10 @@ def test_create_team_slot(self): self.perform_create(user=self.manager) self.assertStatus(status.HTTP_201_CREATED) + ical_response = self.client.get(self.response.json()['links']['ical']) + + self.assertEqual(ical_response.status_code, status.HTTP_200_OK) + def test_create_team_slot_missing_start(self): self.defaults['start'] = None self.perform_create(user=self.manager) diff --git a/bluebottle/time_based/urls/api.py b/bluebottle/time_based/urls/api.py index e7265814f6..7790f9008b 100644 --- a/bluebottle/time_based/urls/api.py +++ b/bluebottle/time_based/urls/api.py @@ -12,7 +12,8 @@ TimeContributionDetail, DateSlotDetailView, DateSlotListView, SlotParticipantListView, SlotParticipantDetailView, SlotParticipantTransitionList, - DateActivityIcalView, ActivitySlotIcalView, DateParticipantExportView, PeriodParticipantExportView, + DateActivityIcalView, ActivitySlotIcalView, TeamSlotIcalView, + DateParticipantExportView, PeriodParticipantExportView, SlotRelatedParticipantList, SkillList, SkillDetail, RelatedSlotParticipantListView, TeamSlotListView, TeamSlotDetailView ) @@ -54,6 +55,10 @@ ActivitySlotIcalView.as_view(), name='slot-ical'), + url(r'^/team/ical/(?P\d+)$', + TeamSlotIcalView.as_view(), + name='team-ical'), + url(r'^/period/(?P\d+)$', PeriodActivityDetailView.as_view(), name='period-detail'), diff --git a/bluebottle/time_based/views.py b/bluebottle/time_based/views.py index d942d47b31..c3c89d07a7 100644 --- a/bluebottle/time_based/views.py +++ b/bluebottle/time_based/views.py @@ -507,11 +507,7 @@ def get(self, *args, **kwargs): return response -class ActivitySlotIcalView(PrivateFileView): - queryset = DateActivitySlot.objects.exclude( - status__in=['cancelled', 'deleted', 'rejected'], - activity__status__in=['cancelled', 'deleted', 'rejected'], - ) +class BaseSlotIcalView(PrivateFileView): max_age = 30 * 60 # half an hour @@ -549,6 +545,20 @@ def get(self, *args, **kwargs): return response +class ActivitySlotIcalView(BaseSlotIcalView): + queryset = DateActivitySlot.objects.exclude( + status__in=['cancelled', 'deleted', 'rejected'], + activity__status__in=['cancelled', 'deleted', 'rejected'], + ) + + +class TeamSlotIcalView(BaseSlotIcalView): + queryset = TeamSlot.objects.exclude( + status__in=['cancelled', 'deleted', 'rejected'], + activity__status__in=['cancelled', 'deleted', 'rejected'], + ) + + class DateParticipantExportView(ExportView): filename = "participants" From ab1cf3b62ea6a5477d7cd0caf0d47f13de3e7132 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 10 Aug 2022 14:41:38 +0200 Subject: [PATCH 097/112] Fix test --- bluebottle/activities/tests/test_triggers.py | 22 ++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/bluebottle/activities/tests/test_triggers.py b/bluebottle/activities/tests/test_triggers.py index 150d71cb66..74e613013c 100644 --- a/bluebottle/activities/tests/test_triggers.py +++ b/bluebottle/activities/tests/test_triggers.py @@ -5,7 +5,7 @@ from bluebottle.activities.messages import ( TeamAddedMessage, TeamCancelledMessage, TeamReopenedMessage, - TeamAppliedMessage, TeamAcceptedMessage, TeamCancelledTeamCaptainMessage, TeamWithdrawnMessage, + TeamAppliedMessage, TeamCaptainAcceptedMessage, TeamCancelledTeamCaptainMessage, TeamWithdrawnMessage, TeamWithdrawnActivityOwnerMessage, TeamReappliedMessage ) from bluebottle.activities.effects import TeamContributionTransitionEffect, ResetTeamParticipantsEffect @@ -71,7 +71,25 @@ def test_accept(self): with self.execute(message=message): self.assertEqual(self.model.status, 'open') - self.assertNotificationEffect(TeamAcceptedMessage) + self.model.save() + + def test_accept_team_captain(self): + self.activity.review = True + self.activity.save() + captain = BlueBottleUserFactory.create() + self.model = PeriodParticipantFactory.build( + activity=self.activity, + user=captain, + as_relation='user' + ) + self.model.save() + self.model.states.accept() + + message = 'You were accepted, because you were great' + + with self.execute(message=message): + self.assertEqual(self.model.status, 'open') + self.assertNotificationEffect(TeamCaptainAcceptedMessage) self.assertEqual( self.effects[0].options['message'], message ) From 7aa41d1289b3c3875f0f4e7fe89590d74e5354a7 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 10 Aug 2022 14:54:48 +0200 Subject: [PATCH 098/112] More test to be fixed --- bluebottle/activities/tests/test_triggers.py | 50 +++++++++++++------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/bluebottle/activities/tests/test_triggers.py b/bluebottle/activities/tests/test_triggers.py index 74e613013c..1dca2ff7a8 100644 --- a/bluebottle/activities/tests/test_triggers.py +++ b/bluebottle/activities/tests/test_triggers.py @@ -1,19 +1,15 @@ -from django.core import mail - -from bluebottle.test.utils import TriggerTestCase -from bluebottle.test.factory_models.accounts import BlueBottleUserFactory - +from bluebottle.activities.effects import TeamContributionTransitionEffect, ResetTeamParticipantsEffect from bluebottle.activities.messages import ( TeamAddedMessage, TeamCancelledMessage, TeamReopenedMessage, TeamAppliedMessage, TeamCaptainAcceptedMessage, TeamCancelledTeamCaptainMessage, TeamWithdrawnMessage, TeamWithdrawnActivityOwnerMessage, TeamReappliedMessage ) -from bluebottle.activities.effects import TeamContributionTransitionEffect, ResetTeamParticipantsEffect -from bluebottle.time_based.models import PeriodParticipant from bluebottle.activities.tests.factories import TeamFactory - -from bluebottle.time_based.tests.factories import PeriodActivityFactory, PeriodParticipantFactory +from bluebottle.test.factory_models.accounts import BlueBottleUserFactory +from bluebottle.test.utils import TriggerTestCase +from bluebottle.time_based.models import PeriodParticipant from bluebottle.time_based.states import TimeContributionStateMachine +from bluebottle.time_based.tests.factories import PeriodActivityFactory, PeriodParticipantFactory class TeamTriggersTestCase(TriggerTestCase): @@ -77,25 +73,28 @@ def test_accept_team_captain(self): self.activity.review = True self.activity.save() captain = BlueBottleUserFactory.create() - self.model = PeriodParticipantFactory.build( + self.model = PeriodParticipantFactory.create( activity=self.activity, + team=TeamFactory.create( + activity=self.activity, + owner=captain + ), user=captain, as_relation='user' ) - self.model.save() self.model.states.accept() message = 'You were accepted, because you were great' with self.execute(message=message): - self.assertEqual(self.model.status, 'open') + self.assertEqual(self.model.status, 'accepted') + self.assertEqual(self.model.team.status, 'open') self.assertNotificationEffect(TeamCaptainAcceptedMessage) self.assertEqual( self.effects[0].options['message'], message ) self.model.save() - self.assertTrue(message in mail.outbox[-1].body) def test_cancel(self): self.create() @@ -111,9 +110,6 @@ def test_cancel(self): self.assertNotificationEffect( TeamCancelledMessage, [other_participant.user] ) - self.assertNotificationEffect( - TeamCancelledTeamCaptainMessage, [self.model.owner] - ) self.model.save() self.participant.refresh_from_db() @@ -121,6 +117,28 @@ def test_cancel(self): for contribution in self.participant.contributions.all(): self.assertEqual(contribution.status, TimeContributionStateMachine.failed.value) + def test_cancel_team_captain(self): + self.activity.review = True + self.activity.save() + + captain = BlueBottleUserFactory.create() + self.model = PeriodParticipantFactory.create( + activity=self.activity, + team=TeamFactory.create( + activity=self.activity, + owner=captain + ), + user=captain, + as_relation='user', + status='new' + ) + self.model.states.reject() + + with self.execute(): + self.assertNotificationEffect( + TeamCancelledTeamCaptainMessage, [self.model.owner] + ) + def test_withdrawn(self): self.create() From bfe6f7ec765e043388e4cb9a9a3d0deee90b1865 Mon Sep 17 00:00:00 2001 From: Ernst Odolphi Date: Wed, 10 Aug 2022 15:12:13 +0200 Subject: [PATCH 099/112] Fix tests --- bluebottle/activities/tests/test_notifications.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/bluebottle/activities/tests/test_notifications.py b/bluebottle/activities/tests/test_notifications.py index 6ab302645e..b64815eff7 100644 --- a/bluebottle/activities/tests/test_notifications.py +++ b/bluebottle/activities/tests/test_notifications.py @@ -97,7 +97,10 @@ def test_team_added_notification(self): self.assertRecipients([self.activity.owner]) self.assertSubject("A new team has joined \"Save the world!\"") self.assertTextBodyContains("Team William Shatner has joined your activity \"Save the world!\".") - self.assertBodyContains('Please contact them to sort out any details via kirk@enterprise.com.') + self.assertBodyContains( + 'Add this information to the team via the unscheduled team list on the activity ' + 'page so it is visible for the team members.' + ) self.assertActionLink(self.obj.activity.get_absolute_url()) self.assertActionTitle('View activity') @@ -109,8 +112,10 @@ def test_team_applied_notification(self): self.assertRecipients([self.activity.owner]) self.assertSubject("A new team has applied to \"Save the world!\"") self.assertTextBodyContains("Team William Shatner has applied to your activity \"Save the world!\".") - self.assertBodyContains('Please contact them to sort out any details via kirk@enterprise.com.') - self.assertBodyContains('You can accept or reject the team on the activity page.') + self.assertBodyContains( + 'Add this information to the team via the unscheduled team list on the activity ' + 'page so it is visible for the team members.' + ) self.assertActionLink(self.obj.activity.get_absolute_url()) self.assertActionTitle('View activity') From e9fa27f3d764107f9efd93c908b4c8002b2e489e Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Thu, 11 Aug 2022 13:54:35 +0200 Subject: [PATCH 100/112] Succeed team members + tests --- bluebottle/activities/triggers.py | 42 ++++++++++-- .../time_based/tests/test_periodic_tasks.py | 64 +++++++++++++++++++ 2 files changed, 102 insertions(+), 4 deletions(-) diff --git a/bluebottle/activities/triggers.py b/bluebottle/activities/triggers.py index b4ca2ed50e..3818c87834 100644 --- a/bluebottle/activities/triggers.py +++ b/bluebottle/activities/triggers.py @@ -17,7 +17,7 @@ ) from bluebottle.impact.effects import UpdateImpactGoalEffect from bluebottle.notifications.effects import NotificationEffect -from bluebottle.time_based.states import ParticipantStateMachine, TimeBasedStateMachine +from bluebottle.time_based.states import ParticipantStateMachine, TimeBasedStateMachine, TeamSlotStateMachine def initiative_is_approved(effect): @@ -302,6 +302,10 @@ class TeamTriggers(TriggerManager): ), TeamContributionTransitionEffect(ContributionStateMachine.fail), NotificationEffect(TeamCancelledMessage), + RelatedTransitionEffect( + 'slot', + TeamSlotStateMachine.cancel, + ), ] ), @@ -315,7 +319,11 @@ class TeamTriggers(TriggerManager): ), TeamContributionTransitionEffect(ContributionStateMachine.fail), NotificationEffect(TeamWithdrawnMessage), - NotificationEffect(TeamWithdrawnActivityOwnerMessage) + NotificationEffect(TeamWithdrawnActivityOwnerMessage), + RelatedTransitionEffect( + 'slot', + TeamSlotStateMachine.cancel, + ), ] ), @@ -330,6 +338,10 @@ class TeamTriggers(TriggerManager): contributor_is_active ] ), + RelatedTransitionEffect( + 'slot', + TeamSlotStateMachine.reopen + ), ] ), @@ -345,7 +357,11 @@ class TeamTriggers(TriggerManager): ] ), NotificationEffect(TeamReappliedMessage), - NotificationEffect(TeamAddedMessage) + NotificationEffect(TeamAddedMessage), + RelatedTransitionEffect( + 'slot', + TeamSlotStateMachine.reopen, + ), ] ), @@ -357,7 +373,25 @@ class TeamTriggers(TriggerManager): contribution_conditions=[activity_is_active, contributor_is_active] ), ResetTeamParticipantsEffect, - NotificationEffect(TeamAddedMessage) + NotificationEffect(TeamAddedMessage), + RelatedTransitionEffect( + 'slot', + TeamSlotStateMachine.reopen, + ), + ] ), + + TransitionTrigger( + TeamStateMachine.finish, + effects=[ + TeamContributionTransitionEffect( + ContributionStateMachine.succeed, + contribution_conditions=[ + contributor_is_active + ] + ), + ] + ), + ] diff --git a/bluebottle/time_based/tests/test_periodic_tasks.py b/bluebottle/time_based/tests/test_periodic_tasks.py index 4a4bf741fd..8fbb783bde 100644 --- a/bluebottle/time_based/tests/test_periodic_tasks.py +++ b/bluebottle/time_based/tests/test_periodic_tasks.py @@ -926,3 +926,67 @@ def test_finish_team_slot(self): self.run_task(self.after) self.assertStatus(self.slot, 'finished') self.assertStatus(self.slot.team, 'finished') + self.assertStatus(self.participant.contributions.first(), 'succeeded') + + def test_finish_cancelled_team(self): + mail.outbox = [] + self.participant.team.states.cancel(save=True) + self.run_task(self.after) + + self.assertStatus(self.slot, 'cancelled') + self.assertStatus(self.slot.team, 'cancelled') + self.assertStatus(self.participant, 'accepted') + self.assertStatus(self.participant.contributions.first(), 'failed') + + def test_finish_restore_team(self): + mail.outbox = [] + self.participant.team.states.cancel(save=True) + self.participant.team.states.reopen(save=True) + self.run_task(self.after) + + self.assertStatus(self.slot, 'finished') + self.assertStatus(self.slot.team, 'finished') + self.assertStatus(self.participant, 'accepted') + self.assertStatus(self.participant.contributions.first(), 'succeeded') + + def test_finish_withdrawn_team(self): + mail.outbox = [] + self.participant.team.states.withdraw(save=True) + self.run_task(self.after) + + self.assertStatus(self.slot, 'cancelled') + self.assertStatus(self.slot.team, 'withdrawn') + self.assertStatus(self.participant, 'accepted') + self.assertStatus(self.participant.contributions.first(), 'failed') + + def test_finish_reapply_team(self): + mail.outbox = [] + self.participant.team.states.withdraw(save=True) + self.participant.team.states.reapply(save=True) + self.run_task(self.after) + + self.assertStatus(self.slot, 'finished') + self.assertStatus(self.slot.team, 'finished') + self.assertStatus(self.participant, 'accepted') + self.assertStatus(self.participant.contributions.first(), 'succeeded') + + def test_finish_withdrawn_team_member(self): + mail.outbox = [] + self.participant.states.withdraw(save=True) + self.run_task(self.after) + + self.assertStatus(self.slot, 'finished') + self.assertStatus(self.slot.team, 'finished') + self.assertStatus(self.participant, 'withdrawn') + self.assertStatus(self.participant.contributions.first(), 'failed') + + def test_finish_reapplied_team_member(self): + mail.outbox = [] + self.participant.states.withdraw(save=True) + self.participant.states.reapply(save=True) + self.run_task(self.after) + + self.assertStatus(self.slot, 'finished') + self.assertStatus(self.slot.team, 'finished') + self.assertStatus(self.participant, 'accepted') + self.assertStatus(self.participant.contributions.first(), 'succeeded') From f9a47d84f43ce8fe11d1f109e566a59102932e86 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Thu, 11 Aug 2022 15:12:47 +0200 Subject: [PATCH 101/112] Fix tests --- bluebottle/fsm/effects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bluebottle/fsm/effects.py b/bluebottle/fsm/effects.py index 84543dd940..9a2f6a704f 100644 --- a/bluebottle/fsm/effects.py +++ b/bluebottle/fsm/effects.py @@ -142,7 +142,7 @@ class BaseRelatedTransitionEffect(Effect): def __init__(self, *args, **kwargs): super(BaseRelatedTransitionEffect, self).__init__(*args, **kwargs) self.executed = False - relation = getattr(self.instance, self.relation) + relation = getattr(self.instance, self.relation, []) try: self.instances = list(relation.all()) From 5eb21e9230ee8cb35bec5ac49884049373a96116 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Mon, 15 Aug 2022 11:54:06 +0200 Subject: [PATCH 102/112] Fix how activity fills/opens when slots change --- bluebottle/time_based/tests/test_triggers.py | 30 ++++++++++++++++++++ bluebottle/time_based/triggers.py | 9 ++++++ 2 files changed, 39 insertions(+) diff --git a/bluebottle/time_based/tests/test_triggers.py b/bluebottle/time_based/tests/test_triggers.py index 17f8aad08e..b45dd39a7f 100644 --- a/bluebottle/time_based/tests/test_triggers.py +++ b/bluebottle/time_based/tests/test_triggers.py @@ -2130,6 +2130,36 @@ def test_unfill_slot(self): self.assertStatus(self.slot2, 'open') self.assertStatus(self.activity, 'open') + def test_extend_slot_unfills(self): + self.assertStatus(self.activity, 'open') + SlotParticipantFactory.create(slot=self.slot1, participant=self.participant) + participant2 = DateParticipantFactory.create(activity=self.activity) + SlotParticipantFactory.create(slot=self.slot1, participant=participant2) + participant2 = DateParticipantFactory.create(activity=self.activity) + SlotParticipantFactory.create(slot=self.slot2, participant=participant2) + self.assertStatus(self.slot1, 'full') + self.assertStatus(self.slot2, 'full') + self.assertStatus(self.activity, 'full') + + self.slot1.capacity = 10 + self.slot1.save() + self.assertStatus(self.slot1, 'open') + self.assertStatus(self.activity, 'open') + + def test_cancel_open_slot_fills(self): + self.assertStatus(self.activity, 'open') + self.assertStatus(self.slot1, 'open') + SlotParticipantFactory.create(slot=self.slot2, participant=self.participant) + self.assertStatus(self.slot1, 'open') + self.assertStatus(self.slot2, 'full') + self.assertStatus(self.activity, 'open') + self.slot1.states.cancel(save=True) + self.assertStatus(self.activity, 'full') + self.slot3 = DateActivitySlotFactory.create(activity=self.activity) + self.assertStatus(self.activity, 'open') + self.slot3.delete() + self.assertStatus(self.activity, 'full') + def test_fill_new_slot(self): self.slot_part = SlotParticipantFactory.create(slot=self.slot2, participant=self.participant) self.assertStatus(self.slot2, 'full') diff --git a/bluebottle/time_based/triggers.py b/bluebottle/time_based/triggers.py index e1137ce175..eccc27b9d4 100644 --- a/bluebottle/time_based/triggers.py +++ b/bluebottle/time_based/triggers.py @@ -663,6 +663,15 @@ class DateActivitySlotTriggers(ActivitySlotTriggers): activity_has_accepted_participants ] ), + RelatedTransitionEffect( + 'activity', + TimeBasedStateMachine.lock, + conditions=[ + not_all_slots_finished, + all_slots_will_be_full, + slot_selection_is_free + ] + ), ActiveTimeContributionsTransitionEffect(TimeContributionStateMachine.fail) ] ), From f78f63c6f6b77a4f1617762803ce38ef4a0b50bf Mon Sep 17 00:00:00 2001 From: Ernst Odolphi Date: Mon, 15 Aug 2022 17:23:41 +0200 Subject: [PATCH 103/112] Compensate for timezone differences if duplicated date is outside of summertime or vice versa --- bluebottle/time_based/tests/test_utils.py | 35 +++++++++++++++++++---- bluebottle/time_based/utils.py | 4 ++- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/bluebottle/time_based/tests/test_utils.py b/bluebottle/time_based/tests/test_utils.py index d44e9211de..10ea36a446 100644 --- a/bluebottle/time_based/tests/test_utils.py +++ b/bluebottle/time_based/tests/test_utils.py @@ -1,12 +1,15 @@ import datetime -from pytz import UTC +from django.utils.timezone import get_current_timezone from bluebottle.test.utils import BluebottleTestCase from bluebottle.time_based.tests.factories import DateActivityFactory, DateActivitySlotFactory from bluebottle.time_based.utils import duplicate_slot +tz = get_current_timezone() + + class DuplicateSlotTestCase(BluebottleTestCase): def setUp(self): @@ -16,7 +19,7 @@ def setUp(self): ) self.slot = DateActivitySlotFactory.create( activity=self.activity, - start=datetime.datetime(2022, 5, 15, tzinfo=UTC), + start=tz.localize(datetime.datetime(2022, 5, 15, 10, 0)), status='cancelled' ) @@ -27,7 +30,7 @@ def _get_slot_statuses(self): return [s.status for s in self.activity.slots.all()] def test_duplicate_every_day(self): - end = datetime.datetime(2022, 5, 20, tzinfo=UTC).date() + end = datetime.date(2022, 5, 20) duplicate_slot(self.slot, 'day', end) self.assertEqual( self._get_slot_dates(), @@ -44,8 +47,28 @@ def test_duplicate_every_day(self): ] ) + def test_duplicate_every_day_end_dst(self): + self.slot.start = tz.localize(datetime.datetime(2022, 10, 27, 10, 0)) + self.slot.save() + + end = datetime.date(2022, 11, 2) + duplicate_slot(self.slot, 'day', end) + + self.assertEqual( + self._get_slot_dates(), + [ + '2022-10-27', '2022-10-28', '2022-10-29', + '2022-10-30', '2022-10-31', '2022-11-01', + '2022-11-02' + ] + ) + + for slot in self.activity.slots.all(): + self.assertEqual(slot.start.astimezone(tz).hour, 10) + self.assertEqual(slot.start.astimezone(tz).minute, 0) + def test_duplicate_every_week(self): - end = datetime.datetime(2022, 7, 1, tzinfo=UTC).date() + end = datetime.date(2022, 7, 1) duplicate_slot(self.slot, 'week', end) self.assertEqual( self._get_slot_dates(), @@ -57,7 +80,7 @@ def test_duplicate_every_week(self): ) def test_duplicate_every_monthday(self): - end = datetime.datetime(2023, 2, 1, tzinfo=UTC).date() + end = datetime.date(2023, 2, 1) duplicate_slot(self.slot, 'monthday', end) self.assertEqual( self._get_slot_dates(), @@ -69,7 +92,7 @@ def test_duplicate_every_monthday(self): ) def test_duplicate_every_3rd_sunday(self): - end = datetime.datetime(2022, 10, 1, tzinfo=UTC).date() + end = datetime.date(2022, 10, 1) duplicate_slot(self.slot, 'month', end) self.assertEqual( self._get_slot_dates(), diff --git a/bluebottle/time_based/utils.py b/bluebottle/time_based/utils.py index 901d62eca3..2985b1f0dc 100644 --- a/bluebottle/time_based/utils.py +++ b/bluebottle/time_based/utils.py @@ -29,6 +29,8 @@ def duplicate_slot(slot, interval, end): for date in dates: slot.id = None - slot.start = slot.start.replace(day=date.day, month=date.month, year=date.year) + slot.start = slot.start.tzinfo.localize( + slot.start.replace(tzinfo=None, day=date.day, month=date.month, year=date.year) + ) slot.status = 'open' slot.save() From 773bd23f6406ddf9e2067dafd366ba47348d48ea Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Mon, 15 Aug 2022 17:40:47 +0200 Subject: [PATCH 104/112] Fix mails for joining a team --- bluebottle/activities/effects.py | 4 +-- bluebottle/activities/messages.py | 2 +- bluebottle/time_based/messages.py | 2 +- bluebottle/time_based/tests/test_api.py | 34 +++++++++++++++++++++++++ bluebottle/time_based/triggers.py | 14 +++++++--- 5 files changed, 49 insertions(+), 7 deletions(-) diff --git a/bluebottle/activities/effects.py b/bluebottle/activities/effects.py index 5bcc6af512..73cd95dfbc 100644 --- a/bluebottle/activities/effects.py +++ b/bluebottle/activities/effects.py @@ -74,11 +74,11 @@ def is_valid(self): self.instance.activity.team_activity == Activity.TeamActivityChoices.teams ) - def post_save(self, **kwargs): + def pre_save(self, **kwargs): if self.instance.accepted_invite: self.instance.team = self.instance.accepted_invite.contributor.team - self.instance.save() + def post_save(self, **kwargs): if not self.instance.team: self.instance.team = Team.objects.create( owner=self.instance.user, diff --git a/bluebottle/activities/messages.py b/bluebottle/activities/messages.py index 27d50c0449..8b26240ccb 100644 --- a/bluebottle/activities/messages.py +++ b/bluebottle/activities/messages.py @@ -382,7 +382,7 @@ def get_recipients(self): class TeamMemberAddedMessage(ActivityNotification): - subject = pgettext('email', "New team member") + subject = pgettext('email', 'Someone has joined your team for "{title}"') template = 'messages/team_member_added' context = { diff --git a/bluebottle/time_based/messages.py b/bluebottle/time_based/messages.py index 466c007b2b..d8c4c86ef3 100644 --- a/bluebottle/time_based/messages.py +++ b/bluebottle/time_based/messages.py @@ -460,7 +460,7 @@ class TeamParticipantJoinedNotification(TimeBasedInfoMixin, TransitionMessage): """ The participant joined """ - subject = pgettext('email', 'You have registered your team for "{title}"') + subject = pgettext('email', 'You have joined a team for "{title}"') template = 'messages/team_participant_joined' context = { 'title': 'activity.title', diff --git a/bluebottle/time_based/tests/test_api.py b/bluebottle/time_based/tests/test_api.py index 3d74cdfb22..80e06344cc 100644 --- a/bluebottle/time_based/tests/test_api.py +++ b/bluebottle/time_based/tests/test_api.py @@ -1888,6 +1888,40 @@ class PeriodParticipantListAPIViewTestCase(ParticipantListViewTestCase, Bluebott document_url_name = 'period-participant-document' participant_type = 'contributors/time-based/period-participants' + def test_join_team(self): + self.activity.team_activity = 'teams' + self.activity.save() + captain = PeriodParticipantFactory.create( + activity=self.activity + ) + self.data['data']['relationships']['accepted-invite'] = { + 'data': { + 'type': 'activities/invites', + 'id': str(captain.invite.id) + } + } + mail.outbox = [] + self.response = self.client.post(self.url, json.dumps(self.data), user=self.user) + self.assertEqual(self.response.status_code, status.HTTP_201_CREATED) + self.assertEqual(len(mail.outbox), 2) + + self.assertEqual( + mail.outbox[0].subject, + f'You have joined a team for "{self.activity.title}"' + ) + self.assertEqual( + mail.outbox[0].to[0], + self.user.email + ) + self.assertEqual( + mail.outbox[1].subject, + f'Someone has joined your team for "{self.activity.title}"' + ) + self.assertEqual( + mail.outbox[1].to[0], + captain.user.email + ) + class ParticipantDetailViewTestCase(): def setUp(self): diff --git a/bluebottle/time_based/triggers.py b/bluebottle/time_based/triggers.py index 3ccbae58a5..d39e53feb7 100644 --- a/bluebottle/time_based/triggers.py +++ b/bluebottle/time_based/triggers.py @@ -8,7 +8,8 @@ ActivityExpiredNotification, ActivityRejectedNotification, ActivityCancelledNotification, ActivityRestoredNotification, ParticipantWithdrewConfirmationNotification, - TeamMemberWithdrewMessage, TeamMemberRemovedMessage, TeamCaptainAcceptedMessage, TeamCancelledTeamCaptainMessage + TeamMemberWithdrewMessage, TeamMemberRemovedMessage, TeamCaptainAcceptedMessage, TeamCancelledTeamCaptainMessage, + TeamMemberAddedMessage ) from bluebottle.activities.states import OrganizerStateMachine, TeamStateMachine from bluebottle.activities.triggers import ( @@ -1299,14 +1300,21 @@ class ParticipantTriggers(ContributorTriggers): ParticipantJoinedNotification, conditions=[ automatically_accept, - not_team_captain + is_not_team_activity ] ), NotificationEffect( TeamParticipantJoinedNotification, conditions=[ automatically_accept, - is_team_activity + has_accepted_invite + ] + ), + NotificationEffect( + TeamMemberAddedMessage, + conditions=[ + not_team_captain, + has_team ] ), NotificationEffect( From fae79e30260fcfff3b00193ac11f1e5f46652f54 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Tue, 16 Aug 2022 12:24:27 +0200 Subject: [PATCH 105/112] Fix more emails and triggers --- .../activities/tests/test_notifications.py | 2 +- bluebottle/activities/triggers.py | 32 ++++++++----- bluebottle/time_based/messages.py | 25 ++++++++++- .../mails/messages/team_member_joined.html | 36 +++++++++++++++ .../messages/team_participant_joined.html | 4 +- .../time_based/tests/test_notifications.py | 2 +- bluebottle/time_based/tests/test_triggers.py | 45 +++++++++++++------ bluebottle/time_based/triggers.py | 6 +-- 8 files changed, 120 insertions(+), 32 deletions(-) create mode 100644 bluebottle/time_based/templates/mails/messages/team_member_joined.html diff --git a/bluebottle/activities/tests/test_notifications.py b/bluebottle/activities/tests/test_notifications.py index f1c6421343..1c64375f4f 100644 --- a/bluebottle/activities/tests/test_notifications.py +++ b/bluebottle/activities/tests/test_notifications.py @@ -218,7 +218,7 @@ def test_team_member_added_notification(self): self.message_class = TeamMemberAddedMessage self.create() self.assertRecipients([self.captain]) - self.assertSubject("New team member") + self.assertSubject('Someone has joined your team for "Save the world!"') self.assertHtmlBodyContains( f"{self.obj.user.full_name} is now part of your team for the activity ‘Save the world!’." ) diff --git a/bluebottle/activities/triggers.py b/bluebottle/activities/triggers.py index 3818c87834..782266d234 100644 --- a/bluebottle/activities/triggers.py +++ b/bluebottle/activities/triggers.py @@ -17,6 +17,7 @@ ) from bluebottle.impact.effects import UpdateImpactGoalEffect from bluebottle.notifications.effects import NotificationEffect +from bluebottle.time_based.messages import TeamParticipantJoinedNotification from bluebottle.time_based.states import ParticipantStateMachine, TimeBasedStateMachine, TeamSlotStateMachine @@ -207,18 +208,11 @@ def contributor_is_active(contribution): ] -def automatically_accept(effect): +def automatically_accept_team(effect): """ automatically accept team """ - captain = effect.instance.activity\ - .contributors.not_instance_of(Organizer)\ - .filter(user=effect.instance.owner).first() - return ( - not hasattr(effect.instance.activity, 'review') or - not effect.instance.activity.review or - (captain and captain.status == 'accepted') - ) + return getattr(effect.instance.activity, 'review', False) is False def needs_review(effect): @@ -253,6 +247,15 @@ def team_activity_will_not_be_full(effect): ) +def user_is_team_captain(effect): + """ + current user is team captain + """ + if 'user' not in effect.options: + return False + return effect.instance.owner == effect.options['user'] + + @register(Team) class TeamTriggers(TriggerManager): triggers = [ @@ -261,7 +264,7 @@ class TeamTriggers(TriggerManager): effects=[ NotificationEffect( TeamAddedMessage, - conditions=[automatically_accept] + conditions=[automatically_accept_team] ), NotificationEffect( TeamAppliedMessage, @@ -270,7 +273,7 @@ class TeamTriggers(TriggerManager): TransitionEffect( TeamStateMachine.accept, conditions=[ - automatically_accept + automatically_accept_team ] ) ] @@ -279,6 +282,13 @@ class TeamTriggers(TriggerManager): TransitionTrigger( TeamStateMachine.accept, effects=[ + NotificationEffect( + TeamParticipantJoinedNotification, + conditions=[ + automatically_accept_team, + user_is_team_captain + ] + ), RelatedTransitionEffect( 'members', ParticipantStateMachine.accept, diff --git a/bluebottle/time_based/messages.py b/bluebottle/time_based/messages.py index d8c4c86ef3..5ec2b359be 100644 --- a/bluebottle/time_based/messages.py +++ b/bluebottle/time_based/messages.py @@ -460,7 +460,7 @@ class TeamParticipantJoinedNotification(TimeBasedInfoMixin, TransitionMessage): """ The participant joined """ - subject = pgettext('email', 'You have joined a team for "{title}"') + subject = pgettext('email', 'You have registered your team for "{title}"') template = 'messages/team_participant_joined' context = { 'title': 'activity.title', @@ -563,6 +563,29 @@ def get_recipients(self): return [self.obj.user] +class TeamMemberJoinedNotification(TimeBasedInfoMixin, TransitionMessage): + """ + The participant joined as a team joined + """ + subject = pgettext('email', 'You have joined {team_name} for "{title}"') + template = 'messages/team_member_joined' + context = { + 'title': 'activity.title', + 'team_name': 'team.name' + } + # delay = 60 + + @property + def action_link(self): + return self.obj.activity.get_absolute_url() + + action_title = pgettext('email', 'View activity') + + def get_recipients(self): + """participant""" + return [self.obj.user] + + class ParticipantAcceptedNotification(TimeBasedInfoMixin, TransitionMessage): """ The participant got accepted after review diff --git a/bluebottle/time_based/templates/mails/messages/team_member_joined.html b/bluebottle/time_based/templates/mails/messages/team_member_joined.html new file mode 100644 index 0000000000..22fd23e68b --- /dev/null +++ b/bluebottle/time_based/templates/mails/messages/team_member_joined.html @@ -0,0 +1,36 @@ +{% extends "mails/messages/participant_base.html" %} +{% load i18n %} + +{% block message %} +

    + {% blocktrans context 'email' %} + You joined {{ team_name }} for an activity on {{ site_name }}! + {% endblocktrans %} +

    + +

    + {{ title }} +

    + + {% if slots %} + {% include 'mails/messages/partial/slots.html' %} + {% else %} +

    + {% blocktrans context 'email' %} + The activity manager will be in touch to confirm details such as time, date and location. + {% endblocktrans %} +

    + {% endif %} + +{% endblock %} + + +{% block end_message %} +

    + + {% blocktrans context 'email' %} + If you are unable to participate, please withdraw via the activity page so that others can take your place. + {% endblocktrans %} + +

    +{% endblock %} diff --git a/bluebottle/time_based/templates/mails/messages/team_participant_joined.html b/bluebottle/time_based/templates/mails/messages/team_participant_joined.html index 74a2a53d68..78c3fd9178 100644 --- a/bluebottle/time_based/templates/mails/messages/team_participant_joined.html +++ b/bluebottle/time_based/templates/mails/messages/team_participant_joined.html @@ -13,7 +13,9 @@

    - The activity manager will be in touch to confirm details such as time, date and location. You can start inviting team members, or wait until the details have been set. + {% blocktrans context 'email' %} + The activity manager will be in touch to confirm details such as time, date and location. You can start inviting team members, or wait until the details have been set. + {% endblocktrans %}

    diff --git a/bluebottle/time_based/tests/test_notifications.py b/bluebottle/time_based/tests/test_notifications.py index 7c35874bf4..1474a962e9 100644 --- a/bluebottle/time_based/tests/test_notifications.py +++ b/bluebottle/time_based/tests/test_notifications.py @@ -240,7 +240,7 @@ def test_team_joined_notification(self): self.message_class = TeamParticipantJoinedNotification self.create() self.assertRecipients([self.supporter]) - self.assertSubject('You have registered your team for "Save the world!"') + self.assertSubject('You have joined a team for "Save the world!"') self.assertActionLink(self.activity.get_absolute_url()) self.assertActionTitle('View activity') self.assertBodyNotContains( diff --git a/bluebottle/time_based/tests/test_triggers.py b/bluebottle/time_based/tests/test_triggers.py index 5f21af134b..ac5e46f6b2 100644 --- a/bluebottle/time_based/tests/test_triggers.py +++ b/bluebottle/time_based/tests/test_triggers.py @@ -8,7 +8,7 @@ from tenant_extras.utils import TenantLanguage from bluebottle.activities.messages import ParticipantWithdrewConfirmationNotification, \ - TeamMemberWithdrewMessage + TeamMemberWithdrewMessage, TeamMemberAddedMessage from bluebottle.activities.messages import TeamMemberRemovedMessage, TeamCancelledTeamCaptainMessage, \ TeamCancelledMessage from bluebottle.activities.models import Organizer, Activity @@ -21,7 +21,7 @@ ParticipantAppliedNotification, ParticipantRemovedNotification, ParticipantRemovedOwnerNotification, NewParticipantNotification, TeamParticipantJoinedNotification, ParticipantAddedNotification, ParticipantRejectedNotification, ParticipantAddedOwnerNotification, TeamSlotChangedNotification, - ParticipantWithdrewNotification + ParticipantWithdrewNotification, TeamParticipantAppliedNotification, TeamMemberJoinedNotification ) from bluebottle.time_based.tests.factories import ( DateActivityFactory, PeriodActivityFactory, @@ -1762,13 +1762,15 @@ def test_team_join(self): user=user, as_user=user ) + self.assertStatus(participant, 'accepted') + self.assertStatus(participant.team, 'open') self.assertEqual(len(mail.outbox), 2) self.assertEqual( - mail.outbox[0].subject, + mail.outbox[1].subject, f'A new team has joined "{self.activity.title}"' ) self.assertEqual( - mail.outbox[1].subject, + mail.outbox[0].subject, f'You have registered your team for "{self.activity.title}"' ) prep = participant.preparation_contributions.first() @@ -1922,7 +1924,7 @@ def test_add_participant(self): self.assertNotificationEffect(ParticipantAddedOwnerNotification) self.assertNotificationEffect(ParticipantAddedNotification) - def test_start_team_participant(self): + def test_start_team(self): self.activity.team_activity = 'teams' self.activity.save() user = BlueBottleUserFactory.create() @@ -1932,26 +1934,41 @@ def test_start_team_participant(self): ) with self.execute(user=user): self.assertNoNotificationEffect(NewParticipantNotification) - self.assertNotificationEffect(TeamParticipantJoinedNotification) + self.assertNoNotificationEffect(TeamParticipantJoinedNotification) + self.assertNoNotificationEffect(ParticipantJoinedNotification) + + def test_apply_team(self): + self.activity.team_activity = 'teams' + self.activity.review = True + self.activity.save() + user = BlueBottleUserFactory.create() + self.model = self.participant_factory.build( + activity=self.activity, + user=user + ) + with self.execute(user=user): + self.assertNotificationEffect(TeamParticipantAppliedNotification) + self.assertNoNotificationEffect(ParticipantJoinedNotification) def test_join_team_participant(self): self.activity.team_activity = 'teams' self.activity.save() user = BlueBottleUserFactory.create() - captain = BlueBottleUserFactory.create() - team = TeamFactory.create( - owner=captain, - activity=self.activity + captain = self.participant_factory.create( + activity=self.activity, + user=BlueBottleUserFactory.create() ) self.model = self.participant_factory.build( - team=team, + accepted_invite=captain.invite, activity=self.activity, user=user ) - with self.execute(user=user): + with self.execute(user=user, send_messages=True): self.assertNoNotificationEffect(NewParticipantNotification) - self.assertNotificationEffect(TeamParticipantJoinedNotification) - self.assertNotificationEffect(ParticipantJoinedNotification) + self.assertNoNotificationEffect(ParticipantJoinedNotification) + self.assertNoNotificationEffect(TeamParticipantJoinedNotification) + self.assertNotificationEffect(TeamMemberJoinedNotification) + self.assertNotificationEffect(TeamMemberAddedMessage) def test_remove_participant(self): self.model = self.participant_factory.create( diff --git a/bluebottle/time_based/triggers.py b/bluebottle/time_based/triggers.py index d39e53feb7..9cec87161e 100644 --- a/bluebottle/time_based/triggers.py +++ b/bluebottle/time_based/triggers.py @@ -43,9 +43,9 @@ ActivitySucceededManuallyNotification, ParticipantChangedNotification, ParticipantWithdrewNotification, ParticipantAddedOwnerNotification, TeamParticipantAddedNotification, - ParticipantRemovedOwnerNotification, ParticipantJoinedNotification, TeamParticipantJoinedNotification, + ParticipantRemovedOwnerNotification, ParticipantJoinedNotification, ParticipantAppliedNotification, TeamParticipantAppliedNotification, SlotCancelledNotification, - TeamSlotChangedNotification + TeamSlotChangedNotification, TeamMemberJoinedNotification ) from bluebottle.time_based.models import ( DateActivity, PeriodActivity, @@ -1304,7 +1304,7 @@ class ParticipantTriggers(ContributorTriggers): ] ), NotificationEffect( - TeamParticipantJoinedNotification, + TeamMemberJoinedNotification, conditions=[ automatically_accept, has_accepted_invite From febba5c889e0c4bce0cbc72cca6b88ab0d578437 Mon Sep 17 00:00:00 2001 From: Ernst Odolphi Date: Tue, 16 Aug 2022 14:42:34 +0200 Subject: [PATCH 106/112] Re-add teams field in serializer. This fixes the info block for team activities --- bluebottle/time_based/serializers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bluebottle/time_based/serializers.py b/bluebottle/time_based/serializers.py index e5ee9b1d5b..7f3f634d61 100644 --- a/bluebottle/time_based/serializers.py +++ b/bluebottle/time_based/serializers.py @@ -47,6 +47,8 @@ class TimeBasedBaseSerializer(BaseActivitySerializer): review = serializers.BooleanField(required=False) is_online = serializers.BooleanField(required=False, allow_null=True) + teams = TeamsField() + class Meta(BaseActivitySerializer.Meta): fields = BaseActivitySerializer.Meta.fields + ( 'capacity', @@ -55,6 +57,7 @@ class Meta(BaseActivitySerializer.Meta): 'review', 'contributors', 'my_contributor', + 'teams', ) class JSONAPIMeta(BaseActivitySerializer.JSONAPIMeta): From 27624d8eafac51178e8558078851687dbcb2b4c0 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Tue, 16 Aug 2022 15:03:58 +0200 Subject: [PATCH 107/112] Change time slot mail --- bluebottle/time_based/messages.py | 4 ++++ bluebottle/time_based/models.py | 5 +++++ .../mails/messages/changed_team_date.html | 15 ++++++++------- .../mails/messages/partial/team_slot.html | 12 ++++++++++++ .../mails/messages/reminder_team_slot.html | 2 +- 5 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 bluebottle/time_based/templates/mails/messages/partial/team_slot.html diff --git a/bluebottle/time_based/messages.py b/bluebottle/time_based/messages.py index 466c007b2b..586f515f2d 100644 --- a/bluebottle/time_based/messages.py +++ b/bluebottle/time_based/messages.py @@ -200,6 +200,8 @@ class ReminderTeamSlotNotification(TransitionMessage): 'start': 'start', 'duration': 'duration', 'end': 'end', + 'timezone': 'timezone', + 'location': 'location', } def already_send(self, recipient): @@ -281,6 +283,8 @@ class TeamSlotChangedNotification(TransitionMessage): 'start': 'start', 'duration': 'duration', 'end': 'end', + 'timezone': 'timezone', + 'location': 'location', } @property diff --git a/bluebottle/time_based/models.py b/bluebottle/time_based/models.py index 826d49f6b5..bdc93bc385 100644 --- a/bluebottle/time_based/models.py +++ b/bluebottle/time_based/models.py @@ -526,6 +526,11 @@ def end(self): if self.start and self.duration: return self.start + self.duration + @property + def timezone(self): + if self.start: + return self.start.strftime("%Z %z") + @property def is_complete(self): return self.start and self.duration diff --git a/bluebottle/time_based/templates/mails/messages/changed_team_date.html b/bluebottle/time_based/templates/mails/messages/changed_team_date.html index 637e17abbd..fbb07a343f 100644 --- a/bluebottle/time_based/templates/mails/messages/changed_team_date.html +++ b/bluebottle/time_based/templates/mails/messages/changed_team_date.html @@ -6,19 +6,20 @@ Some details of the team activity you are a part of have changed: {% endblocktrans %}

    +

    + {% blocktrans context 'email' %} + Updated details below: + {% endblocktrans %} +

    {{ team_name }}

    -{{ title }} +"{{ title }}"

    -{% if duration %} - {% include 'mails/messages/partial/period.html' %} -{% else %} - {% include 'mails/messages/partial/slots.html' %} -{% endif %} -

    +{% include 'mails/messages/partial/team_slot.html' %} +

    {% blocktrans context 'email' %} Please view your team from the activity page to see the changes. diff --git a/bluebottle/time_based/templates/mails/messages/partial/team_slot.html b/bluebottle/time_based/templates/mails/messages/partial/team_slot.html new file mode 100644 index 0000000000..f246078df6 --- /dev/null +++ b/bluebottle/time_based/templates/mails/messages/partial/team_slot.html @@ -0,0 +1,12 @@ +{% load i18n tz %} +

    + {% trans 'Date' %}: {{ start|date:"SHORT_DATE_FORMAT" }} +

    +

    + {% trans 'Time' %}: {{ start|date:'TIME_FORMAT' }} - {{ end|date:'TIME_FORMAT' }} {{ timezone }} +

    +{% if location %} +

    + {% trans 'Location' %}: {{ location }} {{ location_hint }} +

    +{% endif %} diff --git a/bluebottle/time_based/templates/mails/messages/reminder_team_slot.html b/bluebottle/time_based/templates/mails/messages/reminder_team_slot.html index 47a4c17866..bf9338fd3d 100644 --- a/bluebottle/time_based/templates/mails/messages/reminder_team_slot.html +++ b/bluebottle/time_based/templates/mails/messages/reminder_team_slot.html @@ -13,7 +13,7 @@ {{ title }}

    -{% include 'mails/messages/partial/period.html' %} +{% include 'mails/messages/partial/team_slot.html' %}

    From a8f01a8522e92844b2fe32f05f6a070535dcbea2 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 17 Aug 2022 09:17:26 +0200 Subject: [PATCH 108/112] Fix trigger --- bluebottle/time_based/triggers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bluebottle/time_based/triggers.py b/bluebottle/time_based/triggers.py index 3ccbae58a5..25e4e1525f 100644 --- a/bluebottle/time_based/triggers.py +++ b/bluebottle/time_based/triggers.py @@ -1107,7 +1107,7 @@ def has_accepted_invite(effect): def is_not_team_activity(effect): - """Contributor is not part of a team""" + """Activity is not for teams""" return effect.instance.activity.team_activity != 'teams' @@ -1299,7 +1299,7 @@ class ParticipantTriggers(ContributorTriggers): ParticipantJoinedNotification, conditions=[ automatically_accept, - not_team_captain + is_not_team_activity ] ), NotificationEffect( From 4cee19144c0215db142f685b4aa84db2183b232e Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 17 Aug 2022 11:24:15 +0200 Subject: [PATCH 109/112] Fix more tests --- bluebottle/activities/messages.py | 2 +- .../mails/messages/team_accepted.html | 15 --------------- .../messages/team_cancelled_team_captain.html | 2 +- .../mails/messages/team_captain_accepted.html | 18 ++++++++++++++++++ bluebottle/time_based/messages.py | 4 ++-- bluebottle/time_based/tests/test_api.py | 2 +- bluebottle/time_based/tests/test_scenarios.py | 2 +- 7 files changed, 24 insertions(+), 21 deletions(-) delete mode 100644 bluebottle/activities/templates/mails/messages/team_accepted.html create mode 100644 bluebottle/activities/templates/mails/messages/team_captain_accepted.html diff --git a/bluebottle/activities/messages.py b/bluebottle/activities/messages.py index 8b26240ccb..4449ac2f33 100644 --- a/bluebottle/activities/messages.py +++ b/bluebottle/activities/messages.py @@ -303,7 +303,7 @@ class TeamAppliedMessage(TeamNotification): class TeamCaptainAcceptedMessage(TeamNotification): subject = pgettext('email', 'Your team has been accepted for "{title}"') - template = 'messages/team_accepted' + template = 'messages/team_captain_accepted' context = { 'title': 'activity.title', diff --git a/bluebottle/activities/templates/mails/messages/team_accepted.html b/bluebottle/activities/templates/mails/messages/team_accepted.html deleted file mode 100644 index 816a9aadb1..0000000000 --- a/bluebottle/activities/templates/mails/messages/team_accepted.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "mails/messages/activity_base.html" %} -{% load i18n %} - -{% block message %} -

    - {% if custom_message %} - {{custom_message|linebreaks}} - {% else %} - {% blocktrans context 'email' %}Your team has been accepted for the activity '{{title}}'.{% endblocktrans %} - {% endif %} -

    -

    - {% blocktrans context 'email' %}On the activity page you will find the link to invite your team members.{% endblocktrans %} -

    -{% endblock %} diff --git a/bluebottle/activities/templates/mails/messages/team_cancelled_team_captain.html b/bluebottle/activities/templates/mails/messages/team_cancelled_team_captain.html index 16be1d0c04..a7ba3c269c 100644 --- a/bluebottle/activities/templates/mails/messages/team_cancelled_team_captain.html +++ b/bluebottle/activities/templates/mails/messages/team_cancelled_team_captain.html @@ -1,7 +1,7 @@ {% extends "mails/messages/activity_base.html" %} {% load i18n %} -{% block message%} +{% block message %}

    {% if custom_message %} {{custom_message|linebreaks}} diff --git a/bluebottle/activities/templates/mails/messages/team_captain_accepted.html b/bluebottle/activities/templates/mails/messages/team_captain_accepted.html new file mode 100644 index 0000000000..c5ef81623e --- /dev/null +++ b/bluebottle/activities/templates/mails/messages/team_captain_accepted.html @@ -0,0 +1,18 @@ +{% extends "mails/messages/activity_base.html" %} +{% load i18n %} + +{% block message %} +

    + {% if custom_message %} + {{ custom_message|linebreaks }} + {% else %} + {% blocktrans context 'email' %}Your team has been accepted for the activity '{{ title }}'. + {% endblocktrans %} + {% endif %} +

    +

    + {% blocktrans context 'email' %} + On the activity page you will find the link to invite your team members. + {% endblocktrans %} +

    +{% endblock %} diff --git a/bluebottle/time_based/messages.py b/bluebottle/time_based/messages.py index 5ec2b359be..cc65e0ac30 100644 --- a/bluebottle/time_based/messages.py +++ b/bluebottle/time_based/messages.py @@ -475,8 +475,8 @@ def action_link(self): action_title = pgettext('email', 'View activity') def get_recipients(self): - """participant""" - return [self.obj.user] + """team captain""" + return [self.obj.owner] class ParticipantChangedNotification(TimeBasedInfoMixin, TransitionMessage): diff --git a/bluebottle/time_based/tests/test_api.py b/bluebottle/time_based/tests/test_api.py index 80e06344cc..50e2051a07 100644 --- a/bluebottle/time_based/tests/test_api.py +++ b/bluebottle/time_based/tests/test_api.py @@ -1907,7 +1907,7 @@ def test_join_team(self): self.assertEqual( mail.outbox[0].subject, - f'You have joined a team for "{self.activity.title}"' + f'You have joined {captain.team.name} for "{self.activity.title}"' ) self.assertEqual( mail.outbox[0].to[0], diff --git a/bluebottle/time_based/tests/test_scenarios.py b/bluebottle/time_based/tests/test_scenarios.py index 5b44827c1f..f838c7b6d3 100644 --- a/bluebottle/time_based/tests/test_scenarios.py +++ b/bluebottle/time_based/tests/test_scenarios.py @@ -338,7 +338,7 @@ def setUp(self): ) self.client = JSONAPITestClient() - def test_user_joins_activity(self): + def test_user_starts_a_team(self): api_user_joins_period_activity(self, self.activity, self.supporter) assert_participant_status(self, self.activity, self.supporter, status='accepted') self.assertEqual(self.activity.teams.count(), 1) From 7e6efb49a9b8b17548e84435b9509266f00d7489 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 17 Aug 2022 12:19:17 +0200 Subject: [PATCH 110/112] Fix more messages and tests --- bluebottle/activities/triggers.py | 31 ++++++++++--------- bluebottle/time_based/messages.py | 2 +- bluebottle/time_based/tests/test_api.py | 8 ++--- .../time_based/tests/test_notifications.py | 2 +- bluebottle/time_based/tests/test_triggers.py | 4 +-- 5 files changed, 24 insertions(+), 23 deletions(-) diff --git a/bluebottle/activities/triggers.py b/bluebottle/activities/triggers.py index 782266d234..9eeb340857 100644 --- a/bluebottle/activities/triggers.py +++ b/bluebottle/activities/triggers.py @@ -212,7 +212,14 @@ def automatically_accept_team(effect): """ automatically accept team """ - return getattr(effect.instance.activity, 'review', False) is False + captain = effect.instance.activity\ + .contributors.not_instance_of(Organizer)\ + .filter(user=effect.instance.owner).first() + return ( + not hasattr(effect.instance.activity, 'review') or + not effect.instance.activity.review or + (captain and captain.status == 'accepted') + ) def needs_review(effect): @@ -247,15 +254,6 @@ def team_activity_will_not_be_full(effect): ) -def user_is_team_captain(effect): - """ - current user is team captain - """ - if 'user' not in effect.options: - return False - return effect.instance.owner == effect.options['user'] - - @register(Team) class TeamTriggers(TriggerManager): triggers = [ @@ -268,14 +266,16 @@ class TeamTriggers(TriggerManager): ), NotificationEffect( TeamAppliedMessage, - conditions=[needs_review] + conditions=[ + needs_review, + ] ), TransitionEffect( TeamStateMachine.accept, conditions=[ automatically_accept_team ] - ) + ), ] ), @@ -285,14 +285,15 @@ class TeamTriggers(TriggerManager): NotificationEffect( TeamParticipantJoinedNotification, conditions=[ - automatically_accept_team, - user_is_team_captain + automatically_accept_team ] ), RelatedTransitionEffect( 'members', ParticipantStateMachine.accept, - conditions=[needs_review] + conditions=[ + needs_review + ] ), RelatedTransitionEffect( 'activity', diff --git a/bluebottle/time_based/messages.py b/bluebottle/time_based/messages.py index cc65e0ac30..dc346cad11 100644 --- a/bluebottle/time_based/messages.py +++ b/bluebottle/time_based/messages.py @@ -456,7 +456,7 @@ def get_recipients(self): return [self.obj.user] -class TeamParticipantJoinedNotification(TimeBasedInfoMixin, TransitionMessage): +class TeamParticipantJoinedNotification(TransitionMessage): """ The participant joined """ diff --git a/bluebottle/time_based/tests/test_api.py b/bluebottle/time_based/tests/test_api.py index 50e2051a07..7ba49b7bf6 100644 --- a/bluebottle/time_based/tests/test_api.py +++ b/bluebottle/time_based/tests/test_api.py @@ -2255,15 +2255,15 @@ def test_accept_with_custom_message(self): self.assertTrue('Great to have you!' in message.body) def test_accept_team_with_custom_message(self): - self.activity.team_activity = 'teams' - self.activity.review = True - self.activity.save() + self.participant.status = 'new' self.participant.team = TeamFactory.create( activity=self.activity, owner=self.participant.user ) - self.participant.status = 'new' self.participant.save() + self.activity.team_activity = 'teams' + self.activity.review = True + self.activity.save() self.data['data']['attributes']['transition'] = 'accept' self.data['data']['attributes']['message'] = 'Great to have you!' mail.outbox = [] diff --git a/bluebottle/time_based/tests/test_notifications.py b/bluebottle/time_based/tests/test_notifications.py index 1474a962e9..7c35874bf4 100644 --- a/bluebottle/time_based/tests/test_notifications.py +++ b/bluebottle/time_based/tests/test_notifications.py @@ -240,7 +240,7 @@ def test_team_joined_notification(self): self.message_class = TeamParticipantJoinedNotification self.create() self.assertRecipients([self.supporter]) - self.assertSubject('You have joined a team for "Save the world!"') + self.assertSubject('You have registered your team for "Save the world!"') self.assertActionLink(self.activity.get_absolute_url()) self.assertActionTitle('View activity') self.assertBodyNotContains( diff --git a/bluebottle/time_based/tests/test_triggers.py b/bluebottle/time_based/tests/test_triggers.py index ac5e46f6b2..cc4be43ccd 100644 --- a/bluebottle/time_based/tests/test_triggers.py +++ b/bluebottle/time_based/tests/test_triggers.py @@ -1766,11 +1766,11 @@ def test_team_join(self): self.assertStatus(participant.team, 'open') self.assertEqual(len(mail.outbox), 2) self.assertEqual( - mail.outbox[1].subject, + mail.outbox[0].subject, f'A new team has joined "{self.activity.title}"' ) self.assertEqual( - mail.outbox[0].subject, + mail.outbox[1].subject, f'You have registered your team for "{self.activity.title}"' ) prep = participant.preparation_contributions.first() From e6884eb9e96d0691cda1beb4e27ecc81789410a5 Mon Sep 17 00:00:00 2001 From: Loek van Gent Date: Wed, 17 Aug 2022 12:54:18 +0200 Subject: [PATCH 111/112] Fix some mails --- bluebottle/time_based/tests/test_triggers.py | 13 ++++++------- bluebottle/time_based/triggers.py | 11 ++++++++++- scripts/generate_notifications_documentation.py | 3 +++ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/bluebottle/time_based/tests/test_triggers.py b/bluebottle/time_based/tests/test_triggers.py index 5f21af134b..290cf27de8 100644 --- a/bluebottle/time_based/tests/test_triggers.py +++ b/bluebottle/time_based/tests/test_triggers.py @@ -8,7 +8,7 @@ from tenant_extras.utils import TenantLanguage from bluebottle.activities.messages import ParticipantWithdrewConfirmationNotification, \ - TeamMemberWithdrewMessage + TeamMemberWithdrewMessage, TeamMemberAddedMessage from bluebottle.activities.messages import TeamMemberRemovedMessage, TeamCancelledTeamCaptainMessage, \ TeamCancelledMessage from bluebottle.activities.models import Organizer, Activity @@ -1938,20 +1938,19 @@ def test_join_team_participant(self): self.activity.team_activity = 'teams' self.activity.save() user = BlueBottleUserFactory.create() - captain = BlueBottleUserFactory.create() - team = TeamFactory.create( - owner=captain, + captain = self.participant_factory.create( + user=BlueBottleUserFactory.create(), activity=self.activity ) self.model = self.participant_factory.build( - team=team, + accepted_invite=captain.invite, activity=self.activity, user=user ) with self.execute(user=user): self.assertNoNotificationEffect(NewParticipantNotification) - self.assertNotificationEffect(TeamParticipantJoinedNotification) - self.assertNotificationEffect(ParticipantJoinedNotification) + self.assertNoNotificationEffect(TeamParticipantJoinedNotification) + self.assertNotificationEffect(TeamMemberAddedMessage) def test_remove_participant(self): self.model = self.participant_factory.create( diff --git a/bluebottle/time_based/triggers.py b/bluebottle/time_based/triggers.py index 25e4e1525f..af22706b4f 100644 --- a/bluebottle/time_based/triggers.py +++ b/bluebottle/time_based/triggers.py @@ -8,7 +8,8 @@ ActivityExpiredNotification, ActivityRejectedNotification, ActivityCancelledNotification, ActivityRestoredNotification, ParticipantWithdrewConfirmationNotification, - TeamMemberWithdrewMessage, TeamMemberRemovedMessage, TeamCaptainAcceptedMessage, TeamCancelledTeamCaptainMessage + TeamMemberWithdrewMessage, TeamMemberRemovedMessage, TeamCaptainAcceptedMessage, TeamCancelledTeamCaptainMessage, + TeamMemberAddedMessage ) from bluebottle.activities.states import OrganizerStateMachine, TeamStateMachine from bluebottle.activities.triggers import ( @@ -1306,9 +1307,17 @@ class ParticipantTriggers(ContributorTriggers): TeamParticipantJoinedNotification, conditions=[ automatically_accept, + is_team_captain, is_team_activity ] ), + NotificationEffect( + TeamMemberAddedMessage, + conditions=[ + is_team_activity, + not_team_captain, + ] + ), NotificationEffect( ParticipantAcceptedNotification, conditions=[ diff --git a/scripts/generate_notifications_documentation.py b/scripts/generate_notifications_documentation.py index 349d95ed7f..aee6f21467 100644 --- a/scripts/generate_notifications_documentation.py +++ b/scripts/generate_notifications_documentation.py @@ -74,11 +74,13 @@ def run(*args): data = response.json() version = data['version']['number'] + 1 html = '' + total = 0 for model in models: model_class = import_string(model['model']) messages = document_notifications(model_class) if len(messages): + total += len(messages) html += "

    {}

    ".format(model_class._meta.verbose_name) html += generate_notification_html(messages) @@ -101,5 +103,6 @@ def run(*args): response = requests.put(url, json=data, auth=(api['user'], api['key'])) if response.status_code == 200: print("[OK]") + print(f"{total} messages") else: print("[ERROR]") From d7609ad80d6ea2ee65b238799289821016c0fa79 Mon Sep 17 00:00:00 2001 From: Ernst Odolphi Date: Thu, 18 Aug 2022 10:29:06 +0200 Subject: [PATCH 112/112] Fix dst change better --- bluebottle/time_based/utils.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bluebottle/time_based/utils.py b/bluebottle/time_based/utils.py index 2985b1f0dc..6701867834 100644 --- a/bluebottle/time_based/utils.py +++ b/bluebottle/time_based/utils.py @@ -1,4 +1,5 @@ from datetime import timedelta +from django.utils.timezone import get_current_timezone def nth_weekday(date): @@ -10,10 +11,10 @@ def nth_weekday(date): def duplicate_slot(slot, interval, end): - dates = [] + tz = get_current_timezone() - start = slot.start + start = slot.start.astimezone(tz) for n in range(int((end - start.date()).days)): date = start + timedelta(days=n + 1) if interval == 'day': @@ -29,8 +30,8 @@ def duplicate_slot(slot, interval, end): for date in dates: slot.id = None - slot.start = slot.start.tzinfo.localize( - slot.start.replace(tzinfo=None, day=date.day, month=date.month, year=date.year) + slot.start = tz.localize( + start.replace(tzinfo=None, day=date.day, month=date.month, year=date.year) ) slot.status = 'open' slot.save()