Esempio n. 1
0
def stats_view(request):
    """
    This view lets accountants view statistics:
    how many membership applications, real members, shares, etc.
    """
    # countries_dict = C3sMember.get_countries_list()
    _cl = C3sMember.get_countries_list()
    _cl_sorted = _cl.items()
    # print "die liste: {}".format(_cl_sorted)
    import operator
    _cl_sorted.sort(key=operator.itemgetter(1), reverse=True)
    # print "sortiert: {}".format(_cl_sorted)
    share_information = request.registry.share_information
    return {
        # form submissions
        '_number_of_datasets': C3sMember.get_number(),
        'afm_shares_unpaid': C3sMember.afm_num_shares_unpaid(),
        'afm_shares_paid': C3sMember.afm_num_shares_paid(),
        # shares
        'num_shares_members': share_information.get_share_count(),
        # 'num_shares_mem_norm': Shares.get_sum_norm(),
        # 'num_shares_mem_inv': Shares.get_sum_inv(),

        # memberships
        'num_members_accepted': C3sMember.get_num_members_accepted(),
        'num_non_accepted': C3sMember.get_num_non_accepted(),
        'num_nonmember_listing': C3sMember.nonmember_listing_count(),
        'num_duplicates': len(C3sMember.get_duplicates()),
        # 'num_empty_slots': C3sMember.get_num_empty_slots(),
        # normal persons vs. legal entities
        'num_ms_nat_acc': C3sMember.get_num_mem_nat_acc(),
        'num_ms_jur_acc': C3sMember.get_num_mem_jur_acc(),
        # normal vs. investing memberships
        'num_ms_norm': C3sMember.get_num_mem_norm(),
        'num_ms_inves': C3sMember.get_num_mem_invest(),
        'num_ms_features': C3sMember.get_num_mem_other_features(),
        'num_membership_lost': C3sMember.get_num_membership_lost(),
        # membership_numbers
        'num_memnums': C3sMember.get_num_membership_numbers(),
        'max_memnum': C3sMember.get_highest_membership_number(),
        'next_memnum': C3sMember.get_next_free_membership_number(),

        # countries
        'num_countries': C3sMember.get_num_countries(),
        'countries_list': _cl_sorted,
        #    key=lambda x: x[1]
        # ),  # XXX TODO: sorte

        # dues stats
        'dues15_stats': Dues15Invoice.get_monthly_stats(),
        'dues16_stats': Dues16Invoice.get_monthly_stats(),
        'dues17_stats': Dues17Invoice.get_monthly_stats(),

        # staff figures
        'num_staff': len(C3sStaff.get_all())
    }
Esempio n. 2
0
def stats_view(request):
    """
    This view lets accountants view statistics:
    how many membership applications, real members, shares, etc.
    """
    # countries_dict = C3sMember.get_countries_list()
    _cl = C3sMember.get_countries_list()
    _cl_sorted = _cl.items()
    # print "die liste: {}".format(_cl_sorted)
    import operator
    _cl_sorted.sort(key=operator.itemgetter(1), reverse=True)
    # print "sortiert: {}".format(_cl_sorted)
    return {
        # form submissions
        '_number_of_datasets': C3sMember.get_number(),
        'afm_shares_unpaid': C3sMember.afm_num_shares_unpaid(),
        'afm_shares_paid': C3sMember.afm_num_shares_paid(),
        # shares
        # 'num_shares': C3sMember.get_total_shares(),
        'num_shares_members': Shares.get_total_shares(),
        # 'num_shares_mem_norm': Shares.get_sum_norm(),
        # 'num_shares_mem_inv': Shares.get_sum_inv(),

        # memberships
        'num_members_accepted': C3sMember.get_num_members_accepted(),
        'num_non_accepted': C3sMember.get_num_non_accepted(),
        'num_nonmember_listing': C3sMember.nonmember_listing_count(),
        'num_duplicates': len(C3sMember.get_duplicates()),
        # 'num_empty_slots': C3sMember.get_num_empty_slots(),
        # normal persons vs. legal entities
        'num_ms_nat_acc': C3sMember.get_num_mem_nat_acc(),
        'num_ms_jur_acc': C3sMember.get_num_mem_jur_acc(),
        # normal vs. investing memberships
        'num_ms_norm': C3sMember.get_num_mem_norm(),
        'num_ms_inves': C3sMember.get_num_mem_invest(),
        'num_ms_features': C3sMember.get_num_mem_other_features(),
        # membership_numbers
        'num_memnums': C3sMember.get_num_membership_numbers(),
        'max_memnum': C3sMember.get_highest_membership_number(),
        'next_memnum': C3sMember.get_next_free_membership_number(),

        # countries
        'num_countries': C3sMember.get_num_countries(),
        'countries_list': _cl_sorted,
        #    key=lambda x: x[1]
        # ),  # XXX TODO: sorte

        # dues stats
        'dues15_stats': Dues15Invoice.get_monthly_stats(),
        'dues16_stats': Dues16Invoice.get_monthly_stats(),

        # staff figures
        'num_staff': len(C3sStaff.get_all())
    }
