Просмотр исходного кода

Merge branch 'c3s-master'

master
Родитель
Сommit
7de7442d08
Не найден GPG ключ соответствующий данной подписи Идентификатор GPG ключа: 49B08FCFF6BC3B2
25 измененных файлов: 1340 добавлений и 640 удалений
  1. +14
    -0
      CHANGES.rst
  2. +1
    -1
      VERSION
  3. +27
    -0
      alembic/versions/34c421bb0d0c_invitations_ga_bc_2018.py
  4. +3
    -0
      c3smembership/__init__.py
  5. +1
    -1
      c3smembership/accountants_views.py
  6. +6
    -3
      c3smembership/business/membership_application.py
  7. +18
    -8
      c3smembership/business/tests/test_membership_application.py
  8. +14
    -13
      c3smembership/invite_members.py
  9. +5
    -5
      c3smembership/invite_members_texts.py
  10. +69
    -74
      c3smembership/locale/en/LC_MESSAGES/c3smembership.po
  11. +13
    -1
      c3smembership/membership_list.py
  12. +43
    -8
      c3smembership/models.py
  13. +7
    -7
      c3smembership/presentation/templates/memberships_list_backend.pt
  14. +3
    -1
      c3smembership/presentation/templates/toolbox.pt
  15. +192
    -0
      c3smembership/templates/mail/bcga2018_invite_body_de.txt
  16. +191
    -0
      c3smembership/templates/mail/bcga2018_invite_body_en.txt
  17. +1
    -0
      c3smembership/templates/mail/bcga2018_invite_subject_de.txt
  18. +1
    -0
      c3smembership/templates/mail/bcga2018_invite_subject_en.txt
  19. +7
    -4
      c3smembership/tests/test_api_views.py
  20. +21
    -17
      c3smembership/tests/test_invite_member.py
  21. +292
    -0
      c3smembership/tests/test_membership_application.py
  22. +267
    -326
      c3smembership/tests/test_models.py
  23. +2
    -0
      c3smembership/tests/test_webtest.py
  24. +103
    -162
      c3smembership/views/afm.py
  25. +39
    -9
      c3smembership/views/tests/test_afm.py

+ 14
- 0
CHANGES.rst Просмотреть файл

@@ -2,10 +2,24 @@ Next Release
============


- Fix double entry when applicant edits details



1.20.5
======


- Remove editing of number of shares hold by a member.

- Remove old import and export functionality.

- Show error message if applicant is younger than 18 years old.

- Invitations for general assembly and bar camp 2018.

- Hide invoice 2017 sending in membership list and toolbox.



1.20.4


+ 1
- 1
VERSION Просмотреть файл

@@ -1 +1 @@
1.20.4
1.20.5

+ 27
- 0
alembic/versions/34c421bb0d0c_invitations_ga_bc_2018.py Просмотреть файл

@@ -0,0 +1,27 @@
"""Invitations for general assembly and bar camp 2018

Revision ID: 34c421bb0d0c
Revises: 2fbe1bde5df8
Create Date: 2018-04-23 20:03:17.014936

"""

# revision identifiers, used by Alembic.
revision = '34c421bb0d0c'
down_revision = '2fbe1bde5df8'

from alembic import op
import sqlalchemy as sa


def upgrade():
op.add_column('members', sa.Column('email_invite_date_bcgv18', sa.DateTime(), nullable=True))
op.add_column('members', sa.Column('email_invite_flag_bcgv18', sa.Boolean(), nullable=True))
op.add_column('members', sa.Column('email_invite_token_bcgv18', sa.Unicode(length=255), nullable=True))


def downgrade():
with op.batch_alter_table('members') as batch_op:
batch_op.drop_column('email_invite_token_bcgv18')
batch_op.drop_column('email_invite_flag_bcgv18')
batch_op.drop_column('email_invite_date_bcgv18')

+ 3
- 0
c3smembership/__init__.py Просмотреть файл

@@ -88,6 +88,9 @@ def main(global_config, **settings):
share_acquisition = ShareAcquisition(ShareRepository)
config.registry.share_acquisition = share_acquisition

from pyramid_mailer import get_mailer
config.registry.get_mailer = get_mailer

# Membership application process
# Step 1 (join.pt): home is /, the membership application form
config.add_route('join', '/')


+ 1
- 1
c3smembership/accountants_views.py Просмотреть файл

@@ -383,7 +383,7 @@ def mail_payment_confirmation(request):
if 'detail' in request.referrer:
return HTTPFound(request.route_url(
'detail',
memberid=request.matchdict['memberid']))
memberid=member.id))
else:
return get_dashboard_redirect(request, member.id)



+ 6
- 3
c3smembership/business/membership_application.py Просмотреть файл

