A webapp/form for people to join pEp coop. Fork of Cultural Commons Collecting Society (C3S) SCE
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

membership_list.py 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. # -*- coding: utf-8 -*-
  2. """
  3. This module holds functionality to handle the C3S SCEs membership list.
  4. Having and maintaining an alphabetical membership list is one of the
  5. obligations of an association like the C3S SCE.
  6. The list is available in several formats:
  7. - HTML with clickable links for browsing
  8. - HTML without links for printout
  9. - PDF (created with pdflatex) for printout (preferred!)
  10. There are also some historic utility functions for reference:
  11. - Turn founders into members
  12. - Turn crowdfunders into members
  13. - Turn form users into members
  14. - Flag duplicates
  15. - Merge duplicates
  16. """
  17. from datetime import (
  18. date,
  19. datetime,
  20. )
  21. import os
  22. import shutil
  23. import subprocess
  24. import tempfile
  25. from pyramid.httpexceptions import HTTPFound
  26. from pyramid.response import Response
  27. from pyramid.view import view_config
  28. from c3smembership.data.model.base import DBSession
  29. from c3smembership.models import (
  30. C3sMember,
  31. )
  32. from c3smembership.tex_tools import TexTools
  33. DEBUG = False
  34. # How is the membership list reconstructed? By the processes only? This can
  35. # involve changes of the firstname, lastname, address, membership status etc.
  36. @view_config(permission='manage',
  37. route_name='membership_listing_date_pdf')
  38. def member_list_date_pdf_view(request):
  39. """
  40. The membership list *for a given date* for printout as PDF.
  41. The date is supplied in and parsed from the URL, e.g.
  42. http://0.0.0.0:6543/aml-2014-12-31.pdf
  43. The PDF is generated using pdflatex.
  44. If the date is not parseable, an error message is shown.
  45. """
  46. effective_date_string = ''
  47. try:
  48. effective_date_string = request.matchdict['date']
  49. effective_date = datetime.strptime(effective_date_string, '%Y-%m-%d') \
  50. .date()
  51. except (KeyError, ValueError):
  52. request.session.flash(
  53. "Invalid date! '{}' does not compute! "
  54. "try again, please! (YYYY-MM-DD)".format(
  55. effective_date_string),
  56. 'message_to_user'
  57. )
  58. return HTTPFound(request.route_url('error_page'))
  59. shares_count_printed = 0
  60. # TODO: repositories are data layer and must only be used by the business
  61. # layer. Introduce business layer logic which uses the repositories and can
  62. # be accessed by this view via the request.
  63. shares_count = request.registry.share_information.get_share_count(
  64. effective_date)
  65. member_information = request.registry.member_information
  66. members_count = member_information.get_accepted_members_count(
  67. effective_date)
  68. members = member_information.get_accepted_members_sorted(
  69. effective_date)
  70. """
  71. Then a LaTeX file is constructed...
  72. """
  73. here = os.path.dirname(__file__)
  74. latex_header_tex = os.path.abspath(
  75. os.path.join(here, '../membership_list_pdflatex/header'))
  76. latex_footer_tex = os.path.abspath(
  77. os.path.join(here, '../membership_list_pdflatex/footer'))
  78. # a temporary directory for the latex run
  79. tempdir = tempfile.mkdtemp()
  80. # now we prepare a .tex file to be pdflatex'ed
  81. latex_file = tempfile.NamedTemporaryFile(
  82. suffix='.tex',
  83. dir=tempdir,
  84. delete=False, # directory will be deleted anyways
  85. )
  86. # and where to store the output
  87. pdf_file = tempfile.NamedTemporaryFile(
  88. dir=tempdir,
  89. delete=False, # directory will be deleted anyways
  90. )
  91. pdf_file.name = latex_file.name.replace('.tex', '.pdf')
  92. # construct latex data: header + variables
  93. latex_data = '''
  94. \\input{%s}
  95. \\def\\numMembers{%s}
  96. \\def\\numShares{%s}
  97. \\def\\sumShares{%s}
  98. \\def\\today{%s}
  99. ''' % (
  100. latex_header_tex,
  101. members_count,
  102. shares_count,
  103. shares_count * 50,
  104. effective_date.strftime('%d.%m.%Y'),
  105. )
  106. # add to the latex document
  107. latex_data += '''
  108. \\input{%s}''' % latex_footer_tex
  109. # print '*' * 70
  110. # print latex_data
  111. # print '*' * 70
  112. latex_file.write(latex_data.encode('utf-8'))
  113. # make table rows per member
  114. for member in members:
  115. address = '''\\scriptsize{}'''
  116. address += '''{}'''.format(
  117. unicode(TexTools.escape(member.address1)).encode('utf-8'))
  118. # check for contents of address2:
  119. if len(member.address2) > 0:
  120. address += '''\\linebreak {}'''.format(
  121. unicode(TexTools.escape(member.address2)).encode('utf-8'))
  122. # add more...
  123. address += ''' \\linebreak {} '''.format(
  124. unicode(TexTools.escape(member.postcode)).encode('utf-8'))
  125. address += '''{}'''.format(
  126. unicode(TexTools.escape(member.city)).encode('utf-8'))
  127. address += ''' ({})'''.format(
  128. unicode(TexTools.escape(member.country)).encode('utf-8'))
  129. member_share_count = \
  130. request.registry.share_information.get_member_share_count(
  131. member.membership_number,
  132. effective_date)
  133. shares_count_printed += member_share_count
  134. membership_loss = u''
  135. if member.membership_loss_date is not None:
  136. membership_loss += \
  137. member.membership_loss_date.strftime('%d.%m.%Y') + \
  138. '\\linebreak '
  139. if member.membership_loss_type is not None:
  140. membership_loss += unicode(TexTools.escape(
  141. member.membership_loss_type)).encode('utf-8')
  142. latex_file.write(
  143. ''' {0} & {1} & {2} & {3} & {4} & {5} & {6} \\\\\\hline %
  144. '''.format(
  145. TexTools.escape(member.lastname).encode('utf-8'), # 0
  146. ' \\footnotesize ' + TexTools.escape(
  147. member.firstname).encode('utf-8'), # 1
  148. ' \\footnotesize ' + TexTools.escape(
  149. str(member.membership_number)), # 2
  150. address, # 3
  151. ' \\footnotesize ' + member.membership_date.strftime(
  152. '%d.%m.%Y'), # 4
  153. ' \\footnotesize ' + membership_loss + ' ', # 5
  154. ' \\footnotesize ' + str(member_share_count) # 6
  155. ))
  156. latex_file.write('''
  157. %\\end{tabular}%
  158. \\end{longtable}%
  159. \\label{LastPage}
  160. \\end{document}
  161. ''')
  162. latex_file.seek(0) # rewind
  163. # pdflatex latex_file to pdf_file
  164. fnull = open(os.devnull, 'w') # hide output
  165. pdflatex_output = subprocess.call(
  166. [
  167. 'pdflatex',
  168. '-output-directory=%s' % tempdir,
  169. latex_file.name
  170. ],
  171. stdout=fnull, stderr=subprocess.STDOUT # hide output
  172. )
  173. if DEBUG: # pragma: no cover
  174. print("the output of pdflatex run: %s" % pdflatex_output)
  175. # if run was a success, run X times more...
  176. if pdflatex_output == 0:
  177. for i in range(2):
  178. pdflatex_output = subprocess.call(
  179. [
  180. 'pdflatex',
  181. '-output-directory=%s' % tempdir,
  182. latex_file.name
  183. ],
  184. stdout=fnull, stderr=subprocess.STDOUT # hide output
  185. )
  186. if DEBUG: # pragma: no cover
  187. print("run #{} finished.".format(i+1))
  188. # sanity check: did we print exactly as many shares as calculated?
  189. assert(shares_count == shares_count_printed)
  190. # return a pdf file
  191. response = Response(content_type='application/pdf')
  192. response.app_iter = open(pdf_file.name, "r")
  193. shutil.rmtree(tempdir, ignore_errors=True) # delete temporary directory
  194. return response
  195. @view_config(renderer='templates/member_list.pt',
  196. permission='manage',
  197. route_name='membership_listing_alphabetical')
  198. def member_list_print_view(request):
  199. """
  200. This view produces printable HTML output, i.e. HTML without links
  201. It was used before the PDF-generating view above existed
  202. """
  203. all_members = C3sMember.member_listing(
  204. 'lastname', how_many=C3sMember.get_number(), offset=0, order=u'asc')
  205. member_list = []
  206. count = 0
  207. for member in all_members:
  208. if member.is_member():
  209. # check membership number
  210. try:
  211. assert(member.membership_number is not None)
  212. except AssertionError:
  213. if DEBUG: # pragma: no cover
  214. print u"failed at id {} lastname {}".format(
  215. member.id, member.lastname)
  216. member_list.append(member)
  217. count += 1
  218. # sort members alphabetically
  219. import locale
  220. locale.setlocale(locale.LC_ALL, "de_DE.UTF-8")
  221. member_list.sort(key=lambda x: x.firstname, cmp=locale.strcoll)
  222. member_list.sort(key=lambda x: x.lastname, cmp=locale.strcoll)
  223. return {
  224. 'members': member_list,
  225. 'count': count,
  226. '_today': date.today(),
  227. }
  228. @view_config(permission='manage',
  229. route_name='merge_member')
  230. def merge_member_view(request):
  231. """
  232. Merges member duplicates into one member record.
  233. Some people have more than one entry in our C3SMember table,
  234. e.g. because they used the application form more than once
  235. to acquire more shares.
  236. They shall not, however, become members twice and get more than one
  237. membership number. So we try and merge them:
  238. If a person is already a member and acquires a second package of shares,
  239. this package of shares is added to the former membership entry.
  240. The second entry in the C3sMember table is given the 'is_duplicate' flag
  241. and also the 'duplicate_of' is given the *id* of the original entry.
  242. """
  243. afm_id = request.matchdict['afm_id']
  244. member_id = request.matchdict['mid']
  245. if DEBUG: # pragma: no cover
  246. print "shall merge {} to {}".format(afm_id, member_id)
  247. orig = C3sMember.get_by_id(member_id)
  248. merg = C3sMember.get_by_id(afm_id)
  249. if not orig.membership_accepted:
  250. request.session.flash(
  251. 'you can only merge to accepted members!',
  252. 'merge_message')
  253. HTTPFound(request.route_url('make_member', afm_id=afm_id))
  254. exceeds_60 = int(orig.num_shares) + int(merg.num_shares) > 60
  255. if exceeds_60:
  256. request.session.flash(
  257. 'merger would exceed 60 shares!',
  258. 'merge_message')
  259. return HTTPFound(request.route_url('make_member', afm_id=afm_id))
  260. # TODO: this needs fixing!!!
  261. # date must be set manually according to date of approval of the board
  262. shares_date_of_acquisition = merg.signature_received_date if (
  263. merg.signature_received_date > merg.payment_received_date
  264. ) else merg.payment_received_date
  265. share_acquisition = request.registry.share_acquisition
  266. share_id = share_acquisition.create(
  267. orig.membership_number,
  268. merg.num_shares,
  269. shares_date_of_acquisition)
  270. share_acquisition.set_signature_reception(
  271. share_id,
  272. date(
  273. merg.signature_received_date.year,
  274. merg.signature_received_date.month,
  275. merg.signature_received_date.day))
  276. share_acquisition.set_signature_confirmation(
  277. share_id,
  278. date(
  279. merg.signature_confirmed_date.year,
  280. merg.signature_confirmed_date.month,
  281. merg.signature_confirmed_date.day))
  282. share_acquisition.set_payment_reception(
  283. share_id,
  284. date(
  285. merg.payment_received_date.year,
  286. merg.payment_received_date.month,
  287. merg.payment_received_date.day))
  288. share_acquisition.set_payment_confirmation(
  289. share_id,
  290. date(
  291. merg.payment_confirmed_date.year,
  292. merg.payment_confirmed_date.month,
  293. merg.payment_confirmed_date.day))
  294. share_acquisition.set_reference_code(
  295. share_id,
  296. merg.email_confirm_code)
  297. DBSession.delete(merg)
  298. return HTTPFound(request.route_url('detail', memberid=member_id))
  299. @view_config(renderer='templates/make_member.pt',
  300. permission='manage',
  301. route_name='make_member')
  302. def make_member_view(request):
  303. """
  304. Turns a membership applicant into an accepted member.
  305. When both the signature and the payment for the shares have arrived at
  306. headquarters, an application for membership can be turned into an
  307. **accepted membership**, if the board of directors decides so.
  308. This view lets staff enter a date of approval through a form.
  309. It also provides staff with listings of
  310. * members with same first name
  311. * members with same last name
  312. * members with same email address
  313. * members with same date of birth
  314. so staff can decide if this may become a proper membership
  315. or whether this application is a duplicate of some accepted membership
  316. and should be merged with some other entry.
  317. In case of duplicate/merge, also check if the number of shares
  318. when combining both entries would exceed 60,
  319. the maximum number of shares a member can hold.
  320. """
  321. afm_id = request.matchdict['afm_id']
  322. try: # does that id make sense? member exists?
  323. member = C3sMember.get_by_id(afm_id)
  324. assert(isinstance(member, C3sMember)) # is an application
  325. # assert(isinstance(member.membership_number, NoneType)
  326. # not has number
  327. except AssertionError:
  328. return HTTPFound(
  329. location=request.route_url('dashboard'))
  330. if member.membership_accepted:
  331. # request.session.flash('id {} is already accepted member!')
  332. return HTTPFound(request.route_url('detail', memberid=member.id))
  333. if not (member.signature_received and member.payment_received):
  334. request.session.flash('signature or payment missing!', 'messages')
  335. return HTTPFound(request.route_url('dashboard'))
  336. if 'make_member' in request.POST:
  337. # print "yes! contents: {}".format(request.POST['make_member'])
  338. try:
  339. member.membership_date = datetime.strptime(
  340. request.POST['membership_date'], '%Y-%m-%d').date()
  341. except ValueError, value_error:
  342. request.session.flash(value_error.message, 'merge_message')
  343. return HTTPFound(
  344. request.route_url('make_member', afm_id=member.id))
  345. member.membership_accepted = True
  346. if member.is_legalentity:
  347. member.membership_type = u'investing'
  348. else:
  349. member.is_legalentity = False
  350. member.membership_number = C3sMember.get_next_free_membership_number()
  351. # Currently, the inconsistent data model stores the amount of applied
  352. # shares in member.num_shares which must be moved to a membership
  353. # application process property. As the acquisition of shares increases
  354. # the amount of shares and this is still read from member.num_shares,
  355. # this value must first be reset to 0 so that it can be increased by
  356. # the share acquisition. Once the new data model is complete the
  357. # property num_shares will not exist anymore. Instead, a membership
  358. # application process stores the number of applied shares and the
  359. # shares store the number of actual shares.
  360. num_shares = member.num_shares
  361. member.num_shares = 0
  362. share_id = request.registry.share_acquisition.create(
  363. member.membership_number,
  364. num_shares,
  365. member.membership_date)
  366. share_acquisition = request.registry.share_acquisition
  367. share_acquisition.set_signature_reception(
  368. share_id,
  369. date(
  370. member.signature_received_date.year,
  371. member.signature_received_date.month,
  372. member.signature_received_date.day))
  373. share_acquisition.set_payment_confirmation(
  374. share_id,
  375. date(
  376. member.payment_received_date.year,
  377. member.payment_received_date.month,
  378. member.payment_received_date.day))
  379. share_acquisition.set_reference_code(
  380. share_id,
  381. member.email_confirm_code)
  382. # return the user to the page she came from
  383. if 'referrer' in request.POST:
  384. if request.POST['referrer'] == 'dashboard':
  385. return HTTPFound(request.route_url('dashboard'))
  386. if request.POST['referrer'] == 'detail':
  387. return HTTPFound(
  388. request.route_url('detail', memberid=member.id))
  389. return HTTPFound(request.route_url('detail', memberid=member.id))
  390. referrer = ''
  391. if 'dashboard' in request.referrer:
  392. referrer = 'dashboard'
  393. if 'detail' in request.referrer:
  394. referrer = 'detail'
  395. return {
  396. 'member': member,
  397. 'next_mship_number': C3sMember.get_next_free_membership_number(),
  398. 'same_mships_firstn': C3sMember.get_same_firstnames(member.firstname),
  399. 'same_mships_lastn': C3sMember.get_same_lastnames(member.lastname),
  400. 'same_mships_email': C3sMember.get_same_email(member.email),
  401. 'same_mships_dob': C3sMember.get_same_date_of_birth(
  402. member.date_of_birth),
  403. # keep information about the page the user came from in order to
  404. # return her to this page
  405. 'referrer': referrer,
  406. }