Skip to content

Commit

Permalink
Refs #31571 CCA; refactoring of mayorsadapt
Browse files Browse the repository at this point in the history
  • Loading branch information
Tiberiu Ichim committed Aug 19, 2016
1 parent c7e7337 commit f6b2d48
Show file tree
Hide file tree
Showing 16 changed files with 410 additions and 402 deletions.
117 changes: 49 additions & 68 deletions eea/climateadapt/city_profile.py
@@ -1,12 +1,14 @@
from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText
from plone import api
""" City profile content
"""

from AccessControl.SecurityInfo import ClassSecurityInfo
from datetime import date, timedelta
from plone.api.portal import getSite
from plone.directives import dexterity, form
from tokenlib.errors import ExpiredTokenError
from tokenlib.errors import InvalidSignatureError
from tokenlib.errors import MalformedTokenError
from zope.annotation.interfaces import IAnnotations
from zope.component import getMultiAdapter
from zope.globalrequest import getRequest
from zope.interface import Interface, implements
import binascii
Expand All @@ -15,6 +17,12 @@
import tokenlib


TOKEN_COOKIE_NAME = 'cptk'
TOKEN_EXPIRES_KEY = 'eea.climateadapt.cityprofile_token_expires'
SECRET_KEY = 'eea.climateadapt.cityprofile_secret'
TIMEOUT = 2419200 # How long a token link is valid? 28 days default


