Skip to content

Commit

Permalink
Merged master
Browse files Browse the repository at this point in the history
  • Loading branch information
johnmartin committed Oct 17, 2013
2 parents 36cd347 + 1e5b72f commit 5eb3c40
Show file tree
Hide file tree
Showing 61 changed files with 1,475 additions and 377 deletions.
3 changes: 3 additions & 0 deletions .coveragerc
@@ -0,0 +1,3 @@
[run]
omit = /ckan/migration/*, /ckan/tests/*, */tests/*
source = ckan, ckanext
2 changes: 2 additions & 0 deletions .travis.yml
Expand Up @@ -14,3 +14,5 @@ notifications:
on_failure: change
template:
- "%{repository} %{branch} %{commit} %{build_url} %{author}: %{message}"
after_success:
- coveralls
4 changes: 4 additions & 0 deletions README.rst
Expand Up @@ -5,6 +5,10 @@ CKAN: The Open Source Data Portal Software
:target: http://travis-ci.org/okfn/ckan
:alt: Build Status

.. image:: https://coveralls.io/repos/okfn/ckan/badge.png?branch=coveralls
:target: https://coveralls.io/r/okfn/ckan?branch=coveralls
:alt: Test coverage

**CKAN is the world’s leading open-source data portal platform**.
CKAN makes it easy to publish, share and work with data. It's a data management
system that provides a powerful platform for cataloging, storing and accessing
Expand Down
2 changes: 1 addition & 1 deletion bin/travis-build
Expand Up @@ -63,7 +63,7 @@ MOCHA_ERROR=$?
killall paster

# And finally, run the nosetests
nosetests --ckan --with-pylons=test-core.ini --nologcapture ckan ckanext
nosetests --ckan --with-pylons=test-core.ini --nologcapture ckan ckanext --with-coverage --cover-package=ckanext --cover-package=ckan
# Did an error occur?
NOSE_ERROR=$?

Expand Down
2 changes: 1 addition & 1 deletion ckan/config/environment.py
Expand Up @@ -159,7 +159,7 @@ def find_controller(self, controller):
'''
This code is based on Genshi code
Copyright © 2006-2012 Edgewall Software
Copyright © 2006-2012 Edgewall Software
All rights reserved.
Redistribution and use in source and binary forms, with or
Expand Down
1 change: 1 addition & 0 deletions ckan/config/routing.py
Expand Up @@ -359,6 +359,7 @@ def make_map():
action='followers', ckan_icon='group')
m.connect('user_edit', '/user/edit/{id:.*}', action='edit',
ckan_icon='cog')
m.connect('user_delete', '/user/delete/{id}', action='delete')
m.connect('/user/reset/{id:.*}', action='perform_reset')
m.connect('register', '/user/register', action='register')
m.connect('login', '/user/login', action='login')
Expand Down
13 changes: 13 additions & 0 deletions ckan/controllers/group.py
Expand Up @@ -612,6 +612,19 @@ def member_new(self, id):
data_dict = clean_dict(dict_fns.unflatten(
tuplize_dict(parse_params(request.params))))
data_dict['id'] = id

email = data_dict.get('email')
if email:
user_data_dict = {
'email': email,
'group_id': data_dict['id'],
'role': data_dict['role']
}
del data_dict['email']
user_dict = self._action('user_invite')(context,
user_data_dict)
data_dict['username'] = user_dict['name']

c.group_dict = self._action('group_member_create')(context, data_dict)
self._redirect_to(controller='group', action='members', id=id)
else:
Expand Down
26 changes: 22 additions & 4 deletions ckan/controllers/user.py
Expand Up @@ -183,6 +183,22 @@ def new(self, data=None, errors=None, error_summary=None):
c.form = render(self.new_user_form, extra_vars=vars)
return render('user/new.html')

def delete(self, id):
'''Delete user with id passed as parameter'''
context = {'model': model,
'session': model.Session,
'user': c.user,
'auth_user_obj': c.userobj}
data_dict = {'id': id}

try:
get_action('user_delete')(context, data_dict)
user_index = h.url_for(controller='user', action='index')
h.redirect_to(user_index)
except NotAuthorized:
msg = _('Unauthorized to delete user with id "{user_id}".')
abort(401, msg.format(user_id=id))

def _save_new(self, context):
try:
data_dict = logic.clean_dict(unflatten(
Expand Down Expand Up @@ -392,6 +408,9 @@ def request_reset(self):
if request.method == 'POST':
id = request.params.get('user')

context = {'model': model,
'user': c.user}

data_dict = {'id': id}
user_obj = None
try:
Expand Down Expand Up @@ -435,18 +454,16 @@ def perform_reset(self, id):
# FIXME We should reset the reset key when it is used to prevent
# reuse of the url
context = {'model': model, 'session': model.Session,
'user': c.user,
'auth_user_obj': c.userobj,
'user': id,
'keep_sensitive_data': True}

data_dict = {'id': id}

try:
check_access('user_reset', context)
except NotAuthorized:
abort(401, _('Unauthorized to reset password.'))

try:
data_dict = {'id': id}
user_dict = get_action('user_show')(context, data_dict)

# Be a little paranoid, and get rid of sensitive data that's
Expand All @@ -468,6 +485,7 @@ def perform_reset(self, id):
new_password = self._get_form_password()
user_dict['password'] = new_password
user_dict['reset_key'] = c.reset_key
user_dict['state'] = model.State.ACTIVE
user = get_action('user_update')(context, user_dict)

h.flash_success(_("Your password has been reset."))
Expand Down
1 change: 0 additions & 1 deletion ckan/lib/app_globals.py
Expand Up @@ -39,7 +39,6 @@
# has been setup in load_environment():
'ckan.site_id': {},
'ckan.recaptcha.publickey': {'name': 'recaptcha_publickey'},
'ckan.recaptcha.privatekey': {'name': 'recaptcha_publickey'},
'ckan.template_title_deliminater': {'default': '-'},
'ckan.template_head_end': {},
'ckan.template_footer_end': {},
Expand Down
22 changes: 14 additions & 8 deletions ckan/lib/authenticator.py
Expand Up @@ -12,9 +12,9 @@ class OpenIDAuthenticator(object):

def authenticate(self, environ, identity):
if 'repoze.who.plugins.openid.userid' in identity:
openid = identity.get('repoze.who.plugins.openid.userid')
openid = identity['repoze.who.plugins.openid.userid']
user = User.by_openid(openid)
if user is None:
if user is None or not user.is_active():
return None
else:
return user.name
Expand All @@ -25,14 +25,20 @@ class UsernamePasswordAuthenticator(object):
implements(IAuthenticator)

def authenticate(self, environ, identity):
if not 'login' in identity or not 'password' in identity:
if not ('login' in identity and 'password' in identity):
return None
user = User.by_name(identity.get('login'))

login = identity['login']
user = User.by_name(login)

if user is None:
log.debug('Login failed - username %r not found', identity.get('login'))
return None
if user.validate_password(identity.get('password')):
log.debug('Login failed - username %r not found', login)
elif not user.is_active():
log.debug('Login as %r failed - user isn\'t active', login)
elif not user.validate_password(identity['password']):
log.debug('Login as %r failed - password not valid', login)
else:
return user.name
log.debug('Login as %r failed - password not valid', identity.get('login'))

return None

5 changes: 3 additions & 2 deletions ckan/lib/base.py
Expand Up @@ -314,8 +314,9 @@ def _identify_user_default(self):
if c.user:
c.user = c.user.decode('utf8')
c.userobj = model.User.by_name(c.user)
if c.userobj is None:
# This occurs when you are logged in, clean db
if c.userobj is None or not c.userobj.is_active():
# This occurs when a user that was still logged in is deleted,
# or when you are logged in, clean db
# and then restart (or when you change your username)
# There is no user object, so even though repoze thinks you
# are logged in and your cookie has ckan_display_name, we
Expand Down
3 changes: 2 additions & 1 deletion ckan/lib/create_test_data.py
Expand Up @@ -519,8 +519,9 @@ def _create_user_without_commit(cls, name='', **user_dict):

@classmethod
def create_user(cls, name='', **kwargs):
cls._create_user_without_commit(name, **kwargs)
user = cls._create_user_without_commit(name, **kwargs)
model.Session.commit()
return user

@classmethod
def flag_for_deletion(cls, pkg_names=[], tag_names=[], group_names=[],
Expand Down
3 changes: 3 additions & 0 deletions ckan/lib/dictization/model_dictize.py
Expand Up @@ -204,6 +204,9 @@ def package_dictize(pkg, context):
if not result:
raise logic.NotFound
result_dict = d.table_dictize(result, context)
#strip whitespace from title
if result_dict.get('title'):
result_dict['title'] = result_dict['title'].strip()
#resources
res_rev = model.resource_revision_table
resource_group = model.resource_group_table
Expand Down
59 changes: 41 additions & 18 deletions ckan/lib/mailer.py
Expand Up @@ -100,21 +100,37 @@ def mail_user(recipient, subject, body, headers={}):
mail_recipient(recipient.display_name, recipient.email, subject,
body, headers=headers)

def get_reset_link_body(user):
reset_link_message = _(
'''You have requested your password on %(site_title)s to be reset.
RESET_LINK_MESSAGE = _(
'''You have requested your password on %(site_title)s to be reset.
Please click the following link to confirm this request:
Please click the following link to confirm this request:
%(reset_link)s
''')

%(reset_link)s
''')
d = {
'reset_link': get_reset_link(user),
'site_title': g.site_title
}
return reset_link_message % d

def make_key():
return uuid.uuid4().hex[:10]
def get_invite_body(user):
invite_message = _(
'''You have been invited to %(site_title)s. A user has already been created to
you with the username %(user_name)s. You can change it later.
def create_reset_key(user):
user.reset_key = unicode(make_key())
model.repo.commit_and_remove()
To accept this invite, please reset your password at:
%(reset_link)s
''')

d = {
'reset_link': get_reset_link(user),
'site_title': g.site_title,
'user_name': user.name,
}
return invite_message % d

def get_reset_link(user):
return urljoin(g.site_url,
Expand All @@ -123,17 +139,24 @@ def get_reset_link(user):
id=user.id,
key=user.reset_key))

def get_reset_link_body(user):
d = {
'reset_link': get_reset_link(user),
'site_title': g.site_title
}
return RESET_LINK_MESSAGE % d

def send_reset_link(user):
create_reset_key(user)
body = get_reset_link_body(user)
mail_user(user, _('Reset your password'), body)
subject = _('Reset your password')
mail_user(user, subject, body)

def send_invite(user):
create_reset_key(user)
body = get_invite_body(user)
subject = _('Invite for {site_title}'.format(site_title=g.site_title))
mail_user(user, subject, body)

def create_reset_key(user):
user.reset_key = unicode(make_key())
model.repo.commit_and_remove()

def make_key():
return uuid.uuid4().hex[:10]

def verify_reset_link(user, key):
if not key:
Expand Down
63 changes: 63 additions & 0 deletions ckan/logic/action/create.py
@@ -1,6 +1,8 @@
'''API functions for adding data to CKAN.'''

import logging
import random
import re

from pylons import config
import paste.deploy.converters
Expand All @@ -15,6 +17,8 @@
import ckan.lib.dictization.model_dictize as model_dictize
import ckan.lib.dictization.model_save as model_save
import ckan.lib.navl.dictization_functions
import ckan.lib.navl.validators as validators
import ckan.lib.mailer as mailer

from ckan.common import _

Expand Down Expand Up @@ -837,6 +841,65 @@ def user_create(context, data_dict):
log.debug('Created user {name}'.format(name=user.name))
return user_dict


def user_invite(context, data_dict):
'''Invite a new user.
You must be authorized to create group members.
:param email: the email of the user to be invited to the group
:type email: string
:param group_id: the id or name of the group
:type group_id: string
:param role: role of the user in the group. One of ``member``, ``editor``,
or ``admin``
:type role: string
:returns: the newly created yser
:rtype: dictionary
'''
_check_access('user_invite', context, data_dict)

schema = context.get('schema',
ckan.logic.schema.default_user_invite_schema())
data, errors = _validate(data_dict, schema, context)
if errors:
raise ValidationError(errors)

name = _get_random_username_from_email(data['email'])
password = str(random.SystemRandom().random())
data['name'] = name
data['password'] = password
data['state'] = ckan.model.State.PENDING
user_dict = _get_action('user_create')(context, data)
user = ckan.model.User.get(user_dict['id'])
member_dict = {
'username': user.id,
'id': data['group_id'],
'role': data['role']
}
_get_action('group_member_create')(context, member_dict)
mailer.send_invite(user)
return model_dictize.user_dictize(user, context)


def _get_random_username_from_email(email):
localpart = email.split('@')[0]
cleaned_localpart = re.sub(r'[^\w]', '-', localpart)

# if we can't create a unique user name within this many attempts
# then something else is probably wrong and we should give up
max_name_creation_attempts = 100

for i in range(max_name_creation_attempts):
random_number = random.SystemRandom().random() * 10000
name = '%s-%d' % (cleaned_localpart, random_number)
if not ckan.model.User.get(name):
return name

return cleaned_localpart


## Modifications for rest api

def package_create_rest(context, data_dict):
Expand Down

0 comments on commit 5eb3c40

Please sign in to comment.