@@ -13,13 +13,16 @@ from c3smembership.mail_utils import (
from pyramid_mailer.message import Message


_make_signature_confirmation_email = make_signature_confirmation_email
_send_message = send_message


class MembershipApplication(object):
"""
Provides functionality for the membership application process.
"""

datetime = datetime
# pylint: disable=invalid-name

def __init__(self, member_repository):
"""
@@ -136,7 +139,7 @@ class MembershipApplication(object):
# - Remove dependency to pyramid_mail and move to separate service.
member = self.member_repository.get_member_by_id(member_id)
# pylint: disable=too-many-function-args
email_subject, email_body = make_signature_confirmation_email(member)
email_subject, email_body = _make_signature_confirmation_email(member)
message = Message(
subject=email_subject,
sender='yes@c3s.cc',
@@ -144,6 +147,6 @@ class MembershipApplication(object):
body=email_body
)
# pylint: disable=too-many-function-args
send_message(request, message)
_send_message(request, message)
member.signature_confirmed = True
member.signature_confirmed_date = self.datetime.now()

+ 18
- 8
c3smembership/business/tests/test_membership_application.py Просмотреть файл

@@ -8,6 +8,8 @@ from datetime import (

from unittest import TestCase

import c3smembership.business.membership_application as \
membership_application_package
from c3smembership.business.membership_application import (
MembershipApplication,
)
@@ -141,10 +143,16 @@ class MemberApplicationTest(TestCase):
signature_confirmed=None)
member_repository_mock.get_member_by_id.side_effect = [member_mock]
membership_application = MembershipApplication(member_repository_mock)
membership_application.make_signature_confirmation_email = mock.Mock()
membership_application.make_signature_confirmation_email.side_effect = \

make_signature_confirmation_email = mock.Mock()
make_signature_confirmation_email.side_effect = \
[('email subject', 'email body')]
membership_application.send_message = mock.Mock()
send_message = mock.Mock()

membership_application_package._make_signature_confirmation_email = \
make_signature_confirmation_email
membership_application_package._send_message = send_message

membership_application.datetime = mock.Mock()
membership_application.datetime.now.side_effect = ['now result']

@@ -152,21 +160,23 @@ class MemberApplicationTest(TestCase):
'member id',
'pyramid request')

make_signature_confirmation_email.assert_called_with(member_mock)
member_repository_mock.get_member_by_id.assert_called_with('member id')
self.assertEqual(
membership_application.send_message.call_args[0][0],
send_message.call_args[0][0],
'pyramid request')
self.assertEqual(
membership_application.send_message.call_args[0][1].subject,
send_message.call_args[0][1].subject,
'email subject')
self.assertEqual(
membership_application.send_message.call_args[0][1].sender,
send_message.call_args[0][1].sender,
'yes@c3s.cc')
self.assertEqual(
membership_application.send_message.call_args[0][1].recipients,
send_message.call_args[0][1].recipients,
['jane@example.com'])
self.assertEqual(
membership_application.send_message.call_args[0][1].body,
send_message.call_args[0][1].body,
'email body')

self.assertTrue(member_mock.signature_confirmed)
self.assertEqual(member_mock.signature_confirmed_date, 'now result')

+ 14
- 13
c3smembership/invite_members.py Просмотреть файл

@@ -16,6 +16,7 @@ It was then reused for:
- BarCamp and General Assembly 2015
- BarCamp and General Assembly 2016
- BarCamp and General Assembly 2017
- BarCamp and General Assembly 2018

How it works
------------
@@ -43,7 +44,7 @@ from pyramid.httpexceptions import HTTPFound
from pyramid.view import view_config
from pyramid_mailer.message import Message

from c3smembership.invite_members_texts import make_bcga17_invitation_email
from c3smembership.invite_members_texts import make_bcga18_invitation_email
from c3smembership.mail_utils import send_message
from c3smembership.membership_certificate import make_random_token
from c3smembership.models import C3sMember
@@ -82,16 +83,16 @@ def invite_member_bcgv(request):
return get_memberhip_listing_redirect(request, member_id)

# prepare a random token iff none is set
if member.email_invite_token_bcgv17 is None:
member.email_invite_token_bcgv17 = make_random_token()
if member.email_invite_token_bcgv18 is None:
member.email_invite_token_bcgv18 = make_random_token()
url = URL_PATTERN.format(
ticketing_url=request.registry.settings['ticketing.url'],
token=member.email_invite_token_bcgv17,
token=member.email_invite_token_bcgv18,
email=member.email)

LOG.info("mailing event invitation to to member id %s", member.id)

email_subject, email_body = make_bcga17_invitation_email(member, url)
email_subject, email_body = make_bcga18_invitation_email(member, url)
message = Message(
subject=email_subject,
sender='yes@c3s.cc',
@@ -104,8 +105,8 @@ def invite_member_bcgv(request):
send_message(request, message)

# member._token = _looong_token
member.email_invite_flag_bcgv17 = True
member.email_invite_date_bcgv17 = datetime.now()
member.email_invite_flag_bcgv18 = True
member.email_invite_date_bcgv18 = datetime.now()
return get_memberhip_listing_redirect(request, member.id)


@@ -146,16 +147,16 @@ def batch_invite(request):

for member in invitees:
# prepare a random token iff none is set
if member.email_invite_token_bcgv17 is None:
member.email_invite_token_bcgv17 = make_random_token()
if member.email_invite_token_bcgv18 is None:
member.email_invite_token_bcgv18 = make_random_token()
url = URL_PATTERN.format(
ticketing_url=request.registry.settings['ticketing.url'],
token=member.email_invite_token_bcgv17,
token=member.email_invite_token_bcgv18,
email=member.email)

LOG.info("mailing event invitation to to member id %s", member.id)

email_subject, email_body = make_bcga17_invitation_email(member, url)
email_subject, email_body = make_bcga18_invitation_email(member, url)
message = Message(
subject=email_subject,
sender='yes@c3s.cc',
@@ -167,8 +168,8 @@ def batch_invite(request):
)
send_message(request, message)

member.email_invite_flag_bcgv17 = True
member.email_invite_date_bcgv17 = datetime.now()
member.email_invite_flag_bcgv18 = True
member.email_invite_date_bcgv18 = datetime.now()
num_sent += 1
ids_sent.append(member.id)



+ 5
- 5
c3smembership/invite_members_texts.py Просмотреть файл

@@ -11,7 +11,7 @@ from c3smembership.mail_utils import (
DEBUG = False


def make_bcga17_invitation_email(member, url):
def make_bcga18_invitation_email(member, url):
"""
Create email subject and body for an invitation email for members.

@@ -23,18 +23,18 @@ def make_bcga17_invitation_email(member, url):
print(u"the member.locale: {}".format(member.locale))
print(u"the url: {}".format(url))
print(u"the subject: {}".format(
get_template_text('bcga2017_invite_subject', member.locale)))
get_template_text('bcga2018_invite_subject', member.locale)))
print(u"the salutation: {}".format(get_salutation(member)))
print(u"the footer: {}".format(get_email_footer(member.locale)))
print(u"the body: {}".format(
get_template_text('bcga2017_invite_body', member.locale).format(
get_template_text('bcga2018_invite_body', member.locale).format(
salutation=get_salutation(member),
invitation_url=url,
footer=get_email_footer(member.locale))))
return (
get_template_text('bcga2017_invite_subject', member.locale).rstrip(
get_template_text('bcga2018_invite_subject', member.locale).rstrip(
'\n'), # remove newline (\n) from mail subject
get_template_text('bcga2017_invite_body', member.locale).format(
get_template_text('bcga2018_invite_body', member.locale).format(
salutation=get_salutation(member),
invitation_url=url,
footer=get_email_footer(member.locale)


+ 69
- 74
c3smembership/locale/en/LC_MESSAGES/c3smembership.po Просмотреть файл

@@ -3,21 +3,19 @@
# This file is distributed under the same license as the c3smembership project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2018.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: c3smembership 1.20.4\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2018-04-17 16:37+0200\n"
"PO-Revision-Date: 2018-04-17 16:41+0200\n"
"Language-Team: \n"
"POT-Creation-Date: 2018-04-01 16:28+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.1.1\n"
"X-Generator: Poedit 1.8.11\n"
"Last-Translator: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Language: en\n"

#: c3smembership/accountants_views.py:79 c3smembership/administration.py:542
#: c3smembership/annual_accounting.py:71 c3smembership/edit_member.py:498
@@ -27,33 +25,32 @@ msgstr ""

#: c3smembership/accountants_views.py:80 c3smembership/administration.py:543
#: c3smembership/edit_member.py:499 c3smembership/new_member.py:214
#: c3smembership/views/afm.py:349
#: c3smembership/views/afm.py:351
msgid "Reset"
msgstr ""

#: c3smembership/accountants_views.py:91 c3smembership/administration.py:566
#: c3smembership/annual_accounting.py:89 c3smembership/edit_member.py:529
#: c3smembership/new_member.py:252 c3smembership/shares_views.py:105
#: c3smembership/views/afm.py:372
#: c3smembership/views/afm.py:374
msgid "Please note: There were errors, please check the form below."
msgstr ""

#: c3smembership/administration.py:485 c3smembership/edit_member.py:322
#: c3smembership/new_member.py:118 c3smembership/views/afm.py:186
#: c3smembership/new_member.py:118 c3smembership/views/afm.py:188
msgid "Yes"
msgstr ""

#: c3smembership/administration.py:486 c3smembership/edit_member.py:323
#: c3smembership/new_member.py:119 c3smembership/views/afm.py:187
#: c3smembership/new_member.py:119 c3smembership/views/afm.py:189
msgid "No"
msgstr ""

#: c3smembership/administration.py:489 c3smembership/views/afm.py:190
msgid ""
"I want to become a ... (choose membership type, see C3S SCE statute sec. 4)"
msgstr "I want to become a ... (choose membership type, see pEp SCE statute)"
#: c3smembership/administration.py:489 c3smembership/views/afm.py:192
msgid "I want to become a ... (choose membership type, see C3S SCE statute sec. 4)"
msgstr ""

#: c3smembership/administration.py:491 c3smembership/views/afm.py:192
#: c3smembership/administration.py:491 c3smembership/views/afm.py:194
msgid "choose the type of membership."
msgstr ""

@@ -64,14 +61,14 @@ msgid ""
"lyricists and remixers. They get a vote."
msgstr ""

#: c3smembership/administration.py:500 c3smembership/views/afm.py:204
#: c3smembership/administration.py:500 c3smembership/views/afm.py:206
msgid ""
"INVESTING member. Investing members can be natural or legal entities or "
"private companies that do not register works with C3S. They do not get a "
"vote, but may counsel."
msgstr ""

#: c3smembership/administration.py:510 c3smembership/views/afm.py:214
#: c3smembership/administration.py:510 c3smembership/views/afm.py:216
msgid "Currently, I am a member of (at least) one other collecting society."
msgstr ""

@@ -150,27 +147,27 @@ msgid "Addess Line 1"
msgstr ""

#: c3smembership/edit_member.py:175 c3smembership/templates/success.pt:65
#: c3smembership/views/afm.py:137
#: c3smembership/views/afm.py:128
msgid "Address Line 2"
msgstr ""

#: c3smembership/edit_member.py:179 c3smembership/templates/success.pt:66
#: c3smembership/views/afm.py:141
#: c3smembership/views/afm.py:132
msgid "Postal Code"
msgstr ""

#: c3smembership/edit_member.py:184 c3smembership/templates/success.pt:67
#: c3smembership/views/afm.py:146
#: c3smembership/views/afm.py:137
msgid "City"
msgstr ""

#: c3smembership/edit_member.py:189 c3smembership/templates/success.pt:68
#: c3smembership/views/afm.py:151
#: c3smembership/views/afm.py:142
msgid "Country"
msgstr ""

#: c3smembership/edit_member.py:197 c3smembership/templates/success.pt:69
#: c3smembership/views/afm.py:159
#: c3smembership/views/afm.py:150
msgid "Date of Birth"
msgstr ""

@@ -297,7 +294,7 @@ msgid "Resignations are only allowed to the 31st of December of a year."
msgstr ""

#: c3smembership/edit_member.py:428 c3smembership/new_member.py:197
#: c3smembership/templates/success.pt:59 c3smembership/views/afm.py:330
#: c3smembership/templates/success.pt:59 c3smembership/views/afm.py:332
msgid "Personal Data"
msgstr ""

@@ -322,7 +319,7 @@ msgid "E-Mail"
msgstr ""

#: c3smembership/new_member.py:205 c3smembership/templates/success.pt:79
#: c3smembership/views/afm.py:336
#: c3smembership/views/afm.py:338
msgid "Shares"
msgstr ""

@@ -499,8 +496,7 @@ msgstr ""

#. Default: Membership Application for Cultural Commons Collecting Society (C3S
#. SCE)
#: c3smembership/templates/base.pt:7
#: c3smembership/templates/base_bootstrap.pt:7
#: c3smembership/templates/base.pt:7 c3smembership/templates/base_bootstrap.pt:7
msgid "membership-form-title"
msgstr ""

@@ -566,14 +562,13 @@ msgid "check-email-paragraph-check-email-hint"
msgstr ""

#. Default: C3S: confirm your email address and load your PDF
#: c3smembership/templates/check-mail.pt:66 c3smembership/views/afm.py:541
#: c3smembership/templates/check-mail.pt:66 c3smembership/views/afm.py:548
msgid "check-email-paragraph-check-email-subject"
msgstr ""

#. Default: Join the Cultural Commons Collecting Society (C3S)
#. Default: Join Cultural Commons Collecting Society (C3S)
#: c3smembership/templates/join.pt:24
#: c3smembership/templates/verify_password.pt:9
#: c3smembership/templates/join.pt:24 c3smembership/templates/verify_password.pt:9
msgid "join-form-title"
msgstr ""

@@ -673,16 +668,16 @@ msgstr ""
#: c3smembership/templates/mtype-form.pt:35
msgid ""
"as mentioned in our mail we would like to give you the opportunity to add "
"some information to your C3S account. We need your help so we can tell if "
"you are eligible to vote or not."
"some information to your C3S account. We need your help so we can tell if you"
" are eligible to vote or not."
msgstr ""

#: c3smembership/templates/mtype-form.pt:38
msgid ""
"In case you are unsure, please take a look at our FAQ (see link below) or "
"just send a mail to info@c3s.cc. However, for those of you who are curious "
"to know more about the details, there’s a link to our statutes (see below). "
"Be prepared to enjoy some legal talk..."
"just send a mail to info@c3s.cc. However, for those of you who are curious to"
" know more about the details, there’s a link to our statutes (see below). Be "
"prepared to enjoy some legal talk..."
msgstr ""

#: c3smembership/templates/mtype-form.pt:40
@@ -801,7 +796,7 @@ msgstr ""
msgid "success-headline-data-received"
msgstr ""

#: c3smembership/templates/success.pt:64 c3smembership/views/afm.py:132
#: c3smembership/templates/success.pt:64 c3smembership/views/afm.py:123
msgid "Address Line 1"
msgstr ""

@@ -932,124 +927,124 @@ msgstr ""
msgid "load-pdf-notification"
msgstr ""

#: c3smembership/views/afm.py:125
msgid "Password (to protect access to your data)"
#: c3smembership/views/afm.py:161
msgid "Sorry, but we do not believe that the birthday you entered is correct."
msgstr ""

#: c3smembership/views/afm.py:126
#: c3smembership/views/afm.py:164
msgid ""
"We need a password to protect your data. After verifying your email you will "
"have to enter it."
"Unfortunately, the membership application of an underaged person is currently"
" not possible via our web form. Please send an email to office@c3s.cc."
msgstr ""

#: c3smembership/views/afm.py:170
msgid "Sorry, we do not believe that you are that old"
#: c3smembership/views/afm.py:175
msgid "Password (to protect access to your data)"
msgstr ""

#: c3smembership/views/afm.py:171
#: c3smembership/views/afm.py:176
msgid ""
"Unfortunately, the membership application of an underaged person is "
"currently not possible via our web form. Please send an email to office@c3s."
"cc."
"We need a password to protect your data. After verifying your email you will "
"have to enter it."
msgstr ""

#: c3smembership/views/afm.py:197
#: c3smembership/views/afm.py:199
msgid ""
"FULL member. Full members have to be natural persons who register at least "
"three works they created themselves with C3S. This applies to composers, "
"lyricists and remixers. They get a vote."
msgstr ""

#: c3smembership/views/afm.py:224
msgid ""
"If so, which one(s)? Please separate multiple collecting societies by comma."
#: c3smembership/views/afm.py:226
msgid "If so, which one(s)? Please separate multiple collecting societies by comma."
msgstr ""

#: c3smembership/views/afm.py:226
#: c3smembership/views/afm.py:228
msgid ""
"Please tell us which collecting societies you are a member of. If more than "
"one, please separate them by comma."
msgstr ""

#: c3smembership/views/afm.py:247
#: c3smembership/views/afm.py:249
msgid ""
"I want to buy the following number of Shares (50€ each, up to 3000€, see C3S "
"statute sec. 5)"
msgstr ""

#: c3smembership/views/afm.py:250
#: c3smembership/views/afm.py:252
msgid "You can choose any amount of shares between 1 and 60."
msgstr ""

#: c3smembership/views/afm.py:258
#: c3smembership/views/afm.py:260
msgid "You need at least one share of 50 €."
msgstr ""

#: c3smembership/views/afm.py:259
#: c3smembership/views/afm.py:261
msgid "You may choose 60 shares at most (3000 €)."
msgstr ""

#: c3smembership/views/afm.py:280
#: c3smembership/views/afm.py:282
msgid ""
"I acknowledge that the statutes and membership dues regulations determine "
"periodic contributions for full members."
msgstr ""

#: c3smembership/views/afm.py:284
#: c3smembership/views/afm.py:286
msgid ""
"An electronic copy of the statute of the C3S SCE has been made available to "
"me (see link below)."
msgstr ""

#: c3smembership/views/afm.py:287
#: c3smembership/views/afm.py:289
msgid "You must confirm to have access to the statute."
msgstr ""

#: c3smembership/views/afm.py:307
#: c3smembership/views/afm.py:309
msgid ""
"An electronic copy of the temporary membership dues regulations of the C3S "
"SCE has been made available to me (see link below)."
msgstr ""

#: c3smembership/views/afm.py:311
msgid ""
"You must confirm to have access to the temporary membership dues regulations."
#: c3smembership/views/afm.py:313
msgid "You must confirm to have access to the temporary membership dues regulations."
msgstr ""

#: c3smembership/views/afm.py:333
#: c3smembership/views/afm.py:335
msgid "Membership Data"
msgstr ""

#: c3smembership/views/afm.py:339
#: c3smembership/views/afm.py:341
msgid "Acknowledgement"
msgstr ""

#: c3smembership/views/afm.py:348
#: c3smembership/views/afm.py:350
msgid "Next"
msgstr ""

#: c3smembership/views/afm.py:384
msgid "Please re-enter your password."
#: c3smembership/views/afm.py:386
msgid ""
"Please re-enter your password. For security reasons your password is not "
"cached and therefore needs to be re-entered in case of validation issues."
msgstr ""

#: c3smembership/views/afm.py:631
#: c3smembership/views/afm.py:638
msgid ""
"Not found. Check verification URL. If all seems right, please use the form "
"again."
msgstr ""

#: c3smembership/views/afm.py:646
#: c3smembership/views/afm.py:653
msgid "Wrong Password!"
msgstr ""

#: c3smembership/views/afm.py:698
#: c3smembership/views/afm.py:705
msgid "Success. Load your PDF!"
msgstr ""

#: c3smembership/views/afm.py:703
#: c3smembership/views/afm.py:710
msgid "Please enter your password."
msgstr ""

#: c3smembership/views/afm.py:748
#: c3smembership/views/afm.py:755
msgid "[yes][ALERT] check the logs!"
msgstr ""


+ 13
- 1
c3smembership/membership_list.py Просмотреть файл

@@ -407,9 +407,21 @@ def make_member_view(request):
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,
member.num_shares,
num_shares,
member.membership_date)
share_acquisition = request.registry.share_acquisition
share_acquisition.set_signature_reception(


+ 43
- 8
c3smembership/models.py Просмотреть файл

@@ -55,6 +55,10 @@ from c3smembership.data.model.base import (
DBSession,
)


# pylint: disable=no-member


CRYPT = cryptacular.bcrypt.BCRYPTPasswordManager()


@@ -143,6 +147,7 @@ class Group(Base):


# table for relation between staffers and groups
# pylint: disable=invalid-name
staff_groups = Table(
'staff_groups', Base.metadata,
Column(
@@ -336,6 +341,7 @@ class Shares(Base):


# table for relation between membership and shares
# pylint: disable=invalid-name
members_shares = Table(
'members_shares', Base.metadata,
Column(
@@ -592,6 +598,9 @@ class C3sMember(Base):
email_invite_flag_bcgv17 = Column(Boolean, default=False)
email_invite_date_bcgv17 = Column(DateTime(), default=datetime(1970, 1, 1))
email_invite_token_bcgv17 = Column(Unicode(255))
email_invite_flag_bcgv18 = Column(Boolean, default=False)
email_invite_date_bcgv18 = Column(DateTime(), default=datetime(1970, 1, 1))
email_invite_token_bcgv18 = Column(Unicode(255))
# legal entities
is_legalentity = Column(Boolean, default=False)
court_of_law = Column(Unicode(255))
@@ -867,7 +876,7 @@ class C3sMember(Base):
object: C3sMember object
"""
return DBSession.query(cls).filter(
cls.email_invite_token_bcgv17 == token).first()
cls.email_invite_token_bcgv18 == token).first()

@classmethod
def check_for_existing_confirm_code(cls, email_confirm_code):
@@ -876,7 +885,7 @@ class C3sMember(Base):
"""
check = DBSession.query(cls).filter(
cls.email_confirm_code == email_confirm_code).first()
return (check is not None)
return check is not None

@classmethod
def get_by_id(cls, member_id):
@@ -935,10 +944,10 @@ class C3sMember(Base):
and_(
cls.is_member_filter(),
or_(
(cls.email_invite_flag_bcgv17 == 0),
(cls.email_invite_flag_bcgv17 == ''),
(cls.email_invite_flag_bcgv18 == 0),
(cls.email_invite_flag_bcgv18 == ''),
# pylint: disable=singleton-comparison
(cls.email_invite_flag_bcgv17 == None),
(cls.email_invite_flag_bcgv18 == None),
)
)
).slice(0, num).all()
@@ -1005,6 +1014,10 @@ class C3sMember(Base):
Returns:
a list of *n* member objects
"""

# In SqlAlchemy the True comparison must be done as "a == True" and not
# in the python default way "a is True". Therefore:
# pylint: disable=singleton-comparison
return DBSession.query(cls).filter(
and_(
cls.membership_accepted == True,
@@ -1353,6 +1366,10 @@ class C3sMember(Base):

@classmethod
def nonmember_listing_count(cls):
"""
Gets the number of applicants which have not been accepted as members
yet.
"""
query = DBSession.query(cls).filter(
or_(
cls.membership_accepted == 0,
@@ -1367,6 +1384,10 @@ class C3sMember(Base):
# count for statistics
@classmethod
def afm_num_shares_unpaid(cls):
"""
Gets the number of shares for which membership applicant has not yet
paid the price.
"""
rows = DBSession.query(cls).all()
num_shares_unpaid = 0
for row in rows:
@@ -1376,6 +1397,10 @@ class C3sMember(Base):

@classmethod
def afm_num_shares_paid(cls):
"""
Gets the number of shares for which membership applicant has already
paid the price.
"""
rows = DBSession.query(cls).all()
num_shares_paid = 0
for row in rows:
@@ -1386,6 +1411,12 @@ class C3sMember(Base):
# workflow: need approval by the board
@classmethod
def afms_ready_for_approval(cls):
"""
Gets the list of membership applicants who can be granted membership by
the board of directors because they have fulfilled their duty of
sending in a signed membership application as well as paying the
share's price.
"""
return DBSession.query(cls).filter(
and_(
(cls.membership_accepted == 0),
@@ -1653,6 +1684,10 @@ class C3sMember(Base):
self.dues17_amount_reduced = Decimal('NaN')

def get_url_safe_name(self):
"""
Gets a url-safe version of the member's name in which all characters
except 0-9, a-z and A-Z are replaced by a dash.
"""
return re.sub( # # replace characters
'[^0-9a-zA-Z]', # other than these
'-', # with a -
@@ -1873,7 +1908,7 @@ class Dues15Invoice(Base):
"""
check = DBSession.query(cls).filter(
cls.token == dues_token).first()
return (check is not None)
return check is not None

@classmethod
def get_monthly_stats(cls):
@@ -2073,7 +2108,7 @@ class Dues16Invoice(Base):
"""
check = DBSession.query(cls).filter(
cls.token == dues_token).first()
return (check is not None)
return check is not None

@classmethod
def get_monthly_stats(cls):
@@ -2273,7 +2308,7 @@ class Dues17Invoice(Base):
"""
check = DBSession.query(cls).filter(
cls.token == dues_token).first()
return (check is not None)
return check is not None

@classmethod
def get_monthly_stats(cls):


+ 7
- 7
c3smembership/presentation/templates/memberships_list_backend.pt Просмотреть файл

@@ -96,16 +96,16 @@
title="Sort by id: descending"
class="glyphicon glyphicon-chevron-down"></a>
</th>
<!--!
<th>
bc &amp; ga<br />
invitation
</th>
-->
<!--!
<th>
dues17<br />
invoice
</th>
-->
<th>
certificate
</th>
@@ -158,20 +158,19 @@
</a>
</div>
</td>
<!--!
<td>
<div tal:condition="member.is_member()" tal:omit-tag="">
<a tal:condition="member.email_invite_flag_bcgv17 is not True"
<a tal:condition="member.email_invite_flag_bcgv18 is not True"
href="${request.route_url('invite_member', m_id=member.id)}"
title="invitation not sent yet. Click to send!"
class="btn btn-danger"></a>
<a tal:condition="member.email_invite_flag_bcgv17 is True"
<a tal:condition="member.email_invite_flag_bcgv18 is True"
href="${request.route_url('invite_member', m_id=member.id)}"
title="gesendet ${member.email_invite_date_bcgv17.strftime('am %d.%m.%Y um %H:%M')}"
title="gesendet ${member.email_invite_date_bcgv18.strftime('am %d.%m.%Y um %H:%M')}"
class="btn btn-success"></a>
</div>
</td>
-->
<!--!
<td>
<div tal:omit-tag="" tal:condition="not member.membership_date > date(2017,12,31) and (member.membership_loss_date is None or member.membership_loss_date >= date(2017,1,1))">
<a tal:condition="member.dues17_invoice is False"
@@ -184,6 +183,7 @@
class="btn btn-success"></a>
</div>
</td>
-->
<td>
<div tal:condition="member.is_member()" tal:omit-tag="">
<a tal:condition="not member.certificate_email"


+ 3
- 1
c3smembership/presentation/templates/toolbox.pt Просмотреть файл

@@ -88,6 +88,7 @@
</form>
</p>
-->
<!--!
<h3>Mail Invoices for Membership Dues 2017</h3>
<p>
<a href="${request.route_url('send_dues17_invoice_batch', number=5)}"
@@ -100,6 +101,7 @@
<input type='submit' name='submit'></input>
</form>
</p>
-->

<h2>Search</h2>
<p>
@@ -149,7 +151,7 @@
</div>
</p>

<h4>Mail Invitations for GA &amp; BC 2017</h4>
<h4>Mail Invitations for GA &amp; BC 2018</h4>
<p>
<a href="${request.route_url('invite_batch', number=5)}"
title="Note: change number in URL as appropriate! Default is 5."


+ 192
- 0
c3smembership/templates/mail/bcga2018_invite_body_de.txt Просмотреть файл

@@ -0,0 +1,192 @@

Hallo {salutation},

der Verwaltungsrat der

Cultural Commons Collecting Society SCE
mit beschränkter Haftung
- C3S SCE -
Rochusstr. 44
40479 Düsseldorf

lädt Dich ein

* zur 5. ordentlichen Generalversammlung nach § 13 der Satzung der
C3S SCE [1] am 03.06.2018 und
* zum C3S-Barcamp 2018 am Vortag der Generalversammlung

Bitte lies den gesamten Einladungstext, er enthält wichtige Hinweise.

*************************** W I C H T I G ***************************
Dies ist Dein individueller Link zur Anmeldung:

{invitation_url}

Bitte teile uns dort frühzeitig mit, ob Du teilnimmst oder nicht oder
ob du dich vertreten lassen möchtest.
Auch Absagen sind erwünscht. Auf der verlinkten Seite kannst Du separat
die Teilnahme für die Generalversammlung und das Barcamp bestätigen.
***************************

Die Generalversammlung
======================

Die Generalversammlung 2018 der C3S SCE findet in Düsseldorf im
selben Gebäude statt, in dem wir auch unser Büro haben:

03.06.2018: 5. ordentliche Generalversammlung der C3S SCE
Rochusstr. 44
40479 Düsseldorf [2]
13 - 17 Uhr
Akkreditierung ab 12 Uhr
Eintritt frei

Bitte komme zeitig, um Verzögerungen zu vermeiden, da die Akkreditierung
Zeit benötigt. Die Teilnahme an der GV ist selbstverständlich kostenlos.

Das Barcamp
===========

02.06.2018: Barcamp [3]
Rochusstr. 44
40479 Düsseldorf [2]
13 - 19 Uhr
Eintritt frei

Wir gehen davon aus, dass es zu einigen Themen mehr Informations-
und Diskussionsbedarf gibt, als wir in der Generalversammlung selbst
unterbringen können. Um die Generalversammlung vorzubereiten und
Diskussionen vorzuverlagern, bieten wir am Vortag ein Barcamp [3] an,
das einen halben Tag lang in mehreren parallelen Slots Zeit für
Diskussion und Austausch bietet. Ziel ist es am Ende des Barcamps kurze
und verständliche Zusammenfassungen des Besprochenen zu erstellen.

Dies ist der Link zu unserer wiki-Seite, auf der Du die Themen für das
Barcamp ergänzen und einsehen kannst:

https://wiki.c3s.cc/index.php/Themenvorschl%C3%A4ge_BarCamp2018

Zugangsdaten:
Name: schwarm
Kennwort: letmein

Nach einem anstrengend-produktiven Barcamp braucht man auch wieder neue
Energiezufuhr. Das wollen wir gerne in der gemeinsamen Runde
bewerkstelligen. Deshalb würde es uns sehr freuen, wenn Du nach dem
Barcamp mit uns gemeinsam Essen gehst -- wahrscheinlich in einem
italienischen Restaurant. In jedem Fall wird es aber eine Auswahl
an vegetarischen Speisen geben. Bitte vermerke das in der Anmeldung,
damit wir entsprechend reservieren können.


Agenda der Generalversammlung 2018 der C3S SCE
==============================================

Begrüßung der Anwesenden

# 1 Wahl der Versammlungsleitung und de(s/r) Protokollführer(s/in)

# 2 Genehmigung der Tagesordnung

# 3 Wiederkehrende Tagesordnungspunkte

## 3.1 Entgegennahme der Tätigkeitsberichte der geschäftsführenden
Direktoren und des Verwaltungsrates mit anschließender Aussprache
## 3.2 Feststellung des Jahresabschlusses
## 3.3 Entscheidung über die Verwendung des Jahresüberschusses und die
Verrechnung des Jahresfehlbetrages
## 3.4 Entlastung der geschäftsführenden Direktoren und des
Verwaltungsrates

# 4 Nachwahl von Mitgliedern des Verwaltungsrates

# 5 Bericht zum Stand des Zulassungsverfahrens beim Deutschen Patent-
und Markenamt (DPMA)

# 6 Bericht vom C3S-Barcamp 2018

# 7 Berichte aus den Beratungskommissionen

## 7.1 Kommission Tarife
## 7.2 Kommission Verteilung und Beitragsordnung
## 7.3 Kommission Wahrnehmungsverträge

# 8 Diskussion zu und ggfs. Beschlussfassung über Festlegung einer
vorläufigen Beitragsordnung
https://url.c3s.cc/ga5bv8de

# 9 Diskussionen / Sonstiges

Beschlussanträge und Anträge zur Änderung der Tagesordnung kannst Du bis
zum 23.05.2018 (Ausschlussfrist 24 Uhr MESZ/CEST, d.h. UTC +2) in Textform
unter agenda@c3s.cc einreichen.


Organisatorisches
=================

Teilnahme
---------
Teilnahmeberechtigt an der Generalversammlung sind nur Mitglieder der
C3S SCE oder Bevollmächtigte nicht anwesender Mitglieder.

Stellvertretende Bevollmächtigte
--------------------------------
Solltest Du nicht teilnehmen können und stimmberechtigt sein,
also nutzendes Mitglied, kannst Du eine Vollmacht erteilen. Weitere
Informationen hierzu findest du bei der Anmeldung.

Audio-Protokoll
---------------
Während der Generalversammlung wird ein Audio-Mitschnitt aufgezeichnet, um
ein fehlerfreies Protokoll zu gewährleisten. Der Mitschnitt wird nicht
veröffentlicht, aber intern als Anhang zum Protokoll archiviert.
Wer nicht möchte, dass sein Redebeitrag aufgezeichnet wird, kann dem vor
Beginn seines/ihres Beitrags widersprechen.


Warum ist Deine Teilnahme wichtig?
==================================

Auf dem Barcamp kannst Du ausführlich zu den aktuellen Themen mitdiskutieren
und Dich mit dem Kernteam der C3S austauschen. Die Generalversammlung ist das
Organ, das die grundlegenden Entscheidungen der C3S SCE trifft.
(Gemeinsam mit den anderen Mitgliedern bist Du die Generalversammlung.)

Das war's! Versorge uns mit Themenvorschlägen, plane Deine Fahrt - dann sehen
wir uns Anfang Juni in Düsseldorf! Bei Fragen kannst Du Dich, wie immer, an
info@c3s.cc wenden.

Wir freuen uns auf Dich & Deine Ideen!

Der Verwaltungsrat der C3S SCE
Veit Winkler - Vorsitzender


====================
Der Verwaltungsrat der C3S SCE setzt sich zusammen aus:

Vorsitz:
Veit Winkler, Vorsitzender des VR
Danny Bruder, stellv. Vorsitzender des VR

Weitere Mitglieder:
* Johanna Breuckmann
* m.eik michalke
* Thomas Mielke
* Christoph Scheid
* Elmar Schuck
* Holger Schwetter

====================

Links:

[1] Satzung der C3S SCE: https://url.c3s.cc/satzung
[2] Karte Veranstaltungsort:
https://www.openstreetmap.org/node/2679106873#map=19/51.23274/6.78834
[3] Was ist ein Barcamp? https://url.c3s.cc/bcerklaerung

{footer}



+ 191
- 0
c3smembership/templates/mail/bcga2018_invite_body_en.txt Просмотреть файл

@@ -0,0 +1,191 @@

Hello {salutation},

the administrative board of the

CulturalCommons Collecting Society SCE
mit beschränkter Haftung
- C3S SCE -
Rochusstr. 44
40479 Düsseldorf

invites you

* to the 5th statutory general assembly, according to § 13 of the
articles of association of the C3S SCE [1] on 3rd June, 2018 and
* to the C3S barcamp 2018 on the day preceding the general assembly.

Please read the whole text of the invitation. It contains important
information.

*************************** Important ***************************
This is your individual registration link:

{invitation_url}

Please use it to inform us early on about whether or not you would
like to participate or if you want to be represented by somebody.
Cancellations are requested as well. You can separately confirm your
participation in the general assembly and the barcamp.
***************************


The general assembly
====================

The general assembly 2018 of the C3S SCE will be held in Düsseldorf
in the building where we also have our office.

June 3rd, 2018: 5th statutory general assembly of the C3S SCE
Rochusstr. 44
40479 Düsseldorf [2]
1.00 pm - 5.00 pm
Accreditation from 12.00 pm
Admission free

Please be punctual in order to avoid delays, because the accreditation
will take some time. Of course, participation is free.

The Barcamp
===========

June 2nd, 2018: Barcamp [3]
Rochusstr. 44
40479 Düsseldorf [2]
1.00 pm - 7.00 pm
Admission free


Again, we are of the opinion that on some topics there will be more
need for information and discussion than we can deal with in the general
assembly. In order to prepare the general assembly and to move up
debates, we are offering a barcamp [3], where half a day is devoted to
discussions and idea exchange in parallel time slots.
The goal is to produce short and comprehensible summaries of what
has been discussed at the barcamp.

On the following wiki page you can add to and view the topics for the
barcamp: https://wiki.c3s.cc/index.php/Themenvorschl%C3%A4ge_BarCamp2018

Login details:
Name: schwarm
Password: letmein

At the end of an exhausting and productive barcamp, everyone needs new
energy. Therefore we would be happy to have dinner with you after the
barcamp – likely at an Italian restaurant. In any case there will be
vegetarian dishes as well. Please note on registration wether you
want to attend so that we can make reservations accordingly.


Agenda of the general assembly 2018 of the C3S SCE
==================================================

Welcoming address

# 1 Appointment of the chairperson of the assembly
and the minute taker

# 2 Approval of the agenda

# 3 Recurring items on the agenda

## 3.1 Acceptance of the progress report of the executive directors and
the administrative board, followed by debate
## 3.2 Approval of the annual financial statement
## 3.3 Resolution on the appropriation
of the net income and the offsetting ofthe annual net loss
## 3.4 Discharge of the executive directors and the members of the
adminstrative board

# 4 By-election of members of the administrative board

# 5 Status report regarding the approval procedure by the German Patent
and Trade Mark Office

# 6 Report from the C3S barcamp 2018

# 7 Reports from the consulting committees

## 7.1 Tariffs commission
## 7.2 Distribution and contributing rules commission
## 7.3 Collection agreements commission

# 8 Discussion and, if applicable, resolution on the determination of
temporary contribution rules
https://url.c3s.cc/ga5bv8en

# 9 Discussion / Other issues

You may contribute written proposals for resolutions and for alterations
of the agenda until 23rd Mai, 2018 (midnight MESZ/CEST, i.e. UTC +2)
to agenda@c3s.cc.


Organizational issues
=====================

Participation
-------------
Participation in the general assembly is limited to members of the C3S
SCE or authorized representatives of absent members.

Authorized representatives
--------------------------------
If you are an active member and therefore entitled to vote, but unable
to participate, you may grant a power of attorney. Further information
on that will be included in the registration e-mail.

Audio recording
---------------
During the general assembly, an audio recording will be made in order to
ensure error-free minutes. The recording will not be published but
archived internally as an appendix to the minutes.

Those who do not wish their speech contributions to be recorded, may
veto before commencing to speak.


Why is it important that you participate?
=========================================

At the barcamp, you will be able to make detailed contributions to the
discussions of the current topics and talk to the core team of the C3S.
The general assembly is the body that takes the fundamental decisions
for the C3S. (You are the general assembly, together with the other
members.)

That's all! Please let us know your proposals for topics, plan your
trip - and we'll meet in Düsseldorf in June! If you have questions,
you can get in touch, as always, via info@c3s.cc.

We look forward to you and your ideas!

For and on behalf of the administrative board of the C3S SCE
Veit Winkler - Chairperson

===============
The administrative board of the C3S SCE consists of:

Chairpersons:
Veit Winkler, chairperson of the administrative board
Danny Bruder, deputy chairperson of the administrative board

Other members:
* Johanna Breuckmann
* m.eik michalke
* Thomas Mielke
* Christoph Scheid
* Elmar Schuck
* Holger Schwetter

======================

Links:

[1] Articles of association of the C3S SCE: https://url.c3s.cc/statutes
[2] Map of location:
https://www.openstreetmap.org/node/2679106873#map=19/51.23274/6.78834
[3] What is a barcamp? https://url.c3s.cc/bcexplanation

{footer}

+ 1
- 0
c3smembership/templates/mail/bcga2018_invite_subject_de.txt Просмотреть файл

@@ -0,0 +1 @@
[C3S] Einladung zu Barcamp und Generalversammlung

+ 1
- 0
c3smembership/templates/mail/bcga2018_invite_subject_en.txt Просмотреть файл

@@ -0,0 +1 @@
[C3S] Invitation to Barcamp and General Assembly

+ 7
- 4
c3smembership/tests/test_api_views.py Просмотреть файл

@@ -59,14 +59,17 @@ class TestApiViews(unittest.TestCase):
name_of_colsoc=u"GEMA",
num_shares=u'23',
)
# pylint: disable=no-member
DBSession.add(member1)
member1.email_invite_token_bcgv17 = u'MEMBERS_TOKEN'
member1.email_invite_token_bcgv18 = u'MEMBERS_TOKEN'
# pylint: disable=no-member
DBSession.flush()

self.testapp = TestApp(app)

def tearDown(self):
testing.tearDown()
# pylint: disable=no-member
DBSession.close()
DBSession.remove()

@@ -109,7 +112,7 @@ class TestApiViews(unittest.TestCase):
# now use the correct auth token
_auth_info = {'X-messaging-token': 'SECRETAUTHTOKEN'}

# ..but a non-existing refcode (email_invite_token_bcgv17)
# ..but a non-existing refcode (email_invite_token_bcgv18)
# returns no user (None)
res = self.testapp.put_json(
'/lm', dict(token='foo'), headers=_auth_info, status=200)
@@ -121,9 +124,9 @@ class TestApiViews(unittest.TestCase):

member1 = C3sMember.get_by_id(1) # load member from DB for crosscheck

# now try a valid refcode (email_invite_token_bcgv17)
# now try a valid refcode (email_invite_token_bcgv18)
res2 = self.testapp.put_json(
'/lm', dict(token=member1.email_invite_token_bcgv17),
'/lm', dict(token=member1.email_invite_token_bcgv18),
headers=_auth_info, status=200)
self.assertTrue(json.loads(res2.body)['firstname'], member1.firstname)
self.assertTrue(json.loads(res2.body)['lastname'], member1.lastname)


+ 21
- 17
c3smembership/tests/test_invite_member.py Просмотреть файл

@@ -33,6 +33,8 @@ def init_testing_db():
# There is a side effect of test_initialization.py after which there are
# still records in the database although it is setup from scratch.
# Therefore, remove all members to have an empty table.

# pylint: disable=no-member
members = C3sMember.get_all()
for member in members:
DBSession.delete(member)
@@ -148,6 +150,7 @@ class TestInvitation(unittest.TestCase):
self.session = init_testing_db()

def tearDown(self):
# pylint: disable=no-member
DBSession.close()
DBSession.remove()
testing.tearDown()
@@ -157,15 +160,15 @@ class TestInvitation(unittest.TestCase):
Test the invitation procedure for one single member at a time.

Load this member from the DB,
assure the email_invite_flag_bcgv17 and token are not set,
assure the email_invite_flag_bcgv18 and token are not set,
prepare cookies, invite this member,
assure the email_invite_flag_bcgv17 and token are now set,
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_bcgv17, False)
self.assertTrue(member1.email_invite_token_bcgv17 is None)
self.assertEqual(member1.email_invite_flag_bcgv18, False)
self.assertTrue(member1.email_invite_token_bcgv18 is None)

req = testing.DummyRequest()
# have some cookies
@@ -181,8 +184,8 @@ class TestInvitation(unittest.TestCase):
req.matchdict = {'m_id': member1.id}
res = invite_member_bcgv(req)

self.assertEqual(member1.email_invite_flag_bcgv17, True)
self.assertTrue(member1.email_invite_token_bcgv17 is not None)
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'
@@ -196,23 +199,23 @@ class TestInvitation(unittest.TestCase):
in mailer.outbox[1].subject)
self.assertTrue(member1.firstname
in mailer.outbox[1].body)
self.assertTrue(member1.email_invite_token_bcgv17
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_bcgv17, False)
self.assertTrue(member2.email_invite_token_bcgv17 is None)
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_bcgv17, True)
self.assertTrue(member2.email_invite_token_bcgv17 is not None)
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_bcgv17
self.assertTrue(member2.email_invite_token_bcgv18
in mailer.outbox[2].body)

def test_invitation_batch(self):
@@ -223,8 +226,8 @@ class TestInvitation(unittest.TestCase):

members = C3sMember.get_all()
for member in members:
self.assertEqual(member.email_invite_flag_bcgv17, False)
self.assertTrue(member.email_invite_token_bcgv17 is None)
self.assertEqual(member.email_invite_flag_bcgv18, False)
self.assertTrue(member.email_invite_token_bcgv18 is None)
self.assertTrue(member.membership_accepted is True)

req = testing.DummyRequest()
@@ -238,6 +241,7 @@ class TestInvitation(unittest.TestCase):
res = batch_invite(req)

_messages = req.session.peek_flash('message_to_staff')
# pylint: disable=superfluous-parens
print(_messages)
self.assertTrue(
'sent out 1 mails (to members with ids [1])' in _messages)
@@ -280,12 +284,12 @@ class TestInvitation(unittest.TestCase):

for member in members:
# has been invited
self.assertEqual(member.email_invite_flag_bcgv17, True)
self.assertEqual(member.email_invite_flag_bcgv18, True)
# has a token
self.assertTrue(member.email_invite_token_bcgv17 is not None)
self.assertTrue(member.email_invite_token_bcgv18 is not None)
# firstname and token are in email body
self.assertTrue(
members[member.id - 1].firstname in mailer.outbox[member.id - 1].body)
self.assertTrue(
members[member.id - 1].email_invite_token_bcgv17 in mailer.outbox[
members[member.id - 1].email_invite_token_bcgv18 in mailer.outbox[
member.id - 1].body)

+ 292
- 0
c3smembership/tests/test_membership_application.py Просмотреть файл

@@ -0,0 +1,292 @@
# -*- coding: utf-8 -*-
"""
Tests the c3smembership.data.repository.share_repository package.
"""

import datetime
import re
import transaction
import unittest

from pyramid import testing
from sqlalchemy import engine_from_config
from sqlalchemy.sql import func
from webtest import TestApp

from c3smembership import main
from c3smembership.data.model.base import (
DBSession,
Base,
)
from c3smembership.models import (
C3sStaff,
Group,
C3sMember,
)


class MailerDummy(object):

def __init__(self):
self._email = None

def send(self, email):
self._email = email

def get_email(self):
return self._email


class GetMailerDummy(object):

def __init__(self):
self._mailer = MailerDummy()

def __call__(self, request):
return self._mailer


class DateTimeDummy(object):

def __init__(self, now):
self._now = now

def now(self):
return self._now


class MembershipApplicationTest(unittest.TestCase):

def setUp(self):
my_settings = {
'sqlalchemy.url': 'sqlite:///:memory:',
'api_auth_token': u"SECRETAUTHTOKEN",
'c3smembership.url': u'localhost',
'testing.mail_to_console': u'false',
}
self.config = testing.setUp()
app = main({}, **my_settings)
self.get_mailer = GetMailerDummy()
app.registry.get_mailer = self.get_mailer
app.registry.membership_application.datetime = DateTimeDummy(
datetime.datetime(2018, 4, 26, 12, 23, 34))

engine = engine_from_config(my_settings)
DBSession.configure(bind=engine)
Base.metadata.create_all(engine)

with transaction.manager:
# a group for accountants/staff
accountants_group = Group(name=u"staff")
DBSession.add(accountants_group)
DBSession.flush()
# staff personnel
staffer1 = C3sStaff(
login=u"rut",
password=u"berries",
email=u"noreply@c3s.cc",
)
staffer1.groups = [accountants_group]
DBSession.add(accountants_group)
DBSession.add(staffer1)
DBSession.flush()

self.testapp = TestApp(app)

def tearDown(self):
testing.tearDown()
DBSession.close()
DBSession.remove()

def _login(self):
"""
Log into the membership backend
"""
res = self.testapp.get('/login', status=200)
self.failUnless('login' in res.body)
form = res.form
form['login'] = 'rut'
form['password'] = 'berries'
res = form.submit('submit', status=302)

def _validate_dashboard_redirect(self, res):
"""
Validate that res is redirecting to the dashboard
"""
res = res.follow() # being redirected to dashboard with parameters
self.__validate_dashboard(res)

def _validate_dashboard(self, res):
"""
Validate that res is the dashboard
"""
self.failUnless('Dashboard' in res.body)

@classmethod
def _response_to_bare_text(cls, res):
html = res.normal_body
# remove JavaScript
html = re.sub(re.compile('<script.*</script>'), '', html)
# remove all tags
html = re.sub(re.compile('<.*?>'), '', html)
# remove html characters like &nbsp;
html = re.sub(re.compile('&[A-Za-z]+;'), '', html)
return 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'soenke@example.com',
'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: soenke@example.com' 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, ['soenke@example.com'])
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'] = 'worst password ever chosen'
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 soenke@example.com' 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)

+ 267
- 326
c3smembership/tests/test_models.py
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 2
- 0
c3smembership/tests/test_webtest.py Просмотреть файл

@@ -14,6 +14,7 @@ There are two 'areas' covered:
# import os
import unittest
from pyramid import testing
from pyramid_mailer import get_mailer
from c3smembership.data.model.base import (
DBSession,
Base,
@@ -665,6 +666,7 @@ class FunctionalTests(unittest.TestCase):

from c3smembership import main
app = main({}, **my_settings)
app.registry.get_mailer = get_mailer

from webtest import TestApp
self.testapp = TestApp(app)


+ 103
- 162
c3smembership/views/afm.py Просмотреть файл

@@ -17,15 +17,18 @@ Tests for these functions can be found in

"""

from datetime import (
date,
datetime,
)
import logging
from types import NoneType

import colander
from colander import (
Invalid,
Range,
)
from datetime import (
date,
datetime,
)
import deform
from deform import ValidationFailure
from c3smembership.deform_text_input_slider_widget import (
@@ -35,35 +38,49 @@ from c3smembership.deform_text_input_slider_widget import (
from pyramid.i18n import (
get_locale_name,
)
from pyramid.httpexceptions import HTTPFound
from pyramid.view import view_config
from pyramid_mailer import get_mailer
from pyramid_mailer.message import Message
from pyramid.httpexceptions import HTTPFound
from sqlalchemy.exc import (
IntegrityError,
InvalidRequestError,
)

from types import NoneType
from c3smembership.data.model.base import DBSession
from c3smembership.models import C3sMember
from c3smembership.utils import (
generate_pdf,
accountant_mail,
country_codes
)
from c3smembership.presentation.i18n import (
_,
ZPT_RENDERER,
)
from c3smembership.utils import (
generate_pdf,
accountant_mail,
)
import customization

DEBUG = False
LOGGING = True

if LOGGING:
import logging
log = logging.getLogger(__name__)
LOG = logging.getLogger(__name__)


def statute_validator(node, value):
"""
Validator for statute confirmation.
"""
if not value:
# raise without additional error message as the description
# already explains the necessity of the checkbox
raise Invalid(node, u'')


def dues_regulations_validator(node, value):
"""
Validator for dues regulations confirmation.
"""
if not value:
# raise without additional error message as the description
# already explains the necessity of the checkbox
raise Invalid(node, u'')


@view_config(renderer='c3smembership:templates/join.pt',
@@ -96,6 +113,7 @@ def join_c3s(request):
except AttributeError:
print(dir(customization))
country_default = 'GB'

if DEBUG:
print("== locale is :" + str(locale_name))
print("== choosing :" + str(country_default))
@@ -169,7 +187,9 @@ def join_c3s(request):
date.today().year-18,
date.today().month,
date.today().day),
min_err=_(u'Sorry, we do not believe that you are that old'),
min_err=_(
u'Sorry, but we do not believe that the birthday you '
u'entered is correct.'),
max_err=_(
u'Unfortunately, the membership application of an '
u'underaged person is currently not possible via our web '
@@ -241,7 +261,6 @@ def join_c3s(request):
oid='membership_custom_fee',
default=customization.membership_fee_custom_min,
validator=Range(min=customization.membership_fee_custom_min,max=None,min_err=_(u'please enter at least the minimum fee for sustaining members'))

)


@@ -275,15 +294,6 @@ def join_c3s(request):
some legal requirements
"""

def statute_validator(node, value):
"""
Validator for statute confirmation.
"""
if not value:
# raise without additional error message as the description
# already explains the necessity of the checkbox
raise Invalid(node, u'')

got_statute = colander.SchemaNode(
colander.Bool(true_val=u'yes'),
#title=(u''),
@@ -300,16 +310,7 @@ def join_c3s(request):
validator=statute_validator,
required=True,
oid='got_statute',
#label=_('Yes, really'),
)
def dues_regulations_validator(node, value):
"""
Validator for dues regulations confirmation.
"""
if not value:
# raise without additional error message as the description
# already explains the necessity of the checkbox
raise Invalid(node, u'')

got_dues_regulations = colander.SchemaNode(
colander.Bool(true_val=u'yes'),
@@ -325,10 +326,8 @@ def join_c3s(request):
validator=dues_regulations_validator,
required=True,
oid='got_dues_regulations',
#label=_('Yes'),
)


class MembershipForm(colander.Schema):
"""