Skip to content

Commit

Permalink
Members-only classes, email members for uploaded timetable
Browse files Browse the repository at this point in the history
  • Loading branch information
rebkwok committed Aug 4, 2024
1 parent eebc6d4 commit 24caa6a
Show file tree
Hide file tree
Showing 22 changed files with 191 additions and 35 deletions.
3 changes: 3 additions & 0 deletions booking/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ class EventAdmin(admin.ModelAdmin):
'fields': ('cancellation_period',),
'description': '<div class="help">%s</div>' % CANCELLATION_TEXT,
}),
('Permissions', {
'fields': ('allowed_group_override', "members_only"),
}),
]

def get_spaces_left(self, obj):
Expand Down
16 changes: 11 additions & 5 deletions booking/context_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,17 @@ def get_event_context(context, event, user, booking=None):
context['bookable'] = False
context['needs_permission'] = True
description = event.allowed_group_description
extra_info = f" ({description})" if description else ""
booking_info_text = "<span class='cancel-warning'>NOT AVAILABLE FOR BOOKING</br>" \
f"This class requires additional permission{extra_info}. Please contact " \
"<a href='mailto:{}' target=_blank>{}</a> to request to have your account " \
"upgraded.</span>".format(event.contact_email, event.contact_email)
if event.members_only:
permission_msg = f"This {ev_type_str} is open to members only."
else:
extra_info = f" ({description})" if description else ""
permission_msg = (
f"This class requires additional permission{extra_info}. Please contact "
f"<a href='mailto:{event.contact_email}' target=_blank>{event.contact_email}</a> "
"to request to have your account upgraded.</span>"
)

booking_info_text = f"<span class='cancel-warning'>NOT AVAILABLE FOR BOOKING</br>{permission_msg}"
else:
if auto_cancelled:
context['auto_cancelled'] = True
Expand Down
8 changes: 7 additions & 1 deletion booking/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,13 @@ class ChooseMembershipForm(forms.Form):
queryset=Membership.objects.purchasable(),
widget=forms.RadioSelect,
)
agree_to_terms = forms.BooleanField(required=True, label="Please tick to confirm that you understand and agree that by setting up a membership, your payment details will be held by Stripe and collected on a recurring basis")
agree_to_terms = forms.BooleanField(
required=True,
label=(
"I understand that by setting up a membership my payment details will be held by Stripe "
"and membership payments will be collected on a recurring monthly basis."
)
)

def __init__(self, *args, **kwargs):
has_cancelled_current_membership = kwargs.pop("has_cancelled_current_membership")
Expand Down
20 changes: 20 additions & 0 deletions booking/migrations/0104_event_members_only.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 4.2.13 on 2024-08-04 15:48

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("booking", "0103_remove_blocktype_assign_free_class_on_completion_and_more"),
]

operations = [
migrations.AddField(
model_name="event",
name="members_only",
field=models.BooleanField(
default=False, help_text="Can only be booked by members"
),
),
]
4 changes: 4 additions & 0 deletions booking/models/booking_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,8 @@ class Event(models.Model):
help_text="Override group allowed to book this event (defaults to same group as the event type)"
)

members_only = models.BooleanField(default=False, help_text="Can only be booked by members")