class ICityProfile(form.Schema):
"""
Defines content-type schema for CityProfile
Expand All @@ -33,89 +41,32 @@ class ICityProfileStaging(Interface):

def generate_secret():
""" Generates the token secret key """
return binascii.hexlify(os.urandom(16)).upper()

# The urls look like /cptk/<token>/city-profile/somecity
TOKENID = 'cptk'

curdir = os.path.dirname(__file__)
tpl_path = os.path.join(curdir, "mayorsadapt/pt/mail_token.txt")

with open(tpl_path) as f:
MAIL_TEXT_TEMPLATE = f.read()


def send_token_mail(city):
""" Sends a multipart email that contains the link with token """

mail_host = api.portal.get_tool(name='MailHost')
request = getRequest()
renderer = getMultiAdapter((city, request), name='token_mail')

body_html = renderer()
body_plain = renderer.MAIL_TEXT_TEMPLATE

emailto = city.official_email
email_subject = 'New token email'
emailfrom = str(api.portal.getSite().email_from_address)

if not (emailto and emailfrom):
return

mime_msg = MIMEMultipart('related')
mime_msg['Subject'] = email_subject
mime_msg['From'] = emailfrom
mime_msg['To'] = emailto
mime_msg.preamble = 'This is a multi-part message in MIME format.'

msgAlternative = MIMEMultipart('alternative')
mime_msg.attach(msgAlternative)

# Attach plain text
msg_txt = MIMEText(body_plain, _charset='utf-8')
msgAlternative.attach(msg_txt)

# Attach html
msg_txt = MIMEText(body_html, _subtype='html', _charset='utf-8')
msgAlternative.attach(msg_txt)

return mail_host.send(mime_msg.as_string())


def handle_city_added(city, event):
""" Event handler for when a new city is added """
send_token_mail(city)
return binascii.hexlify(os.urandom(16)).upper()


class CityProfile(dexterity.Container):
implements(ICityProfile)

search_type = "MAYORSADAPT"
security = ClassSecurityInfo()

def __init__(self, *args, **kw):
super(CityProfile, self).__init__(*args, **kw)
token_secret = generate_secret()
IAnnotations(self)['eea.climateadapt.cityprofile_secret'] = token_secret
self._reset_secret_key()

@property
def __ac_local_roles__(self):
req = getRequest()

# Comment the try if session problems
try:
secret_token = req.SESSION.get(TOKENID)
except KeyError:
secret_token = req.cookies.get(TOKENID)

# secret_token = req.cookies.get(TOKENID)
public_token = getRequest().cookies.get(TOKEN_COOKIE_NAME)

if not secret_token:
if not public_token:
return {}

# Parses the token to check for errors
try:
secret = IAnnotations(self)['eea.climateadapt.cityprofile_secret']
tokenlib.parse_token(secret_token, secret=secret)
secret = IAnnotations(self)[SECRET_KEY]
tokenlib.parse_token(public_token, secret=secret)
return {'CityMayor': ['Owner', ]}
except ExpiredTokenError:
return {}
Expand All @@ -125,3 +76,33 @@ def __ac_local_roles__(self):
return {}

return {}

def _reset_secret_key(self):
IAnnotations(self)[SECRET_KEY] = generate_secret()

def _get_public_token(self):
""" When asked for a new public token, it will generate a new public
key, with a new expiration date
"""

secret = IAnnotations(self)[SECRET_KEY]
public = tokenlib.make_token({}, secret=secret, timeout=TIMEOUT)

time_now = date.today()
expiry_time = time_now + timedelta(seconds=TIMEOUT)
IAnnotations(self)[TOKEN_EXPIRES_KEY] = expiry_time

return public

security.declareProtected('Modify portal content', 'get_private_edit_link')
def get_private_edit_link(self):
""" Returns the link to edit a city profile
"""

site = getSite()
url = "{0}/cptk/{1}/{2}".format(site.absolute_url(),
self._get_public_token(),
self.getId()
)
print url
return url
124 changes: 98 additions & 26 deletions eea/climateadapt/mayorsadapt/admin.py
@@ -1,40 +1,40 @@
""" Admin views for city profiles
"""

from AccessControl.unauthorized import Unauthorized
from Products.CMFPlone.utils import getToolByName
from Products.Five.browser import BrowserView
from eea.climateadapt.city_profile import generate_secret
from eea.climateadapt.city_profile import send_token_mail
from zope.annotation.interfaces import IAnnotations
from zope.interface import Interface
from zope.interface import implements
from plone.api.portal import show_message
from datetime import date
from eea.climateadapt.city_profile import TOKEN_EXPIRES_KEY
from eea.climateadapt.mayorsadapt.token_email import send_expired_email
from eea.climateadapt.mayorsadapt.token_email import send_newtoken_email
from eea.climateadapt.mayorsadapt.token_email import send_remainder_email
from plone.api import user
from plone.api.content import get_state


class ICityAdminView (Interface):
""" City Profile Administrator Interface """
from plone.api.portal import show_message
from zope.annotation.interfaces import IAnnotations


class CityAdminView (BrowserView):
""" Custom view for the administration of city-profiles
The process for resetting a profile token goes like this:
- Select checkboxes
- Press submit
- Searches for the respective city profiles
- Processes each city profile
- Generates new token and sends email to respective owners
"""
""" Administration of city-profiles. Allow mass sending of token emails
implements(ICityAdminView)
ATTENTION: This resets tokens on all cities!
"""

def __call__(self):
catalog = getToolByName(self.context, 'portal_catalog')

if 'submit' in self.request.form:
counter = 0
for cityid in self.request.form['city']:
for b in self.context.portal_catalog.searchResults(id=[cityid.lower()]):
cityobject = b.getObject()
newtokenid = generate_secret()
annot = IAnnotations(cityobject)
annot['eea.climateadapt.cityprofile_secret'] = newtokenid
send_token_mail(cityobject)
for b in catalog.searchResults(id=[cityid.lower()]):
city = b.getObject()
city._reset_secret_key()
send_newtoken_email(city)
counter += 1

show_message("Email(s) sent", request=self.request, type='info')
show_message("{0} Email(s) sent".format(counter),
request=self.request, type='info')

cat = self.context.portal_catalog
q = { 'portal_type': 'eea.climateadapt.city_profile' }
Expand All @@ -47,3 +47,75 @@ def get_status(self, city):
return get_state(city)
except Exception, e:
return "Error: %s" % e


class SendTokenEmail(BrowserView):
""" Form handler to send token email to city mayor
"""

def __call__(self):
if not self.request.method == 'POST':
return

roles = ['Editor', 'Manager']
if not set(roles).intersection(set(user.get_roles())):
raise Unauthorized("You are not allowed to send token email")
email = self.context.official_email
if email:
send_newtoken_email(self.context)
show_message("Email Sent to {0}".format(email),
request=self.request, type="info")
else:
show_message("Official email is not set",
request=self.request, type="error")

return self.request.response.redirect(self.context.absolute_url())


class BatchSendReminders(BrowserView):
""" A view to be called from cron that will send email reminders
TODO: needs to be refactored into a zoperunner script
"""

def __call__(self):
# TODO: don't check all cityprofiles, only checkout copied or
# non-published
catalog = getToolByName(self.context, 'portal_catalog')
search = catalog.searchResults
for city in search(portal_type='eea.climateadapt.city_profile'):
city = city.getObject()

if has_token(city):
diff = time_difference(city)

if diff <= 7:
""" Send mail informing he has 1 week left
"""
send_remainder_email(city)

if diff == 0:
""" Send mail saying that the period expired to mayor
and administrator
"""
send_expired_email(city)
else:
pass

return self.index()


def has_token(city):
if IAnnotations(city).get(TOKEN_EXPIRES_KEY):
return True
else:
return False


def time_difference(city):
time_left = IAnnotations(city).get(TOKEN_EXPIRES_KEY)
time_now = date.today()

time_delta = (time_left - time_now).days

return time_delta

0 comments on commit f6b2d48

Please sign in to comment.