Esempio n. 3
0
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.
    """
    _id = request.matchdict['afm_id']
    try:  # does that id make sense? member exists?
        member = C3sMember.get_by_id(_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()
        shares = Shares(
            number=member.num_shares,
            date_of_acquisition=member.membership_date,
            reference_code=member.email_confirm_code,
            signature_received=member.signature_received,
            signature_received_date=member.signature_received_date,
            payment_received=member.payment_received,
            payment_received_date=member.payment_received_date
        )
        DBSession.add(shares)
        member.shares = [shares]
        # 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))
Esempio n. 4
0
            payment_received_date=member.payment_received_date
        )
        DBSession.add(shares)
        member.shares = [shares]
        # 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))

    referrer = ''
    if 'dashboard' in request.referrer:
        referrer = 'dashboard'
    if 'detail' in request.referrer:
        referrer = 'detail'
    return {
        'member': member,
        'next_mship_number': C3sMember.get_next_free_membership_number(),
        'same_mships_firstn': C3sMember.get_same_firstnames(member.firstname),
        'same_mships_lastn': C3sMember.get_same_lastnames(member.lastname),
        'same_mships_email': C3sMember.get_same_email(member.email),
        'same_mships_dob': C3sMember.get_same_date_of_birth(
            member.date_of_birth),
        # keep information about the page the user came from in order to
        # return her to this page
        'referrer': referrer,
    }
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()

        share_id = request.registry.share_acquisition.create(
            member.membership_number,
            member.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))
        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))

    referrer = ''
    if 'dashboard' in request.referrer:
        referrer = 'dashboard'
    if 'detail' in request.referrer:
        referrer = 'detail'
    return {
        'member': member,
        'next_mship_number': C3sMember.get_next_free_membership_number(),
        'same_mships_firstn': C3sMember.get_same_firstnames(member.firstname),
        'same_mships_lastn': C3sMember.get_same_lastnames(member.lastname),
        'same_mships_email': C3sMember.get_same_email(member.email),
        'same_mships_dob': C3sMember.get_same_date_of_birth(
            member.date_of_birth),
        # keep information about the page the user came from in order to
        # return her to this page
        'referrer': referrer,
    }
Esempio n. 7
0
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,
    }
    appstruct['shares'] = {
        'num_shares': member.num_shares
    }
    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),
            validator=Range(
                min=date(1913, 1, 1),
                max=date(2000, 1, 1),
                min_err=_(u'${val} is earlier than earliest date ${min}.'),
                max_err=_(u'${val} is later than latest date ${max}.')
            ),
            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',
        )

    class Shares(colander.Schema):
        """
        the number of shares a member wants to hold
        """
        num_shares = colander.SchemaNode(
            colander.Integer(),
            title=_('Number of Shares (1-60)'),
            default='1',
            validator=colander.Range(
                min=1,
                max=60,
                min_err=_(u'At least one share must be acquired.'),
                max_err=_(u'At most 60 shares can be acquired.'),
            ),
            oid='num_shares')

    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')
        )
        shares = Shares(
            title=_(u'Shares')
        )

    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']
            ),
            ('num_shares', appstruct['shares']['num_shares']),
            (
                '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}
Esempio n. 8
0
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 test_membership_application(self):
        """
        Test the membership application process.

         1. Enter applicant data to application form
         2. Verify entered data and confirm
         3. Verify sent confirmation email
         4. Confirm email address via confirmation link
         5. Login to backend
         6. Verify applicant's detail page
         7. Set payment received
         8. Set signature received
         9. Make member
        10. Verify member details
        """
        self.testapp.reset()

        # 1. Enter applicant data to application form
        res = self.testapp.get('/', status=200)
        properties = {
            'firstname': u'Sönke',
            'lastname': u'Blømqvist',
            'email': u'*****@*****.**',
            'address1': u'℅ Big Boss',
            'address2': u'Håkanvägen 12',
            'postcode': u'ABC1234',
            'city': u'Stockholm',
            'year': u'1980',
            'month': u'01',
            'day': u'02',
            'name_of_colsoc': u'Svenska Tonsättares Internationella Musikbyrå',
            'num_shares': u'15',
            'password': u'worst password ever chosen',
            'password-confirm': u'worst password ever chosen',
        }
        for key, value in properties.iteritems():
            res.form[key] = value
        res.form['country'].select(text=u'Sweden')
        res.form['membership_type'].value__set(u'normal')
        res.form['other_colsoc'].value__set(u'yes')
        res.form['got_statute'].checked = True
        res.form['got_dues_regulations'].checked = True
        res = res.form.submit(u'submit', status=302)
        res = res.follow()

        # 2. Verify entered data and confirm
        body = self._response_to_bare_text(res)
        self.assertTrue('First Name: Sönke' in body)
        self.assertTrue('Last Name: Blømqvist' in body)
        self.assertTrue('Email Address: [email protected]' in body)
        self.assertTrue('Address Line 1: ℅ Big Boss' in body)
        self.assertTrue('Address Line 2: Håkanvägen 12' in body)
        self.assertTrue('Postal Code: ABC1234' in body)
        self.assertTrue('City: Stockholm' in body)
        self.assertTrue('Country: SE' in body)
        self.assertTrue('Date of Birth: 1980-01-02' in body)
        self.assertTrue('Type of Membership:normal' in body)
        self.assertTrue('Member of other Collecting Society: yes' in body)
        self.assertTrue(
            'Membership(s): Svenska Tonsättares Internationella Musikbyrå' in
            body)
        self.assertTrue('Number of Shares: 15' in body)
        self.assertTrue('Cost of Shares (50 € each): 750 €' in body)
        res = res.forms[1].submit(status=200)

        # 3. Verify sent confirmation email
        mailer = self.get_mailer(None)
        email = mailer.get_email()
        self.assertEqual(email.recipients, ['*****@*****.**'])
        self.assertEqual(email.subject,
                         'C3S: confirm your email address and load your PDF')

        # 4. Confirm email address via confirmation link
        match = re.search('localhost(?P<url>[^\s]+)', email.body)

        self.assertTrue(match is not None)
        res = self.testapp.get(match.group('url'), status=200)

        self.assertTrue(u'password in order to verify your email' in res.body)
        res.form['password'] = '******'
        res = res.form.submit(u'submit', status=200)

        # 5. Login to backend
        self.testapp.reset()
        self._login()

        # 6. Verify applicant's detail page
        member_id = DBSession.query(func.max(C3sMember.id)).scalar()
        res = self.testapp.get('/detail/{0}'.format(member_id), status=200)

        body = self._response_to_bare_text(res)
        self.assertTrue('firstname Sönke' in body)
        self.assertTrue('lastname Blømqvist' in body)
        self.assertTrue('email [email protected]' in body)
        self.assertTrue('email confirmed? Yes' in body)
        self.assertTrue('address1 ℅ Big Boss' in body)
        self.assertTrue('address2 Håkanvägen 12' in body)
        self.assertTrue('postcode ABC1234' in body)
        self.assertTrue('city Stockholm' in body)
        self.assertTrue('country SE' in body)
        self.assertTrue('date_of_birth 1980-01-02' in body)
        self.assertTrue('membership_accepted  No' in body)
        self.assertTrue('entity type Person' in body)
        self.assertTrue('membership type normal' in body)
        self.assertTrue('member_of_colsoc Yes' in body)
        self.assertTrue(
            'name_of_colsoc Svenska Tonsättares Internationella Musikbyrå' in
            body)
        self.assertTrue('date_of_submission ' in body)
        self.assertTrue('signature received?   Nein' in body)
        self.assertTrue('signature confirmed (mail sent)?No' in body)
        self.assertTrue('payment received?   Nein' in body)
        self.assertTrue('payment confirmed?No' in body)
        self.assertTrue('# shares  total: 15' in body)
        # TODO:
        # - code
        # - locale, set explicitly and test both German and English
        # - date of submission

        # 7. Set payment received
        res = self.testapp.get('/switch_pay/{0}'.format(member_id),
                               headers={'Referer': 'asdf'},
                               status=302)
        res = res.follow()
        body = self._response_to_bare_text(res)
        self.assertTrue('payment received?    Ja' in body)
        self.assertTrue('payment reception date 2018-04-26 12:23:34' in body)

        # 8. Set signature received
        res = self.testapp.get('/switch_sig/{0}'.format(member_id),
                               headers={'Referer': 'asdf'},
                               status=302)
        res = res.follow()
        body = self._response_to_bare_text(res)
        self.assertTrue('signature received?    Ja' in body)
        self.assertTrue('signature reception date2018-04-26 12:23:34' in body)

        # 9. Make member
        res = self.testapp.get('/make_member/{0}'.format(member_id),
                               headers={'Referer': 'asdf'},
                               status=200)
        res.form['membership_date'] = '2018-04-27'
        res = res.form.submit('submit', status=302)
        res = res.follow()

        # 10. Verify member details
        membership_number = C3sMember.get_next_free_membership_number() - 1
        body = self._response_to_bare_text(res)
        self.assertTrue('membership_accepted  Yes' in body)
        self.assertTrue(
            'membership_number  {0}'.format(membership_number) in body)
        self.assertTrue('membership_date 2018-04-27' in body)
        self.assertTrue('# shares  total: 15' in body)
        self.assertTrue('1 package(s)' in body)
        self.assertTrue('15 shares   (2018-04-27)' in body)