Skip to content

Commit

Permalink
Merge b34a8f5 into 1a53d85
Browse files Browse the repository at this point in the history
  • Loading branch information
teleyinex committed May 10, 2018
2 parents 1a53d85 + b34a8f5 commit 91ee33e
Show file tree
Hide file tree
Showing 11 changed files with 446 additions and 115 deletions.
25 changes: 24 additions & 1 deletion pybossa/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -760,7 +760,7 @@ def push_notification(project_id, **kwargs):
from pybossa.core import project_repo
project = project_repo.get(project_id)
if project.info.get('onesignal'):
app_id = current_app.config.get('ONESIGNAL_APP_ID')
app_id = current_app.config.get('ONESIGNAL_APP_ID')
api_key = current_app.config.get('ONESIGNAL_API_KEY')
client = PybossaOneSignal(app_id=app_id, api_key=api_key)
filters = [{"field": "tag", "key": project_id, "relation": "exists"}]
Expand All @@ -769,3 +769,26 @@ def push_notification(project_id, **kwargs):
launch_url=kwargs['launch_url'],
web_buttons=kwargs['web_buttons'],
filters=filters)


def delete_account(user_id, **kwargs):
"""Delete user account from the system."""
from pybossa.core import user_repo
from pybossa.core import newsletter
newsletter.init_app(current_app)
user = user_repo.get(user_id)
email = user.email_addr
mailchimp_deleted = newsletter.delete_user(email)
brand = current_app.config.get('BRAND')
user_repo.delete(user)
subject = '[%s]: Your account has been deleted' % brand
body = """Hi,\n Your account and personal data has been deleted from the %s.""" % brand
if not mailchimp_deleted:
body += '\nWe could not delete your Mailchimp account, please contact us to fix this issue.'
if current_app.config.get('DISQUS_SECRET_KEY'):
body += '\nDisqus does not provide an API method to delete your account. You will have to do it by hand yourself in the disqus.com site.'
recipients = [email]
for em in current_app.config.get('ADMINS'):
recipients.append(em)
mail_dict = dict(recipients=recipients, subject=subject, body=body)
send_mail(mail_dict)
93 changes: 60 additions & 33 deletions pybossa/newsletter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
# along with PYBOSSA. If not, see <http://www.gnu.org/licenses/>.
"""PYBOSSA module for subscribing users to Mailchimp lists."""

import mailchimp
from mailchimp import Error
import json
import hashlib
import requests
from requests.auth import HTTPBasicAuth


class Newsletter(object):
Expand All @@ -34,47 +36,72 @@ def __init__(self, app=None):
def init_app(self, app):
"""Configure newsletter Mailchimp client."""
self.app = app
self.client = mailchimp.Mailchimp(app.config.get('MAILCHIMP_API_KEY'))
self.dc = app.config.get('MAILCHIMP_API_KEY').split('-')[1]
self.root = 'https://%s.api.mailchimp.com/3.0' % self.dc
self.auth = HTTPBasicAuth('user', app.config.get('MAILCHIMP_API_KEY'))
self.list_id = app.config.get('MAILCHIMP_LIST_ID')

def is_initialized(self):
return self.app is not None

def ask_user_to_subscribe(self, user):
return (user.newsletter_prompted is False and
self.is_user_subscribed(user.email_addr) is False)
self.is_user_subscribed(user.email_addr)[0] is False)

def get_email_hash(self, email):
"""Return MD5 user email hash."""
self.md5 = hashlib.md5()
self.md5.update(email)
return self.md5.hexdigest()

def is_user_subscribed(self, email, list_id=None):
"""Check if user is subscribed or not."""
try:
if list_id is None:
list_id = self.list_id
"""Check if user is subscibed or not."""
if list_id is None:
list_id = self.list_id
url = '%s/lists/%s/members/%s' % (self.root,
list_id,
self.get_email_hash(email))
res = requests.get(url, auth=self.auth)
res = res.json()
if res['status'] == 200:
return True, res
else:
return False, res

res = self.client.lists.member_info(list_id, [{'email': email}])
return (res.get('success_count') == 1 and
res['data'][0]['email'] == email)
except Error as e:
msg = 'MAILCHIMP: An error occurred: %s - %s' % (e.__class__, e)
self.app.logger.error(msg)
raise
def delete_user(self, email, list_id=None):
"""Delete user from list_id."""
if list_id is None:
list_id = self.list_id
url = '%s/lists/%s/members/%s' % (self.root,
list_id,
self.get_email_hash(email))
res = requests.delete(url, auth=self.auth)
if res.status_code == 204:
return True
else:
return False

