diff --git a/VERSION b/VERSION
index 20dfc069d..b57acd400 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-nursix-dev-2812-g48aaea7 (2019-04-12 21:55:05)
+nursix-dev-2813-g25fd3d6 (2019-04-12 23:57:22)
diff --git a/controllers/br.py b/controllers/br.py
index edbc257ca..06d576f2d 100644
--- a/controllers/br.py
+++ b/controllers/br.py
@@ -42,6 +42,15 @@ def person():
action = s3db.pr_Contacts,
)
+ # ID Card Export
+ id_card_layout = settings.get_br_id_card_layout()
+ id_card_export_roles = settings.get_br_id_card_export_roles()
+ if id_card_layout and id_card_export_roles and \
+ auth.s3_has_roles(id_card_export_roles):
+ id_card_export = True
+ else:
+ id_card_export = False
+
def prep(r):
# Filter to persons who have a case registered
@@ -126,6 +135,21 @@ def prep(r):
insertable = insertable,
)
+ # Configure ID Cards
+ if id_card_export:
+ if r.representation == "card":
+ # Configure ID card layout
+ resource.configure(pdf_card_layout = id_card_layout,
+ #pdf_card_pagesize="A4",
+ )
+
+ if not r.id and not r.component:
+ # Add export-icon for ID cards
+ export_formats = list(settings.get_ui_export_formats())
+ export_formats.append(("card", "fa fa-id-card", T("Export ID Cards")))
+ settings.ui.export_formats = export_formats
+ s3.formats["card"] = r.url(method="")
+
if not r.component:
# Module-specific field and form configuration
@@ -431,10 +455,22 @@ def postp(r, output):
from s3 import S3AnonymizeWidget
anonymize = S3AnonymizeWidget.widget(r, _class="action-btn anonymize-btn")
- # TODO ID-Card button
+ # ID-Card button
+ if id_card_export:
+ card_button = A(T("ID Card"),
+ data = {"url": URL(c="br", f="person",
+ args = ["%s.card" % r.id]
+ ),
+ },
+ _class = "action-btn s3-download-button",
+ _script = "alert('here')",
+ )
+ else:
+ card_button = ""
# Render in place of the delete-button
- buttons["delete_btn"] = TAG[""](anonymize,
+ buttons["delete_btn"] = TAG[""](card_button,
+ anonymize,
)
return output
s3.postp = postp
diff --git a/modules/s3/codecs/card.py b/modules/s3/codecs/card.py
index e2a49e10a..a1fd6a721 100644
--- a/modules/s3/codecs/card.py
+++ b/modules/s3/codecs/card.py
@@ -192,7 +192,8 @@ def encode(self, resource, **attr):
return output_stream
# -------------------------------------------------------------------------
- def extract(self, resource, fields, orderby=None):
+ @staticmethod
+ def extract(resource, fields, orderby=None):
"""
Extract the data items from the given resource
@@ -216,7 +217,8 @@ def extract(self, resource, fields, orderby=None):
)
# -------------------------------------------------------------------------
- def get_flowables(self, layout, resource, items, labels=None, cards_per_page=1):
+ @staticmethod
+ def get_flowables(layout, resource, items, labels=None, cards_per_page=1):
"""
Get the Flowable-instances for the data items
@@ -354,7 +356,8 @@ def __init__(self,
)
# -------------------------------------------------------------------------
- def number_of_cards(self, pagesize, cardsize, margins, spacing):
+ @staticmethod
+ def number_of_cards(pagesize, cardsize, margins, spacing):
"""
Compute the number of cards for one page dimension
@@ -674,7 +677,12 @@ def draw_qrcode(self, value, x, y, size=40, halign=None, valign=None):
qr_code = qr.QrCodeWidget(value)
- bounds = qr_code.getBounds()
+ try:
+ bounds = qr_code.getBounds()
+ except ValueError:
+ # Value contains invalid characters
+ return
+
w = bounds[2] - bounds[0]
h = bounds[3] - bounds[1]
diff --git a/modules/s3cfg.py b/modules/s3cfg.py
index eaf686a84..9cbbce8ec 100644
--- a/modules/s3cfg.py
+++ b/modules/s3cfg.py
@@ -2751,6 +2751,18 @@ def get_br_needs_org_specific(self):
"""
return self.br.get("needs_org_specific", True)
+ def get_br_id_card_layout(self):
+ """
+ Layout class for beneficiary ID cards
+ """
+ return self.br.get("id_card_layout")
+
+ def get_br_id_card_export_roles(self):
+ """
+ User roles permitted to export beneficiary ID cards
+ """
+ return self.br.get("id_card_export_roles")
+
def get_br_case_hide_default_org(self):
"""
Hide the organisation field in cases if only one allowed
diff --git a/modules/templates/BRCMS/config.py b/modules/templates/BRCMS/config.py
index 8b9dc8603..e4e09c21d 100644
--- a/modules/templates/BRCMS/config.py
+++ b/modules/templates/BRCMS/config.py
@@ -10,6 +10,8 @@
#from s3 import FS, IS_ONE_OF
from s3dal import original_tablename
+from templates.BRCMS.idcards import IDCardLayout
+
# =============================================================================
def config(settings):
"""
@@ -152,6 +154,12 @@ def config(settings):
# Terminology to use when referring to measures of assistance (Counseling|Assistance)
#settings.br.assistance_terminology = "Counseling"
+ # ID Card Layout
+ settings.br.id_card_layout = IDCardLayout
+
+ # Roles with permission to generate beneficiary ID cards
+ settings.br.id_card_export_roles = ["CASE_MANAGEMENT"]
+
# -------------------------------------------------------------------------
# CMS Module Settings
#
diff --git a/modules/templates/BRCMS/idcards.py b/modules/templates/BRCMS/idcards.py
new file mode 100644
index 000000000..69904ae35
--- /dev/null
+++ b/modules/templates/BRCMS/idcards.py
@@ -0,0 +1,329 @@
+# -*- coding: utf-8 -*-
+
+import os
+
+from reportlab.lib.colors import HexColor
+from reportlab.platypus import Paragraph
+from reportlab.lib.styles import getSampleStyleSheet
+from reportlab.lib.enums import TA_CENTER
+
+from gluon import current
+
+from s3.codecs.card import S3PDFCardLayout
+from s3 import s3_format_fullname, s3_str
+
+# Fonts we use in this layout
+NORMAL = "Helvetica"
+BOLD = "Helvetica-Bold"
+
+# =============================================================================
+class IDCardLayout(S3PDFCardLayout):
+ """
+ Layout for printable beneficiary ID cards
+ """
+
+ # -------------------------------------------------------------------------
+ @classmethod
+ def fields(cls, resource):
+ """
+ The layout-specific list of fields to look up from the resource
+
+ @param resource: the resource
+
+ @returns: list of field selectors
+ """
+
+ return ["id",
+ "pe_id",
+ "pe_label",
+ "first_name",
+ "middle_name",
+ "last_name",
+ "case.organisation_id$root_organisation",
+ ]
+
+ # -------------------------------------------------------------------------
+ @classmethod
+ def lookup(cls, resource, items):
+ """
+ Look up layout-specific common data for all cards
+
+ @param resource: the resource
+ @param items: the items
+
+ @returns: a dict with common data
+ """
+
+ db = current.db
+ s3db = current.s3db
+
+ defaultpath = os.path.join(current.request.folder, 'uploads')
+
+ # Get all root organisations
+ root_orgs = set(item["_row"]["org_organisation.root_organisation"]
+ for item in items)
+
+ # Get localized root organisation names
+ ctable = s3db.br_case
+ represent = ctable.organisation_id.represent
+ if represent.bulk:
+ root_org_names = represent.bulk(list(root_orgs), show_link=False)
+ else:
+ root_org_names = None
+
+ # Get all PE IDs
+ pe_ids = set(item["_row"]["pr_person.pe_id"] for item in items)
+
+ # Look up all profile pictures
+ itable = s3db.pr_image
+ query = (itable.pe_id.belongs(pe_ids)) & \
+ (itable.profile == True) & \
+ (itable.deleted == False)
+ rows = db(query).select(itable.pe_id, itable.image)
+
+ field = itable.image
+ path = field.uploadfolder if field.uploadfolder else defaultpath
+ pictures = {row.pe_id: os.path.join(path, row.image) for row in rows if row.image}
+
+ return {"pictures": pictures,
+ "root_org_names": root_org_names,
+ }
+
+ # -------------------------------------------------------------------------
+ def draw(self):
+ """
+ Draw the card (one side)
+
+ Instance attributes (NB draw-function should not modify them):
+ - self.canv...............the canvas (provides the drawing methods)
+ - self.resource...........the resource
+ - self.item...............the data item (dict)
+ - self.labels.............the field labels (dict)
+ - self.backside...........this instance should render the backside
+ of a card
+ - self.multiple...........there are multiple cards per page
+ - self.width..............the width of the card (in points)
+ - self.height.............the height of the card (in points)
+
+ NB Canvas coordinates are relative to the lower left corner of the
+ card's frame, drawing must not overshoot self.width/self.height
+ """
+
+ T = current.T
+
+ c = self.canv
+ w = self.width
+ #h = self.height
+ common = self.common
+
+ blue = HexColor(0x27548F)
+
+ item = self.item
+ raw = item["_row"]
+
+ root_org = raw["org_organisation.root_organisation"]
+
+ # Get the localized root org name
+ org_names = common.get("root_org_names")
+ if org_names:
+ root_org_name = org_names.get(root_org)
+
+ #draw_field = self.draw_field
+ draw_value = self.draw_value
+ draw_label = self.draw_label
+
+ code = raw["pr_person.pe_label"]
+
+ if not self.backside:
+
+ # Horizontal alignments
+ LEFT = w / 4 - 5
+ CENTER = w / 2 - 5
+ RIGHT = w * 3 / 4 - 5
+
+ # Vertical alignments
+ TOP = 200
+ #LOWER = [76, 58, 40]
+ BOTTOM = 16
+
+ # Organisation name
+ if root_org_name:
+ draw_value(LEFT, TOP, root_org_name,
+ width = 55,
+ height = 55,
+ size = 10,
+ valign = "middle",
+ )
+
+ # Get the profile picture
+ pictures = common.get("pictures")
+ if pictures:
+ picture = pictures.get(raw["pr_person.pe_id"])
+ if picture:
+ self.draw_image(picture, RIGHT, TOP,
+ width = 60,
+ height = 55,
+ valign = "middle",
+ halign = "center",
+ )
+
+ # Center fields in reverse order so that vertical positions
+ # can be adjusted for very long and hence wrapping strings
+ y = 98
+
+ # ID
+ ah = draw_value(CENTER, y, code, height=24, size=8)
+ draw_label(CENTER, y, None, T("ID Number"))
+
+ # Name
+ y += ah + 12
+ name = s3_format_fullname(fname = raw["pr_person.first_name"],
+ mname = raw["pr_person.middle_name"],
+ lname = raw["pr_person.last_name"],
+ truncate = False,
+ )
+ draw_value(CENTER, y, name, height=24, size=10)
+ draw_label(CENTER, y, None, T("Name"))
+
+ # Barcode
+ if code:
+ self.draw_barcode(s3_str(code), CENTER, BOTTOM,
+ height = 12,
+ halign = "center",
+ maxwidth = w - 15,
+ )
+
+ # Graphics
+ c.setFillColor(blue)
+ c.rect(0, 0, w, 12, fill=1, stroke=0)
+ c.rect(w - 12, 0, 12, 154, fill=1, stroke=0)
+
+ # Add a utting line with multiple cards per page
+ if self.multiple:
+ c.setDash(1, 2)
+ self.draw_outline()
+ else:
+ # Horizontal alignments
+ CENTER = w / 2
+
+ # Vertical alignments
+ TOP = 200
+ MIDDLE = 85
+ BOTTOM = 16
+
+ # QR Code
+ if code:
+ identity = "%s//%s:%s:%s" % (code,
+ raw["pr_person.first_name"] or "",
+ raw["pr_person.middle_name"] or "",
+ raw["pr_person.last_name"] or "",
+ )
+ self.draw_qrcode(identity, CENTER, MIDDLE,
+ size=60, halign="center", valign="center")
+ # Barcode
+ if code:
+ self.draw_barcode(s3_str(code), CENTER, BOTTOM,
+ height = 12,
+ halign = "center",
+ maxwidth = w - 15
+ )
+
+ # Graphics
+ c.setFillColor(blue)
+ c.rect(0, 0, w, 10, fill=1, stroke=0)
+
+ # -------------------------------------------------------------------------
+ def draw_field(self, x, y, colname, size=7, bold=True):
+ """
+ Helper function to draw a centered field value of self.item above
+ position (x, y)
+
+ @param x: drawing position
+ @param y: drawing position
+ @param colname: the column name of the field to look up the value
+ @param size: the font size (points)
+ @param bold: use bold font
+ """
+
+ c = self.canv
+
+ font = BOLD if bold else NORMAL
+
+ value = self.item.get(colname)
+ if value:
+ c.setFont(font, size)
+ c.drawCentredString(x, y, s3_str(value))
+
+ # -------------------------------------------------------------------------
+ def draw_value(self, x, y, value, width=120, height=40, size=7, bold=True, valign=None):
+ """
+ Helper function to draw a centered text above position (x, y);
+ allows the text to wrap if it would otherwise exceed the given
+ width
+
+ @param x: drawing position
+ @param y: drawing position
+ @param value: the text to render
+ @param width: the maximum available width (points)
+ @param height: the maximum available height (points)
+ @param size: the font size (points)
+ @param bold: use bold font
+ @param valign: vertical alignment ("top"|"middle"|"bottom"),
+ default "bottom"
+
+ @returns: the actual height of the text element drawn
+ """
+
+ # Preserve line breaks by replacing them with
tags
+ value = s3_str(value).strip("\n").replace('\n','
\n')
+
+ stylesheet = getSampleStyleSheet()
+ style = stylesheet["Normal"]
+ style.fontName = BOLD if bold else NORMAL
+ style.fontSize = size
+ style.leading = size + 2
+ style.splitLongWords = False
+ style.alignment = TA_CENTER
+
+ para = Paragraph(value, style)
+ aw, ah = para.wrap(width, height)
+
+ while((ah > height or aw > width) and style.fontSize > 4):
+ # Reduce font size to make fit
+ style.fontSize -= 1
+ style.leading = style.fontSize + 2
+ para = Paragraph(value, style)
+ aw, ah = para.wrap(width, height)
+
+ if valign == "top":
+ vshift = ah
+ elif valign == "middle":
+ vshift = ah / 2.0
+ else:
+ vshift = 0
+
+ para.drawOn(self.canv, x - para.width / 2, y - vshift)
+ return ah
+
+ # -------------------------------------------------------------------------
+ def draw_label(self, x, y, colname, default=""):
+ """
+ Helper function to draw a centered label below position (x, y)
+
+ @param x: drawing position
+ @param y: drawing position
+ @param colname: the column name of the field to look up the label
+ @param default: the default label (if label cannot be looked up),
+ pass colname=None to enforce using the default
+ """
+
+ if colname:
+ label = self.labels.get(colname, default)
+ else:
+ label = default
+
+ c = self.canv
+ c.setFont(NORMAL, 5)
+ c.drawCentredString(x, y - 6, s3_str(label))
+
+# END =========================================================================
diff --git a/modules/templates/default/config.py b/modules/templates/default/config.py
index c777163d9..34fc63404 100644
--- a/modules/templates/default/config.py
+++ b/modules/templates/default/config.py
@@ -504,6 +504,10 @@ def config(settings):
#settings.br.case_language_details = False
# Control household size tracking in case files: False, True or "auto" (=default)
#settings.br.household_size = "auto"
+ # Layout class for beneficiary ID cards
+ #settings.br.id_card_layout = IDCardLayout
+ # User roles with permission to export beneficiary ID cards
+ #settings.br.id_card_export_roles = ["ORG_ADMIN", "CASE_MANAGEMENT"]
# --- Case File Tabs ---
# Hide the contact info tab in case files
@@ -512,6 +516,8 @@ def config(settings):
#settings.br.case_id_tab = True
# Hide the family members tab in case files
#settings.br.case_family_tab = False
+ # Show tab with notes journal
+ #settings.br.case_notes_tab = True
# Show the photos-tab in case files
#settings.br.case_photos_tab = True
# Hide the documents-tab in case files