diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..a4db4c4
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,12 @@
+language: python
+python:
+ - "2.7"
+ - "pypy"
+install:
+ - pip install -r requirements.txt --use-mirrors
+ - pip install -r test_requirements.txt --use-mirrors
+script:
+ - nosetests tests --with-coverage
+notifications:
+ email: false
+ irc: "irc.freenode.net#hasgeek-dev"
diff --git a/instance/testing.py b/instance/testing.py
new file mode 100644
index 0000000..081437c
--- /dev/null
+++ b/instance/testing.py
@@ -0,0 +1,101 @@
+# -*- coding: utf-8 -*-
+from flask import Markup
+
+#: The title of this site
+SITE_TITLE = 'Lastuser'
+
+#: Support contact email
+SITE_SUPPORT_EMAIL = 'test@example.com'
+
+#: TypeKit code for fonts
+TYPEKIT_CODE = ''
+
+#: Google Analytics code UA-XXXXXX-X
+GA_CODE = ''
+
+#: Database backend
+SQLALCHEMY_BINDS = {
+ 'lastuser': 'postgresql://postgres@localhost/myapp_test',
+ }
+
+#: Cache type
+CACHE_TYPE = 'redis'
+
+#: Secret key
+SECRET_KEY = 'random_string_here'
+
+#: Timezone
+TIMEZONE = 'Asia/Calcutta'
+
+#: Reserved usernames
+#: Add to this list but do not remove any unless you want to break
+#: the website
+RESERVED_USERNAMES = set([
+ 'app',
+ 'apps',
+ 'auth',
+ 'client',
+ 'confirm',
+ 'login',
+ 'logout',
+ 'new',
+ 'profile',
+ 'reset',
+ 'register',
+ 'token',
+ 'organizations',
+ ])
+
+#: Mail settings
+#: MAIL_FAIL_SILENTLY : default True
+#: MAIL_SERVER : default 'localhost'
+#: MAIL_PORT : default 25
+#: MAIL_USE_TLS : default False
+#: MAIL_USE_SSL : default False
+#: MAIL_USERNAME : default None
+#: MAIL_PASSWORD : default None
+#: DEFAULT_MAIL_SENDER : default None
+MAIL_FAIL_SILENTLY = False
+MAIL_SERVER = 'localhost'
+DEFAULT_MAIL_SENDER = ('Lastuser', 'test@example.com')
+MAIL_DEFAULT_SENDER = DEFAULT_MAIL_SENDER # For new versions of Flask-Mail
+
+#: Logging: recipients of error emails
+ADMINS = []
+
+#: Log file
+LOGFILE = 'error.log'
+
+#: Use SSL for some URLs
+USE_SSL = False
+
+#: Twitter integration
+OAUTH_TWITTER_KEY = ''
+OAUTH_TWITTER_SECRET = ''
+
+#: GitHub integration
+OAUTH_GITHUB_KEY = ''
+OAUTH_GITHUB_SECRET = ''
+
+#: Recaptcha for the registration form
+RECAPTCHA_USE_SSL = USE_SSL
+RECAPTCHA_PUBLIC_KEY = ''
+RECAPTCHA_PRIVATE_KEY = ''
+RECAPTCHA_OPTIONS = ''
+
+#: SMS gateways
+SMS_SMSGUPSHUP_MASK = ''
+SMS_SMSGUPSHUP_USER = ''
+SMS_SMSGUPSHUP_PASS = ''
+
+#: Messages (text or HTML)
+MESSAGE_FOOTER = Markup('Copyright © HasGeek. Powered by Lastuser, open source software from HasGeek.')
+USERNAME_REASON = ''
+EMAIL_REASON = 'Please provide an email address to complete your profile'
+BIO_REASON = ''
+TIMEZONE_REASON = 'Dates and times will be shown in your preferred timezone'
+ORG_NAME_REASON = u"Your company’s name as it will appear in the URL. Letters, numbers and dashes only"
+ORG_TITLE_REASON = u"Your organization’s given name, preferably without legal suffixes"
+ORG_DESCRIPTION_REASON = u"A few words about your organization (optional). Plain text only"
+LOGIN_MESSAGE_1 = ""
+LOGIN_MESSAGE_2 = ""
diff --git a/lastuser_oauth/forms/login.py b/lastuser_oauth/forms/login.py
index 5aac3e1..24d6a3b 100644
--- a/lastuser_oauth/forms/login.py
+++ b/lastuser_oauth/forms/login.py
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
from flask import Markup, url_for, current_app
+import wtforms
+import wtforms.fields.html5
import flask.ext.wtf as wtf
from coaster import valid_username
from baseframe.forms import Form
@@ -9,46 +11,46 @@
class LoginForm(Form):
- username = wtf.TextField('Username or Email', validators=[wtf.Required()])
- password = wtf.PasswordField('Password', validators=[wtf.Required()])
+ username = wtforms.TextField('Username or Email', validators=[wtforms.validators.Required()])
+ password = wtforms.PasswordField('Password', validators=[wtforms.validators.Required()])
def validate_username(self, field):
existing = getuser(field.data)
if existing is None:
- raise wtf.ValidationError("User does not exist")
+ raise wtforms.ValidationError("User does not exist")
def validate_password(self, field):
user = getuser(self.username.data)
if user is None or not user.password_is(field.data):
- raise wtf.ValidationError("Incorrect password")
+ raise wtforms.ValidationError("Incorrect password")
self.user = user
class RegisterForm(Form):
- fullname = wtf.TextField('Full name', validators=[wtf.Required()])
- email = wtf.html5.EmailField('Email address', validators=[wtf.Required(), wtf.Email()])
- username = wtf.TextField('Username', validators=[wtf.Required()],
+ fullname = wtforms.TextField('Full name', validators=[wtforms.validators.Required()])
+ email = wtforms.fields.html5.EmailField('Email address', validators=[wtforms.validators.Required(), wtforms.validators.Email()])
+ username = wtforms.TextField('Username', validators=[wtforms.validators.Required()],
description="Single word that can contain letters, numbers and dashes")
- password = wtf.PasswordField('Password', validators=[wtf.Required()])
- confirm_password = wtf.PasswordField('Confirm password',
- validators=[wtf.Required(), wtf.EqualTo('password')])
+ password = wtforms.PasswordField('Password', validators=[wtforms.validators.Required()])
+ confirm_password = wtforms.PasswordField('Confirm password',
+ validators=[wtforms.validators.Required(), wtforms.validators.EqualTo('password')])
recaptcha = wtf.RecaptchaField('Are you human?',
description="Type both words into the text box to prove that you are a human and not a computer program")
def validate_username(self, field):
if field.data in current_app.config['RESERVED_USERNAMES']:
- raise wtf.ValidationError, "That name is reserved"
+ raise wtforms.ValidationError, "That name is reserved"
if not valid_username(field.data):
- raise wtf.ValidationError(u"Invalid characters in name. Names must be made of ‘a-z’, ‘0-9’ and ‘-’, without trailing dashes")
+ raise wtforms.ValidationError(u"Invalid characters in name. Names must be made of ‘a-z’, ‘0-9’ and ‘-’, without trailing dashes")
existing = User.query.filter_by(username=field.data).first()
if existing is not None:
- raise wtf.ValidationError("That username is taken")
+ raise wtforms.ValidationError("That username is taken")
def validate_email(self, field):
field.data = field.data.lower() # Convert to lowercase
existing = UserEmail.query.filter_by(email=field.data).first()
if existing is not None:
- raise wtf.ValidationError(Markup(
+ raise wtforms.ValidationError(Markup(
'This email address is already registered. Do you want to login instead?'
% url_for('.login')
))
diff --git a/lastuser_oauth/forms/profile.py b/lastuser_oauth/forms/profile.py
index 619b665..069de38 100644
--- a/lastuser_oauth/forms/profile.py
+++ b/lastuser_oauth/forms/profile.py
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
from flask import g, current_app
-import flask.ext.wtf as wtf
+import wtforms
+import wtforms.fields.html5
from coaster import valid_username, sorted_timezones
from baseframe.forms import Form, ValidEmailDomain
@@ -11,48 +12,48 @@
class PasswordResetRequestForm(Form):
- username = wtf.TextField('Username or Email', validators=[wtf.Required()])
+ username = wtforms.TextField('Username or Email', validators=[wtforms.validators.Required()])
def validate_username(self, field):
user = getuser(field.data)
if user is None:
- raise wtf.ValidationError("Could not find a user with that id")
+ raise wtforms.ValidationError("Could not find a user with that id")
self.user = user
class PasswordResetForm(Form):
- username = wtf.TextField('Username or Email', validators=[wtf.Required()],
+ username = wtforms.TextField('Username or Email', validators=[wtforms.validators.Required()],
description="Please reconfirm your username or email address")
- password = wtf.PasswordField('New password', validators=[wtf.Required()])
- confirm_password = wtf.PasswordField('Confirm password',
- validators=[wtf.Required(), wtf.EqualTo('password')])
+ password = wtforms.PasswordField('New password', validators=[wtforms.validators.Required()])
+ confirm_password = wtforms.PasswordField('Confirm password',
+ validators=[wtforms.validators.Required(), wtforms.validators.EqualTo('password')])
def validate_username(self, field):
user = getuser(field.data)
if user is None or user != self.user:
- raise wtf.ValidationError(
+ raise wtforms.ValidationError(
"This username or email does not match the user the reset code is for")
class PasswordChangeForm(Form):
- old_password = wtf.PasswordField('Current password', validators=[wtf.Required()])
- password = wtf.PasswordField('New password', validators=[wtf.Required()])
- confirm_password = wtf.PasswordField('Confirm password',
- validators=[wtf.Required(), wtf.EqualTo('password')])
+ old_password = wtforms.PasswordField('Current password', validators=[wtforms.validators.Required()])
+ password = wtforms.PasswordField('New password', validators=[wtforms.validators.Required()])
+ confirm_password = wtforms.PasswordField('Confirm password',
+ validators=[wtforms.validators.Required(), wtforms.validators.EqualTo('password')])
def validate_old_password(self, field):
if g.user is None:
- raise wtf.ValidationError, "Not logged in"
+ raise wtforms.ValidationError, "Not logged in"
if not g.user.password_is(field.data):
- raise wtf.ValidationError, "Incorrect password"
+ raise wtforms.ValidationError, "Incorrect password"
class ProfileForm(Form):
- fullname = wtf.TextField('Full name', validators=[wtf.Required()])
- email = wtf.html5.EmailField('Email address', validators=[wtf.Required(), wtf.Email(), ValidEmailDomain()])
- username = wtf.TextField('Username', validators=[wtf.Required()])
- description = wtf.TextAreaField('Bio')
- timezone = wtf.SelectField('Timezone', validators=[wtf.Required()], choices=timezones)
+ fullname = wtforms.TextField('Full name', validators=[wtforms.validators.Required()])
+ email = wtforms.fields.html5.EmailField('Email address', validators=[wtforms.validators.Required(), wtforms.validators.Email(), ValidEmailDomain()])
+ username = wtforms.TextField('Username', validators=[wtforms.validators.Required()])
+ description = wtforms.TextAreaField('Bio')
+ timezone = wtforms.SelectField('Timezone', validators=[wtforms.validators.Required()], choices=timezones)
def __init__(self, *args, **kwargs):
super(ProfileForm, self).__init__(*args, **kwargs)
@@ -65,22 +66,22 @@ def validate_username(self, field):
# return
field.data = field.data.lower() # Usernames can only be lowercase
if not valid_username(field.data):
- raise wtf.ValidationError("Usernames can only have alphabets, numbers and dashes (except at the ends)")
+ raise wtforms.ValidationError("Usernames can only have alphabets, numbers and dashes (except at the ends)")
if field.data in current_app.config['RESERVED_USERNAMES']:
- raise wtf.ValidationError("This name is reserved")
+ raise wtforms.ValidationError("This name is reserved")
existing = User.query.filter_by(username=field.data).first()
if existing is not None and existing.id != self.edit_id:
- raise wtf.ValidationError("This username is taken")
+ raise wtforms.ValidationError("This username is taken")
existing = Organization.query.filter_by(name=field.data).first()
if existing is not None:
- raise wtf.ValidationError("This username is taken")
+ raise wtforms.ValidationError("This username is taken")
# TODO: Move to function and place before ValidEmailDomain()
def validate_email(self, field):
field.data = field.data.lower() # Convert to lowercase
existing = UserEmail.query.filter_by(email=field.data).first()
if existing is not None and existing.user != self.edit_obj:
- raise wtf.ValidationError("This email address has been claimed by another user.")
+ raise wtforms.ValidationError("This email address has been claimed by another user.")
class ProfileMergeForm(Form):
diff --git a/lastuser_oauth/providers/openid.py b/lastuser_oauth/providers/openid.py
index b0df251..6924f24 100644
--- a/lastuser_oauth/providers/openid.py
+++ b/lastuser_oauth/providers/openid.py
@@ -3,7 +3,8 @@
from __future__ import absolute_import
from flask import Markup, session
-import flask.ext.wtf as wtf
+import wtforms
+import wtforms.fields.html5
from baseframe.forms import Form
from lastuser_core.registry import LoginProvider
from ..views.login import oid
@@ -13,7 +14,7 @@
class OpenIdForm(Form):
- openid = wtf.html5.URLField('Login with OpenID', validators=[wtf.Required()], default='http://',
+ openid = wtforms.fields.html5.URLField('Login with OpenID', validators=[wtforms.validators.Required()], default='http://',
description=Markup("Don't forget the http://
or https://
prefix"))
diff --git a/lastuser_ui/forms/client.py b/lastuser_ui/forms/client.py
index dc1a5c4..8f610a9 100644
--- a/lastuser_ui/forms/client.py
+++ b/lastuser_ui/forms/client.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from flask import g
-import flask.ext.wtf as wtf
+import wtforms
+import wtforms.fields.html5
from baseframe.forms import Form
from coaster import valid_username
@@ -12,38 +13,38 @@ class ConfirmDeleteForm(Form):
"""
Confirm a delete operation
"""
- delete = wtf.SubmitField('Delete')
- cancel = wtf.SubmitField('Cancel')
+ delete = wtforms.SubmitField('Delete')
+ cancel = wtforms.SubmitField('Cancel')
class RegisterClientForm(Form):
"""
Register a new OAuth client application
"""
- title = wtf.TextField('Application title', validators=[wtf.Required()],
+ title = wtforms.TextField('Application title', validators=[wtforms.validators.Required()],
description="The name of your application")
- description = wtf.TextAreaField('Description', validators=[wtf.Required()],
+ description = wtforms.TextAreaField('Description', validators=[wtforms.validators.Required()],
description="A description to help users recognize your application")
- client_owner = wtf.RadioField('Owner', validators=[wtf.Required()],
+ client_owner = wtforms.RadioField('Owner', validators=[wtforms.validators.Required()],
description="User or organization that owns this application. Changing the owner "
"will revoke all currently assigned permissions for this app")
- website = wtf.html5.URLField('Application website', validators=[wtf.Required(), wtf.URL()],
+ website = wtforms.fields.html5.URLField('Application website', validators=[wtforms.validators.Required(), wtforms.validators.URL()],
description="Website where users may access this application")
- redirect_uri = wtf.html5.URLField('Redirect URL', validators=[wtf.Optional(), wtf.URL()],
+ redirect_uri = wtforms.fields.html5.URLField('Redirect URL', validators=[wtforms.validators.Optional(), wtforms.validators.URL()],
description="OAuth2 Redirect URL")
- notification_uri = wtf.html5.URLField('Notification URL', validators=[wtf.Optional(), wtf.URL()],
+ notification_uri = wtforms.fields.html5.URLField('Notification URL', validators=[wtforms.validators.Optional(), wtforms.validators.URL()],
description="When the user's data changes, Lastuser will POST a notice to this URL. "
"Other notices may be posted too")
- iframe_uri = wtf.html5.URLField('IFrame URL', validators=[wtf.Optional(), wtf.URL()],
+ iframe_uri = wtforms.fields.html5.URLField('IFrame URL', validators=[wtforms.validators.Optional(), wtforms.validators.URL()],
description="Front-end notifications URL. This is loaded in a hidden iframe to notify the app that the "
"user updated their profile in some way (not yet implemented)")
- resource_uri = wtf.html5.URLField('Resource URL', validators=[wtf.Optional(), wtf.URL()],
+ resource_uri = wtforms.fields.html5.URLField('Resource URL', validators=[wtforms.validators.Optional(), wtforms.validators.URL()],
description="URL at which this application provides resources as per the Lastuser Resource API "
"(not yet implemented)")
- allow_any_login = wtf.BooleanField('Allow anyone to login', default=True,
+ allow_any_login = wtforms.BooleanField('Allow anyone to login', default=True,
description="If your application requires access to be restricted to specific users, uncheck this, "
"and only users who have been assigned a permission to the app will be able to login")
- team_access = wtf.BooleanField('Requires access to teams', default=False,
+ team_access = wtforms.BooleanField('Requires access to teams', default=False,
description="If your application is capable of assigning access permissions to teams, check this. "
"Organization owners will then able to grant access to teams in their organizations")
@@ -54,7 +55,7 @@ def validate_client_owner(self, field):
else:
orgs = [org for org in g.user.organizations_owned() if org.userid == field.data]
if len(orgs) != 1:
- raise wtf.ValidationError("Invalid owner")
+ raise wtforms.ValidationError("Invalid owner")
self.user = None
self.org = orgs[0]
@@ -63,16 +64,16 @@ class PermissionForm(Form):
"""
Create or edit a permission
"""
- name = wtf.TextField('Permission name', validators=[wtf.Required()],
+ name = wtforms.TextField('Permission name', validators=[wtforms.validators.Required()],
description='Name of the permission as a single word in lower case. '
'This is passed to the application when a user logs in. '
'Changing the name will not automatically update it everywhere. '
'You must reassign the permission to users who had it with the old name')
- title = wtf.TextField('Title', validators=[wtf.Required()],
+ title = wtforms.TextField('Title', validators=[wtforms.validators.Required()],
description='Permission title that is displayed to users')
- description = wtf.TextAreaField('Description',
+ description = wtforms.TextAreaField('Description',
description='An optional description of what the permission is for')
- context = wtf.RadioField('Context', validators=[wtf.Required()],
+ context = wtforms.RadioField('Context', validators=[wtforms.validators.Required()],
description='Context where this permission is available')
def validate(self):
@@ -110,7 +111,7 @@ def validate_context(self, field):
else:
orgs = [org for org in g.user.organizations_owned() if org.userid == field.data]
if len(orgs) != 1:
- raise wtf.ValidationError("Invalid context")
+ raise wtforms.ValidationError("Invalid context")
self.user = None
self.org = orgs[0]
@@ -119,14 +120,14 @@ class UserPermissionAssignForm(Form):
"""
Assign permissions to a user
"""
- username = wtf.TextField("User", validators=[wtf.Required()],
+ username = wtforms.TextField("User", validators=[wtforms.validators.Required()],
description='Lookup a user by their username or email address')
- perms = wtf.SelectMultipleField("Permissions", validators=[wtf.Required()])
+ perms = wtforms.SelectMultipleField("Permissions", validators=[wtforms.validators.Required()])
def validate_username(self, field):
existing = getuser(field.data)
if existing is None:
- raise wtf.ValidationError("User does not exist")
+ raise wtforms.ValidationError("User does not exist")
self.user = existing
@@ -134,14 +135,14 @@ class TeamPermissionAssignForm(Form):
"""
Assign permissions to a team
"""
- team_id = wtf.RadioField("Team", validators=[wtf.Required()],
+ team_id = wtforms.RadioField("Team", validators=[wtforms.validators.Required()],
description='Select a team to assign permissions to')
- perms = wtf.SelectMultipleField("Permissions", validators=[wtf.Required()])
+ perms = wtforms.SelectMultipleField("Permissions", validators=[wtforms.validators.Required()])
def validate_team_id(self, field):
teams = [team for team in self.org.teams if team.userid == field.data]
if len(teams) != 1:
- raise wtf.ValidationError("Unknown team")
+ raise wtforms.ValidationError("Unknown team")
self.team = teams[0]
@@ -149,65 +150,65 @@ class PermissionEditForm(Form):
"""
Edit a user or team's permissions
"""
- perms = wtf.SelectMultipleField("Permissions", validators=[wtf.Required()])
+ perms = wtforms.SelectMultipleField("Permissions", validators=[wtforms.validators.Required()])
class ResourceForm(Form):
"""
Edit a resource provided by an application
"""
- name = wtf.TextField('Resource name', validators=[wtf.Required()],
+ name = wtforms.TextField('Resource name', validators=[wtforms.validators.Required()],
description="Name of the resource as a single word in lower case. "
"This is provided by applications as part of the scope "
"when requesting access to a user's resources.")
- title = wtf.TextField('Title', validators=[wtf.Required()],
+ title = wtforms.TextField('Title', validators=[wtforms.validators.Required()],
description='Resource title that is displayed to users')
- description = wtf.TextAreaField('Description',
+ description = wtforms.TextAreaField('Description',
description='An optional description of what the resource is')
- siteresource = wtf.BooleanField('Site resource',
+ siteresource = wtforms.BooleanField('Site resource',
description='Enable if this resource is generic to the site and not owned by specific users')
- trusted = wtf.BooleanField('Trusted applications only',
+ trusted = wtforms.BooleanField('Trusted applications only',
description='Enable if access to the resource should be restricted to trusted '
'applications. You may want to do this for sensitive information like billing data')
def validate_name(self, field):
if not valid_username(field.data):
- raise wtf.ValidationError("Name contains invalid characters.")
+ raise wtforms.ValidationError("Name contains invalid characters.")
if field.data in resource_registry:
- raise wtf.ValidationError("This name is reserved for internal use")
+ raise wtforms.ValidationError("This name is reserved for internal use")
existing = Resource.query.filter_by(name=field.data).first()
if existing and existing.id != self.edit_id:
- raise wtf.ValidationError("A resource with that name already exists")
+ raise wtforms.ValidationError("A resource with that name already exists")
class ResourceActionForm(Form):
"""
Edit an action associated with a resource
"""
- name = wtf.TextField('Action name', validators=[wtf.Required()],
+ name = wtforms.TextField('Action name', validators=[wtforms.validators.Required()],
description="Name of the action as a single word in lower case. "
"This is provided by applications as part of the scope in the form "
"'resource/action' when requesting access to a user's resources. "
"Read actions are implicit when applications request just 'resource' "
"in the scope and do not need to be specified as an explicit action.")
- title = wtf.TextField('Title', validators=[wtf.Required()],
+ title = wtforms.TextField('Title', validators=[wtforms.validators.Required()],
description='Action title that is displayed to users')
- description = wtf.TextAreaField('Description',
+ description = wtforms.TextAreaField('Description',
description='An optional description of what the action is')
def validate_name(self, field):
if not valid_username(field.data):
- raise wtf.ValidationError("Name contains invalid characters.")
+ raise wtforms.ValidationError("Name contains invalid characters.")
existing = ResourceAction.query.filter_by(name=field.data, resource=self.edit_resource).first()
if existing and existing.id != self.edit_id:
- raise wtf.ValidationError("An action with that name already exists for this resource")
+ raise wtforms.ValidationError("An action with that name already exists for this resource")
class ClientTeamAccessForm(Form):
"""
Select organizations that the client has access to the teams of
"""
- organizations = wtf.SelectMultipleField('Organizations')
+ organizations = wtforms.SelectMultipleField('Organizations')
diff --git a/lastuser_ui/forms/org.py b/lastuser_ui/forms/org.py
index 1a9cf11..090d670 100644
--- a/lastuser_ui/forms/org.py
+++ b/lastuser_ui/forms/org.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from flask import current_app
-import flask.ext.wtf as wtf
+import wtforms
from coaster import valid_username
from baseframe.forms import Form, HiddenMultiField
@@ -9,9 +9,9 @@
class OrganizationForm(Form):
- title = wtf.TextField('Organization name', validators=[wtf.Required()])
- name = wtf.TextField('URL name', validators=[wtf.Required()])
- description = wtf.TextAreaField('Description')
+ title = wtforms.TextField('Organization name', validators=[wtforms.validators.Required()])
+ name = wtforms.TextField('URL name', validators=[wtforms.validators.Required()])
+ description = wtforms.TextAreaField('Description')
def validate_name(self, field):
if not valid_username(field.data):
@@ -27,5 +27,5 @@ def validate_name(self, field):
class TeamForm(Form):
- title = wtf.TextField('Team name', validators=[wtf.Required()])
- users = HiddenMultiField('Users', validators=[wtf.Required()])
+ title = wtforms.TextField('Team name', validators=[wtforms.validators.Required()])
+ users = HiddenMultiField('Users', validators=[wtforms.validators.Required()])
diff --git a/lastuser_ui/forms/profile.py b/lastuser_ui/forms/profile.py
index f66a6c6..a277ca6 100644
--- a/lastuser_ui/forms/profile.py
+++ b/lastuser_ui/forms/profile.py
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
from flask import g
-import flask.ext.wtf as wtf
+import wtforms
+import wtforms.fields.html5
from baseframe.forms import Form, ValidEmailDomain
from lastuser_core.utils import strip_phone, valid_phone
@@ -9,7 +10,7 @@
class NewEmailAddressForm(Form):
- email = wtf.html5.EmailField('Email address', validators=[wtf.Required(), wtf.Email(), ValidEmailDomain()])
+ email = wtforms.fields.html5.EmailField('Email address', validators=[wtforms.validators.Required(), wtforms.validators.Email(), ValidEmailDomain()])
# TODO: Move to function and place before ValidEmailDomain()
def validate_email(self, field):
@@ -17,41 +18,41 @@ def validate_email(self, field):
existing = UserEmail.query.filter_by(email=field.data).first()
if existing is not None:
if existing.user == g.user:
- raise wtf.ValidationError("You have already registered this email address.")
+ raise wtforms.ValidationError("You have already registered this email address.")
else:
- raise wtf.ValidationError("This email address has already been claimed.")
+ raise wtforms.ValidationError("This email address has already been claimed.")
existing = UserEmailClaim.query.filter_by(email=field.data, user=g.user).first()
if existing is not None:
- raise wtf.ValidationError("This email address is pending verification.")
+ raise wtforms.ValidationError("This email address is pending verification.")
class NewPhoneForm(Form):
- phone = wtf.TextField('Phone number', default='+91', validators=[wtf.Required()],
+ phone = wtforms.TextField('Phone number', default='+91', validators=[wtforms.validators.Required()],
description="Indian mobile numbers only")
def validate_phone(self, field):
existing = UserPhone.query.filter_by(phone=field.data).first()
if existing is not None:
if existing.user == g.user:
- raise wtf.ValidationError("You have already registered this phone number.")
+ raise wtforms.ValidationError("You have already registered this phone number.")
else:
- raise wtf.ValidationError("That phone number has already been claimed.")
+ raise wtforms.ValidationError("That phone number has already been claimed.")
existing = UserPhoneClaim.query.filter_by(phone=field.data, user=g.user).first()
if existing is not None:
- raise wtf.ValidationError("That phone number is pending verification.")
+ raise wtforms.ValidationError("That phone number is pending verification.")
# Step 1: Remove punctuation in number
field.data = strip_phone(field.data)
# Step 2: Validate number format
if not valid_phone(field.data):
- raise wtf.ValidationError("Invalid phone number (must be in international format with a leading + symbol)")
+ raise wtforms.ValidationError("Invalid phone number (must be in international format with a leading + symbol)")
# Step 3: Check if Indian number (startswith('+91'))
if not field.data.startswith('+91') or len(field.data) != 13:
- raise wtf.ValidationError("Only Indian mobile numbers are allowed at this time")
+ raise wtforms.ValidationError("Only Indian mobile numbers are allowed at this time")
class VerifyPhoneForm(Form):
- verification_code = wtf.TextField('Verification code', validators=[wtf.Required()])
+ verification_code = wtforms.TextField('Verification code', validators=[wtforms.validators.Required()])
def validate_verification_code(self, field):
if self.phoneclaim.verification_code != field.data:
- raise wtf.ValidationError("Verification code does not match.")
+ raise wtforms.ValidationError("Verification code does not match.")
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..a27c7f0
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,8 @@
+[nosetests]
+match=^test
+nocapture=1
+cover-package=lastuser_core, lastuser_oauth, lastuser_ui
+with-coverage=1
+cover-erase=1
+with-doctest=1
+where=tests
diff --git a/sitecustomize.py b/sitecustomize.py
index 0cbc9eb..6752327 100644
--- a/sitecustomize.py
+++ b/sitecustomize.py
@@ -1,3 +1,5 @@
# Required to make OpenID work with Wordpress (first instance where it came up)
import sys
+if not hasttr(sys, 'setdefaultencoding'):
+ reload(sys)
sys.setdefaultencoding("utf-8")
diff --git a/test_requirements.txt b/test_requirements.txt
new file mode 100644
index 0000000..a6c88f5
--- /dev/null
+++ b/test_requirements.txt
@@ -0,0 +1,2 @@
+nose
+coverage
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/fixtures.py b/tests/fixtures.py
new file mode 100644
index 0000000..969668b
--- /dev/null
+++ b/tests/fixtures.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+
+from coaster import buid
+from lastuserapp import db, app
+from lastuser_core.models import *
+
+def make_fixtures():
+ user1 = User(username=u"user1", fullname=u"User 1")
+ user2 = User(username=u"user2", fullname=u"User 2")
+ db.session.add_all([user1, user2])
+
+ email1 = UserEmail(email=u"user1@example.com", user=user1)
+ phone1 = UserPhone(phone=u"1234567890", user=user1)
+ email2 = UserEmail(email=u"user2@example.com", user=user2)
+ phone2 = UserPhone(phone=u"1234567891", user=user2)
+ db.session.add_all([email1, phone1, email2, phone2])
+
+ org = Organization(name=u"org", title=u"Organization")
+ org.owners.users.append(user1)
+ db.session.add(org)
+
+ client = Client(title=u"Test Application", org=org, user=user1, website=u"http://example.com")
+ db.session.add(client)
+
+ resource = Resource(name=u"test_resource", title=u"Test Resource", client=client)
+ db.session.add(resource)
+
+ action = ResourceAction(name=u"read", title=u"Read", resource=resource)
+ db.session.add(action)
+
+ db.session.commit()
diff --git a/tests/test_db.py b/tests/test_db.py
new file mode 100644
index 0000000..e5c4105
--- /dev/null
+++ b/tests/test_db.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+
+import unittest
+from lastuserapp import app, db, init_for
+from .fixtures import make_fixtures
+
+class TestDatabaseFixture(unittest.TestCase):
+ def setUp(self):
+ init_for('testing')
+ app.config['TESTING'] = True
+ db.app = app
+ db.drop_all()
+ db.create_all()
+ self.db = db
+ make_fixtures()
+
+ def tearDown(self):
+ self.db.drop_all()