class Meta:
ordering = ['-date']
indexes = [
Expand Down Expand Up @@ -300,6 +302,8 @@ def allowed_group(self):
return self.event_type.allowed_group

def has_permission_to_book(self, user):
if self.members_only:
return user.has_membership()
return self.allowed_group.has_permission(user)

@property
Expand Down
5 changes: 4 additions & 1 deletion booking/models/membership_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ def generate_stripe_product_id(self):
return slug

def active_user_memberships(self):
not_cancelled_yet = self.user_memberships.filter(models.Q(end_date__isnull=True) | models.Q(end_date__gt=timezone.now()))
not_cancelled_yet = self.user_memberships.filter(
models.Q(subscription_status__in=["active", "past_due", "setup_pending"])
& (models.Q(end_date__isnull=True) | models.Q(end_date__gt=timezone.now()))
)
results = {
"all": [], "ongoing": [], "cancelling": []
}
Expand Down
12 changes: 12 additions & 0 deletions booking/tests/test_ajax_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,18 @@ def test_creating_booking_with_unpaid_user_block(self):
self.assertIsNone(bookings[0].block)
self.assertFalse(bookings[0].paid)

def test_cannot_book_for_members_only_class(self):
event = baker.make_recipe(
'booking.future_PC', members_only=True, cost=5
)
url = reverse('booking:ajax_create_booking', args=[event.id]) + "?ref=events"
self.client.login(username=self.user.username, password='test')
resp = self.client.post(url)
assert resp.status_code == 400
assert resp.content.decode('utf-8') == (
"Only members are allowed to book this class; please contact the studio for further information."
)

def test_cannot_book_for_pole_practice_without_permission(self):
"""
Test trying to create a booking for pole practice without permission returns 400
Expand Down
21 changes: 21 additions & 0 deletions booking/tests/test_event_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,15 @@ def test_event_list_with_booked_events(self):
self.assertEqual(len(booked_events), 1)
self.assertTrue(event.id in booked_events)

def test_event_list_members_only(self):
resp = self.client.get(self.url)
assert "Members only" not in resp.rendered_content
event = self.events[0]
event.members_only = True
event.save()
resp = self.client.get(self.url)
assert "Members only" in resp.rendered_content

def test_event_list_booked_paid_events(self):
"""
test that booked events are shown on listing
Expand Down Expand Up @@ -671,6 +680,18 @@ def test_with_logged_in_user(self):
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.context_data['ev_type_for_url'], 'events')

def test_event_members_only(self):
self.client.force_login(self.user)
url = reverse('booking:event_detail', kwargs={'slug': self.event.slug})

resp = self.client.get(url)
assert "open to members only" not in resp.rendered_content

self.event.members_only = True
self.event.save()
resp = self.client.get(url)
assert "open to members only" in resp.rendered_content

