def edit_group(request, group_id): """Edit a group.""" group = get_object_or_404( model.Group.objects.select_related('owner'), pk=group_id) if group.owner != request.user.profile: raise http.Http404 if request.method == 'POST': if request.POST.get('remove', False): with xact.xact(): group.delete() messages.success(request, "Group '%s' removed." % group) return redirect(home.redirect_home(request.user)) form = forms.GroupForm(request.POST, instance=group) if form.is_valid(): with xact.xact(): group = form.save() tracking.track(request, 'edited group') return redirect('group', group_id=group.id) else: form = forms.GroupForm(instance=group) return TemplateResponse( request, 'village/group_form/edit_group.html', { 'form': form, 'group': group, }, )
def edit_student(request, student_id): """Edit a student.""" rel = get_relationship_or_404(student_id, request.user.profile) group = get_querystring_group(request, rel.student) if request.method == 'POST': if request.POST.get('remove', False): with xact.xact(): rel.delete() messages.success(request, "Student '%s' removed." % rel.student) if group: return redirect('group', group_id=group.id) else: return redirect('all_students') form = forms.StudentForm( request.POST, instance=rel.student, elder=rel.elder) if form.is_valid(): with xact.xact(): student = form.save() tracking.track(request, 'edited student') return redirect_to_village(student, group) else: form = forms.StudentForm(instance=rel.student, elder=rel.elder) return TemplateResponse( request, 'village/student_form/edit_student.html', { 'form': form, 'student': rel.student, 'group': group, }, )
def test_savepoint_rollback(mock_transaction_methods): with pytest.raises(IntegrityError): with xact.xact(): with xact.xact(): raise IntegrityError() assert mock_transaction_methods['savepoint_rollback'].call_count == 1 assert mock_transaction_methods['savepoint_commit'].call_count == 0
def edit_elder(request, elder_id, student_id=None, group_id=None): """Edit a village elder.""" elder = get_object_or_404( model.Profile.objects.select_related('user'), id=elder_id) # can't edit the profile of another school staff if elder.school_staff: raise http.Http404 if student_id is not None: teacher_rel = get_relationship_or_404(student_id, request.user.profile) editor = model.elder_in_context(teacher_rel) elder_rel = get_relationship_or_404(student_id, elder) elder = model.elder_in_context(elder_rel) all_elders = elder_rel.student.elder_relationships group = get_querystring_group(request, elder_rel.student) else: elder_rel = None editor = request.user.profile if group_id is not None: group = get_object_or_404(model.Group.objects.filter( owner=request.user.profile), pk=group_id) else: group = model.AllStudentsGroup(request.user.profile) all_elders = group.all_elders if request.method == 'POST': form = forms.EditFamilyForm(request.POST, instance=elder, rel=elder_rel) success = False if elder_rel and request.POST.get('remove', False): with xact.xact(): elder_rel.delete() success = True elif form.is_valid(): with xact.xact(): form.save(editor=editor) messages.success(request, u"Changes saved!") success = True if success: if elder_rel: return redirect('village', student_id=student_id) elif group and not group.is_all: return redirect('group', group_id=group.id) return redirect('all_students') else: form = forms.EditFamilyForm(instance=elder, rel=elder_rel) return TemplateResponse( request, 'village/elder_form/edit_elder.html', { 'form': form, 'group': group, 'student': elder_rel.student if elder_rel else None, 'inviter': editor, 'elder': elder, 'elders': model.contextualized_elders( all_elders).order_by('school_staff', 'name'), }, )
def test_nested_xact_uses_savepoints(mock_transaction_methods): mock_sp_commit = mock_transaction_methods['savepoint_commit'] mock_sp = mock_transaction_methods['savepoint'] mock_sp.return_value = 3 with xact.xact(): with xact.xact(): pass assert mock_sp.call_count == 1 assert mock_sp_commit.call_count == 1 mock_sp_commit.assert_called_with(3, 'default')
def test_error_in_savepoint_commit_causes_rollback(mock_transaction_methods): mock_sp_commit = mock_transaction_methods['savepoint_commit'] mock_sp_rollback = mock_transaction_methods['savepoint_rollback'] mock_sp = mock_transaction_methods['savepoint'] mock_sp.return_value = 4 mock_sp_commit.side_effect = IntegrityError() with pytest.raises(IntegrityError): with xact.xact(): with xact.xact(): pass assert mock_sp_commit.call_count == 1 assert mock_sp_rollback.call_count == 1 mock_sp_rollback.assert_called_with(4, 'default')
def invite_family(request, student_id): """Invite family member to a student's village.""" rel = get_relationship_or_404(student_id, request.user.profile) group = get_querystring_group(request, rel.student) if request.method == 'POST': form = forms.InviteFamilyForm(request.POST, rel=rel) if form.is_valid(): with xact.xact(): form.save() tracking.track(request, 'invited family') return redirect_to_village(rel.student, group) else: phone = request.GET.get('phone', None) initial = {} if phone is not None: initial['phone'] = phone form = forms.InviteFamilyForm(initial=initial, rel=rel) return TemplateResponse( request, 'village/invite_elder/family.html', { 'group': group, 'student': rel.student, 'inviter': model.elder_in_context(rel), 'elders': model.contextualized_elders( rel.student.elder_relationships).order_by( 'school_staff', 'name'), 'form': form, }, )
def add_student(request, group_id=None): """Add a student.""" group = get_object_or_404(model.Group, id=group_id) if group_id else None if request.method == 'POST': form = forms.AddStudentForm( request.POST, elder=request.user.profile, group=group) if form.is_valid(): with xact.xact(): student = form.save() tracking.track(request, 'added student') return redirect_to_village(student, group) else: form = forms.AddStudentForm(elder=request.user.profile, group=group) return TemplateResponse( request, 'village/student_form/add_student.html', { 'form': form, 'group': group, 'code': group.code if group else request.user.profile.code, 'default_lang_code': settings.LANGUAGE_CODE, 'pyo_phone': formats.display_phone( request.user.profile.source_phone), 'group_just_created': group and request.GET.get('created', None), }, )
def invite_teacher_to_group(request, group_id): """Invite teacher to a group.""" group = get_object_or_404( model.Group.objects.filter(owner=request.user.profile), id=group_id) if request.method == 'POST': form = forms.InviteTeacherForm(request.POST, group=group) if form.is_valid(): with xact.xact(): teacher = form.save() tracking.track( request, 'invited teacher', invitedEmail=teacher.user.email, groupId=group_id, ) return redirect('group', group_id=group.id) else: form = forms.InviteTeacherForm(group=group) return TemplateResponse( request, 'village/invite_elder/teacher_to_group.html', { 'group': group, 'form': form, 'elders': model.contextualized_elders( group.all_elders).order_by('school_staff', 'name'), }, )
def invite_teacher(request, student_id): """Invite teacher to a student's village.""" rel = get_relationship_or_404(student_id, request.user.profile) group = get_querystring_group(request, rel.student) if request.method == 'POST': form = forms.InviteTeacherForm(request.POST, rel=rel) if form.is_valid(): with xact.xact(): teacher = form.save() tracking.track( request, 'invited teacher', invitedEmail=teacher.user.email, studentId=student_id, ) return redirect_to_village(rel.student, group) else: form = forms.InviteTeacherForm(rel=rel) return TemplateResponse( request, 'village/invite_elder/teacher.html', { 'group': group, 'student': rel.student, 'form': form, 'elders': model.contextualized_elders( rel.student.elder_relationships).order_by( 'school_staff', 'name'), }, )
def test_error_in_commit(mock_transaction_methods): """Error in commit causes rollback and re-raises error.""" mock_transaction_methods['commit'].side_effect = IntegrityError() with pytest.raises(IntegrityError): with xact.xact(): pass assert mock_transaction_methods['rollback'].call_count == 1
def test_tasks_run_after_transaction(self, sms): """Task is not applied until after transaction-in-progress commits.""" # If we actually touched the database in this test, we would need the # `transactional_db` fixture; but we don't, so we avoid the slowdown. with xact.xact(): tasks.send_sms.delay('+15555555555', '+15555555555', 'something') assert len(sms.outbox) == 0 assert len(sms.outbox) == 1
def test_can_use_twice(): """Can use same xact object as context manager twice.""" cm = xact.xact() assert not transaction.is_managed() for i in range(2): with cm: assert transaction.is_managed() assert not transaction.is_managed()
def donate(request): if request.method == 'POST': form = forms.DonateForm(request.POST) if form.is_valid(): with xact.xact(): stripe.api_key = STRIPE_PRIVATE_KEY token = request.POST['stripeToken'] try: charge = stripe.Charge.create( amount=int(form.cleaned_data['amount']) * 100, # amount needs to be in cents currency="usd", card=token, description=form.cleaned_data['email']) school = form.cleaned_data['school'] if school.id is None: # this could just set country_code and then school.save(), but that # creates a race condition for two users creating same school at # same time, resulting in IntegrityError school, created = model.School.objects.get_or_create( name=school.name, postcode=school.postcode, defaults={ 'country_code': form.cleaned_data['country_code'], 'auto': school.auto, }, ) donation = model.Donation( name=form.cleaned_data['name'], email=form.cleaned_data['email'], phone=form.cleaned_data['phone'], country_code=form.cleaned_data['country_code'], school=school, amount=int(form.cleaned_data['amount']) * 100, charge_data=json.dumps(charge), # We could add a currency field if donating in CAD is necessary ) donation.save() except stripe.AuthenticationError: # The stripe key was incorrect messages.failure( request, u"Sorry, there was an issue with the payment.") except stripe.CardError, e: # The card has been declined messages.failure( request, u"Sorry, there was an issue with the payment.") messages.success(request, u"Payment accepted!") return redirect(redirect_home(request.user))
def edit_profile(request): if request.method == 'POST': form = forms.EditProfileForm( request.POST, instance=request.user.profile) if form.is_valid(): with xact.xact(): form.save() messages.success(request, u"Profile changes saved!") return redirect(redirect_home(request.user)) else: form = forms.EditProfileForm(instance=request.user.profile) return TemplateResponse(request, 'users/edit_profile.html', {'form': form})
def edit_profile(request): if request.method == 'POST': form = forms.EditProfileForm(request.POST, instance=request.user.profile) if form.is_valid(): with xact.xact(): form.save() messages.success(request, u"Profile changes saved!") return redirect(redirect_home(request.user)) else: form = forms.EditProfileForm(instance=request.user.profile) return TemplateResponse(request, 'users/edit_profile.html', {'form': form})
def test_post_commit_listener_uses_db(transactional_db): """Test that a post_commit listener can safely query the database.""" called = [] def _my_listener(**kwargs): assert model.Profile.objects.count() == 1 called.append(True) xact.post_commit.connect(_my_listener) with xact.xact(): factories.ProfileFactory.create() assert called == [True]
def donate(request): if request.method == 'POST': form = forms.DonateForm(request.POST) if form.is_valid(): with xact.xact(): stripe.api_key = STRIPE_PRIVATE_KEY token = request.POST['stripeToken'] try: charge = stripe.Charge.create( amount=int(form.cleaned_data['amount'])*100, # amount needs to be in cents currency="usd", card=token, description=form.cleaned_data['email'] ) school = form.cleaned_data['school'] if school.id is None: # this could just set country_code and then school.save(), but that # creates a race condition for two users creating same school at # same time, resulting in IntegrityError school, created = model.School.objects.get_or_create( name=school.name, postcode=school.postcode, defaults={ 'country_code': form.cleaned_data['country_code'], 'auto': school.auto, }, ) donation = model.Donation( name=form.cleaned_data['name'], email=form.cleaned_data['email'], phone=form.cleaned_data['phone'], country_code=form.cleaned_data['country_code'], school=school, amount = int(form.cleaned_data['amount'])*100, charge_data = json.dumps(charge), # We could add a currency field if donating in CAD is necessary ) donation.save() except stripe.AuthenticationError: # The stripe key was incorrect messages.failure(request, u"Sorry, there was an issue with the payment.") except stripe.CardError, e: # The card has been declined messages.failure(request, u"Sorry, there was an issue with the payment.") messages.success(request, u"Payment accepted!") return redirect(redirect_home(request.user))
def test_tasks_discarded_if_transaction_rolled_back(self, sms): """If transaction is rolled back, pending tasks are discarded.""" class TestException(Exception): pass try: with xact.xact(): tasks.send_sms.delay( '+15555555555', '+15555555555', 'something') # exception causes transaction to be rolled back raise TestException() except TestException: pass # task was discarded because transaction was rolled back assert len(sms.outbox) == 0
def twilio_receive(request): """Receive an SMS via Twilio.""" source = request.POST['From'] to = request.POST['To'] body = request.POST['Body'] with xact.xact(): reply = hook.receive_sms(source, to, body) response = twiml.Response() if reply: for chunk in split_sms(reply): response.sms(chunk) return response
def twilio_receive(request): """Receive an SMS via Twilio.""" source = request.POST["From"] to = request.POST["To"] body = request.POST["Body"] with xact.xact(): reply = hook.receive_sms(source, to, body) response = twiml.Response() if reply: for chunk in split_sms(reply): response.sms(chunk) return response
def test_tasks_discarded_if_transaction_rolled_back(self, sms): """If transaction is rolled back, pending tasks are discarded.""" class TestException(Exception): pass try: with xact.xact(): tasks.send_sms.delay('+15555555555', '+15555555555', 'something') # exception causes transaction to be rolled back raise TestException() except TestException: pass # task was discarded because transaction was rolled back assert len(sms.outbox) == 0
def register(request): if request.method == 'POST': form = forms.RegistrationForm(request.POST) if form.is_valid(): with xact.xact(): profile = form.save() user = profile.user user.backend = settings.AUTHENTICATION_BACKENDS[0] auth.login(request, user) token_generator = tokens.EmailConfirmTokenGenerator() invites.send_invite_email( profile, 'emails/welcome', token_generator=token_generator, ) messages.success( request, "Welcome to Portfoliyo! " "Grab your phone and add yourself as a parent " "to see how it works!" ) tracking.track( request, 'registered', email_notifications=( 'yes' if form.cleaned_data.get('email_notifications') else 'no' ), user_id=user.id, ) return redirect(redirect_home(user)) else: form = forms.RegistrationForm() return TemplateResponse( request, 'users/register.html', {'form': form}, )
def confirm_email(request, uidb36, token): """Confirm an email address.""" try: uid_int = base36_to_int(uidb36) user = model.User.objects.get(id=uid_int) except (ValueError, model.User.DoesNotExist): user = None token_generator = tokens.EmailConfirmTokenGenerator() if user is not None and token_generator.check_token(user, token): with xact.xact(): user.is_active = True user.save(force_update=True) profile = user.profile profile.email_confirmed = True profile.save(force_update=True) messages.success(request, "Email address %s confirmed!" % user.email) tracking.track(request, 'confirmed email') return redirect(redirect_home(user)) return TemplateResponse(request, 'users/confirmation_failed.html')
def add_group(request): """Add a group.""" if request.method == 'POST': form = forms.AddGroupForm(request.POST, owner=request.user.profile) if form.is_valid(): with xact.xact(): group = form.save() tracking.track(request, 'added group') if not group.students.exists(): return redirect( reverse('add_student', kwargs={'group_id': group.id}) + '?created=1') return redirect('group', group_id=group.id) else: form = forms.AddGroupForm(owner=request.user.profile) return TemplateResponse( request, 'village/group_form/add_group.html', { 'form': form, }, )
def register(request): if request.method == 'POST': form = forms.RegistrationForm(request.POST) if form.is_valid(): with xact.xact(): profile = form.save() user = profile.user user.backend = settings.AUTHENTICATION_BACKENDS[0] auth.login(request, user) token_generator = tokens.EmailConfirmTokenGenerator() invites.send_invite_email( profile, 'emails/welcome', token_generator=token_generator, ) messages.success( request, "Welcome to Portfoliyo! " "Grab your phone and add yourself as a parent " "to see how it works!") tracking.track( request, 'registered', email_notifications=( 'yes' if form.cleaned_data.get('email_notifications') else 'no'), user_id=user.id, ) return redirect(redirect_home(user)) else: form = forms.RegistrationForm() return TemplateResponse( request, 'users/register.html', {'form': form}, )
def wrap_view(self, view): """Wrap in transaction; undo csrf-exempt.""" wrapper = xact.xact(super(PortfoliyoResource, self).wrap_view(view)) wrapper.csrf_exempt = False return wrapper
def admin_view(self, *args, **kwargs): """Wrap all admin views in a transaction.""" wrapped = super(AdminSite, self).admin_view(*args, **kwargs) return xact.xact(wrapped)
def create_post(request, student_id=None, group_id=None): """ Create a post. If ``student_id`` is provided in the URL, the post will be a single-village post. If ``group_id`` is provided, it will be a group bulk post. If neither is provided, it will be an all-students bulk post. POST parameters accepted: ``text`` The text of the post to create. Must be few enough characters that, when the user's auto-signature is appended, the resulting full SMS message is <160 characters. ``type`` The type of post to create: "message", "note", "call", or "meeting". This parameter is ignored for bulk posts; all bulk posts are of type "message". ``elder`` A list of elder IDs connected with this post. For a "message" type post, these users will receive the post via SMS. For a "meeting" or "call" type post, these are the users who were present on the call or at the meeting. ``extra_name`` A list of additional names connected with this post. (For instance, for a "meeting" or "call" type post, these are names of additional people present at the meeting or on the call, who are not actually elders in the village.) ``author_sequence_id`` An increasing numeric ID for posts authored by this user in this browser session. This value is opaque to the server and not stored anywhere, but is round-tripped through Pusher back to the client, to simplify matching up post data and avoid creating duplicates on the client. For non-bulk posts, an ``attachment`` file-upload parameter is also optionally accepted. Returns JSON object with boolean key ``success``. If ``success`` is ``False``, a human-readable message will be provided in the ``error`` key. If ``success`` is ``True``, the ``objects`` key will be a list containing one JSON-serialized post object. (Even though this view will only ever return one post, it still returns a list for better compatibility with other client-side JSON-handling code.) """ if 'text' not in request.POST: return http.HttpResponseBadRequest( json.dumps( { 'error': "Must provide a 'text' querystring parameter.", 'success': False, } ), content_type='application/json', ) extra_kwargs = {} group = None rel = None post_model = model.BulkPost profile_ids = 'all' if student_id is not None: rel = get_relationship_or_404(student_id, request.user.profile) post_model = model.Post target = rel.student profile_ids = request.POST.getlist('elder') extra_kwargs['extra_names'] = request.POST.getlist('extra_name') extra_kwargs['post_type'] = request.POST.get('type') if 'attachment' in request.FILES: extra_kwargs['attachments'] = request.FILES.getlist('attachment') redirect_url = reverse('village', kwargs={'student_id': student_id}) qs_group = get_querystring_group(request, rel.student) if qs_group: redirect_url += "?group=%s" % qs_group.id elif group_id is not None: group = get_object_or_404( model.Group.objects.filter(owner=request.user.profile), pk=group_id) target = group redirect_url = reverse('group', kwargs={'group_id': group_id}) else: target = None redirect_url = reverse('all_students') text = request.POST['text'] sequence_id = request.POST.get('author_sequence_id') limit = model.post_char_limit(rel or request.user.profile) if len(text) > limit: return http.HttpResponseBadRequest( json.dumps( { 'error': 'Posts are limited to %s characters.' % limit, 'success': False, } ), content_type='application/json', ) with xact.xact(): post = post_model.create( request.user.profile, target, text, profile_ids=profile_ids, sequence_id=sequence_id, **extra_kwargs) if request.is_ajax(): data = { 'success': True, 'objects': [ serializers.post2dict( post, author_sequence_id=sequence_id, unread=False, mine=True) ], } return http.HttpResponse( json.dumps(data), content_type='application/json') else: return http.HttpResponseRedirect(redirect_url)