def subscribe_user(self, user, list_id=None, old_email=None):
def subscribe_user(self, user, list_id=None, update=False):
"""Subscribe, update a user of a mailchimp list."""
try:
update_existing = False
if list_id is None:
list_id = self.list_id
merge_vars = {'FNAME': user.fullname}
if old_email:
email = {'email': old_email}
merge_vars['new-email'] = user.email_addr
update_existing = self.is_user_subscribed(old_email)
else:
email = {'email': user.email_addr}
if list_id is None:
list_id = self.list_id
url = '%s/lists/%s/members/' % (self.root,
list_id)
data = dict(email_address=user.email_addr,
status='pending',
merge_fields=dict(FNAME=user.fullname)
)
if update is False:
res = requests.post(url, data=json.dumps(data),
headers={'content-type': 'application/json'},
auth=self.auth)
else:
data['status_if_new'] = 'pending'
url = '%s/lists/%s/members/%s' % (self.root,
list_id,
self.get_email_hash(user.email_addr))

self.client.lists.subscribe(list_id, email, merge_vars,
update_existing=update_existing)
except Error, e:
msg = 'MAILCHIMP: An error occurred: %s - %s' % (e.__class__, e)
self.app.logger.error(msg)
raise
res = requests.put(url, data=json.dumps(data),
headers={'content-type': 'application/json'},
auth=self.auth)
return res.json()
24 changes: 24 additions & 0 deletions pybossa/repositories/user_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@

from pybossa.repositories import Repository
from pybossa.model.user import User
from pybossa.model.task_run import TaskRun
from pybossa.exc import WrongObjectError, DBIntegrityError
from faker import Faker
from yacryptopan import CryptoPAn
from flask import current_app


class UserRepository(Repository):
Expand Down Expand Up @@ -76,6 +80,26 @@ def update(self, new_user):
self.db.session.rollback()
raise DBIntegrityError(e)

def fake_user_id(self, user):
faker = Faker()
cp = CryptoPAn(current_app.config.get('CRYPTOPAN_KEY'))
task_runs = self.db.session.query(TaskRun).filter_by(user_id=user.id)
for tr in task_runs:
tr.user_id = None
tr.user_ip = cp.anonymize(faker.ipv4())
self.db.session.merge(tr)
self.db.session.commit()

def delete(self, user):
self._validate_can_be('deleted', user)
try:
self.fake_user_id(user)
self.db.session.delete(user)
self.db.session.commit()
except IntegrityError as e:
self.db.session.rollback()
raise DBIntegrityError(e)

def _validate_can_be(self, action, user):
if not isinstance(user, User):
name = user.__class__.__name__
Expand Down
27 changes: 26 additions & 1 deletion pybossa/view/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
from pybossa.util import fuzzyboolean
from pybossa.cache import users as cached_users
from pybossa.auth import ensure_authorized_to
from pybossa.jobs import send_mail
from pybossa.jobs import send_mail, delete_account
from pybossa.core import user_repo, ldap
from pybossa.feed import get_update_feed
from pybossa.messages import *
Expand All @@ -63,6 +63,8 @@

mail_queue = Queue('email', connection=sentinel.master)

super_queue = Queue('super', connection=sentinel.master)


@blueprint.route('/')
@blueprint.route('/page/<int:page>')
Expand Down Expand Up @@ -846,6 +848,29 @@ def reset_api_key(name):
return jsonify(csrf)


@blueprint.route('/<name>/delete')
@login_required
def delete(name):
"""
Delete user account.
"""
user = user_repo.get_by_name(name)
if not user:
return abort(404)
if current_user.name != name:
return abort(403)

super_queue.enqueue(delete_account, user.id)

if (request.headers.get('Content-Type') == 'application/json' or
request.args.get('response_format') == 'json'):

response = dict(job='enqueued', template='account/delete.html')
return handle_content_type(response)
else:
return redirect(url_for('account.signout'))


