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()