def test_calculate_partial_dues15(self): """ A test to check if partial dues are calculated the right way. "Partial dues" means you have to pay for half a year only, for example. """ from c3smembership.views.membership_dues import ( calculate_partial_dues15) member = C3sMember.get_by_id(1) res = calculate_partial_dues15(member) # print res # print member.membership_date assert res == (u'q1_2015', D('50')) # english member member_en = C3sMember.get_by_id(2) res = calculate_partial_dues15(member_en) # print res assert res == (u'q1_2015', D('50')) member_en.membership_date = date(2015, 6, 1) res = calculate_partial_dues15(member_en) # print res assert res == (u'q2_2015', D('37.50')) member_en.membership_date = date(2015, 9, 1) res = calculate_partial_dues15(member_en) # print res assert res == (u'q3_2015', D('25')) member_en.membership_date = date(2015, 11, 1) res = calculate_partial_dues15(member_en) # print res assert res == (u'q4_2015', D('12.50'))
def mail_payment_confirmation(request): """ send a mail to membership applicant informing her about reception of payment """ _id = request.matchdict['memberid'] _member = C3sMember.get_by_id(_id) if _member.locale == 'de': _subject = u'[C3S AFM] Wir haben Deine Zahlung erhalten. Dankeschön!' else: _subject = u'[C3S AFM] We have received your payment. Thanks!' message = Message( subject=_subject, sender='*****@*****.**', recipients=[_member.email], body=make_payment_confirmation_emailbody(_member) ) #print(message.body) mailer = get_mailer(request) mailer.send(message) _member.payment_confirmed = True _member.payment_confirmed_date = datetime.now() return HTTPFound(request.route_url('dashboard', number=request.cookies['on_page'], order=request.cookies['order'], orderby=request.cookies['orderby'], ) )
def mail_payment_reminder(request): """ send a mail to membership applicant reminding her about lack of signature """ _id = request.matchdict['memberid'] _member = C3sMember.get_by_id(_id) message = Message( subject=u"C3S: don't forget to pay your shares / Bitte Anteile bezahlen", sender='*****@*****.**', #bcc=[request.registry.settings['reminder_blindcopy']], recipients=[_member.email], body=make_payment_reminder_emailbody(_member) ) mailer = get_mailer(request) mailer.send(message) try: # if value is int _member.sent_payment_reminder += 1 except: # pragma: no cover # if value was None (after migration of DB schema) _member.sent_payment_reminder = 1 _member.sent_payment_reminder_date = datetime.now() return HTTPFound(request.route_url( 'dashboard', number=request.cookies['on_page'], order=request.cookies['order'], orderby=request.cookies['orderby']) + '#member_' + str(_member.id) )
def send_certificate_email(request): """ Send email to a member with a link so the member can get her membership certificate. """ _special_condition = False # for redirects to referrer mid = request.matchdict["id"] member = C3sMember.get_by_id(mid) if isinstance(member, NoneType) or not member.is_member(): return Response("that id does not exist or is not an accepted member. go back", status="404 Not Found") # create a token for the certificate member.certificate_token = make_random_token() email_subject, email_body = make_membership_certificate_email(request, member) the_message = Message(subject=email_subject, sender="*****@*****.**", recipients=[member.email], body=email_body) send_message(request, the_message) member.certificate_email = True member.certificate_email_date = datetime.now() try: # pragma: no cover if "detail" in request.referrer: _special_condition = True except TypeError: # pragma: no cover pass if _special_condition: # pragma: no cover return HTTPFound(location=request.referrer + "#certificate") else: return get_memberhip_listing_redirect(request, member.id)
def switch_pay(request): """ This view lets accountants switch member signature info has their signature arrived? """ memberid = request.matchdict['memberid'] dashboard_page = request.cookies['on_page'] order = request.cookies['order'] order_by = request.cookies['orderby'] _member = C3sMember.get_by_id(memberid) if _member.payment_received is True: # change to NOT SET _member.payment_received = False _member.payment_received_date = datetime(1970, 1, 1) elif _member.payment_received is False: # set to NOW _member.payment_received = True _member.payment_received_date = datetime.now() log.info( "payment info of member.id %s changed by %s to %s" % ( _member.id, request.user.login, _member.payment_received ) ) return HTTPFound( request.route_url('dashboard', number=dashboard_page, order=order, orderby=order_by))
def switch_sig(request): """ This view lets accountants switch member signature info has their signature arrived? """ memberid = request.matchdict['memberid'] #log.info("the id: %s" % memberid) # store the dashboard page the admin came from dashboard_page = request.cookies['on_page'] order = request.cookies['order'] order_by = request.cookies['orderby'] _member = C3sMember.get_by_id(memberid) if _member.signature_received is True: _member.signature_received = False _member.signature_received_date = datetime(1970, 1, 1) elif _member.signature_received is False: _member.signature_received = True _member.signature_received_date = datetime.now() log.info( "signature status of member.id %s changed by %s to %s" % ( _member.id, request.user.login, _member.signature_received ) ) return HTTPFound( request.route_url('dashboard', number=dashboard_page, order=order, orderby=order_by))
def test_shares_edit(self): ''' tests for the shares_edit view ''' # unauthorized access must be prevented res = self.testapp.reset() # delete cookie res = self.testapp.get('/shares_edit/1', status=403) assert('Access was denied to this resource' in res.body) res = self.testapp.get('/login', status=200) self.failUnless('login' in res.body) # try valid user form = res.form form['login'] = u'rut' form['password'] = u'berries' res2 = form.submit('submit', status=302) # # being logged in ... res3 = res2.follow() # being redirected to dashboard with parameters self.failUnless('Dashboard' in res3.body) # no member in DB, so redirecting to dashboard res = self.testapp.get('/shares_edit/1', status=302) res2 = res.follow() self.make_member_with_shares() # now there is a member with shares in the DB # # lets try invalid input res = self.testapp.get('/shares_edit/foo', status=302) res2 = res.follow() self.failUnless('Dashboard' in res2.body) # now try valid id res = self.testapp.get('/shares_edit/1', status=200) self.failUnless('Edit Details for Shares' in res.body) # now we change details, really editing that member form = res.form if DEBUG: print "form.fields: {}".format(form.fields) self.assertTrue('2' in form['number'].value) self.assertTrue(datetime.today().strftime( '%Y-%m-%d') in form['date_of_acquisition'].value) # print(form['date_of_acquisition'].value) form['number'] = u'3' form['date_of_acquisition'] = u'2015-01-02' # try to submit now. this must fail, # because the date of birth is wrong # ... and other dates are missing res2 = form.submit('submit', status=200) # check data in DB _m1 = C3sMember.get_by_id(1) self.assertTrue(_m1.shares[0].number is 3) self.assertTrue(str( _m1.shares[0].date_of_acquisition) in str(datetime(2015, 1, 2)))
def test_send_certificate_email_english(self): """ test the send_certificate_email view (english) """ if DEBUG: print('test_send_certificate_email_english') from c3smembership.membership_certificate import send_certificate_email self.config.add_route('join', '/') self.config.add_route('dashboard', '/') self.config.add_route('certificate_pdf', '/') self.config.add_route('membership_listing_backend', '/') from pyramid_mailer import get_mailer request = testing.DummyRequest() request.matchdict = { 'id': '2', 'name': 'foobar', 'token': 'hotzenplotz' # WRONG/INVALID token } request.cookies['on_page'] = 1 request.cookies['order'] = 'asc' request.cookies['orderby'] = 'id' mailer = get_mailer(request) result = send_certificate_email(request) # print result self.assertTrue(result.status_code == 404) # not found self.assertEqual(len(mailer.outbox), 0) request.matchdict = { 'id': '2', 'name': 'foobar', 'token': 'hotzenplotz123' } member2 = C3sMember.get_by_id(2) member2.membership_accepted = True # the request needs stuff to be in the cookie (for redirects) request.cookies['m_on_page'] = 23 request.cookies['m_order'] = 'asc' request.cookies['m_orderby'] = 'id' request.referrer = 'dashboard' result = send_certificate_email(request) # print result self.assertTrue(result.status_code == 302) # redirect self.assertEqual(len(mailer.outbox), 1) self.assertEqual( mailer.outbox[0].subject, u"C3S membership certificate" ) # print mailer.outbox[0].body self.assertTrue( u"Hello AAASomeFirstnäme XXXSomeLastnäme," in mailer.outbox[0].body) self.assertTrue( u"your personal membership certificate" in mailer.outbox[0].body)
def dues16_notice(request): """ notice of arrival for transferral of dues """ # member: sanity checks try: member_id = request.matchdict['member_id'] member = C3sMember.get_by_id(member_id) # is in database assert member.membership_accepted # is a member assert 'investing' not in member.membership_type # is normal member except (KeyError, AssertionError): # pragma: no cover request.session.flash( u"No member OR not accepted OR not normal member", 'dues16notice_message_to_staff' # message queue for staff ) return HTTPFound( request.route_url('detail', memberid=member.id) + '#dues16') # sanity check: the given amount is a positive decimal try: paid_amount = D(request.POST['amount']) assert not paid_amount.is_signed() if DEBUG: print("DEBUG: payment of {}".format(paid_amount)) except (KeyError, AssertionError): # pragma: no cover request.session.flash( (u"Invalid amount to pay: '{}' " u"Use the dot ('.') as decimal mark, e.g. '23.42'".format( request.POST['amount'])), 'dues16notice_message_to_staff' # message queue for user ) return HTTPFound( request.route_url('detail', memberid=member.id) + '#dues16') # sanity check: the given date is a valid date try: paid_date = datetime.strptime( request.POST['payment_date'], '%Y-%m-%d') if DEBUG: print("DEBUG: payment received on {}".format(paid_date)) except (KeyError, AssertionError): # pragma: no cover request.session.flash( (u"Invalid date for payment: '{}' " u"Use YYYY-MM-DD, e.g. '2016-09-11'".format( request.POST['payment_date'])), 'dues16notice_message_to_staff' # message queue for user ) return HTTPFound( request.route_url('detail', memberid=member.id) + '#dues16') # persist info about payment member.set_dues16_payment(paid_amount, paid_date) return HTTPFound( request.route_url('detail', memberid=member.id) + '#dues16')
def mail_mtype_fixer_link(request): ''' send email to member to set her membership type details by visiting a form ''' afmid = request.matchdict['afmid'] afm = C3sMember.get_by_id(afmid) if isinstance(afm, NoneType): request.session.flash( 'id not found. no mail sent.', 'messages') return HTTPFound(request.route_url('dashboard', number=request.cookies['on_page'], order=request.cookies['order'], orderby=request.cookies['orderby'])) import random import string _looong_token = u''.join( random.choice( string.ascii_uppercase + string.digits) for x in range(13)) _url = (request.registry.settings['c3smembership.url'] + '/mtype/' + afm.email_confirm_code + '/' + _looong_token + '/' + afm.email) from .mail_mtype_util import make_mtype_email_body _body = make_mtype_email_body(afm, _url) log.info("mailing membership status form link to AFM # %s" % afm.id) if afm.locale == 'de': _subject = u'[C3S] Hilfe benötigt: Dein Mitgliedschaftsstatus' else: _subject = u'[C3S] Help needed: Your Membership Status' message = Message( subject=_subject, sender='*****@*****.**', recipients=[ afm.email, request.registry.settings['c3smembership.mailaddr']], body=_body ) #print(message.subject) #print(message.body) mailer = get_mailer(request) mailer.send(message) afm.mtype_confirm_token = _looong_token afm.mtype_email_date = datetime.now() afm.membership_type = u'pending' return HTTPFound(request.route_url('dashboard', number=request.cookies['on_page'], order=request.cookies['order'], orderby=request.cookies['orderby']) + '#member_' + str(afm.id))
def delete_entry(request): """ This view lets accountants delete entries (doublettes) """ memberid = request.matchdict["memberid"] dashboard_page = request.cookies["on_page"] _member = C3sMember.get_by_id(memberid) C3sMember.delete_by_id(_member.id) log.info("member.id %s was deleted by %s" % (_member.id, request.user.login)) return HTTPFound(request.route_url("dashboard", number=dashboard_page))
def test_generate_certificate_english(self): """ test the certificate download view (english) """ from c3smembership.membership_certificate import generate_certificate request = testing.DummyRequest() request.matchdict = { 'id': '2', 'name': 'foobar', 'token': 'hotzenplotz' } result = generate_certificate(request) if DEBUG: print(result) # check: this is *not* found because the token is *invalid* self.assertTrue(result.status_code == 404) # not found request.matchdict = { 'id': '2', 'name': 'foobar', 'token': 'hotzenplotz123' } member2 = C3sMember.get_by_id(2) member2.certificate_token = u'hotzenplotz123' # now the database matches the matchdict member2.certificate_email_date = datetime.now() - timedelta(weeks=1) member2.membership_accepted = True member2.membership_loss_date = date.today() - timedelta(days=1) result = generate_certificate(request) self.assertEqual(result.status_code, 404) member2.certificate_email_date = datetime.now() - timedelta(weeks=1) member2.membership_accepted = True member2.membership_loss_date = date.today() + timedelta(days=1) result = generate_certificate(request) self.assertEqual(result.status_code, 200) member2.certificate_email_date = datetime.now() - timedelta(weeks=1) member2.membership_accepted = True member2.membership_loss_date = None result = generate_certificate(request) if DEBUG: # pragma: no cover print("size of resulting certificate PDF here: {}".format( len(result.body))) print("min and max: {} {}".format( _min_PDF_size, _max_PDF_size)) self.assertTrue(_min_PDF_size < len(result.body) < _max_PDF_size) self.assertTrue(result.content_type == 'application/pdf')
def invite_member_BCGV(request): ''' Send email to member with link to ticketing. === ===================================== URL http://app:port/invite_member/{m_id} === ===================================== ''' mid = request.matchdict['m_id'] _m = C3sMember.get_by_id(mid) if isinstance(_m, NoneType): request.session.flash( 'id not found. no mail sent.', 'messages') return get_dashboard_redirect(request) # prepare a random token iff none is set if _m.email_invite_token_bcgv16 is None: _m.email_invite_token_bcgv16 = make_random_token() _url = ( request.registry.settings['ticketing.url'] + '/lu/' + _m.email_invite_token_bcgv16 + '/' + _m.email) email_subject, email_body = make_bcga16_invitation_email(_m, _url) log.info("mailing event invitation to to member id %s" % _m.id) message = Message( subject=email_subject, sender='*****@*****.**', recipients=[_m.email], body=email_body, extra_headers={ 'Reply-To': '*****@*****.**', } ) print_mail = True if 'true' in request.registry.settings[ 'testing.mail_to_console'] else False if print_mail: # pragma: no cover print(message.body.encode('utf-8')) else: # print "sending mail" mailer = get_mailer(request) mailer.send(message) # _m._token = _looong_token _m.email_invite_flag_bcgv16 = True _m.email_invite_date_bcgv16 = datetime.now() return get_dashboard_redirect(request, _m.id)
def test_api_userinfo(self): """ Test the api_userinfo service. * must be a PUT, not a GET request * the auth header must be present * returns None if members refcode does not match * returns firstname, lastname, email, membership type """ # try a GET -- must fail res = self.testapp.get("/lm", status=405) self.assertTrue("405 Method Not Allowed" in res.body) self.assertTrue("The method GET is not allowed for this resource." in res.body) # try a PUT -- fails under certain conditions with self.assertRaises(ValueError): res = self.testapp.put("/lm", status=200) # ValueError: No JSON object could be decoded # try a PUT -- fails under certain conditions with self.assertRaises(KeyError): res = self.testapp.put_json("/lm", dict(id=1)) # status=200) # KeyError: 'token' # missing auth token -- must fail with self.assertRaises(KeyError): res = self.testapp.put_json("/lm", dict(token=1)) # status=200) # KeyError: 'HTTP_X_MESSAGING_TOKEN' # try false auth token -- must fail: 401 unauthorized _headers = {"X-messaging-token": "bar"} res = self.testapp.put_json("/lm", dict(token=1), headers=_headers, status=401) # now use the correct auth token _auth_info = {"X-messaging-token": "SECRETAUTHTOKEN"} # ..but a non-existing refcode (email_invite_token_bcgv16) # returns no user (None) res = self.testapp.put_json("/lm", dict(token="foo"), headers=_auth_info, status=200) # body: {"lastname": "None", "firstname": "None"} self.assertTrue(json.loads(res.body)["firstname"], "None") self.assertTrue(json.loads(res.body)["lastname"], "None") self.testapp.reset() m1 = C3sMember.get_by_id(1) # load member from DB for crosscheck # now try a valid refcode (email_invite_token_bcgv16) res2 = self.testapp.put_json("/lm", dict(token=m1.email_invite_token_bcgv16), headers=_auth_info, status=200) self.assertTrue(json.loads(res2.body)["firstname"], m1.firstname) self.assertTrue(json.loads(res2.body)["lastname"], m1.lastname) self.assertTrue(json.loads(res2.body)["email"], m1.email) self.assertTrue(json.loads(res2.body)["mtype"], m1.membership_type)
def test_merge_member_view(self): ''' Tests for the merge_member_view ''' res = self.testapp.reset() # delete cookies afm = C3sMember.get_by_id(2) # an application m = C3sMember.get_by_id(1) # an accepted member self.assertTrue(afm.membership_accepted is False) self.assertEqual(afm.num_shares, 2) self.assertEqual(afm.shares, []) self.assertTrue(m.membership_accepted is True) self.assertEqual(m.num_shares, 23) self.assertEqual(len(m.shares), 2) # 2 shares packages # try unauthenticated access -- must fail! res = self.testapp.get( '/merge_member/{afm_id}/{mid}'.format( afm_id=afm.id, mid=m.id), status=403) self.failUnless('Access was denied to this resource' in res.body) # authenticate/authorize self._MemberTestsBase__login() res = self.testapp.get( '/merge_member/{afm_id}/{mid}'.format( afm_id=afm.id, mid=m.id), status=302) # redirect! self.assertTrue(afm.membership_accepted is False) self.assertTrue(m.membership_accepted is True) self.assertEqual(m.num_shares, 25) self.assertEqual(len(m.shares), 3) # 2 shares packages
def test_generate_certificate_staff(self): """ test the certificate generation option in the backend """ from c3smembership.membership_certificate import \ generate_certificate_staff request = testing.DummyRequest() # wrong id request.matchdict = { 'id': '1000', # token is not necessary here } result = generate_certificate_staff(request) self.assertTrue('Not found. Please check URL.' in result.body) m1 = C3sMember.get_by_id(1) # membership not accepted request.matchdict = { 'id': '1', # token is not necessary here } result = generate_certificate_staff(request) self.assertTrue('is not an accepted member' in result.body) self.assertEqual(result.status_code, 404) # was accepted but lost membership m1.membership_accepted = True m1.membership_loss_date = date.today() - timedelta(days=1) result = generate_certificate_staff(request) self.assertTrue('is not an accepted member' in result.body) self.assertEqual(result.status_code, 404) # was accepted and loses membership in the future m1.membership_accepted = True m1.membership_loss_date = date.today() + timedelta(days=1) result = generate_certificate_staff(request) self.assertEqual(result.status_code, 200) # accepted member m1.membership_accepted = True m1.membership_loss_date = None result = generate_certificate_staff(request) # print("result: {}".format(result)) if DEBUG: # pragma: no cover print("size of resulting certificate PDF: {}".format( len(result.body))) self.assertTrue(_min_PDF_size < len(result.body) < _max_PDF_size) self.assertTrue(result.content_type == 'application/pdf')
def mail_mtype_fixer_link(request): ''' Send email to prospective member to let her set her membership type details by visiting a form. Was needed for crowdfunders from startnext: data was missing. ''' afmid = request.matchdict['afmid'] afm = C3sMember.get_by_id(afmid) if isinstance(afm, NoneType): request.session.flash( 'id not found. no mail sent.', 'messages') return get_dashboard_redirect(request) import random import string _looong_token = u''.join( random.choice( string.ascii_uppercase + string.digits) for x in range(13)) _url = (request.registry.settings['c3smembership.url'] + '/mtype/' + afm.email_confirm_code + '/' + _looong_token + '/' + afm.email) from .mail_mtype_util import make_mtype_email_body _body = make_mtype_email_body(afm, _url) log.info("mailing membership status form link to AFM # %s" % afm.id) if afm.locale == 'de': _subject = u'[C3S] Hilfe benötigt: Dein Mitgliedschaftsstatus' else: _subject = u'[C3S] Help needed: Your Membership Status' message = Message( subject=_subject, sender='*****@*****.**', recipients=[ afm.email, request.registry.settings['c3smembership.mailaddr']], body=_body ) mailer = get_mailer(request) mailer.send(message) afm.mtype_confirm_token = _looong_token afm.mtype_email_date = datetime.now() afm.membership_type = u'pending' return get_dashboard_redirect(request, afm.id)
def delete_entry(request): """ This view lets accountants delete entries (doublettes) """ memberid = request.matchdict['memberid'] _member = C3sMember.get_by_id(memberid) C3sMember.delete_by_id(_member.id) log.info( "member.id %s was deleted by %s" % ( _member.id, request.user.login, ) ) return HTTPFound( request.route_url('dashboard_only', _query={'message': 'Member with id {0} was deleted.'.format(memberid)}))
def generate_certificate_staff(request): """ Generate the membership_certificate of any member for staffers. """ mid = request.matchdict["id"] # print("Member id from matchdict: {}".format(mid)) # chech if member exists member = C3sMember.get_by_id(mid) if member is None: return Response("Not found. Please check URL.") if isinstance(member, NoneType) or not member.is_member(): return Response("Member with this id ({}) is not an accepted member!".format(mid), status="404 Not Found") return gen_cert(member)
def mail_signature_reminder(request): """ send a mail to membership applicant reminding her about lack of signature """ _id = request.matchdict['memberid'] _member = C3sMember.get_by_id(_id) if isinstance(_member, NoneType): request.session.flash( 'that member was not found! (id: {})'.format(_id), 'messages' ) return HTTPFound( request.route_url( 'dashboard', number=request.cookies['on_page'], order=request.cookies['order'], orderby=request.cookies['orderby'])) # first reminder? second? #if ((_member.sent_signature_reminder is None #) or ( ): #_first = message = Message( subject=u"C3S: don't forget to send your form / Bitte Beitrittsformular einsenden", sender='*****@*****.**', #bcc=[request.registry.settings['reminder_blindcopy']], recipients=[_member.email], body=make_signature_reminder_emailbody(_member) ) mailer = get_mailer(request) mailer.send(message) #print u"the mail: {}".format(message.body) #import pdb #pdb.set_trace() try: # if value is int _member.sent_signature_reminder += 1 except: # pragma: no cover # if value was None (after migration of DB schema) _member.sent_signature_reminder = 1 _member.sent_signature_reminder_date = datetime.now() return HTTPFound(request.route_url( 'dashboard', number=request.cookies['on_page'], order=request.cookies['order'], orderby=request.cookies['orderby']) + '#member_' + str(_member.id) )
def get_member(request): """ This function serves an AJAX-call from the dashboard. There will be one call per application for membership listed! """ memberid = request.matchdict['memberid'] member = C3sMember.get_by_id(memberid) if member is None: return {} else: return { 'id': member.id, 'firstname': member.firstname, 'lastname': member.lastname } return None
def test_generate_certificate_founder(self): """ test the certificate download view (german) """ from c3smembership.membership_certificate import generate_certificate request = testing.DummyRequest() request.matchdict = { 'id': '3', 'name': 'foobar', 'token': 'hotzenplotz123' } member = C3sMember.get_by_id(3) member.certificate_token = u'hotzenplotz123' member.membership_accepted = True # need to get the date right! member.certificate_email_date = datetime.now( ) - timedelta(weeks=1) result = generate_certificate(request) # print result.body self.assertTrue(_min_PDF_size < len(result.body) < _max_PDF_size) self.assertTrue(result.content_type == 'application/pdf') # edge case: member has one share member.certificate_token = u'hotzenplotz123' member.num_shares = 1 result = generate_certificate(request) self.assertTrue(_min_PDF_size < len(result.body) < _max_PDF_size) self.assertTrue(result.content_type == 'application/pdf') # edge case: member has one share member.certificate_token = u'hotzenplotz123' member.is_legalentity = True result = generate_certificate(request) member.locale = u'de' result = generate_certificate(request) if DEBUG: # pragma: no cover print("size of resulting certificate PDF: {}".format( len(result.body))) self.assertTrue(_min_PDF_size < len(result.body) < _max_PDF_size) self.assertTrue(result.content_type == 'application/pdf')
def generate_certificate(request): """ Generate a membership_certificate for a member. Member must posess a link containing an id and a valid token. Headquarters sends links to members upon request. """ mid = request.matchdict["id"] token = request.matchdict["token"] try: member = C3sMember.get_by_id(mid) if DEBUG: # pragma: no cover print member.firstname print member.certificate_token print type(member.certificate_token) # NoneType print token print type(token) # unicode # token may not ne None assert member.certificate_token is not None # token must match entry in database assert str(member.certificate_token) in str(token) # and database entry must match token assert str(token) in str(member.certificate_token) # check age of token from datetime import timedelta two_weeks = timedelta(weeks=2) assert member.certificate_email_date is not None delta = datetime.now() - member.certificate_email_date assert delta < two_weeks except AssertionError: return Response( "Not found. Or invalid credentials. <br /><br /> " "Please contact [email protected]. <br /><br /> " "Nicht gefunden. Bitte [email protected] kontaktieren.", status="404 Not Found", ) if isinstance(member, NoneType) or not member.is_member(): return Response("that id does not exist or is not an accepted member. go back", status="404 Not Found") return gen_cert(member)
def delete_entry(request): """ This view lets accountants delete datasets (e.g. doublettes, test entries). """ deletion_confirmed = (request.params.get('deletion_confirmed', '0') == '1') redirection_view = request.params.get('redirect', 'dashboard') LOG.info('redirect to: ' + str(redirection_view)) if deletion_confirmed: memberid = request.matchdict['memberid'] member = C3sMember.get_by_id(memberid) member_lastname = member.lastname member_firstname = member.firstname C3sMember.delete_by_id(member.id) LOG.info( "member.id %s was deleted by %s", member.id, request.user.login, ) message = "member.id %s was deleted" % member.id request.session.flash(message, 'messages') msgstr = u'Member with id {0} \"{1}, {2}\" was deleted.' return HTTPFound( request.route_url( redirection_view, _query={'message': msgstr.format( memberid, member_lastname, member_firstname)}, _anchor='member_{id}'.format(id=str(memberid)) ) ) else: return HTTPFound( request.route_url( redirection_view, _query={'message': ( 'Deleting the member was not confirmed' ' and therefore nothing has been deleted.')} ) )
def test_member_list_date_pdf_view(self): # code lines 80-283 ''' Tests for the member_list_aufstockers_view If called with a faulty date in URL (not parseable) expect redirection to error page. Else: expect a PDF. ''' _date = '2016-02-11' # any date _bad_date = '2016-02-111111' # any bad date res = self.testapp.reset() # delete cookies res = self.testapp.get('/aml-' + _date + '.pdf', status=403) self.failUnless('Access was denied to this resource' in res.body) self._MemberTestsBase__login() # try a bad date (== not convertable to a date) res = self.testapp.get('/aml-' + _bad_date + '.pdf', status=302) self.assertTrue('error' in res) res2 = res.follow() # follow redirect self.assertTrue("Invalid date!" in res2.body) self.assertTrue("'2016-02-111111' does not compute!" in res2.body) self.assertTrue('try again, please! (YYYY-MM-DD)' in res2.body) # try with valid date in URL res = self.testapp.get('/aml-' + _date + '.pdf', status=200) # print(res) self.assertTrue(20000 < len(res.body) < 100000) self.assertEqual(res.content_type, 'application/pdf') # missing coverage of code lines # 125-134, 192-225, m1 = C3sMember.get_by_id(1) m1.membership_date = date(2015, 01, 01) m1.membership_number = 42 m1.shares[0].date_of_acquisition = datetime(2015, 01, 01) m1.shares[1].date_of_acquisition = datetime(2015, 01, 02) # try with valid date in URL res = self.testapp.get('/aml-' + _date + '.pdf', status=200) # print(res) self.assertTrue(20000 < len(res.body) < 100000) self.assertEqual(res.content_type, 'application/pdf')
def mail_signature_reminder(request): """ Send a mail to a membership applicant reminding her about lack of *signature*. Headquarters is still waiting for the *signed form*. This view can only be used by staff. To be approved for membership applicants have to * Transfer money for the shares to acquire (at least one share). * **Send the signed form** back to headquarters. """ member_id = request.matchdict['memberid'] member = C3sMember.get_by_id(member_id) if isinstance(member, NoneType): request.session.flash( 'that member was not found! (id: {})'.format(member_id), 'messages' ) return get_dashboard_redirect(request, member.id) email_subject, email_body = make_signature_reminder_email(member) message = Message( subject=email_subject, sender='*****@*****.**', recipients=[member.email], body=email_body ) send_message(request, message) try: member.sent_signature_reminder += 1 except TypeError: # if value was None (after migration of DB schema) member.sent_signature_reminder = 1 member.sent_signature_reminder_date = datetime.now() if 'detail' in request.referrer: return HTTPFound(request.route_url( 'detail', memberid=request.matchdict['memberid'])) else: return get_dashboard_redirect(request, member.id)
def test_string_start_quarter(self): """ Tests for the strings used for partial time spans. """ from c3smembership.views.membership_dues import ( string_start_quarter) member = C3sMember.get_by_id(1) member.dues15_start = 'q1_2015' res = string_start_quarter(member) # print res assert('Quartal 1' in res) member.dues15_start = 'q2_2015' res = string_start_quarter(member) # print res assert('Quartal 2' in res) member.dues15_start = 'q3_2015' res = string_start_quarter(member) # print res assert('Quartal 3' in res) member.dues15_start = 'q4_2015' res = string_start_quarter(member) # print res assert('Quartal 4' in res) member.locale = u'en' member.dues15_start = 'q1_2015' res = string_start_quarter(member) # print res assert('1st quarter' in res) member.dues15_start = 'q2_2015' res = string_start_quarter(member) # print res assert('2nd quarter' in res) member.dues15_start = 'q3_2015' res = string_start_quarter(member) # print res assert('3rd quarter' in res) member.dues15_start = 'q4_2015' res = string_start_quarter(member) # print res assert('4th quarter' in res)
def member_detail(request): """ This view lets accountants view member details: - has their signature arrived? - how about the payment? Mostly all the info about an application or membership in the database can be seen here. """ from decimal import Decimal as D logged_in = authenticated_userid(request) memberid = request.matchdict['memberid'] LOG.info("member details of id %s checked by %s", memberid, logged_in) member = C3sMember.get_by_id(memberid) if member is None: # that memberid did not produce good results request.session.flash( "A Member with id " "{} could not be found in the DB. run for the backups!".format( memberid), 'message_to_staff' ) return HTTPFound( # back to base request.route_url('toolbox')) # get the members invoices from the DB invoices15 = Dues15Invoice.get_by_membership_no(member.membership_number) invoices16 = Dues16Invoice.get_by_membership_no(member.membership_number) return { 'today': date.today().strftime('%Y-%m-%d'), 'D': D, 'member': member, 'invoices15': invoices15, 'invoices16': invoices16, # 'form': html }
def test_generate_certificate_awkward_characters(self): """ test the certificate generation with awkward characters in datasets because LaTeX interprets some characters as special characters. """ from c3smembership.membership_certificate import generate_certificate request = testing.DummyRequest() request.matchdict = { 'id': '1', 'name': 'foobar', 'token': 'hotzenplotz' } result = generate_certificate(request) self.assertTrue(result.status_code == 404) # not found request.matchdict = { 'id': '1', 'name': 'foobar', 'token': 'hotzenplotz123' } member = C3sMember.get_by_id(1) member.firstname = u"Foobar Corp & Co." member.lastname = u"Your Number #1" member.certificate_token = u'hotzenplotz123' member.membership_accepted = True # # need to get the date right! member.certificate_email_date = datetime.now( ) - timedelta(weeks=1) result = generate_certificate(request) if DEBUG: # pragma: no cover print("size of resulting certificate PDF: {}".format( len(result.body))) self.assertTrue(_min_PDF_size < len(result.body) < _max_PDF_size) self.assertTrue(result.content_type == 'application/pdf')
def test_member_list_alphabetical_view(self): # code lines 286-325 ''' tests for the member_list_alphabetical_view ''' res = self.testapp.reset() # delete cookies res = self.testapp.get('/aml', status=403) self.failUnless('Access was denied to this resource' in res.body) self._MemberTestsBase__login() member4_lost = C3sMember.get_by_id(4) member4_lost.membership_accepted = True member4_lost.membership_number = 9876 res = self.testapp.get('/aml', status=200) self.assertTrue('2 Mitglieder' in res.body) member4_lost.membership_date = date.today() - timedelta(days=365) member4_lost.membership_loss_date = \ date.today() - timedelta(days=30) res = self.testapp.get('/aml', status=200) self.assertTrue('1 Mitglieder' in res.body)
def send_dues16_invoice_email(request, m_id=None): """ Send email to a member to prompt her to pay the membership dues. - For normal members, also send link to invoice. - For investing members that are legal entities, ask for additional support depending on yearly turnover. This view function works both if called via URL, e.g. /dues_invoice/123 and if called as a function with a member id as parameter. The latter is useful for batch processing. When this function is used for the first time for one member, some database fields are filled: - Invoice number - Invoice amount (calculated from date of membership approval by the board) - Invoice token Also, the database table of invoices (and cancellations) is appended. If this function gets called the second time for a member, no new invoice is produced, but the same mail sent again. """ # either we are given a member id via url or function parameter try: # view was called via http/s member_id = request.matchdict['member_id'] batch = False except KeyError: # ...or was called as function with parameter (see batch) member_id = m_id batch = True try: # get member from DB member = C3sMember.get_by_id(member_id) assert(member is not None) except AssertionError: if not batch: request.session.flash( "member with id {} not found in DB!".format(member_id), 'message_to_staff') return HTTPFound(request.route_url('toolbox')) # sanity check:is this a member? try: assert(member.membership_accepted) # must be accepted member! except AssertionError: request.session.flash( "member {} not accepted by the board!".format(member_id), 'message_to_staff') return HTTPFound(request.route_url('toolbox')) if 'normal' not in member.membership_type and \ 'investing' not in member.membership_type: request.session.flash( 'The membership type of member {0} is not specified! The ' 'membership type must either be "normal" or "investing" in order ' 'to be able to send an invoice email.'.format(member.id), 'message_to_staff') return get_memberhip_listing_redirect(request) if member.membership_date >= date(2017,1,1): request.session.flash( 'Member {0} was not a member in 2016. Therefore, you cannot send ' 'an invoice for 2016.'.format(member.id), 'message_to_staff') return get_memberhip_listing_redirect(request) # check if invoice no already exists. # if yes: just send that email again! # also: offer staffers to cancel this invoice if member.dues16_invoice is True: invoice = Dues16Invoice.get_by_invoice_no(member.dues16_invoice_no) member.dues16_invoice_date = datetime.now() else: # if no invoice already exists: # make dues token and ... randomstring = make_random_string() # check if dues token is already used while (Dues16Invoice.check_for_existing_dues16_token(randomstring)): # create a new one, if the new one already exists in the database randomstring = make_random_string() # pragma: no cover # prepare invoice number try: # either we already have an invoice number for that client... invoice_no = member.dues16_invoice_no assert invoice_no is not None except AssertionError: # ... or we create a new one and save it # get max invoice no from db max_invoice_no = Dues16Invoice.get_max_invoice_no() # use the next free number, save it to db new_invoice_no = int(max_invoice_no) + 1 DBSession.flush() # save dataset to DB # calculate dues amount (maybe partial, depending on quarter) dues_start, dues_amount = calculate_partial_dues16(member) # now we have enough info to update the member info # and persist invoice info for bookkeeping # store some info in DB/member table member.dues16_invoice = True member.dues16_invoice_no = new_invoice_no # irrelevant for investing member.dues16_invoice_date = datetime.now() member.dues16_token = randomstring member.dues16_start = dues_start if 'normal' in member.membership_type: # only for normal members member.set_dues16_amount(dues_amount) # store some more info about invoice in invoice table invoice = Dues16Invoice( invoice_no=member.dues16_invoice_no, invoice_no_string=( u'C3S-dues2016-' + str(member.dues16_invoice_no).zfill(4)), invoice_date=member.dues16_invoice_date, invoice_amount=u'' + str(member.dues16_amount), member_id=member.id, membership_no=member.membership_number, email=member.email, token=member.dues16_token, ) DBSession.add(invoice) DBSession.flush() # now: prepare that email # only normal (not investing) members *have to* pay the dues. # only the normal members get an invoice link and PDF produced for them. # only investing legalentities are asked for more support. if 'investing' not in member.membership_type: start_quarter = string_start_quarter_dues16(member) invoice_url = ( request.route_url( 'make_dues16_invoice_no_pdf', email=member.email, code=member.dues16_token, i=str(member.dues16_invoice_no).zfill(4) ) ) email_subject, email_body = make_dues16_invoice_email( member, invoice, invoice_url, start_quarter) message = Message( subject=email_subject, sender='*****@*****.**', recipients=[member.email], body=email_body, extra_headers={ 'Reply-To': '*****@*****.**', } ) elif 'investing' in member.membership_type: if member.is_legalentity: email_subject, email_body = \ make_dues_invoice_legalentity_email(member) else: email_subject, email_body = \ make_dues_invoice_investing_email(member) message = Message( subject=email_subject, sender='*****@*****.**', recipients=[member.email], body=email_body, extra_headers={ 'Reply-To': '*****@*****.**', } ) # print to console or send mail if 'true' in request.registry.settings['testing.mail_to_console']: print(message.body.encode('utf-8')) # pragma: no cover else: send_message(request, message) # now choose where to redirect if 'detail' in request.referrer: return HTTPFound( request.route_url( 'detail', memberid=member.id) + '#dues16') if 'toolbox' in request.referrer: return HTTPFound(request.route_url('toolbox')) else: return get_memberhip_listing_redirect(request, member.id)
def test_invitation(self): """ Test the invitation procedure for one single member at a time. Load this member from the DB, assure the email_invite_flag_bcgv18 and token are not set, prepare cookies, invite this member, assure the email_invite_flag_bcgv18 and token are now set, """ from c3smembership.invite_members import invite_member_bcgv member1 = C3sMember.get_by_id(1) self.assertEqual(member1.email_invite_flag_bcgv18, False) self.assertTrue(member1.email_invite_token_bcgv18 is None) req = testing.DummyRequest() # have some cookies req.cookies['on_page'] = 0 req.cookies['order'] = 'asc' req.cookies['orderby'] = 'id' # try with nonexistant id req.matchdict = {'m_id': 10000} res = invite_member_bcgv(req) self.assertEquals(302, res.status_code) req.matchdict = {'m_id': member1.id} res = invite_member_bcgv(req) self.assertEqual(member1.email_invite_flag_bcgv18, True) self.assertTrue(member1.email_invite_token_bcgv18 is not None) # now really send email self.config.registry.settings['testing.mail_to_console'] = 'false' mailer = get_mailer(req) res = invite_member_bcgv(req) self.assertEqual(len(mailer.outbox), 2) self.assertTrue(u'[C3S] Einladung zu Barcamp und Generalversammlung' in mailer.outbox[0].subject) self.assertTrue(u'[C3S] Einladung zu Barcamp und Generalversammlung' in mailer.outbox[1].subject) self.assertTrue(member1.firstname in mailer.outbox[1].body) self.assertTrue(member1.email_invite_token_bcgv18 in mailer.outbox[1].body) # now send invitation to english member member2 = C3sMember.get_by_id(2) self.assertEqual(member2.email_invite_flag_bcgv18, False) self.assertTrue(member2.email_invite_token_bcgv18 is None) req.matchdict = {'m_id': member2.id} res = invite_member_bcgv(req) self.assertEqual(member2.email_invite_flag_bcgv18, True) self.assertTrue(member2.email_invite_token_bcgv18 is not None) self.assertEqual(len(mailer.outbox), 3) self.assertTrue(u'[C3S] Invitation to Barcamp and General Assembly' in mailer.outbox[2].subject) self.assertTrue(member2.firstname in mailer.outbox[2].body) self.assertTrue(member2.email_invite_token_bcgv18 in mailer.outbox[2].body)
def edit_member(request): """ Let staff edit a member entry. """ try: _id = request.matchdict['_id'] assert (isinstance(int(_id), int)) member = C3sMember.get_by_id(_id) if isinstance(member, NoneType): return HTTPFound(request.route_url('dashboard')) except: return HTTPFound(request.route_url('dashboard')) # if we have a valid id, we can load a members data from the db # and put the data in an appstruct to fill the form appstruct = {} email_is_confirmed = 'yes' if member.email_is_confirmed else 'no' appstruct['person'] = { 'firstname': member.firstname, 'lastname': member.lastname, 'email': member.email, 'email_is_confirmed': email_is_confirmed, 'address1': member.address1, 'address2': member.address2, 'postcode': member.postcode, 'city': member.city, 'country': member.country, 'date_of_birth': member.date_of_birth, 'locale': member.locale, } appstruct['membership_meta'] = { 'membership_accepted': member.membership_accepted, 'membership_date': ( # this is necessary because membership_date's default is # 1970-01-01 which should be changed to None in the future u'' if member.membership_date == date(1970, 1, 1) else member.membership_date), 'is_duplicate': member.is_duplicate, 'is_duplicate_of': (u'' if member.is_duplicate_of is None else member.is_duplicate_of), 'accountant_comment': (u'' if member.accountant_comment is None else member.accountant_comment), 'signature_received': member.signature_received, 'signature_received_date': member.signature_received_date, 'payment_received': member.payment_received, 'payment_received_date': member.payment_received_date, 'membership_loss_date': member.membership_loss_date, 'membership_loss_type': (u'' if member.membership_loss_type is None else member.membership_loss_type), } appstruct['membership_info'] = { 'membership_type': member.membership_type, 'entity_type': u'legalentity' if member.is_legalentity else 'person', 'member_of_colsoc': 'yes' if member.member_of_colsoc else 'no', 'name_of_colsoc': member.name_of_colsoc, } membership_loss_types = (('', _(u'(Select)')), ('resignation', _(u'Resignation')), ('expulsion', _(u'Expulsion')), ('death', _(u'Death')), ('bankruptcy', _(u'Bankruptcy')), ('winding-up', _(u'Winding-up')), ('shares_transfer', _(u'Transfer of remaining shares'))) class PersonalData(colander.MappingSchema): """ Colander schema of the personal data for editing member data. """ firstname = colander.SchemaNode( colander.String(), title=_(u'(Real) First Name'), oid='firstname', ) lastname = colander.SchemaNode( colander.String(), title=_(u'(Real) Last Name'), oid='lastname', ) email = colander.SchemaNode( colander.String(), title=_(u'Email Address'), validator=colander.Email(), oid='email', ) email_is_confirmed = colander.SchemaNode( colander.String(), title=_(u'Email Address Confirmed'), widget=deform.widget.RadioChoiceWidget(values=( (u'yes', _(u'Yes, confirmed')), (u'no', _(u'No, not confirmed')), )), missing=u'', oid='email_is_confirmed', ) passwort = colander.SchemaNode(colander.String(), widget=deform.widget.HiddenWidget(), default='NoneSet', missing='NoneSetPurposefully') address1 = colander.SchemaNode( colander.String(), title=_(u'Addess Line 1'), ) address2 = colander.SchemaNode( colander.String(), missing=u'', title=_(u'Address Line 2'), ) postcode = colander.SchemaNode(colander.String(), title=_(u'Postal Code'), oid='postcode') city = colander.SchemaNode( colander.String(), title=_(u'City'), oid='city', ) country = colander.SchemaNode( colander.String(), title=_(u'Country'), default=COUNTRY_DEFAULT, widget=deform.widget.SelectWidget(values=country_codes), oid='country', ) date_of_birth = colander.SchemaNode( colander.Date(), title=_(u'Date of Birth'), default=date(2013, 1, 1), oid='date_of_birth', ) locale = colander.SchemaNode( colander.String(), title=_(u'Locale'), widget=deform.widget.SelectWidget(values=locale_codes), missing=u'', ) @colander.deferred def membership_loss_date_widget(node, keywords): """ Returns a text or hidden input depending on the value of membership_accepted within the keywords. """ if keywords.get('membership_accepted'): return deform.widget.TextInputWidget() else: return deform.widget.HiddenWidget() @colander.deferred def membership_loss_type_widget(node, keywords): """ Returns a select or hidden input depending on the value of membership_accepted within the keywords. """ if keywords.get('membership_accepted'): return deform.widget.SelectWidget(values=membership_loss_types) else: return deform.widget.HiddenWidget() class MembershipMeta(colander.Schema): """ Colander schema of the meta data for editing member data. """ membership_accepted = colander.SchemaNode( colander.Boolean(), title=_(u'Membership Accepted')) membership_date = colander.SchemaNode( colander.Date(), title=_(u'Membership Acceptance Date'), validator=Range( min=date(2013, 9, 24), max=date.today(), min_err=_(u'${val} is earlier than earliest date ${min}.'), max_err=_(u'${val} is later than latest date ${max}.')), missing=date(1970, 1, 1), oid='membership_date', ) is_duplicate = colander.SchemaNode( colander.Boolean(), title=_(u'Is Duplicate'), oid='is_duplicate', ) is_duplicate_of = colander.SchemaNode( colander.String(), title=_(u'Duplicate Id'), missing=u'', oid='duplicate_of', ) signature_received = colander.SchemaNode( colander.Boolean(), title=_(u'Signature Received'), oid='signature_received', ) signature_received_date = colander.SchemaNode( colander.Date(), title=_('Signature Receipt Date'), validator=Range( min=date(1070, 1, 1), max=date.today(), min_err=_(u'${val} is earlier than earliest date ${min}.'), max_err=_(u'${val} is later than latest date ${max}.')), missing=date(1970, 1, 1), ) payment_received = colander.SchemaNode( colander.Boolean(), title=_(u'Payment Received'), ) payment_received_date = colander.SchemaNode( colander.Date(), title=_(u'Payment Receipt Date'), validator=Range( min=date(1970, 1, 1), max=date.today(), min_err=_(u'${val} is earlier than earliest date ${min}.'), max_err=_(u'${val} is later than latest date ${max}.')), missing=date(1970, 1, 1), oid='_received_date', ) membership_loss_date = colander.SchemaNode( colander.Date(), widget=membership_loss_date_widget, title=_(u'Date of the loss of membership'), default=None, missing=None, oid='membership_loss_date', ) membership_loss_type = colander.SchemaNode( colander.String(), widget=membership_loss_type_widget, title=_(u'Type of membership loss'), default=None, missing=None, oid='membership_loss_type', ) accountant_comment = colander.SchemaNode( colander.String(), title=_(u'Staff Comment: (255 letters)'), missing=u'', oid='accountant_comment', ) class MembershipInfo(colander.Schema): """ Colander schema of the additional data for editing member data. """ yes_no = ( (u'yes', _(u'Yes')), (u'no', _(u'No')), (u'dontknow', _(u'Unknwon')), ) entity_type = colander.SchemaNode( colander.String(), title=_(u'Member Category'), description=_(u'Please choose the member category.'), widget=deform.widget.RadioChoiceWidget(values=( (u'person', _(u'Person')), (u'legalentity', _(u'Legal Entity')), ), ), missing=u'', oid='entity_type', ) membership_type = colander.SchemaNode( colander.String(), title=_(u'Type of Membership (C3S Statute § 4)'), description=_(u'Please choose the type of membership.'), widget=deform.widget.RadioChoiceWidget(values=( (u'normal', _(u'Member')), (u'investing', _(u'Investing (non-user) member')), (u'unknown', _(u'Unknown')), ), ), missing=u'', oid='membership_type', ) member_of_colsoc = colander.SchemaNode( colander.String(), title=_('Member of a Collecting Society'), widget=deform.widget.RadioChoiceWidget(values=yes_no), oid='other_colsoc', default=u'', missing=u'', ) name_of_colsoc = colander.SchemaNode( colander.String(), title=_(u'Names of Collecting Societies'), description=_(u'Please separate multiple collecting societies by ' u'comma.'), missing=u'', oid='colsoc_name', ) def loss_type_and_date_set_validator(form, value): """ Validates whether the membership loss type is set. Membership date and type must both be either set or unset. """ if (value['membership_loss_date'] is None) != \ (value['membership_loss_type'] is None): exc = colander.Invalid(form) exc['membership_loss_type'] = \ _(u'Date and type of membership loss must be set both or ' u'none.') exc['membership_loss_date'] = \ _(u'Date and type of membership loss must be set both or ' u'none.') raise exc def loss_date_larger_acceptance_validator(form, value): """ Validates that the membership loss date is not smaller than the membership acceptance date. As the membership can't be lost before it was granted the membership loss date must be larger than the membership acceptance date. """ if (value['membership_loss_date'] is not None and (value['membership_loss_date'] < value['membership_date'] or not value['membership_accepted'])): exc = colander.Invalid(form) exc['membership_loss_date'] = \ _(u'Date membership loss must be larger than membership ' u'acceptance date.') raise exc def loss_date_resignation_validator(form, value): """ Validates that the membership loss date for resignations is the 31st of December of any year. Resignations are only allowed to the end of the year. """ if (value.get('membership_loss_type', '') == 'resignation' and value['membership_loss_date'] is not None and not (value['membership_loss_date'].day == 31 and value['membership_loss_date'].month == 12)): exc = colander.Invalid(form) exc['membership_loss_date'] = \ _(u'Resignations are only allowed to the 31st of December ' u'of a year.') raise exc class MembershipForm(colander.Schema): """ The form for editing membership information combining all forms for the subject areas. """ person = PersonalData(title=_(u'Personal Data'), ) membership_meta = MembershipMeta( title=_(u'Membership Bureaucracy'), validator=colander.All( loss_type_and_date_set_validator, loss_date_larger_acceptance_validator, loss_date_resignation_validator)).bind( membership_accepted=member.membership_accepted, ) membership_info = MembershipInfo(title=_(u'Membership Requirements')) def membership_loss_type_entity_type_validator(form, value): """ Validates that only natural persons can have loss type 'death' and only legal entites 'winding-up'. """ if (value['membership_meta']['membership_loss_type'] == 'death' and value['membership_info']['entity_type'] != 'person'): exc_type = colander.Invalid( form['membership_meta']['membership_loss_type'], _(u'The membership loss type \'death\' is only allowed for ' u'natural person members and not for legal entity members.')) exc_meta = colander.Invalid(form['membership_meta']) exc_meta.add( exc_type, get_child_position( form['membership_meta'], form['membership_meta']['membership_loss_type'])) exc = colander.Invalid(form) exc.add(exc_meta, get_child_position(form, form['membership_meta'])) raise exc if (value['membership_meta']['membership_loss_type'] == 'winding-up' and value['membership_info']['entity_type'] != 'legalentity'): exc_type = colander.Invalid( form['membership_meta']['membership_loss_type'], _(u'The membership loss type \'winding-up\' is only allowed ' u'for legal entity members and not for natural person ' u'members.')) exc_meta = colander.Invalid(form['membership_meta']) exc_meta.add( exc_type, get_child_position( form['membership_meta'], form['membership_meta']['membership_loss_type'])) exc = colander.Invalid(form) exc.add(exc_meta, get_child_position(form, form['membership_meta'])) raise exc schema = MembershipForm( validator=colander.All(membership_loss_type_entity_type_validator, )) form = deform.Form( schema, buttons=[ deform.Button('submit', _(u'Submit')), deform.Button('reset', _(u'Reset')), ], renderer=ZPT_RENDERER, use_ajax=True, ) def clean_error_messages(error): if error.msg is not None and type(error.msg) == list: error.msg = list(set(error.msg)) if None in error.msg: error.msg.remove(None) if '' in error.msg: error.msg.remove('') error.msg = ' '.join(list(set(error.msg))) for child in error.children: clean_error_messages(child) # if the form has NOT been used and submitted, remove error messages if # any if 'submit' not in request.POST: request.session.pop_flash() # if the form has been used and SUBMITTED, check contents if 'submit' in request.POST: controls = request.POST.items() try: appstruct = form.validate(controls) except ValidationFailure as validationfailure: clean_error_messages(validationfailure.error) request.session.flash(_(u'Please note: There were errors, ' u'please check the form below.'), 'message_above_form', allow_duplicate=False) return {'form': validationfailure.render()} # to store the data in the DB, the old object is updated listing = [ # map data attributes to appstruct items ('firstname', appstruct['person']['firstname']), ('lastname', appstruct['person']['lastname']), ('date_of_birth', appstruct['person']['date_of_birth']), ('email', appstruct['person']['email']), ('email_is_confirmed', 1 if appstruct['person']['email_is_confirmed'] == 'yes' else 0), ('address1', appstruct['person']['address1']), ('address2', appstruct['person']['address2']), ('postcode', appstruct['person']['postcode']), ('city', appstruct['person']['city']), ('country', appstruct['person']['country']), ('locale', appstruct['person']['locale']), ('membership_date', appstruct['membership_meta']['membership_date']), ('is_duplicate', appstruct['membership_meta']['is_duplicate']), ('is_duplicate_of', appstruct['membership_meta']['is_duplicate_of']), ('accountant_comment', appstruct['membership_meta']['accountant_comment']), ('membership_type', appstruct['membership_info']['membership_type']), ('is_legalentity', 1 if (appstruct['membership_info']['entity_type'] == 'legalentity') else 0), ('name_of_colsoc', appstruct['membership_info']['name_of_colsoc']), ('signature_received', appstruct['membership_meta']['signature_received']), ('signature_received_date', appstruct['membership_meta']['signature_received_date']), ('payment_received', appstruct['membership_meta']['payment_received']), ('payment_received_date', appstruct['membership_meta']['payment_received_date']), ('membership_loss_type', appstruct['membership_meta'].get('membership_loss_type', None)), ('membership_loss_date', appstruct['membership_meta'].get('membership_loss_date', None)), ] for thing in listing: attribute_name = thing[0] attribute_value = thing[1] if member.__getattribute__(attribute_name) == attribute_value: pass else: LOG.info(u'{0} changes {1} of id {2} to {3}'.format( authenticated_userid(request), attribute_name, member.id, attribute_value)) setattr(member, attribute_name, attribute_value) # membership acceptance status can be set or unset. if appstruct['membership_meta'][ 'membership_accepted'] == member.membership_accepted: pass else: member.membership_accepted = appstruct['membership_meta'][ 'membership_accepted'] if isinstance(member.membership_number, NoneType) \ and member.membership_accepted: member.membership_number = \ C3sMember.get_next_free_membership_number() if appstruct['membership_info']['entity_type'] == 'legalentity': member.is_legalentity = True else: member.is_legalentity = False # empty the messages queue (as validation worked anyways) deleted_msg = request.session.pop_flash() del deleted_msg return HTTPFound( # redirect to details page location=request.route_url('detail', memberid=member.id), ) form.set_appstruct(appstruct) html = form.render() return {'form': html}
def merge_member_view(request): """ Merges member duplicates into one member record. Some people have more than one entry in our C3SMember table, e.g. because they used the application form more than once to acquire more shares. They shall not, however, become members twice and get more than one membership number. So we try and merge them: If a person is already a member and acquires a second package of shares, this package of shares is added to the former membership entry. The second entry in the C3sMember table is given the 'is_duplicate' flag and also the 'duplicate_of' is given the *id* of the original entry. """ afm_id = request.matchdict['afm_id'] member_id = request.matchdict['mid'] if DEBUG: # pragma: no cover print "shall merge {} to {}".format(afm_id, member_id) orig = C3sMember.get_by_id(member_id) merg = C3sMember.get_by_id(afm_id) if not orig.membership_accepted: request.session.flash( 'you can only merge to accepted members!', 'merge_message') HTTPFound(request.route_url('make_member', afm_id=afm_id)) exceeds_60 = int(orig.num_shares) + int(merg.num_shares) > 60 if exceeds_60: request.session.flash( 'merger would exceed 60 shares!', 'merge_message') return HTTPFound(request.route_url('make_member', afm_id=afm_id)) # TODO: this needs fixing!!! # date must be set manually according to date of approval of the board shares_date_of_acquisition = merg.signature_received_date if ( merg.signature_received_date > merg.payment_received_date ) else merg.payment_received_date share_acquisition = request.registry.share_acquisition share_id = share_acquisition.create( orig.membership_number, merg.num_shares, shares_date_of_acquisition) share_acquisition.set_signature_reception( share_id, date( merg.signature_received_date.year, merg.signature_received_date.month, merg.signature_received_date.day)) share_acquisition.set_signature_confirmation( share_id, date( merg.signature_confirmed_date.year, merg.signature_confirmed_date.month, merg.signature_confirmed_date.day)) share_acquisition.set_payment_reception( share_id, date( merg.payment_received_date.year, merg.payment_received_date.month, merg.payment_received_date.day)) share_acquisition.set_payment_confirmation( share_id, date( merg.payment_confirmed_date.year, merg.payment_confirmed_date.month, merg.payment_confirmed_date.day)) share_acquisition.set_reference_code( share_id, merg.email_confirm_code) DBSession.delete(merg) return HTTPFound(request.route_url('detail', memberid=member_id))
def make_member_view(request): """ Turns a membership applicant into an accepted member. When both the signature and the payment for the shares have arrived at headquarters, an application for membership can be turned into an **accepted membership**, if the board of directors decides so. This view lets staff enter a date of approval through a form. It also provides staff with listings of * members with same first name * members with same last name * members with same email address * members with same date of birth so staff can decide if this may become a proper membership or whether this application is a duplicate of some accepted membership and should be merged with some other entry. In case of duplicate/merge, also check if the number of shares when combining both entries would exceed 60, the maximum number of shares a member can hold. """ afm_id = request.matchdict['afm_id'] try: # does that id make sense? member exists? member = C3sMember.get_by_id(afm_id) assert(isinstance(member, C3sMember)) # is an application # assert(isinstance(member.membership_number, NoneType) # not has number except AssertionError: return HTTPFound( location=request.route_url('dashboard')) if member.membership_accepted: # request.session.flash('id {} is already accepted member!') return HTTPFound(request.route_url('detail', memberid=member.id)) if not (member.signature_received and member.payment_received): request.session.flash('signature or payment missing!', 'messages') return HTTPFound(request.route_url('dashboard')) if 'make_member' in request.POST: # print "yes! contents: {}".format(request.POST['make_member']) try: member.membership_date = datetime.strptime( request.POST['membership_date'], '%Y-%m-%d').date() except ValueError, value_error: request.session.flash(value_error.message, 'merge_message') return HTTPFound( request.route_url('make_member', afm_id=member.id)) member.membership_accepted = True if member.is_legalentity: member.membership_type = u'investing' else: member.is_legalentity = False member.membership_number = C3sMember.get_next_free_membership_number() # Currently, the inconsistent data model stores the amount of applied # shares in member.num_shares which must be moved to a membership # application process property. As the acquisition of shares increases # the amount of shares and this is still read from member.num_shares, # this value must first be reset to 0 so that it can be increased by # the share acquisition. Once the new data model is complete the # property num_shares will not exist anymore. Instead, a membership # application process stores the number of applied shares and the # shares store the number of actual shares. num_shares = member.num_shares member.num_shares = 0 share_id = request.registry.share_acquisition.create( member.membership_number, num_shares, member.membership_date) share_acquisition = request.registry.share_acquisition share_acquisition.set_signature_reception( share_id, date( member.signature_received_date.year, member.signature_received_date.month, member.signature_received_date.day)) share_acquisition.set_payment_confirmation( share_id, date( member.payment_received_date.year, member.payment_received_date.month, member.payment_received_date.day)) share_acquisition.set_reference_code( share_id, member.email_confirm_code) # return the user to the page she came from if 'referrer' in request.POST: if request.POST['referrer'] == 'dashboard': return HTTPFound(request.route_url('dashboard')) if request.POST['referrer'] == 'detail': return HTTPFound( request.route_url('detail', memberid=member.id)) return HTTPFound(request.route_url('detail', memberid=member.id))
def new_member(request): ''' let staff create a new member entry, when receiving input via dead wood ''' # XXX check if submitted, etc... class PersonalData(colander.MappingSchema): """ colander schema for membership application form """ firstname = colander.SchemaNode( colander.String(), title=u'Vorname (b. Körpersch.: Ansprechpartner)', oid="firstname", ) lastname = colander.SchemaNode( colander.String(), title=u'Nachname (b. Körpersch.: Name der Körperschaft)', oid="lastname", ) email = colander.SchemaNode( colander.String(), title=_(u'E-Mail'), validator=colander.Email(), oid="email", ) passwort = colander.SchemaNode( colander.String(), widget=deform.widget.HiddenWidget(), default='NoneSet', missing='NoneSetPurposefully' ) address1 = colander.SchemaNode( colander.String(), title='Adresse Zeile 1' ) address2 = colander.SchemaNode( colander.String(), missing=unicode(''), title='Adresse Zeile 2' ) postcode = colander.SchemaNode( colander.String(), title='Postleitzahl', oid="postcode" ) city = colander.SchemaNode( colander.String(), title='Ort', oid="city", ) country = colander.SchemaNode( colander.String(), title='Land', default=country_default, widget=deform.widget.SelectWidget( values=country_codes), oid="country", ) date_of_birth = colander.SchemaNode( colander.Date(), title='Geburtsdatum', # widget=deform.widget.DatePartsWidget( # inline=True), default=date(1970, 1, 1), oid="date_of_birth", ) locale = colander.SchemaNode( colander.String(), widget=deform.widget.HiddenWidget(), default='de', missing='de', ) class MembershipInfo(colander.Schema): yes_no = ((u'yes', _(u'Yes')), (u'no', _(u'No')), (u'dontknow', _(u'Unknown')),) entity_type = colander.SchemaNode( colander.String(), title=(u'Person oder Körperschaft?'), description=u'Bitte die Kategorie des Mitglied auswählen.', widget=deform.widget.RadioChoiceWidget( values=( (u'person', (u'Person')), (u'legalentity', u'Körperschaft'), ), ), missing=unicode(''), oid='entity_type', ) membership_type = colander.SchemaNode( colander.String(), title=(u'Art der Mitgliedschaft (lt. Satzung, §4)'), description=u'Bitte die Art der Mitgliedschaft auswählen.', widget=deform.widget.RadioChoiceWidget( values=( (u'normal', (u'Normales Mitglied')), (u'investing', u'Investierendes Mitglied'), (u'unknown', u'Unbekannt.'), ), ), missing=unicode(''), oid='membership_type', ) member_of_colsoc = colander.SchemaNode( colander.String(), title='Mitglied einer Verwertungsgesellschaft?', validator=colander.OneOf([x[0] for x in yes_no]), widget=deform.widget.RadioChoiceWidget(values=yes_no), missing=unicode(''), oid="other_colsoc", # validator=colsoc_validator ) name_of_colsoc = colander.SchemaNode( colander.String(), title=(u'Falls ja, welche? (Kommasepariert)'), missing=unicode(''), oid="colsoc_name", # validator=colander.All( # colsoc_validator, # ) ) class Shares(colander.Schema): """ the number of shares a member wants to hold """ num_shares = colander.SchemaNode( colander.Integer(), title='Anzahl Anteile (1-60)', default="1", validator=colander.Range( min=1, max=60, min_err=u'mindestens 1', max_err=u'höchstens 60', ), oid="num_shares") class MembershipForm(colander.Schema): """ The Form consists of - Personal Data - Membership Information - Shares """ person = PersonalData( title=_(u"Personal Data"), # description=_(u"this is a test"), # css_class="thisisjustatest" ) membership_info = MembershipInfo( title=_(u"Membership Requirements") ) shares = Shares( title=_(u"Shares") ) schema = MembershipForm() form = deform.Form( schema, buttons=[ deform.Button('submit', _(u'Submit')), deform.Button('reset', _(u'Reset')) ], use_ajax=True, # renderer=zpt_renderer ) # if the form has NOT been used and submitted, remove error messages if any if 'submit' not in request.POST: request.session.pop_flash() # print('ping!') # if the form has been used and SUBMITTED, check contents if 'submit' in request.POST: controls = request.POST.items() try: appstruct = form.validate(controls) # print("the appstruct from the form: %s \n") % appstruct # for thing in appstruct: # print("the thing: %s") % thing # print("type: %s") % type(thing) # data sanity: if not in collecting society, don't save # collsoc name even if it was supplied through form # if 'no' in appstruct['membership_info']['member_of_colsoc']: # appstruct['membership_info']['name_of_colsoc'] = '' # print appstruct['membership_info']['name_of_colsoc'] # print '-'*80 except ValidationFailure as e: # print("Validation Failure!") # print("the request.POST: %s \n" % request.POST) # for thing in request.POST: # print("the thing: %s") % thing # print("type: %s") % type(thing) # print(e.args) # print(e.error) # print(e.message) request.session.flash( _(u"Please note: There were errors, " "please check the form below."), 'message_above_form', allow_duplicate=False) return{'form': e.render()} def make_random_string(): """ used as email confirmation code """ import random import string return u''.join( random.choice( string.ascii_uppercase + string.digits ) for x in range(10)) # make confirmation code and randomstring = make_random_string() # check if confirmation code is already used while (C3sMember.check_for_existing_confirm_code(randomstring)): # create a new one, if the new one already exists in the database randomstring = make_random_string() # pragma: no cover # to store the data in the DB, an objet is created member = C3sMember( firstname=appstruct['person']['firstname'], lastname=appstruct['person']['lastname'], email=appstruct['person']['email'], password='******', address1=appstruct['person']['address1'], address2=appstruct['person']['address2'], postcode=appstruct['person']['postcode'], city=appstruct['person']['city'], country=appstruct['person']['country'], locale=appstruct['person']['locale'], date_of_birth=appstruct['person']['date_of_birth'], email_is_confirmed=False, email_confirm_code=randomstring, # is_composer=('composer' in appstruct['activity']), # is_lyricist=('lyricist' in appstruct['activity']), # is_producer=('music producer' in appstruct['activity']), # is_remixer=('remixer' in appstruct['activity']), # is_dj=('dj' in appstruct['activity']), date_of_submission=datetime.now(), # invest_member=( # appstruct['membership_info']['invest_member'] == u'yes'), membership_type=appstruct['membership_info']['membership_type'], member_of_colsoc=( appstruct['membership_info']['member_of_colsoc'] == u'yes'), name_of_colsoc=appstruct['membership_info']['name_of_colsoc'], # opt_band=appstruct['opt_band'], # opt_URL=appstruct['opt_URL'], num_shares=appstruct['shares']['num_shares'], ) if 'legalentity' in appstruct['membership_info']['entity_type']: # print "this is a legal entity" member.membership_type = u'investing' member.is_legalentity = True dbsession = DBSession() try: _temp = request.url.split('?')[1].split('=') if 'id' in _temp[0]: _id = _temp[1] # print("the id we want to recreate: %s" % _id) # add a member with a DB id that had seen its entry deleted before _mem = C3sMember.get_by_id(_id) # load from id if isinstance(_mem, NoneType): # check deletion status member.id = _id # set id as specified except: # print "no splitable url params found, creating new entry" pass # add member at next free DB id (default if member.id not set) try: dbsession.add(member) dbsession.flush() # print(member.id) the_new_id = member.id # appstruct['email_confirm_code'] = randomstring # ??? except InvalidRequestError, e: # pragma: no cover print("InvalidRequestError! %s") % e except IntegrityError, ie: # pragma: no cover print("IntegrityError! %s") % ie
def test_send_dues15_invoice_email_single(self): """ test the send_dues15_invoice_email view * calculate invoice amount and send invoice email ** to not accepted member ** to accepted member ** to non-existing member (wrong id) ** to same member (just send email, don't create new invoice) ... and also check email texts for * german normal member * english normal member * german investing member * english investing member * german investing legal entity * english investing legal entity """ from pyramid_mailer import get_mailer from c3smembership.views.membership_dues import send_dues15_invoice_email from c3smembership.models import Dues15Invoice _number_of_invoices = len(Dues15Invoice.get_all()) self.config.add_route('toolbox', '/') self.config.add_route('detail', '/') self.config.add_route('make_dues15_invoice_no_pdf', '/') req = testing.DummyRequest() req.matchdict = { 'member_id': '1', } req.referrer = 'detail' res = send_dues15_invoice_email(req) self.assertTrue(res.status_code == 302) self.assertTrue('http://example.com/' in res.headers['Location']) # member 1 not accepted by the board. problem! _number_of_invoices_2 = len(Dues15Invoice.get_all()) assert (_number_of_invoices == _number_of_invoices_2 == 0) # print("_number_of_invoices: {}".format(_number_of_invoices)) m1 = C3sMember.get_by_id(1) m1.membership_accepted = True res = send_dues15_invoice_email(req) # print('#'*60) # print res # print('#'*60) _number_of_invoices_3 = len(Dues15Invoice.get_all()) # print("_number_of_invoices 3: {}".format(_number_of_invoices_3)) assert (_number_of_invoices_3 == 1) # check for outgoing email mailer = get_mailer(req) # print mailer # print mailer.outbox self.assertEqual(len(mailer.outbox), 1) # print mailer.outbox[0].body self.assertTrue( 'Verwendungszweck: C3S-dues2015-0001' in mailer.outbox[0].body) """ now we try to get an id that does not exist """ req2 = testing.DummyRequest() req2.matchdict = { 'member_id': '1234', } req2.referrer = 'detail' res2 = send_dues15_invoice_email(req2) self.assertTrue(res2.status_code == 302) self.assertTrue('http://example.com/' in res2.headers['Location']) """ what if we call that function (and send email) twice? test that no second invoice is created in DB. """ req3 = testing.DummyRequest() req3.matchdict = { 'member_id': '1', } req3.referrer = 'detail' res3 = send_dues15_invoice_email(req3) self.assertTrue(res3.status_code == 302) self.assertTrue('http://example.com/' in res3.headers['Location']) _number_of_invoices_4 = len(Dues15Invoice.get_all()) self.assertEqual(_number_of_invoices_3, _number_of_invoices_4) """ check for email texts """ self.assertEqual(len(mailer.outbox), 2) self.assertTrue( (u'Dein Mitgliedsbeitrag ab Quartal 1 beträgt also 50 Euro.' ) in mailer.outbox[0].body) # print(mailer.outbox[0].body) self.assertTrue( (u'Dein Mitgliedsbeitrag ab Quartal 1 beträgt also 50 Euro.' ) in mailer.outbox[1].body) # print(mailer.outbox[1].body) """ send email to * english member, * investing members (de/en), * legal entities (de/en) """ # english normal ##################################################### m2 = C3sMember.get_by_id(2) m2.membership_accepted = True req_en_normal = testing.DummyRequest() req_en_normal.matchdict = { 'member_id': '2', } req_en_normal.referrer = 'detail' res_en_normal = send_dues15_invoice_email(req_en_normal) self.assertTrue(res_en_normal.status_code == 302) self.assertEqual(len(mailer.outbox), 3) # print(mailer.outbox[2].body) self.assertTrue((u'Please transfer 50 Euro') in mailer.outbox[2].body) # german investing ################################################### m3 = C3sMember.get_by_id(3) m3.membership_accepted = True req_de_investing = testing.DummyRequest() req_de_investing.matchdict = { 'member_id': '3', } req_de_investing.referrer = 'detail' res_de_investing = send_dues15_invoice_email(req_de_investing) self.assertTrue(res_de_investing.status_code == 302) self.assertEqual(len(mailer.outbox), 4) # print(mailer.outbox[3].body) self.assertTrue( (u'Da Du investierendes Mitglied bist') in mailer.outbox[3].body) # english investing ################################################## m4 = C3sMember.get_by_id(4) m4.membership_accepted = True req_en_investing = testing.DummyRequest() req_en_investing.matchdict = { 'member_id': '4', } req_en_investing.referrer = 'detail' res_en_investing = send_dues15_invoice_email(req_en_investing) self.assertTrue(res_en_investing.status_code == 302) self.assertEqual(len(mailer.outbox), 5) # print(mailer.outbox[4].body) self.assertTrue( (u'Since you are an investing member') in mailer.outbox[4].body) # german legal entity ################################################ m5 = C3sMember.get_by_id(5) m5.membership_accepted = True req_de_legalentity = testing.DummyRequest() req_de_legalentity.matchdict = { 'member_id': '5', } req_de_legalentity.referrer = 'detail' res_de_legalentity = send_dues15_invoice_email(req_de_legalentity) self.assertTrue(res_de_legalentity.status_code == 302) self.assertEqual(len(mailer.outbox), 6) # print(mailer.outbox[5].body) self.assertTrue((u'') in mailer.outbox[5].body) # english legal entity ############################################### m6 = C3sMember.get_by_id(6) m6.membership_accepted = True req_en_legalentity = testing.DummyRequest() req_en_legalentity.matchdict = { 'member_id': '6', } req_en_legalentity.referrer = 'detail' res_en_legalentity = send_dues15_invoice_email(req_en_legalentity) self.assertTrue(res_en_legalentity.status_code == 302) self.assertEqual(len(mailer.outbox), 7) # print(mailer.outbox[6].body) self.assertTrue((u'Da Musikverlag investierendes Mitglied ist' ) in mailer.outbox[6].body) self.assertTrue((u'Für juristische Personen wird empfohlen' ) in mailer.outbox[6].body)
def test_send_dues15_invoice_email_via_BATCH(self): """ test the send_dues15_invoice_batch function for batch processing """ # from pyramid_mailer import get_mailer from c3smembership.views.membership_dues import send_dues15_invoice_batch self.config.add_route('make_dues15_invoice_no_pdf', '/') self.config.add_route('make_dues15_reversal_invoice_pdf', '/') self.config.add_route('detail', '/detail/') self.config.add_route('error_page', '/error_page') self.config.add_route('toolbox', '/toolbox') # have to accept their membersip first m1 = C3sMember.get_by_id(1) # german normal member m1.membership_accepted = True m2 = C3sMember.get_by_id(2) # english normal member m2.membership_accepted = True m3 = C3sMember.get_by_id(3) # german investing member m3.membership_accepted = True m4 = C3sMember.get_by_id(4) # english investing member m4.membership_accepted = True m5 = C3sMember.get_by_id(5) # german investing member m5.membership_accepted = True m5 = C3sMember.get_by_id(6) # english investing member m5.membership_accepted = True # check number of invoices: should be 0 _number_of_invoices_before_batch = len(Dues15Invoice.get_all()) # print("_number_of_invoices_before_batch: {}".format( # _number_of_invoices_before_batch)) assert (_number_of_invoices_before_batch == 0) req = testing.DummyRequest() req.referrer = 'toolbox' res = send_dues15_invoice_batch(req) # print res # check number of invoices: should be 2 _number_of_invoices_batch = len(Dues15Invoice.get_all()) # print("number of invoices after batch: {}".format( # _number_of_invoices_batch)) assert (_number_of_invoices_batch == 2) # try to post a number for batch processing req_post = testing.DummyRequest( post={ 'submit': True, 'number': 24 # lots of values missing }, ) req.referrer = 'toolbox' res = send_dues15_invoice_batch(req_post) assert ('sent out 5 mails (to members with ids [1, 2, 3, 4, 5])' in req.session.pop_flash('message_to_staff')) # try to batch-send once more: # this will respond with a redirect and tell # that there are no invitees left res2 = send_dues15_invoice_batch(req) # print res2 self.assertEquals(res2.status, '302 Found') self.assertEquals(res2.status_code, 302) assert ('no invoicees left. all done!' in req.session.pop_flash('message_to_staff')) """ and now some tests for make_dues15_invoice_no_pdf """ from c3smembership.views.membership_dues import ( make_dues15_invoice_no_pdf) req2 = testing.DummyRequest() # wrong token: must fail! req2.matchdict = { 'email': m1.email, 'code': m1.dues15_token + 'false!!!', # must fail 'i': u'0001', } res = make_dues15_invoice_no_pdf(req2) assert ('application/pdf' not in res.headers['Content-Type']) # no PDF assert ('error_page' in res.headers['Location']) # but error # wrong invoice number: must fail! req2.matchdict = { 'email': m1.email, 'code': m1.dues15_token, 'i': u'1234', # must fail } res = make_dues15_invoice_no_pdf(req2) assert ('application/pdf' not in res.headers['Content-Type']) # no PDF assert ('error_page' in res.headers['Location']) # but error # wrong invoice token: must fail! i2 = Dues15Invoice.get_by_invoice_no(2) i2.token = u'not_matching' req2.matchdict = { 'email': m2.email, 'code': m2.dues15_token, 'i': u'3', # must fail } res = make_dues15_invoice_no_pdf(req2) assert ('application/pdf' not in res.headers['Content-Type']) # no PDF assert ('error_page' in res.headers['Location']) # but error ####################################################################### # one more edge case: # check _inv.token must match code, or else!!! # first, set inv_code to something wrong: i1 = Dues15Invoice.get_by_invoice_no(1) _old_i1_token = i1.token i1.token = u'not_right' req2.matchdict = { 'email': m1.email, 'code': m1.dues15_token, 'i': u'0001', } res = make_dues15_invoice_no_pdf(req2) assert ('application/pdf' not in res.headers['Content-Type']) # no PDF assert ('error_page' in res.headers['Location']) # but error # reset it to what was there before i1.token = _old_i1_token ####################################################################### # one more edge case: # check this invoice is not a reversal, or else no PDF!!! # first, set is_reversal to something wrong: i1 = Dues15Invoice.get_by_invoice_no(1) _old_i1_reversal_status = i1.is_reversal # False i1.is_reversal = True req2.matchdict = { 'email': m1.email, 'code': m1.dues15_token, 'i': u'0001', } res = make_dues15_invoice_no_pdf(req2) assert ('application/pdf' not in res.headers['Content-Type']) # no PDF assert ('error_page' in res.headers['Location']) # but error # reset it to what was there before i1.is_reversal = _old_i1_reversal_status ####################################################################### # retry with valid token: req2.matchdict = { 'email': m1.email, 'code': m1.dues15_token, 'i': u'0001', } res = make_dues15_invoice_no_pdf(req2) # m1. # print("length of the result: {}".format(len(res.body))) # print("headers of the result: {}".format((res.headers))) assert (60000 < len(res.body) < 80000) assert ('application/pdf' in res.headers['Content-Type']) """ test dues listing """ from c3smembership.views.membership_dues import dues15_listing req_list = testing.DummyRequest() resp_list = dues15_listing(req_list) # print resp_list # {'count': 5, # 'invoices': [ # <c3smembership.models.Dues15Invoice object at 0x7f95df761a50>, # <c3smembership.models.Dues15Invoice object at 0x7f95df761690>, # <c3smembership.models.Dues15Invoice object at 0x7f95df815a50>, # <c3smembership.models.Dues15Invoice object at 0x7f95df761c90>, # <c3smembership.models.Dues15Invoice object at 0x7f95df761c10>], # '_today': datetime.date(2015, 9, 1)} assert (resp_list['count'] == 2)
def test_dues15_reduction(self): """ test the dues15_reduction functionality """ # have to accept their membersip first m1 = C3sMember.get_by_id(1) # german normal member m1.membership_accepted = True m2 = C3sMember.get_by_id(2) # english normal member m2.membership_accepted = True self.config.add_route('make_dues15_invoice_no_pdf', '/') self.config.add_route('make_dues15_reversal_invoice_pdf', '/') self.config.add_route('detail', '/detail/') self.config.add_route('error_page', '/error_page') self.config.add_route('toolbox', '/toolbox') req = testing.DummyRequest() req.referrer = 'toolbox' from c3smembership.views.membership_dues import send_dues15_invoice_batch # send out invoices. this is a prerequisite for reductions res = send_dues15_invoice_batch(req) res """ test reduction of dues """ # pre-check self.assertFalse(m1.dues15_reduced) # not reduced yet! _m1_amount_reduced = m1.dues15_amount_reduced # is Decimal('0') _number_of_invoices_before_reduction = len(Dues15Invoice.get_all()) # print("_number_of_invoices_before_reduction: {}".format( # _number_of_invoices_before_reduction)) # we have 2 invoices as of now self.assertEqual(len(Dues15Invoice.get_all()), 2) # import the function under test from c3smembership.views.membership_dues import dues15_reduction ############################################################# # try to reduce to the given calculated amount (edge case coverage) # this will not work, produce no new invoices req_reduce = testing.DummyRequest( # prepare request post={ 'confirmed': 'yes', 'submit': True, 'amount': 50, }, ) req_reduce.matchdict['member_id'] = 1 # do it for member with id 1 res_reduce = dues15_reduction(req_reduce) # call reduce on her self.assertEqual(len(Dues15Invoice.get_all()), 2) # no new invoice ############################################################# # try to reduce above the given calculated amount # this will not work, produce no new invoices req_reduce = testing.DummyRequest( # prepare request post={ 'confirmed': 'yes', 'submit': True, 'amount': 500, }, ) req_reduce.matchdict['member_id'] = 1 # do it for member with id 1 res_reduce = dues15_reduction(req_reduce) # call reduce on her self.assertEqual(len(Dues15Invoice.get_all()), 2) # no new invoice ############################################################# # valid reduction but without confirmation req_reduce = testing.DummyRequest(post={ 'confirmed': 'no', 'submit': True, 'amount': 42, }, ) req_reduce.matchdict['member_id'] = 1 res_reduce = dues15_reduction(req_reduce) self.assertEqual(len(Dues15Invoice.get_all()), 2) # no new invoice ############################################################# # valid reduction req_reduce = testing.DummyRequest(post={ 'confirmed': 'yes', 'submit': True, 'amount': 42, }, ) req_reduce.matchdict['member_id'] = 1 res_reduce = dues15_reduction(req_reduce) _number_of_invoices_after_reduction = len(Dues15Invoice.get_all()) assert ( # two new invoices must have been issued (_number_of_invoices_before_reduction + 2) == _number_of_invoices_after_reduction) assert (_number_of_invoices_after_reduction == 4) assert ('detail' in res_reduce.headers['Location']) # 302 to detail p. assert (_m1_amount_reduced != m1.dues15_amount_reduced) # changed! assert (m1.dues15_amount_reduced == 42) # changed to 42! # check the invoice created _rev_inv = Dues15Invoice.get_by_invoice_no( _number_of_invoices_before_reduction + 1) _new_inv = Dues15Invoice.get_by_invoice_no( _number_of_invoices_before_reduction + 2) # print(_rev_inv.invoice_amount) # print(type(_rev_inv.invoice_amount)) assert (_rev_inv.invoice_amount == D('-50')) assert (_new_inv.invoice_amount == D('42')) # we have 4 invoices as of now self.assertEqual(len(Dues15Invoice.get_all()), 4) ############################################################# # now try to raise above the previous reduction req_reduce = testing.DummyRequest(post={ 'confirmed': 'yes', 'submit': True, 'amount': 50, }, ) req_reduce.matchdict['member_id'] = 1 res_reduce = dues15_reduction(req_reduce) _number_of_invoices_after_reduction = len(Dues15Invoice.get_all()) # no new invoices were created, we still have 4 invoices self.assertEqual(len(Dues15Invoice.get_all()), 4) ############################################################# # try to reduce to the same amount again (edge case coverage) req_reduce = testing.DummyRequest( post={ 'confirmed': 'yes', 'submit': True, 'amount': 42, # lots of values missing }, ) req_reduce.matchdict['member_id'] = 1 res_reduce = dues15_reduction(req_reduce) ############################################################# # try to reduce to zero (edge case coverage) # print("------------- reduction to zero ahead!!!") req_reduce = testing.DummyRequest( post={ 'confirmed': 'yes', 'submit': True, 'amount': 0, # lots of values missing }, ) req_reduce.matchdict['member_id'] = 1 res_reduce = dues15_reduction(req_reduce) req_reduce = testing.DummyRequest( post={ 'confirmed': 'yes', 'submit': True, 'amount': 0, # lots of values missing }, ) req_reduce.matchdict['member_id'] = 2 res_reduce = dues15_reduction(req_reduce) ############################################################# # try to reduce to zero with english member (edge case coverage) # how to do this if you already reduced to zero? reduce to more first! req_reduce = testing.DummyRequest( post={ 'confirmed': 'yes', 'submit': True, 'amount': 1, # lots of values missing }, ) req_reduce.matchdict['member_id'] = 1 res_reduce = dues15_reduction(req_reduce) m1.locale = u'en' req_reduce = testing.DummyRequest( post={ 'confirmed': 'yes', 'submit': True, 'amount': 0, # lots of values missing }, ) req_reduce.matchdict['member_id'] = 1 res_reduce = dues15_reduction(req_reduce) ############################################################# """ test reversal invoice PDF generation """ from c3smembership.views.membership_dues import ( make_dues15_reversal_invoice_pdf) req2 = testing.DummyRequest() # wrong token: must fail! req2.matchdict = { 'email': m1.email, 'code': m1.dues15_token + 'false!!!', # must fail 'no': u'0006', } res = make_dues15_reversal_invoice_pdf(req2) assert ('application/pdf' not in res.headers['Content-Type']) # no PDF assert ('error_page' in res.headers['Location']) # but error # wrong invoice number: must fail! req2.matchdict = { 'email': m1.email, 'code': m1.dues15_token, 'no': u'1234', # must fail } res = make_dues15_reversal_invoice_pdf(req2) assert ('application/pdf' not in res.headers['Content-Type']) # no PDF assert ('error_page' in res.headers['Location']) # but error # wrong invoice token: must fail! i2 = Dues15Invoice.get_by_invoice_no('2') i2.token = u'not_matching' req2.matchdict = { 'email': m2.email, 'code': m2.dues15_token, 'no': u'2', # must fail } res = make_dues15_reversal_invoice_pdf(req2) assert ('application/pdf' not in res.headers['Content-Type']) # no PDF assert ('error_page' in res.headers['Location']) # but error ###################################################################### # wrong invoice type (not a reversal): must fail! (edge case coverage) assert (not i2.is_reversal) # i2 is not a reversal i2.token = m2.dues15_token # we give it a valid token req2.matchdict = { 'email': m2.email, 'code': m2.dues15_token, 'no': u'0002', } res = make_dues15_reversal_invoice_pdf(req2) assert ('application/pdf' not in res.headers['Content-Type']) # no PDF assert ('error_page' in res.headers['Location']) # but error ###################################################################### # retry with valid token: req2.matchdict = { 'email': m1.email, 'code': m1.dues15_token, 'no': u'0003', } res = make_dues15_reversal_invoice_pdf(req2) # print("length of the result: {}".format(len(res.body))) # print("headers of the result: {}".format((res.headers))) assert (60000 < len(res.body) < 80000) assert ('application/pdf' in res.headers['Content-Type'])
def dues16_reduction(request): """ reduce a members dues upon valid request to do so. * change payable amount for member * cancel old invoice by issuing a cancellation * issue a new invoice with the new amount (if new amount != 0) this will only work for *normal* members. """ # member: sanity checks try: member_id = request.matchdict['member_id'] member = C3sMember.get_by_id(member_id) # is in database assert member.membership_accepted # is a member assert 'investing' not in member.membership_type # is normal member except (KeyError, AssertionError): # pragma: no cover request.session.flash( u"No member OR not accepted OR not normal member", 'dues16_message_to_staff' # message queue for staff ) return HTTPFound( request.route_url('detail', memberid=member.id) + '#dues16') # sanity check: the given amount is a positive decimal try: reduced_amount = D(request.POST['amount']) assert not reduced_amount.is_signed() if DEBUG: print("DEBUG: reduction to {}".format(reduced_amount)) except (KeyError, AssertionError): # pragma: no cover request.session.flash( (u"Invalid amount to reduce to: '{}' " u"Use the dot ('.') as decimal mark, e.g. '23.42'".format( request.POST['amount'])), 'dues16_message_to_staff' # message queue for user ) return HTTPFound( request.route_url('detail', memberid=member.id) + '#dues16') if DEBUG: print("DEBUG: member.dues16_amount: {}".format( member.dues16_amount)) print("DEBUG: type(member.dues16_amount): {}".format( type(member.dues16_amount))) print("DEBUG: member.dues16_reduced: {}".format( member.dues16_reduced)) print("DEBUG: member.dues16_amount_reduced: {}".format( member.dues16_amount_reduced)) print("DEBUG: type(member.dues16_amount_reduced): {}".format( type(member.dues16_amount_reduced))) # The hidden input 'confirmed' must have the value 'yes' which is set by # the confirmation dialog. reduction_confirmed = request.POST['confirmed'] if reduction_confirmed != 'yes': request.session.flash( u'Die Reduktion wurde nicht bestätigt.', 'dues16_message_to_staff' # message queue for staff ) return HTTPFound( request.route_url('detail', memberid=member.id) + '#dues16') # check the reduction amount: same as default calculated amount? if ((member.dues16_reduced is False) and ( member.dues16_amount == reduced_amount)): request.session.flash( u"Dieser Beitrag ist der default-Beitrag!", 'dues16_message_to_staff' # message queue for staff ) return HTTPFound( request.route_url('detail', memberid=member.id) + '#dues16') if reduced_amount == member.dues16_amount_reduced: request.session.flash( u"Auf diesen Beitrag wurde schon reduziert!", 'dues16_message_to_staff' # message queue for staff ) return HTTPFound( request.route_url('detail', memberid=member.id) + '#dues16') if member.dues16_reduced \ and reduced_amount > member.dues16_amount_reduced \ or reduced_amount > member.dues16_amount: request.session.flash( u'Beitrag darf nicht über den berechneten oder bereits' u'reduzierten Wert gesetzt werden.', 'dues16_message_to_staff' # message queue for staff ) return HTTPFound( request.route_url('detail', memberid=member.id) + '#dues16') # prepare: get highest invoice no from db max_invoice_no = Dues16Invoice.get_max_invoice_no() # things to be done: # * change dues amount for that member # * cancel old invoice by issuing a reversal invoice # * issue a new invoice with the new amount member.set_dues16_reduced_amount(reduced_amount) request.session.flash('reduction to {}'.format(reduced_amount), 'dues16_message_to_staff') old_invoice = Dues16Invoice.get_by_invoice_no(member.dues16_invoice_no) old_invoice.is_cancelled = True reversal_invoice_amount = -D(old_invoice.invoice_amount) # prepare reversal invoice number new_invoice_no = max_invoice_no + 1 # create reversal invoice reversal_invoice = Dues16Invoice( invoice_no=new_invoice_no, invoice_no_string=( u'C3S-dues2016-' + str(new_invoice_no).zfill(4)) + '-S', invoice_date=datetime.today(), invoice_amount=reversal_invoice_amount.to_eng_string(), member_id=member.id, membership_no=member.membership_number, email=member.email, token=member.dues16_token, ) reversal_invoice.preceding_invoice_no = old_invoice.invoice_no reversal_invoice.is_reversal = True DBSession.add(reversal_invoice) DBSession.flush() old_invoice.succeeding_invoice_no = new_invoice_no # check if this is an exemption (reduction to zero) is_exemption = False # sane default # check if reduction to zero if reduced_amount.is_zero(): is_exemption = True if DEBUG: print("this is an exemption: reduction to zero") else: if DEBUG: print("this is a reduction to {}".format(reduced_amount)) if not is_exemption: # create new invoice new_invoice = Dues16Invoice( invoice_no=new_invoice_no + 1, invoice_no_string=( u'C3S-dues2016-' + str(new_invoice_no + 1).zfill(4)), invoice_date=datetime.today(), invoice_amount=u'' + str(reduced_amount), member_id=member.id, membership_no=member.membership_number, email=member.email, token=member.dues16_token, ) new_invoice.is_altered = True new_invoice.preceding_invoice_no = reversal_invoice.invoice_no reversal_invoice.succeeding_invoice_no = new_invoice_no + 1 DBSession.add(new_invoice) # in the members record, store the current invoice no member.dues16_invoice_no = new_invoice_no + 1 DBSession.flush() # persist newer invoices reversal_url = ( request.route_url( 'make_dues16_reversal_invoice_pdf', email=member.email, code=member.dues16_token, no=str(reversal_invoice.invoice_no).zfill(4) ) ) if is_exemption: email_subject, email_body = make_dues_exemption_email( member, reversal_url) else: invoice_url = ( request.route_url( 'make_dues16_invoice_no_pdf', email=member.email, code=member.dues16_token, i=str(new_invoice_no + 1).zfill(4) ) ) email_subject, email_body = make_dues16_reduction_email( member, new_invoice, invoice_url, reversal_url) message = Message( subject=email_subject, sender='*****@*****.**', recipients=[member.email], body=email_body, extra_headers={ 'Reply-To': '*****@*****.**', } ) if is_exemption: request.session.flash('exemption email was sent to user!', 'dues16_message_to_staff') else: request.session.flash('update email was sent to user!', 'dues16_message_to_staff') send_message(request, message) return HTTPFound( request.route_url( 'detail', memberid=member_id) + '#dues16')