@blueprint.route('/save_metadata/<name>', methods=['POST'])
@login_required
def add_metadata(name):
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
"rq-scheduler>=0.5.1, <0.5.2",
"rq-dashboard",
"unidecode>=0.04.16, <0.05",
"mailchimp",
"flask-plugins",
"humanize",
"pbr>=1.0, <2.0", # keep an eye on pbr: https://github.com/rackspace/pyrax/issues/561
Expand All @@ -63,7 +62,8 @@
"flask_profiler >= 1.6, <1.6.1",
"pycountry",
"wtforms-components>=0.10.3, <0.10.4",
"yacryptopan"
"yacryptopan",
"Faker"
]

setup(
Expand Down
104 changes: 104 additions & 0 deletions test/test_jobs/test_delete_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# -*- coding: utf8 -*-
# This file is part of PYBOSSA.
#
# Copyright (C) 2018 Scifabric LTD.
#
# PYBOSSA is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PYBOSSA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with PYBOSSA. If not, see <http://www.gnu.org/licenses/>.

import json
from default import Test, with_context, FakeResponse
from factories import UserFactory
from pybossa.jobs import delete_account, send_mail
from pybossa.core import user_repo
from mock import patch
from flask import current_app

@patch('pybossa.jobs.mail')
@patch('pybossa.jobs.Message')
class TestDeleteAccount(Test):

@with_context
@patch('requests.delete')
def test_send_mail_creates_message_mailchimp_error(self, mailchimp, Message, mail):
with patch.dict(self.flask_app.config, {'MAILCHIMP_API_KEY': 'k-3',
'MAILCHIMP_LIST_ID': 1}):
user = UserFactory.create()
user_id = user.id
brand = 'PYBOSSA'
subject = '[%s]: Your account has been deleted' % brand
body = """Hi,\n Your account and personal data has been deleted from the %s.""" % brand
body += '\nWe could not delete your Mailchimp account, please contact us to fix this issue.'

recipients = [user.email_addr, 'admin@broken.com']
mail_dict = dict(recipients=recipients,
subject=subject,
body=body)

delete_account(user.id)
Message.assert_called_once_with(**mail_dict)
mail.send.assert_called_once_with(Message())
user = user_repo.get(user_id)
assert user is None

@with_context
@patch('requests.delete')
def test_send_mail_creates_message_mailchimp_ok(self, mailchimp, Message, mail):
with patch.dict(self.flask_app.config, {'MAILCHIMP_API_KEY': 'k-3',
'MAILCHIMP_LIST_ID': 1}):
user = UserFactory.create()
user_id = user.id
brand = 'PYBOSSA'
subject = '[%s]: Your account has been deleted' % brand
body = """Hi,\n Your account and personal data has been deleted from the %s.""" % brand

recipients = [user.email_addr, 'admin@broken.com']
mail_dict = dict(recipients=recipients,
subject=subject,
body=body)

mailchimp.side_effect = [FakeResponse(text=json.dumps(dict(status=204)),
json=lambda : '',
status_code=204)]
delete_account(user.id)
Message.assert_called_once_with(**mail_dict)
mail.send.assert_called_once_with(Message())
user = user_repo.get(user_id)
assert user is None

@with_context
@patch('requests.delete')
def test_send_mail_creates_message_mailchimp_disquss(self, mailchimp, Message, mail):
with patch.dict(self.flask_app.config, {'MAILCHIMP_API_KEY': 'k-3',
'MAILCHIMP_LIST_ID': 1,
'DISQUS_SECRET_KEY': 'key'}):
user = UserFactory.create()
user_id = user.id
brand = 'PYBOSSA'
subject = '[%s]: Your account has been deleted' % brand
body = """Hi,\n Your account and personal data has been deleted from the %s.""" % brand
body += '\nDisqus does not provide an API method to delete your account. You will have to do it by hand yourself in the disqus.com site.'

recipients = [user.email_addr, 'admin@broken.com']
mail_dict = dict(recipients=recipients,
subject=subject,
body=body)

mailchimp.side_effect = [FakeResponse(text=json.dumps(dict(status=204)),
json=lambda : '',
status_code=204)]
delete_account(user.id)
Message.assert_called_once_with(**mail_dict)
mail.send.assert_called_once_with(Message())
user = user_repo.get(user_id)
assert user is None
Loading

0 comments on commit 91ee33e

Please sign in to comment.