def test_with_booked_event(self):
"""
Test that booked event is shown as booked
Expand Down
32 changes: 32 additions & 0 deletions booking/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
Booking, TicketBooking, Ticket, TicketBookingError, BlockVoucher, \
EventVoucher, GiftVoucherType, FilterCategory, UsedBlockVoucher, UsedEventVoucher
from common.tests.helpers import PatchRequestMixin
from stripe_payments.tests.mock_connector import MockConnector


now = timezone.now()

Expand Down Expand Up @@ -1434,3 +1436,33 @@ def test_allowed_group_create():
gp = AllowedGroup.create_with_group(group_name="foo", description="foo group")
assert gp.description == "foo group"
assert Group.objects.filter(name="foo").exists()


@pytest.mark.django_db
@patch("booking.models.membership_models.StripeConnector", MockConnector)
def test_event_has_permission_to_book(configured_user, purchasable_membership):
gp = AllowedGroup.create_with_group(group_name="foo", description="foo group")
event = baker.make_recipe("booking.future_PC")
restricted_event = baker.make_recipe("booking.future_PC", allowed_group_override=gp)
members_only_event = baker.make_recipe("booking.future_PC", members_only=True)

member = baker.make(User)
baker.make("booking.UserMembership", user=member, membership=purchasable_membership, subscription_status="active")

allowed_user = baker.make(User)
gp.add_user(allowed_user)

assert event.has_permission_to_book(configured_user)
assert not restricted_event.has_permission_to_book(configured_user)
assert not members_only_event.has_permission_to_book(configured_user)

assert event.has_permission_to_book(allowed_user)
assert restricted_event.has_permission_to_book(allowed_user)
assert not members_only_event.has_permission_to_book(allowed_user)

assert event.has_permission_to_book(member)
assert not restricted_event.has_permission_to_book(member)
assert members_only_event.has_permission_to_book(member)



12 changes: 7 additions & 5 deletions booking/views/booking_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -817,11 +817,13 @@ def ajax_create_booking(request, event_id):

# if pole practice, make sure this user has permission
if not event.has_permission_to_book(request.user):
logger.error("Attempt to book %s by student without '%s' permission", event.event_type, event.event_type.allowed_group)
return HttpResponseBadRequest(
"Additional permission is required to book this class; please "
"contact the studio for further information."
)
if event.members_only:
logger.error("Attempt to book members-only %s by non-member", event.event_type)
msg = "Only members are allowed to book this class; please contact the studio for further information."
else:
logger.error("Attempt to book %s by student without '%s' permission", event.event_type, event.event_type.allowed_group)
msg = "Additional permission is required to book this class; please contact the studio for further information."
return HttpResponseBadRequest(msg)

# make sure the event isn't full or cancelled
if not event.spaces_left or event.cancelled:
Expand Down
1 change: 1 addition & 0 deletions pipsevents/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = False
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
ACCOUNT_EMAIL_CONFIRMATION_COOLDOWN = 900
ACCOUNT_EMAIL_SUBJECT_PREFIX = "The Watermelon Studio:"
ACCOUNT_PASSWORD_MIN_LENGTH = 6
ACCOUNT_SIGNUP_FORM_CLASS = 'accounts.forms.SignupForm'
Expand Down
10 changes: 7 additions & 3 deletions static/booking/css/custom-v1.14.6.css
Original file line number Diff line number Diff line change
Expand Up @@ -191,27 +191,27 @@ color: #000;
list-style: none;
margin: 0;
padding: 0;
font-size: 90%;
}

.sidebar-nav-sub {
list-style: none;
padding-left: 15px;
padding-left: 5px;
}

.sidebar-title {
font-weight: 600;
background: #D8D8D8;
border-top: solid 1px #BDBDBD;
border-bottom: solid 1px #BDBDBD;

}

.sidebar-help {
padding-left: 18px;
}

.sidebar-nav .collapse li {
text-indent: 20px;
text-indent: 10px;
}

.sidebar-nav li {
Expand Down Expand Up @@ -252,6 +252,10 @@ color: #000;
background: none;
}

ul.dropdown-menu {
border: none;
}

.content-header {
height: 65px;
line-height: 65px;
Expand Down
5 changes: 4 additions & 1 deletion studioadmin/forms/event_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ class Meta:
model = Event
fields = (
'name', 'event_type', 'date', 'categories', 'new_category',
'allowed_group_override',
'allowed_group_override', 'members_only',
'video_link', 'video_link_available_after_class',
'description', 'location',
'max_participants', 'contact_person', 'contact_email', 'cost',
Expand Down Expand Up @@ -385,6 +385,9 @@ class Meta:
'email_studio_when_booked': forms.CheckboxInput(
attrs={'class': "form-check-input"},
),
'members_only': forms.CheckboxInput(
attrs={'class': "form-check-input"},
),
'cancelled': forms.CheckboxInput(
attrs={'class': "form-check-input"}
),
Expand Down
34 changes: 24 additions & 10 deletions studioadmin/views/timetable.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@

from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.contrib import messages
from django.core.mail import send_mail
from django.template.loader import get_template
from django.urls import reverse
from django.shortcuts import HttpResponseRedirect, render, get_object_or_404
from django.views.generic import CreateView, UpdateView
from django.utils.safestring import mark_safe
from braces.views import LoginRequiredMixin

from booking import utils
from booking.models import Event, FilterCategory
from booking.models import Event, FilterCategory, UserMembership
from timetable.models import Session
from studioadmin.forms import TimetableSessionFormSet, SessionAdminForm, \
DAY_CHOICES, UploadTimetableForm
Expand Down Expand Up @@ -254,15 +256,27 @@ def _format_override_option(value):
'override_options': ', '.join([f'{key.replace("_", " ")} ({_format_override_option(value)})' for key, value in override_options.items() if value != "default"]),
}

send_mail(
'{} New timetable upload'.format(
settings.ACCOUNT_EMAIL_SUBJECT_PREFIX
),
f'New timetable has been uploaded: {len(created_classes)} classes',
settings.DEFAULT_FROM_EMAIL,
[settings.SUPPORT_EMAIL],
fail_silently=False
)
visible_created_classes = [
cl for cl in created_classes if cl.visible_on_site
]
members = list(User.objects.filter(id__in=UserMembership.active_member_ids()).values_list("email", flat=True))

if visible_created_classes:
ctx = {
"new_classes": visible_created_classes,
"host": 'http://{}'.format(request.META.get('HTTP_HOST'))
}

send_mail(
'{} New classes have been added'.format(
settings.ACCOUNT_EMAIL_SUBJECT_PREFIX
),
get_template('studioadmin/email/new_classes_uploaded.txt').render(ctx),
settings.DEFAULT_FROM_EMAIL,
[*members, settings.SUPPORT_EMAIL],
html_message=get_template('studioadmin/email/new_classes_uploaded.html').render(ctx),
fail_silently=False
)

return render(
request, 'studioadmin/upload_timetable_confirmation.html',
Expand Down
2 changes: 1 addition & 1 deletion templates/booking/event.html
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ <h3>This {{ ev_type_str }} has been cancelled.</h3>
{% if not past %}
{% if not event.cancelled %}
{% if event.max_participants %}
<h5 class="pt-1">Spaces are {% if event.spaces_left <= 0 %}not {% endif %}available for this {{ ev_type_str }}.</h5>
<p class="pt-1"><strong>Spaces are {% if event.spaces_left <= 0 %}not {% endif %}available for this {{ ev_type_str }}.</strong></p>
{% endif %}
<span id="booked_text">{% include 'booking/includes/event_booking_detail.html' %}</span>
{% endif %} <!-- cancelled -->
Expand Down
1 change: 0 additions & 1 deletion templates/booking/includes/event_booking_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
<p>{{ booking_info_text_cancelled }}<br/>{{ booking_info_text | safe }}

{% if ev_type_code != "OT" %}
<p>See <a href="{% url 'booking:bookings' %}">your bookings</a> for details.</p>
{% if bookable or booked %}
{% if booked and not booking.paid %}
<a href="{% url 'booking:shopping_basket' %}">
Expand Down
1 change: 1 addition & 0 deletions templates/booking/includes/events_row.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
{% elif ev_type_for_url == 'room_hires' %}
<a href="{% url 'booking:room_hire_detail' event.slug %}">{{ event.name }}</a>
{% endif %}
{% if event.members_only %}<span class="badge badge-pill badge-dark">Members only</span>{% endif %}
{% if booking and booking.status == 'OPEN' and not booking.no_show and booking.paid %}
{% if event.show_video_link %}
<a id="video_link_id_{{ event.id }}" class="btn btn-info table-btn" href="{{ event.video_link }}">Join online class</a>
Expand Down
4 changes: 4 additions & 0 deletions templates/booking/membership_create.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
<h2 class="card-title">Memberships</h2>
</div>
<div class="card-body">
<p>
Note: memberships run for a calendar month. Fees are taken on the 25th of each month for the next calendar month. This
allows your membership to run continuously.
</p>
<h4>Memberships available:</h4>
{% for membership in memberships %}
<details>
Expand Down
2 changes: 1 addition & 1 deletion templates/booking/payment_plans.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ <h4>Memberships</h4>
number of classes per calendar month. If you want to book more than your allowance, you can purchase
a block to top up for that month.

Memberships give you some additional benefits, such as merchanise discounts and advance notice of
Memberships give you some additional benefits, such as merchandise discounts and advance notice of
special classes and workshops.
</p>
<p><a href="{% url 'membership_create' %}">Set up a membership</a></p>
Expand Down
Loading

0 comments on commit 24caa6a

Please sign in to comment.