diff --git a/.travis.yml b/.travis.yml
index 9b02f3efe..1f24428d4 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,16 +1,14 @@
language: python
python:
- - 2.7
-cache:
- directories:
- - $HOME/.cache/pip
+ - 3.7
before_script:
- psql -c 'create database hasjob_testing;' -U postgres
install:
- pip install -U pip wheel
- - pip install -r test_requirements.txt
- - pip install -r requirements.txt
+ - pip install --no-cache-dir -r test_requirements.txt
+ - pip install --no-cache-dir -r requirements.txt
- npm install casperjs
+ - make
script: ./runtests.sh
after_success:
- coveralls
@@ -20,6 +18,3 @@ services:
- redis-server
notifications:
email: false
- slack:
- - hasgeek:HDCoMDj3T4ICB59qFFVorCG8
- - friendsofhasgeek:3bLViYSzhfaThJovFYCVD3fX
diff --git a/get-twitter.py b/get-twitter.py
index 29b1ee390..bdfe97715 100644
--- a/get-twitter.py
+++ b/get-twitter.py
@@ -10,8 +10,8 @@
init_for('dev')
auth = tweepy.OAuthHandler(app.config['TWITTER_CONSUMER_KEY'], app.config['TWITTER_CONSUMER_SECRET'])
auth_url = auth.get_authorization_url()
-print 'Please authorize: ' + auth_url
-verifier = raw_input('PIN: ').strip()
+print('Please authorize: ' + auth_url)
+verifier = input('PIN: ').strip()
auth.get_access_token(verifier)
-print "TWITTER_ACCESS_KEY = '%s'" % auth.access_token.key
-print "TWITTER_ACCESS_SECRET = '%s'" % auth.access_token.secret
+print("TWITTER_ACCESS_KEY = '%s'" % auth.access_token.key)
+print("TWITTER_ACCESS_SECRET = '%s'" % auth.access_token.secret)
diff --git a/hasjob/extapi/location.py b/hasjob/extapi/location.py
index 4868f40f5..0746c12b8 100644
--- a/hasjob/extapi/location.py
+++ b/hasjob/extapi/location.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-from urlparse import urljoin
+from urllib.parse import urljoin
from simplejson import JSONDecodeError
import requests
from baseframe import cache
diff --git a/hasjob/forms/all.py b/hasjob/forms/all.py
index 84cae64fc..24853ecf6 100644
--- a/hasjob/forms/all.py
+++ b/hasjob/forms/all.py
@@ -17,20 +17,20 @@ class ConfirmForm(forms.Form):
class WithdrawForm(forms.Form):
really_withdraw = forms.BooleanField(__("Yes, I really want to withdraw the job post"),
- validators=[forms.validators.DataRequired(__(u"You must confirm withdrawal"))])
+ validators=[forms.validators.DataRequired(__("You must confirm withdrawal"))])
class ReportForm(forms.Form):
- report_code = forms.RadioField(__("Code"), coerce=int, validators=[forms.validators.InputRequired(__(u"Pick one"))])
+ report_code = forms.RadioField(__("Code"), coerce=int, validators=[forms.validators.InputRequired(__("Pick one"))])
class RejectForm(forms.Form):
- reason = forms.StringField(__("Reason"), validators=[forms.validators.DataRequired(__(u"Give a reason"))])
+ reason = forms.StringField(__("Reason"), validators=[forms.validators.DataRequired(__("Give a reason"))])
class ModerateForm(forms.Form):
reason = forms.TextAreaField(__("Reason"),
- validators=[forms.validators.DataRequired(__(u"Give a reason")), forms.validators.Length(max=250)])
+ validators=[forms.validators.DataRequired(__("Give a reason")), forms.validators.Length(max=250)])
class PinnedForm(forms.Form):
@@ -43,5 +43,5 @@ class NewLocationForm(forms.Form):
class EditLocationForm(forms.Form):
title = forms.StringField(__("Page title"),
- validators=[forms.validators.DataRequired(__(u"This location needs a name"))])
+ validators=[forms.validators.DataRequired(__("This location needs a name"))])
description = forms.TinyMce4Field(__("Description"), content_css=content_css)
diff --git a/hasjob/forms/board.py b/hasjob/forms/board.py
index 1897f9b64..6d4eaf259 100644
--- a/hasjob/forms/board.py
+++ b/hasjob/forms/board.py
@@ -18,47 +18,47 @@
def jobtype_label(jobtype):
annotations = []
if jobtype.nopay_allowed:
- annotations.append(_(u'zero pay allowed'))
+ annotations.append(_('zero pay allowed'))
if jobtype.webmail_allowed:
- annotations.append(_(u'webmail address allowed'))
+ annotations.append(_('webmail address allowed'))
if annotations:
- return Markup(u'%s (%s)') % (jobtype.title, u', '.join(annotations))
+ return Markup('%s (%s)') % (jobtype.title, ', '.join(annotations))
else:
return jobtype.title
class BoardOptionsForm(forms.Form):
- restrict_listing = forms.BooleanField(__(u"Restrict direct posting on this board to owners and the following users"),
+ restrict_listing = forms.BooleanField(__("Restrict direct posting on this board to owners and the following users"),
default=True,
- description=__(u"As the owner of this board, you can always cross-add jobs from other boards on Hasjob"))
- posting_users = forms.UserSelectMultiField(__(u"Allowed users"),
- description=__(u"These users will be allowed to post jobs on this board under the following terms"),
+ description=__("As the owner of this board, you can always cross-add jobs from other boards on Hasjob"))
+ posting_users = forms.UserSelectMultiField(__("Allowed users"),
+ description=__("These users will be allowed to post jobs on this board under the following terms"),
usermodel=User, lastuser=lastuser)
# Allow turning this off only in siteadmin-approved boards (deleted in the view for non-siteadmins)
- require_pay = forms.BooleanField(__(u"Require pay data for posting on this board?"), default=True,
- description=__(u"Hasjob requires employers to reveal what they intend to pay, "
- u"but you can make it optional for jobs posted from this board. "
- u"Pay data is used to match candidates to jobs. We recommend you collect it"))
- newjob_headline = forms.StringField(__(u"Headline"),
- description=__(u"Optional – The sample headline shown to employers when posting a job"),
+ require_pay = forms.BooleanField(__("Require pay data for posting on this board?"), default=True,
+ description=__("Hasjob requires employers to reveal what they intend to pay, "
+ "but you can make it optional for jobs posted from this board. "
+ "Pay data is used to match candidates to jobs. We recommend you collect it"))
+ newjob_headline = forms.StringField(__("Headline"),
+ description=__("Optional – The sample headline shown to employers when posting a job"),
validators=[
forms.validators.Length(min=0, max=100, message=__("%%(max)d characters maximum"))],
filters=[forms.filters.strip(), forms.filters.none_if_empty()])
- newjob_blurb = forms.TinyMce4Field(__(u"Posting instructions"),
- description=__(u"Optional – What should we tell employers when they post a job on your board? "
- u"Leave blank to use the default text"),
+ newjob_blurb = forms.TinyMce4Field(__("Posting instructions"),
+ description=__("Optional – What should we tell employers when they post a job on your board? "
+ "Leave blank to use the default text"),
content_css=content_css,
validators=[forms.validators.AllUrlsValid()])
types = QuerySelectMultipleField(__("Job types"),
widget=ListWidget(), option_widget=CheckboxInput(),
query_factory=lambda: JobType.query.filter_by(private=False).order_by(JobType.seq), get_label=jobtype_label,
- validators=[forms.validators.DataRequired(__(u"You need to select at least one job type"))],
- description=__(u"Jobs listed directly on this board can use one of the types enabled here"))
+ validators=[forms.validators.DataRequired(__("You need to select at least one job type"))],
+ description=__("Jobs listed directly on this board can use one of the types enabled here"))
categories = QuerySelectMultipleField(__("Job categories"),
widget=ListWidget(), option_widget=CheckboxInput(),
query_factory=lambda: JobCategory.query.filter_by(private=False).order_by(JobCategory.seq), get_label='title',
- validators=[forms.validators.DataRequired(__(u"You need to select at least one category"))],
- description=__(u"Jobs listed directly on this board can use one of the categories enabled here"))
+ validators=[forms.validators.DataRequired(__("You need to select at least one category"))],
+ description=__("Jobs listed directly on this board can use one of the categories enabled here"))
class BoardTaggingForm(forms.Form):
@@ -72,21 +72,21 @@ class BoardTaggingForm(forms.Form):
description=__("Jobs tagged with these keywords will be automatically added to this board"))
auto_types = QuerySelectMultipleField(__("Job types"),
query_factory=lambda: JobType.query.filter_by(private=False).order_by(JobType.seq), get_label='title',
- description=__(u"Jobs of this type will be automatically added to this board"))
+ description=__("Jobs of this type will be automatically added to this board"))
auto_categories = QuerySelectMultipleField(__("Job categories"),
query_factory=lambda: JobCategory.query.filter_by(private=False).order_by(JobCategory.seq), get_label='title',
- description=__(u"Jobs of this category will be automatically added to this board"))
+ description=__("Jobs of this category will be automatically added to this board"))
auto_all = forms.BooleanField(__("All of the above criteria must match"),
- description=__(u"Select this if, for example, you want to match all programming jobs in Bangalore "
- u"and not all programming jobs or Bangalore-based jobs."))
+ description=__("Select this if, for example, you want to match all programming jobs in Bangalore "
+ "and not all programming jobs or Bangalore-based jobs."))
def validate_auto_domains(self, field):
relist = []
for item in field.data:
item = item.strip()
- if u',' in item:
+ if ',' in item:
relist.extend([x.strip() for x in item.split(',')])
- elif u' ' in item:
+ elif ' ' in item:
relist.extend([x.strip() for x in item.split(' ')])
else:
relist.append(item)
@@ -96,7 +96,7 @@ def validate_auto_domains(self, field):
if item:
# FIXME: This will break domains where the subdomain handles email
r = tldextract.extract(item.lower())
- d = u'.'.join([r.domain, r.suffix])
+ d = '.'.join([r.domain, r.suffix])
if not is_public_email_domain(d, default=False):
domains.add(d)
field.data = list(domains)
@@ -120,29 +120,29 @@ class BoardForm(forms.Form):
forms.validators.Optional(),
forms.validators.Length(min=0, max=80, message=__("%%(max)d characters maximum"))],
filters=[forms.filters.strip(), forms.filters.none_if_empty()])
- name = forms.AnnotatedTextField(__("URL Name"), prefix='https://', suffix=u'.hasjob.co',
- description=__(u"Optional — Will be autogenerated if blank"),
+ name = forms.AnnotatedTextField(__("URL Name"), prefix='https://', suffix='.hasjob.co',
+ description=__("Optional — Will be autogenerated if blank"),
validators=[
forms.validators.ValidName(),
forms.validators.Length(min=0, max=63, message=__("%%(max)d characters maximum")),
- AvailableName(__(u"This name has been taken by another board"), model=Board)])
- description = forms.TinyMce4Field(__(u"Description"),
- description=__(u"The description appears at the top of the board, above all jobs. "
- u"Use it to introduce your board and keep it brief"),
+ AvailableName(__("This name has been taken by another board"), model=Board)])
+ description = forms.TinyMce4Field(__("Description"),
+ description=__("The description appears at the top of the board, above all jobs. "
+ "Use it to introduce your board and keep it brief"),
content_css=content_css,
validators=[forms.validators.DataRequired(__("A description of the job board is required")),
forms.validators.AllUrlsValid()])
- userid = forms.SelectField(__(u"Owner"), validators=[forms.validators.DataRequired(__("Select an owner"))],
- description=__(u"Select the user, organization or team who owns this board. "
+ userid = forms.SelectField(__("Owner"), validators=[forms.validators.DataRequired(__("Select an owner"))],
+ description=__("Select the user, organization or team who owns this board. "
"Owners can add jobs to the board and edit these settings"))
- require_login = forms.BooleanField(__(u"Prompt users to login"), default=True,
- description=__(u"If checked, users must login to see all jobs available. "
- u"Logging in provides users better filtering for jobs that may be of interest to them, "
- u"and allows employers to understand how well their post is performing"))
- options = forms.FormField(BoardOptionsForm, __(u"Direct posting options"))
- autotag = forms.FormField(BoardTaggingForm, __(u"Automatic posting options"))
+ require_login = forms.BooleanField(__("Prompt users to login"), default=True,
+ description=__("If checked, users must login to see all jobs available. "
+ "Logging in provides users better filtering for jobs that may be of interest to them, "
+ "and allows employers to understand how well their post is performing"))
+ options = forms.FormField(BoardOptionsForm, __("Direct posting options"))
+ autotag = forms.FormField(BoardTaggingForm, __("Automatic posting options"))
def validate_name(self, field):
if field.data:
if field.data in Board.reserved_names:
- raise forms.ValidationError(_(u"This name is reserved. Please use another name"))
+ raise forms.ValidationError(_("This name is reserved. Please use another name"))
diff --git a/hasjob/forms/campaign.py b/hasjob/forms/campaign.py
index 3a13de9a8..918a65a02 100644
--- a/hasjob/forms/campaign.py
+++ b/hasjob/forms/campaign.py
@@ -24,7 +24,7 @@ class CampaignContentForm(forms.Form):
validators=[forms.validators.Optional(), forms.validators.AllUrlsValid()])
banner_image = forms.URLField(__("Banner image URL"), validators=[forms.validators.Optional()], # TODO: Use ImgeeField
description=__("An image to illustrate your campaign"))
- banner_location = forms.RadioField(__("Banner location"), choices=BANNER_LOCATION.items(), coerce=int,
+ banner_location = forms.RadioField(__("Banner location"), choices=list(BANNER_LOCATION.items()), coerce=int,
description=__("Where should this banner appear relative to text?"))
@@ -34,10 +34,10 @@ class CampaignForm(forms.Form):
filters=[forms.filters.strip()])
start_at = forms.DateTimeField(__("Start at"), naive=False)
end_at = forms.DateTimeField(__("End at"),
- validators=[forms.validators.GreaterThan('start_at', __(u"The campaign can’t end before it starts"))],
+ validators=[forms.validators.GreaterThan('start_at', __("The campaign can’t end before it starts"))],
naive=False)
public = forms.BooleanField(__("This campaign is live"))
- position = forms.RadioField(__("Display position"), choices=CAMPAIGN_POSITION.items(), coerce=int)
+ position = forms.RadioField(__("Display position"), choices=list(CAMPAIGN_POSITION.items()), coerce=int)
priority = forms.IntegerField(__("Priority"), default=0,
description=__("A larger number is higher priority when multiple campaigns are running on the "
"same dates. 0 implies lowest priority"))
@@ -45,17 +45,17 @@ class CampaignForm(forms.Form):
widget=ListWidget(), option_widget=CheckboxInput(),
query_factory=lambda: Board.query.order_by(Board.featured.desc(), Board.title), get_label='title_and_name',
validators=[forms.validators.Optional()],
- description=__(u"Select the boards this campaign is active on"))
+ description=__("Select the boards this campaign is active on"))
geonameids = forms.GeonameSelectMultiField("Locations",
description=__("This campaign will be targetted at users and jobs with matching locations"))
user_required = forms.RadioField(__("User is required"), coerce=getbool,
choices=[
- (None, __(u"N/A – Don’t target by login status")),
- (True, __(u"Yes – Show to logged in users only")),
- (False, __(u"No – Show to anonymous users only"))])
+ (None, __("N/A – Don’t target by login status")),
+ (True, __("Yes – Show to logged in users only")),
+ (False, __("No – Show to anonymous users only"))])
flags = forms.RadioMatrixField("Flags", coerce=getbool, fields=Campaign.flag_choices,
description=__("All selected flags must match the logged in user for the campaign to be shown"),
- choices=[('None', __(u"N/A")), ('True', __(u"True")), ('False', __(u"False"))])
+ choices=[('None', __("N/A")), ('True', __("True")), ('False', __("False"))])
content = forms.FormField(CampaignContentForm, __("Campaign content"))
def validate_geonameids(self, field):
@@ -69,24 +69,24 @@ class CampaignActionForm(forms.Form):
icon = forms.StringField(__("Icon"), validators=[forms.validators.Optional()], filters=[forms.filters.none_if_empty()],
description=__("Optional Font-Awesome icon name"))
public = forms.BooleanField(__("This action is live"))
- type = forms.RadioField(__("Type"), choices=CAMPAIGN_ACTION.items(), validators=[forms.validators.DataRequired(__("This is required"))])
+ type = forms.RadioField(__("Type"), choices=list(CAMPAIGN_ACTION.items()), validators=[forms.validators.DataRequired(__("This is required"))])
group = forms.StringField(__("RSVP group"), validators=[forms.validators.Optional()],
filters=[forms.filters.none_if_empty()],
description=__("If you have multiple RSVP actions, add an optional group name"))
category = forms.RadioField(__("Category"), validators=[forms.validators.DataRequired(__("This is required"))],
widget=forms.InlineListWidget(class_='button-bar', class_prefix='btn btn-'), choices=[
- (u'default', __(u"Default")),
- (u'primary', __(u"Primary")),
- (u'success', __(u"Success")),
- (u'info', __(u"Info")),
- (u'warning', __(u"Warning")),
- (u'danger', __(u"Danger")),
+ ('default', __("Default")),
+ ('primary', __("Primary")),
+ ('success', __("Success")),
+ ('info', __("Info")),
+ ('warning', __("Warning")),
+ ('danger', __("Danger")),
])
message = forms.TinyMce4Field(__("Message"),
description=__("Message shown after the user has performed an action (for forms and RSVP type)"),
content_css=content_css,
validators=[forms.validators.Optional(), forms.validators.AllUrlsValid()])
- link = forms.URLField(__("Link"), description=__(u"URL to redirect to, if type is “follow link”"),
+ link = forms.URLField(__("Link"), description=__("URL to redirect to, if type is “follow link”"),
validators=[
optional_url,
forms.validators.Length(min=0, max=250, message=__("%%(max)d characters maximum")),
diff --git a/hasjob/forms/domain.py b/hasjob/forms/domain.py
index 6b7d86957..ce97319dd 100644
--- a/hasjob/forms/domain.py
+++ b/hasjob/forms/domain.py
@@ -9,7 +9,7 @@
class DomainForm(forms.Form):
- title = forms.StringField(__(u"Common name"),
+ title = forms.StringField(__("Common name"),
validators=[forms.validators.DataRequired(),
forms.validators.Length(min=1, max=250, message=__("%(max)d characters maximum"))],
filters=[forms.filters.strip()],
@@ -18,7 +18,7 @@ class DomainForm(forms.Form):
validators=[forms.validators.Optional(),
forms.validators.Length(min=1, max=250, message=__("%%(max)d characters maximum"))],
filters=[forms.filters.none_if_empty()],
- description=__(u"Optional — The full legal name of your organization"))
+ description=__("Optional — The full legal name of your organization"))
# logo_url = forms.URLField(__("Logo URL"), # TODO: Use ImgeeField
# validators=[forms.validators.Optional(),
# forms.validators.Length(min=0, max=250, message=__("%%(max)d characters maximum"))],
diff --git a/hasjob/forms/filterset.py b/hasjob/forms/filterset.py
index 5fe06eb30..fa7801ffe 100644
--- a/hasjob/forms/filterset.py
+++ b/hasjob/forms/filterset.py
@@ -18,7 +18,7 @@ def format_geonameids(geonameids):
def get_currency_choices():
choices = [('', __('None'))]
- choices.extend(CURRENCY.items())
+ choices.extend(list(CURRENCY.items()))
return choices
diff --git a/hasjob/forms/helper.py b/hasjob/forms/helper.py
index cc443dc09..7c7b111c9 100644
--- a/hasjob/forms/helper.py
+++ b/hasjob/forms/helper.py
@@ -27,4 +27,4 @@ def optional_url(form, field):
try:
return validator(form, field)
except forms.ValidationError as e:
- raise forms.StopValidation(unicode(e))
+ raise forms.StopValidation(str(e))
diff --git a/hasjob/forms/jobpost.py b/hasjob/forms/jobpost.py
index 2a0bb4656..3794defd9 100644
--- a/hasjob/forms/jobpost.py
+++ b/hasjob/forms/jobpost.py
@@ -19,7 +19,7 @@
from . import content_css, invalid_urls, optional_url
-QUOTES_RE = re.compile(ur'[\'"`‘’“”′″‴«»]+')
+QUOTES_RE = re.compile(r'[\'"`‘’“”′″‴«»]+')
CAPS_RE = re.compile('[A-Z]')
SMALL_RE = re.compile('[a-z]')
@@ -47,104 +47,104 @@ class ListingForm(forms.Form):
"""A/B test it?""")),
validators=[forms.validators.DataRequired(__("A headline is required")),
forms.validators.Length(min=1, max=100, message=__("%%(max)d characters maximum")),
- forms.validators.NoObfuscatedEmail(__(u"Do not include contact information in the post"))],
+ forms.validators.NoObfuscatedEmail(__("Do not include contact information in the post"))],
filters=[forms.filters.strip()])
job_headlineb = forms.StringField(__("Headline B"),
- description=__(u"An alternate headline that will be shown to 50%% of users. "
- u"You’ll get a count of views per headline"),
+ description=__("An alternate headline that will be shown to 50%% of users. "
+ "You’ll get a count of views per headline"),
validators=[forms.validators.Optional(),
forms.validators.Length(min=1, max=100, message=__("%%(max)d characters maximum")),
- forms.validators.NoObfuscatedEmail(__(u"Do not include contact information in the post"))],
+ forms.validators.NoObfuscatedEmail(__("Do not include contact information in the post"))],
filters=[forms.filters.strip(), forms.filters.none_if_empty()])
job_type = forms.RadioField(__("Type"), coerce=int,
validators=[forms.validators.InputRequired(__("The job type must be specified"))])
job_category = forms.RadioField(__("Category"), coerce=int,
validators=[forms.validators.InputRequired(__("Select a category"))])
job_location = forms.StringField(__("Location"),
- description=__(u'“Bangalore”, “Chennai”, “Pune”, etc or “Anywhere” (without quotes)'),
- validators=[forms.validators.DataRequired(__(u"If this job doesn’t have a fixed location, use “Anywhere”")),
+ description=__('“Bangalore”, “Chennai”, “Pune”, etc or “Anywhere” (without quotes)'),
+ validators=[forms.validators.DataRequired(__("If this job doesn’t have a fixed location, use “Anywhere”")),
forms.validators.Length(min=3, max=80, message=__("%%(max)d characters maximum"))],
filters=[forms.filters.strip()])
job_relocation_assist = forms.BooleanField(__("Relocation assistance available"))
job_description = forms.TinyMce4Field(__("Description"),
content_css=content_css,
- description=__(u"Don’t just describe the job, tell a compelling story for why someone should work for you"),
+ description=__("Don’t just describe the job, tell a compelling story for why someone should work for you"),
validators=[forms.validators.DataRequired(__("A description of the job is required")),
forms.validators.AllUrlsValid(invalid_urls=invalid_urls),
- forms.validators.NoObfuscatedEmail(__(u"Do not include contact information in the post"))],
+ forms.validators.NoObfuscatedEmail(__("Do not include contact information in the post"))],
tinymce_options={'convert_urls': True})
job_perks = forms.BooleanField(__("Job perks are available"))
job_perks_description = forms.TinyMce4Field(__("Describe job perks"),
content_css=content_css,
- description=__(u"Stock options, free lunch, free conference passes, etc"),
+ description=__("Stock options, free lunch, free conference passes, etc"),
validators=[forms.validators.AllUrlsValid(invalid_urls=invalid_urls),
- forms.validators.NoObfuscatedEmail(__(u"Do not include contact information in the post"))])
+ forms.validators.NoObfuscatedEmail(__("Do not include contact information in the post"))])
job_pay_type = forms.RadioField(__("What does this job pay?"), coerce=int,
validators=[forms.validators.InputRequired(__("You need to specify what this job pays"))],
- choices=PAY_TYPE.items())
- job_pay_currency = ListingPayCurrencyField(__("Currency"), choices=CURRENCY.items(), default=CURRENCY.INR)
+ choices=list(PAY_TYPE.items()))
+ job_pay_currency = ListingPayCurrencyField(__("Currency"), choices=list(CURRENCY.items()), default=CURRENCY.INR)
job_pay_cash_min = forms.StringField(__("Minimum"))
job_pay_cash_max = forms.StringField(__("Maximum"))
job_pay_equity = forms.BooleanField(__("Equity compensation is available"))
job_pay_equity_min = forms.StringField(__("Minimum"))
job_pay_equity_max = forms.StringField(__("Maximum"))
job_how_to_apply = forms.TextAreaField(__("What should a candidate submit when applying for this job?"),
- description=__(u"Example: “Include your LinkedIn and GitHub profiles.” "
- u"We now require candidates to apply through the job board only. "
- u"Do not include any contact information here. Candidates CANNOT "
- u"attach resumes or other documents, so do not ask for that"),
+ description=__("Example: “Include your LinkedIn and GitHub profiles.” "
+ "We now require candidates to apply through the job board only. "
+ "Do not include any contact information here. Candidates CANNOT "
+ "attach resumes or other documents, so do not ask for that"),
validators=[
- forms.validators.DataRequired(__(u"We do not offer screening services. Please specify what candidates should submit")),
- forms.validators.NoObfuscatedEmail(__(u"Do not include contact information in the post"))])
+ forms.validators.DataRequired(__("We do not offer screening services. Please specify what candidates should submit")),
+ forms.validators.NoObfuscatedEmail(__("Do not include contact information in the post"))])
company_name = forms.StringField(__("Employer name"),
- description=__(u"The name of the organization where the position is. "
- u"If your stealth startup doesn't have a name yet, use your own. "
- u"We do not accept posts from third parties such as recruitment consultants. "
- u"Such posts may be removed without notice"),
- validators=[forms.validators.DataRequired(__(u"This is required. Posting any name other than that of the actual organization is a violation of the ToS")),
+ description=__("The name of the organization where the position is. "
+ "If your stealth startup doesn't have a name yet, use your own. "
+ "We do not accept posts from third parties such as recruitment consultants. "
+ "Such posts may be removed without notice"),
+ validators=[forms.validators.DataRequired(__("This is required. Posting any name other than that of the actual organization is a violation of the ToS")),
forms.validators.Length(min=4, max=80, message=__("The name must be within %%(min)d to %%(max)d characters"))],
filters=[forms.filters.strip()])
company_logo = forms.FileField(__("Logo"),
- description=__(u"Optional — Your organization’s logo will appear at the top of your post."),
+ description=__("Optional — Your organization’s logo will appear at the top of your post."),
) # validators=[file_allowed(uploaded_logos, "That image type is not supported")])
company_logo_remove = forms.BooleanField(__("Remove existing logo"))
company_url = forms.URLField(__("URL"),
- description=__(u"Your organization’s website"),
+ description=__("Your organization’s website"),
validators=[forms.validators.DataRequired(), optional_url,
forms.validators.Length(max=255, message=__("%%(max)d characters maximum")), forms.validators.ValidUrl()],
filters=[forms.filters.strip()])
- hr_contact = forms.RadioField(__(u"Is it okay for recruiters and other "
- u"intermediaries to contact you about this post?"), coerce=getbool,
- description=__(u"We’ll display a notice to this effect on the post"),
+ hr_contact = forms.RadioField(__("Is it okay for recruiters and other "
+ "intermediaries to contact you about this post?"), coerce=getbool,
+ description=__("We’ll display a notice to this effect on the post"),
default=0,
- choices=[(0, __(u"No, it is NOT OK")), (1, __(u"Yes, recruiters may contact me"))])
+ choices=[(0, __("No, it is NOT OK")), (1, __("Yes, recruiters may contact me"))])
# Deprecated 2013-11-20
# poster_name = forms.StringField(__("Name"),
# description=__(u"This is your name, for our records. Will not be revealed to applicants"),
# validators=[forms.validators.DataRequired(__("We need your name"))])
poster_email = forms.EmailField(__("Email"),
- description=Markup(__(u"This is where we’ll send your confirmation email and all job applications. "
- u"We recommend using a shared email address such as jobs@your-organization.com. "
- u"Listings are classified by your email domain, "
- u"so use a work email address. "
- u"Your email address will not be revealed to applicants until you respond")),
+ description=Markup(__("This is where we’ll send your confirmation email and all job applications. "
+ "We recommend using a shared email address such as jobs@your-organization.com. "
+ "Listings are classified by your email domain, "
+ "so use a work email address. "
+ "Your email address will not be revealed to applicants until you respond")),
validators=[
forms.validators.DataRequired(__("We need to confirm your email address before the job can be listed")),
forms.validators.Length(min=5, max=80, message=__("%%(max)d characters maximum")),
forms.validators.ValidEmail(__("This does not appear to be a valid email address"))],
filters=[forms.filters.strip()])
twitter = forms.AnnotatedTextField(__("Twitter"),
- description=__(u"Optional — your organization’s Twitter account. "
- u"We’ll tweet mentioning you so you get included on replies"),
+ description=__("Optional — your organization’s Twitter account. "
+ "We’ll tweet mentioning you so you get included on replies"),
prefix='@', validators=[
forms.validators.Optional(),
- forms.validators.Length(min=0, max=15, message=__(u"Twitter accounts can’t be over %%(max)d characters long"))],
+ forms.validators.Length(min=0, max=15, message=__("Twitter accounts can’t be over %%(max)d characters long"))],
filters=[forms.filters.strip(), forms.filters.none_if_empty()])
- collaborators = forms.UserSelectMultiField(__(u"Collaborators"),
- description=__(u"If someone is helping you evaluate candidates, type their names here. "
- u"They must have a HasGeek account. They will not receive email notifications "
- u"— use a shared email address above for that — but they will be able to respond "
- u"to candidates who apply"),
+ collaborators = forms.UserSelectMultiField(__("Collaborators"),
+ description=__("If someone is helping you evaluate candidates, type their names here. "
+ "They must have a HasGeek account. They will not receive email notifications "
+ "— use a shared email address above for that — but they will be able to respond "
+ "to candidates who apply"),
usermodel=User, lastuser=lastuser)
def validate_twitter(self, field):
@@ -171,16 +171,16 @@ def validate_company_name(form, field):
# For now, only 6 capital letters are allowed in company name
if caps > 6:
- raise forms.ValidationError(_(u"Surely your organization isn’t named in uppercase?"))
+ raise forms.ValidationError(_("Surely your organization isn’t named in uppercase?"))
def validate_company_logo(form, field):
if not ('company_logo' in request.files and request.files['company_logo']):
return
try:
g.company_logo = process_image(request.files['company_logo'])
- except IOError, e:
+ except IOError as e:
raise forms.ValidationError(e.message)
- except KeyError, e:
+ except KeyError as e:
raise forms.ValidationError(_("Unknown file format"))
except UploadNotAllowed:
raise forms.ValidationError(_("Unsupported file format. We accept JPEG, PNG and GIF"))
@@ -191,7 +191,7 @@ def validate_job_headline(form, field):
'pragmatic programmer wanted at outstanding organisation',
'pragmatic programmer wanted at outstanding organization') or (
g.board and g.board.newjob_headline and simplify_text(field.data) == simplify_text(g.board.newjob_headline)):
- raise forms.ValidationError(_(u"Come on, write your own headline. You aren’t just another run-of-the-mill employer, right?"))
+ raise forms.ValidationError(_("Come on, write your own headline. You aren’t just another run-of-the-mill employer, right?"))
caps = len(CAPS_RE.findall(field.data))
small = len(SMALL_RE.findall(field.data))
if small == 0 or caps / float(small) > 1.0:
@@ -206,7 +206,7 @@ def validate_job_headlineb(self, field):
def validate_job_location(form, field):
if QUOTES_RE.search(field.data) is not None:
- raise forms.ValidationError(_(u"Don’t use quotes in the location name"))
+ raise forms.ValidationError(_("Don’t use quotes in the location name"))
caps = len(CAPS_RE.findall(field.data))
small = len(SMALL_RE.findall(field.data))
@@ -274,50 +274,50 @@ def validate(self):
success = super(ListingForm, self).validate(send_signals=False)
if success:
if (not self.job_type_ob.nopay_allowed) and self.job_pay_type.data == PAY_TYPE.NOCASH:
- self.job_pay_type.errors.append(_(u"“%%s” cannot pay nothing") % self.job_type_ob.title)
+ self.job_pay_type.errors.append(_("“%%s” cannot pay nothing") % self.job_type_ob.title)
success = False
domain_name = get_email_domain(self.poster_email.data)
domain = Domain.get(domain_name)
if domain and domain.is_banned:
- self.poster_email.errors.append(_(u"%%s is banned from posting jobs on Hasjob") % domain_name)
+ self.poster_email.errors.append(_("%%s is banned from posting jobs on Hasjob") % domain_name)
success = False
elif (not self.job_type_ob.webmail_allowed) and is_public_email_domain(domain_name, default=False):
self.poster_email.errors.append(
- _(u"Public webmail accounts like Gmail are not accepted. Please use your corporate email address"))
+ _("Public webmail accounts like Gmail are not accepted. Please use your corporate email address"))
success = False
# Check for cash pay range
if self.job_pay_type.data in (PAY_TYPE.ONETIME, PAY_TYPE.RECURRING):
if self.job_pay_cash_min.data == 0:
if self.job_pay_cash_max.data == 10000000:
- self.job_pay_cash_max.errors.append(_(u"Please select a range"))
+ self.job_pay_cash_max.errors.append(_("Please select a range"))
success = False
else:
- self.job_pay_cash_min.errors.append(_(u"Please specify a minimum non-zero pay"))
+ self.job_pay_cash_min.errors.append(_("Please specify a minimum non-zero pay"))
success = False
else:
if self.job_pay_cash_max.data == 10000000:
if self.job_pay_currency.data == 'INR':
- figure = _(u"1 crore")
+ figure = _("1 crore")
else:
- figure = _(u"10 million")
+ figure = _("10 million")
self.job_pay_cash_max.errors.append(
- _(u"You’ve selected an upper limit of {figure}. That can’t be right").format(figure=figure))
+ _("You’ve selected an upper limit of {figure}. That can’t be right").format(figure=figure))
success = False
elif (self.job_pay_type.data == PAY_TYPE.RECURRING
and self.job_pay_currency.data == 'INR'
and self.job_pay_cash_min.data < 60000):
self.job_pay_cash_min.errors.append(
- _(u"That’s rather low. Did you specify monthly pay instead of annual pay? Multiply by 12"))
+ _("That’s rather low. Did you specify monthly pay instead of annual pay? Multiply by 12"))
success = False
elif self.job_pay_cash_max.data > self.job_pay_cash_min.data * 4:
- self.job_pay_cash_max.errors.append(_(u"Please select a narrower range, with maximum within 4× minimum"))
+ self.job_pay_cash_max.errors.append(_("Please select a narrower range, with maximum within 4× minimum"))
success = False
if self.job_pay_equity.data:
if self.job_pay_equity_min.data == 0:
if self.job_pay_equity_max.data == 100:
- self.job_pay_equity_max.errors.append(_(u"Please select a range"))
+ self.job_pay_equity_max.errors.append(_("Please select a range"))
success = False
else:
if self.job_pay_equity_min.data <= Decimal('1.0'):
@@ -331,7 +331,7 @@ def validate(self):
if self.job_pay_equity_max.data > self.job_pay_equity_min.data * multiplier:
self.job_pay_equity_max.errors.append(
- _(u"Please select a narrower range, with maximum within %%d× minimum") % multiplier)
+ _("Please select a narrower range, with maximum within %%d× minimum") % multiplier)
success = False
self.send_signals()
return success
@@ -377,17 +377,17 @@ class ApplicationForm(forms.Form):
content_css=content_css,
validators=[forms.validators.DataRequired(__("You need to say something about yourself")),
forms.validators.AllUrlsValid()],
- description=__(u"Please provide all details the employer has requested. To add a resume, "
- u"post it on LinkedIn or host the file on Dropbox and insert the link here"))
+ description=__("Please provide all details the employer has requested. To add a resume, "
+ "post it on LinkedIn or host the file on Dropbox and insert the link here"))
apply_optin = forms.BooleanField(__("Optional: sign me up for a better Hasjob experience"),
- description=__(u"Hasjob’s maintainers may contact you about new features and can see this application for reference"))
+ description=__("Hasjob’s maintainers may contact you about new features and can see this application for reference"))
def __init__(self, *args, **kwargs):
super(ApplicationForm, self).__init__(*args, **kwargs)
self.apply_email.choices = []
if g.user:
self.apply_email.description = Markup(
- _(u'Add new email addresses from your profile').format(
+ _('Add new email addresses from your profile').format(
g.user.profile_url))
try:
self.apply_email.choices = [(e, e) for e in lastuser.user_emails(g.user)]
@@ -417,7 +417,7 @@ def validate_apply_message(form, field):
# Prepare text by replacing non-breaking spaces with spaces (for phone numbers) and removing URLs.
# URLs may contain numbers that are not phone numbers.
- phone_search_text = URL_RE.sub('', field.data.replace(' ', ' ').replace(' ', ' ').replace(u'\xa0', ' '))
+ phone_search_text = URL_RE.sub('', field.data.replace(' ', ' ').replace(' ', ' ').replace('\xa0', ' '))
if EMAIL_RE.search(field.data) is not None or PHONE_DETECT_RE.search(phone_search_text) is not None:
raise forms.ValidationError(_("Do not include your email address or phone number in the application"))
@@ -436,8 +436,8 @@ class KioskApplicationForm(forms.Form):
content_css=content_css,
validators=[forms.validators.DataRequired(__("You need to say something about yourself")),
forms.validators.AllUrlsValid()],
- description=__(u"Please provide all details the employer has requested. To add a resume, "
- u"post it on LinkedIn or host the file on Dropbox and insert the link here"))
+ description=__("Please provide all details the employer has requested. To add a resume, "
+ "post it on LinkedIn or host the file on Dropbox and insert the link here"))
def validate_email(form, field):
oldapp = JobApplication.query.filter_by(jobpost=form.post, user=None, email=field.data).count()
diff --git a/hasjob/models/board.py b/hasjob/models/board.py
index 79afe18e0..194fe38b7 100644
--- a/hasjob/models/board.py
+++ b/hasjob/models/board.py
@@ -119,7 +119,7 @@ class Board(BaseNameMixin, db.Model):
#: Lastuser organization userid that owns this
userid = db.Column(db.Unicode(22), nullable=False, index=True)
#: Welcome text
- description = db.Column(db.UnicodeText, nullable=False, default=u'')
+ description = db.Column(db.UnicodeText, nullable=False, default='')
#: Restrict displayed posts to 24 hours if not logged in?
require_login = db.Column(db.Boolean, nullable=False, default=True)
#: Restrict ability to list via this board?
@@ -161,11 +161,11 @@ def __repr__(self):
@property
def is_root(self):
- return self.name == u'www'
+ return self.name == 'www'
@property
def not_root(self):
- return self.name != u'www'
+ return self.name != 'www'
@property
def options(self):
@@ -183,7 +183,7 @@ def tz(self):
@property
def title_and_name(self):
- return Markup(u'{title} ({name})'.format(
+ return Markup('{title} ({name})'.format(
title=self.title, name=self.name, url=self.url_for()))
def owner_is(self, user):
@@ -278,7 +278,7 @@ def _jobpost_link_to_board(self, board):
def _jobpost_add_to(self, board):
with db.session.no_autoflush:
- if isinstance(board, basestring):
+ if isinstance(board, str):
board = Board.get(board)
if not board:
return
diff --git a/hasjob/models/campaign.py b/hasjob/models/campaign.py
index 9269dd65d..af247c05c 100644
--- a/hasjob/models/campaign.py
+++ b/hasjob/models/campaign.py
@@ -112,9 +112,9 @@ class Campaign(BaseNameMixin, db.Model):
#: Subject (user-facing, unlike the title)
subject = db.Column(db.Unicode(250), nullable=True)
#: Call to action text (for header campaigns)
- blurb = db.Column(db.UnicodeText, nullable=False, default=u'')
+ blurb = db.Column(db.UnicodeText, nullable=False, default='')
#: Full text (for read more click throughs)
- description = db.Column(db.UnicodeText, nullable=False, default=u'')
+ description = db.Column(db.UnicodeText, nullable=False, default='')
#: Banner image
banner_image = db.Column(db.Unicode(250), nullable=True)
#: Banner location
@@ -377,7 +377,7 @@ class CampaignAction(BaseScopedNameMixin, db.Model):
# db.CheckConstraint('type IN (%s)' % ', '.join(["'%s'" % k for k in CAMPAIGN_ACTION.keys()])),
# nullable=False)
#: Action category (for buttons)
- category = db.Column(db.Unicode(20), nullable=False, default=u'default')
+ category = db.Column(db.Unicode(20), nullable=False, default='default')
#: Icon to accompany text
icon = db.Column(db.Unicode(20), nullable=True)
#: Group (for RSVP buttons)
@@ -387,7 +387,7 @@ class CampaignAction(BaseScopedNameMixin, db.Model):
#: Form
form = deferred(db.Column(JsonDict, nullable=False, server_default='{}'))
#: Post action message
- message = db.Column(db.UnicodeText, nullable=False, default=u'')
+ message = db.Column(db.UnicodeText, nullable=False, default='')
__table_args__ = (db.UniqueConstraint('campaign_id', 'name'),)
diff --git a/hasjob/models/filterset.py b/hasjob/models/filterset.py
index 3e46c8ec9..8ea32ef08 100644
--- a/hasjob/models/filterset.py
+++ b/hasjob/models/filterset.py
@@ -51,7 +51,7 @@ class Filterset(BaseScopedNameMixin, db.Model):
parent = db.synonym('board')
#: Welcome text
- description = db.Column(db.UnicodeText, nullable=False, default=u'')
+ description = db.Column(db.UnicodeText, nullable=False, default='')
#: Associated job types
types = db.relationship(JobType, secondary=filterset_jobtype_table)
@@ -66,7 +66,7 @@ class Filterset(BaseScopedNameMixin, db.Model):
pay_currency = db.Column(db.CHAR(3), nullable=True, index=True)
pay_cash = db.Column(db.Integer, nullable=True, index=True)
equity = db.Column(db.Boolean, nullable=False, default=False, index=True)
- keywords = db.Column(db.Unicode(250), nullable=False, default=u'', index=True)
+ keywords = db.Column(db.Unicode(250), nullable=False, default='', index=True)
def __repr__(self):
return '' % (self.board.title, self.title)
diff --git a/hasjob/models/jobpost.py b/hasjob/models/jobpost.py
index b3643d8e5..22d381263 100644
--- a/hasjob/models/jobpost.py
+++ b/hasjob/models/jobpost.py
@@ -3,7 +3,7 @@
from datetime import timedelta
from werkzeug import cached_property
from flask import url_for, escape, Markup
-from flask_babelex import format_datetime
+from flask_babel2 import format_datetime
from sqlalchemy import event, DDL
from sqlalchemy.orm import defer, deferred, load_only
from sqlalchemy.ext.associationproxy import association_proxy
@@ -119,7 +119,7 @@ class JobPost(BaseMixin, db.Model):
# Company details
company_name = db.Column(db.Unicode(80), nullable=False)
company_logo = db.Column(db.Unicode(255), nullable=True)
- company_url = db.Column(db.Unicode(255), nullable=False, default=u'')
+ company_url = db.Column(db.Unicode(255), nullable=False, default='')
twitter = db.Column(db.Unicode(15), nullable=True)
fullname = db.Column(db.Unicode(80), nullable=True) # Deprecated field, used before user_id was introduced
email = db.Column(db.Unicode(80), nullable=False)
@@ -294,7 +294,7 @@ def url_for(self, action='view', _external=False, **kwargs):
# A/B test flag for permalinks
if 'b' in kwargs:
if kwargs['b'] is not None:
- kwargs['b'] = unicode(int(kwargs['b']))
+ kwargs['b'] = str(int(kwargs['b']))
else:
kwargs.pop('b')
@@ -358,10 +358,10 @@ def from_webmail_domain(self):
@property
def company_url_domain_zone(self):
if not self.company_url:
- return u''
+ return ''
else:
r = tldextract.extract(self.company_url)
- return u'.'.join([r.domain, r.suffix])
+ return '.'.join([r.domain, r.suffix])
@property
def pays_cash(self):
@@ -375,7 +375,7 @@ def pays_equity(self):
def pay_label(self):
if self.pay_type is None:
- return u"NA"
+ return "NA"
elif self.pay_type == PAY_TYPE.NOCASH:
cash = None
suffix = ""
@@ -388,15 +388,15 @@ def pay_label(self):
indian = False
if self.pay_currency == "INR":
indian = True
- symbol = u"₹"
+ symbol = "₹"
elif self.pay_currency == "USD":
- symbol = u"$"
+ symbol = "$"
elif self.pay_currency == "EUR":
- symbol = u"€"
+ symbol = "€"
elif self.pay_currency == "GBP":
- symbol = u"£"
+ symbol = "£"
else:
- symbol = u"¤"
+ symbol = "¤"
if self.pay_cash_min == self.pay_cash_max:
cash = symbol + number_abbreviate(self.pay_cash_min, indian)
@@ -561,7 +561,7 @@ def viewstats_helper(jobpost_id, interval, limit, daybatch=False):
else:
from_hour = format_datetime(from_datetime, 'd MMM HH:00')
to_hour = format_datetime(to_datetime, 'HH:00')
- cbuckets[batches - delta - 1] = u"{from_hour} — {to_hour}".format(from_hour=from_hour, to_hour=to_hour)
+ cbuckets[batches - delta - 1] = "{from_hour} — {to_hour}".format(from_hour=from_hour, to_hour=to_hour)
# if current bucket was 18:00-22:00, then
# previous bucket becomes 14:00-18:00
to_datetime = from_datetime
diff --git a/hasjob/models/user.py b/hasjob/models/user.py
index 579befe78..268d61e96 100644
--- a/hasjob/models/user.py
+++ b/hasjob/models/user.py
@@ -74,13 +74,13 @@ def new_from_request(cls, request):
instance.uuid = uuid4()
instance.created_at = utcnow()
instance.referrer = unicode_http_header(request.referrer)[:2083] if request.referrer else None
- instance.utm_source = request.args.get('utm_source', u'')[:250] or None
- instance.utm_medium = request.args.get('utm_medium', u'')[:250] or None
- instance.utm_term = request.args.get('utm_term', u'')[:250] or None
- instance.utm_content = request.args.get('utm_content', u'')[:250] or None
- instance.utm_id = request.args.get('utm_id', u'')[:250] or None
- instance.utm_campaign = request.args.get('utm_campaign', u'')[:250] or None
- instance.gclid = request.args.get('gclid', u'')[:250] or None
+ instance.utm_source = request.args.get('utm_source', '')[:250] or None
+ instance.utm_medium = request.args.get('utm_medium', '')[:250] or None
+ instance.utm_term = request.args.get('utm_term', '')[:250] or None
+ instance.utm_content = request.args.get('utm_content', '')[:250] or None
+ instance.utm_id = request.args.get('utm_id', '')[:250] or None
+ instance.utm_campaign = request.args.get('utm_campaign', '')[:250] or None
+ instance.gclid = request.args.get('gclid', '')[:250] or None
instance.active_at = utcnow()
instance.events = []
return instance
@@ -132,14 +132,14 @@ class EventSession(EventSessionBase, UuidMixin, BaseMixin, db.Model):
# Google Analytics parameters. If any of these is present in the
# current request and different from the current session, a new session is created
- utm_source = db.Column(db.Unicode(250), nullable=False, default=u'')
- utm_medium = db.Column(db.Unicode(250), nullable=False, default=u'')
- utm_term = db.Column(db.Unicode(250), nullable=False, default=u'')
- utm_content = db.Column(db.Unicode(250), nullable=False, default=u'')
- utm_id = db.Column(db.Unicode(250), nullable=False, default=u'')
- utm_campaign = db.Column(db.Unicode(250), nullable=False, default=u'')
+ utm_source = db.Column(db.Unicode(250), nullable=False, default='')
+ utm_medium = db.Column(db.Unicode(250), nullable=False, default='')
+ utm_term = db.Column(db.Unicode(250), nullable=False, default='')
+ utm_content = db.Column(db.Unicode(250), nullable=False, default='')
+ utm_id = db.Column(db.Unicode(250), nullable=False, default='')
+ utm_campaign = db.Column(db.Unicode(250), nullable=False, default='')
# Google click id (for AdWords)
- gclid = db.Column(db.Unicode(250), nullable=False, default=u'')
+ gclid = db.Column(db.Unicode(250), nullable=False, default='')
active_at = db.Column(db.TIMESTAMP(timezone=True), nullable=False)
# This session was closed because the user showed up again under new conditions
@@ -192,11 +192,11 @@ class UserEventBase(object):
@classmethod
def new_from_request(cls, request):
instance = cls()
- instance.ipaddr = request and unicode(request.environ['REMOTE_ADDR'][:45])
+ instance.ipaddr = request and str(request.environ['REMOTE_ADDR'][:45])
instance.useragent = request and unicode_http_header(request.user_agent.string)[:250]
instance.url = request and request.url[:2038]
- instance.method = request and unicode(request.method[:10])
- instance.name = request and (u'endpoint/' + (request.endpoint or '')[:80])
+ instance.method = request and str(request.method[:10])
+ instance.name = request and ('endpoint/' + (request.endpoint or '')[:80])
return instance
def as_dict(self):
diff --git a/hasjob/nlp.py b/hasjob/nlp.py
index b0a591879..a0bf8d2fd 100644
--- a/hasjob/nlp.py
+++ b/hasjob/nlp.py
@@ -9,4 +9,4 @@
def identify_language(post):
- return langid.classify(u'\n'.join([post.headline, bleach.clean(post.description, tags=[], strip=True)]))
+ return langid.classify('\n'.join([post.headline, bleach.clean(post.description, tags=[], strip=True)]))
diff --git a/hasjob/tagging.py b/hasjob/tagging.py
index ea92a153d..23e0e55da 100644
--- a/hasjob/tagging.py
+++ b/hasjob/tagging.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from collections import defaultdict
-from urlparse import urljoin
+from urllib.parse import urljoin
import requests
from coaster.utils import text_blocks
from coaster.nlp import extract_named_entities
diff --git a/hasjob/twitter.py b/hasjob/twitter.py
index 108cf3054..0ffcdbf41 100644
--- a/hasjob/twitter.py
+++ b/hasjob/twitter.py
@@ -1,8 +1,7 @@
# -*- coding: utf-8 -*-
from tweepy import OAuthHandler, API
-import bitlyapi
-import urllib2
+import urllib.request, urllib.error, urllib.parse
import json
import re
from hasjob import app, rq
@@ -17,25 +16,25 @@ def tweet(title, url, location=None, parsed_location=None, username=None):
maxlength = 140 - urllength - 1 # == 116
if username:
maxlength -= len(username) + 2
- locationtag = u''
+ locationtag = ''
if parsed_location:
locationtags = []
for token in parsed_location.get('tokens', []):
if 'geoname' in token and 'token' in token:
locname = token['token'].strip()
if locname:
- locationtags.append(u'#' + locname.title().replace(u' ', ''))
- locationtag = u' '.join(locationtags)
+ locationtags.append('#' + locname.title().replace(' ', ''))
+ locationtag = ' '.join(locationtags)
if locationtag:
maxlength -= len(locationtag) + 1
if not locationtag and location:
# Make a hashtag from the first word in the location. This catches
# locations like 'Anywhere' which have no geonameid but are still valid
- locationtag = u'#' + re.split(r'\W+', location)[0]
+ locationtag = '#' + re.split(r'\W+', location)[0]
maxlength -= len(locationtag) + 1
if len(title) > maxlength:
- text = title[:maxlength - 1] + u'…'
+ text = title[:maxlength - 1] + '…'
else:
text = title[:maxlength]
text = text + ' ' + url # Don't shorten URLs, now that there's t.co
@@ -46,6 +45,7 @@ def tweet(title, url, location=None, parsed_location=None, username=None):
api.update_status(text)
+# TODO: Delete this function
def shorten(url):
if app.config['BITLY_KEY']:
b = bitlyapi.BitLy(app.config['BITLY_USER'], app.config['BITLY_KEY'])
diff --git a/hasjob/uploads.py b/hasjob/uploads.py
index 2e459adc7..c6aed1e9f 100644
--- a/hasjob/uploads.py
+++ b/hasjob/uploads.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from PIL import Image
-from StringIO import StringIO
+from io import StringIO
from os.path import splitext
from werkzeug import FileStorage
diff --git a/hasjob/utils.py b/hasjob/utils.py
index 634f762ed..e6eece20e 100644
--- a/hasjob/utils.py
+++ b/hasjob/utils.py
@@ -82,7 +82,7 @@ def base36encode(number, alphabet='0123456789abcdefghijklmnopqrstuvwxyz'):
>>> base36encode(60466176)
'100000'
"""
- if not isinstance(number, (int, long)):
+ if not isinstance(number, int):
raise TypeError('number must be an integer')
# Special case for zero
if number == 0:
@@ -137,7 +137,7 @@ def cointoss():
PHONE_DETECT_RE = re.compile('(^|[^0-9])([0-9][ .()_-]*){10}($|[^0-9])')
-def redactemail(data, message=u'[redacted]'):
+def redactemail(data, message='[redacted]'):
"""
Remove email addresses from the given text, replacing with a redacted message.
@@ -255,7 +255,7 @@ def escape_for_sql_like(query):
>>> escape_for_sql_like("query%_[]")
"query\%\_%"
"""
- return query.replace(u'%', r'\%').replace(u'_', r'\_').replace(u'[', u'').replace(u']', u'') + u'%'
+ return query.replace('%', r'\%').replace('_', r'\_').replace('[', '').replace(']', '') + '%'
def strip_null(text):
diff --git a/hasjob/views/admin_filterset.py b/hasjob/views/admin_filterset.py
index 2f4a96b04..667d2be67 100644
--- a/hasjob/views/admin_filterset.py
+++ b/hasjob/views/admin_filterset.py
@@ -30,12 +30,12 @@ def new(self):
try:
db.session.add(filterset)
db.session.commit()
- flash(u"Created a filterset", 'success')
+ flash("Created a filterset", 'success')
return render_redirect(filterset.url_for(), code=303)
except ValueError:
db.session.rollback()
- flash(u"There already exists a filterset with the selected criteria", 'interactive')
- return render_form(form=form, title=u"Create a filterset…", submit="Create",
+ flash("There already exists a filterset with the selected criteria", 'interactive')
+ return render_form(form=form, title="Create a filterset…", submit="Create",
formid="filterset_new", ajax=False)
@route('/edit', methods=['GET', 'POST'])
@@ -49,12 +49,12 @@ def edit(self):
form.populate_obj(self.obj)
try:
db.session.commit()
- flash(u"Updated filterset", 'success')
+ flash("Updated filterset", 'success')
return render_redirect(self.obj.url_for(), code=303)
except ValueError:
db.session.rollback()
- flash(u"There already exists a filterset with the selected criteria", 'interactive')
- return render_form(form=form, title=u"Edit filterset…", submit="Update",
+ flash("There already exists a filterset with the selected criteria", 'interactive')
+ return render_form(form=form, title="Edit filterset…", submit="Update",
formid="filterset_edit", ajax=False)
diff --git a/hasjob/views/admindash.py b/hasjob/views/admindash.py
index 3b36cb647..2dc2dea23 100644
--- a/hasjob/views/admindash.py
+++ b/hasjob/views/admindash.py
@@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
from collections import defaultdict
-from cStringIO import StringIO
+from io import StringIO
from flask import g, render_template
-import unicodecsv
+import csv
from baseframe import __
from coaster.views import route, viewdata
@@ -84,7 +84,7 @@ def daystats(self, period):
stats[slot]['rejections'] = count
outfile = StringIO()
- out = unicodecsv.writer(outfile, 'excel')
+ out = csv.writer(outfile, 'excel')
out.writerow(['slot', 'newusers', 'users', 'anon_users', 'jobs', 'applications', 'replies', 'rejections'])
for slot, c in sorted(stats.items()):
@@ -116,7 +116,7 @@ def historical_userdays(self, q):
response=EMPLOYER_RESPONSE.REPLIED)
outfile = StringIO()
- out = unicodecsv.writer(outfile, 'excel')
+ out = csv.writer(outfile, 'excel')
out.writerow(['month', '50%', '60%', '70%', '80%', '90%',
'91%', '92%', '93%', '94%', '95%', '96%', '97%', '98%', '99%', '100%', 'users'])
diff --git a/hasjob/views/board.py b/hasjob/views/board.py
index 7a7d3f995..aed43bbaa 100644
--- a/hasjob/views/board.py
+++ b/hasjob/views/board.py
@@ -24,7 +24,7 @@ def remove_subdomain_parameter(endpoint, values):
g.board = None
return # Don't bother processing for static endpoints either (only applies in dev)
- g.board = Board.query.filter_by(name=subdomain or u'www').first()
+ g.board = Board.query.filter_by(name=subdomain or 'www').first()
if subdomain and g.board is None:
abort(404)
@@ -55,10 +55,10 @@ def board_new():
if post:
board.add(post)
db.session.commit()
- flash(u"Created a job board named %s" % board.title, 'success')
+ flash("Created a job board named %s" % board.title, 'success')
return render_redirect(url_for('board_view', board=board.name), code=303)
- return render_form(form=form, title=u"Create a job board…", submit="Next",
- message=u"Make your own job board with just the jobs you want to showcase. "
+ return render_form(form=form, title="Create a job board…", submit="Next",
+ message="Make your own job board with just the jobs you want to showcase. "
"Your board will appear as a subdomain",
formid="board_new", cancel_url=url_for('index'), ajax=False)
@@ -79,16 +79,16 @@ def board_edit(board):
form.userid.choices = g.user.allowner_choices()
if board.userid not in g.user.allowner_ids():
# We could get here if we are a siteadmin editing someone's board
- form.userid.choices.insert(0, (board.userid, u"(Preserve existing owner)"))
+ form.userid.choices.insert(0, (board.userid, "(Preserve existing owner)"))
if form.validate_on_submit():
form.populate_obj(board)
if not board.name:
board.make_name()
db.session.commit()
- flash(u"Edited board settings.", 'success')
+ flash("Edited board settings.", 'success')
return render_redirect(url_for('index', subdomain=board.name), code=303)
- return render_form(form=form, title=u"Edit board settings", submit="Save",
+ return render_form(form=form, title="Edit board settings", submit="Save",
formid="board_edit", cancel_url=url_for('index', subdomain=board.name), ajax=False)
@@ -96,9 +96,9 @@ def board_edit(board):
@lastuser.requires_login
@load_model(Board, {'name': 'board'}, 'board', permission='delete')
def board_delete(board):
- return render_delete_sqla(board, db, title=u"Confirm delete",
- message=u"Delete board '%s'?" % board.title,
- success=u"You have deleted board '%s'." % board.title,
+ return render_delete_sqla(board, db, title="Confirm delete",
+ message="Delete board '%s'?" % board.title,
+ success="You have deleted board '%s'." % board.title,
next=url_for('index'))
@@ -119,5 +119,5 @@ def board_add(board, jobpost):
db.session.commit()
# cache bust
# dogpile.invalidate_region('hasjob_index')
- flash(u"You’ve added this job to %s" % board.title, 'interactive')
+ flash("You’ve added this job to %s" % board.title, 'interactive')
return redirect(jobpost.url_for(subdomain=board.name))
diff --git a/hasjob/views/campaign.py b/hasjob/views/campaign.py
index c5a0908d7..b05de6aca 100644
--- a/hasjob/views/campaign.py
+++ b/hasjob/views/campaign.py
@@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
from collections import defaultdict
-from cStringIO import StringIO
+from io import StringIO
from datetime import timedelta
from functools import wraps
from flask import Markup, abort, flash, g, redirect, render_template, request, url_for
-import unicodecsv
+import csv
from pytz import UTC
from baseframe import __
@@ -77,7 +77,7 @@ def list_disabled(self):
@route('new', methods=['GET', 'POST'])
@viewdata(tab=True, index=4, title=__("New"))
def new(self):
- self.message = u"Campaigns appear around the job board and provide a call to action for users"
+ self.message = "Campaigns appear around the job board and provide a call to action for users"
form = CampaignForm()
if request.method == 'GET' and g.board:
form.boards.data = [g.board]
@@ -87,10 +87,10 @@ def new(self):
campaign.name = suuid() # Use a random name since it's also used in user action submit forms
db.session.add(campaign)
db.session.commit()
- flash(u"Created a campaign", 'success')
+ flash("Created a campaign", 'success')
return render_redirect(campaign.url_for(), code=303)
- return render_form(form=form, title=u"Create a campaign…", submit="Next",
+ return render_form(form=form, title="Create a campaign…", submit="Next",
formid="campaign_new", cancel_url=url_for(self.list_current.endpoint), ajax=False)
@@ -164,17 +164,17 @@ def edit(self):
if form.validate_on_submit():
form.populate_obj(self.obj)
db.session.commit()
- flash(u"Edited campaign settings", 'success')
+ flash("Edited campaign settings", 'success')
return render_redirect(self.obj.url_for(), code=303)
- return render_form(form=form, title=u"Edit campaign settings", submit="Save",
+ return render_form(form=form, title="Edit campaign settings", submit="Save",
formid="campaign_edit", cancel_url=self.obj.url_for(), ajax=False)
@route('delete', methods=['GET', 'POST'])
def delete(self):
- return render_delete_sqla(self.obj, db, title=u"Confirm delete",
- message=u"Delete campaign '%s'?" % self.obj.title,
- success=u"You have deleted campaign '%s'." % self.obj.title,
+ return render_delete_sqla(self.obj, db, title="Confirm delete",
+ message="Delete campaign '%s'?" % self.obj.title,
+ success="You have deleted campaign '%s'." % self.obj.title,
next=url_for(getattr(self, self.current_tab).endpoint),
cancel_url=self.obj.url_for())
@@ -190,10 +190,10 @@ def action_new(self):
form.populate_obj(action)
action.name = suuid() # Use a random name since it needs to be permanent
db.session.commit()
- flash(u"Added campaign action ‘%s’" % action.title, 'interactive')
+ flash("Added campaign action ‘%s’" % action.title, 'interactive')
return redirect(self.obj.url_for(), code=303)
- return render_form(form=form, title=u"Add a new campaign action…", submit="Save",
+ return render_form(form=form, title="Add a new campaign action…", submit="Save",
formid="campaign_action_new", cancel_url=self.obj.url_for(), ajax=False)
@route('views.csv')
@@ -279,7 +279,7 @@ def view_counts(self):
row[0] = row[0].isoformat()
outfile = StringIO()
- out = unicodecsv.writer(outfile, 'excel')
+ out = csv.writer(outfile, 'excel')
out.writerow(['_hour', '_site', '_views', '_combined'] + action_names)
out.writerows(viewlist)
@@ -312,17 +312,17 @@ def edit(self):
if form.validate_on_submit():
form.populate_obj(self.obj)
db.session.commit()
- flash(u"Edited campaign action ‘%s’" % self.obj.title, 'interactive')
+ flash("Edited campaign action ‘%s’" % self.obj.title, 'interactive')
return redirect(self.obj.parent.url_for(), code=303)
- return render_form(form=form, title=u"Edit campaign action", submit="Save",
+ return render_form(form=form, title="Edit campaign action", submit="Save",
formid="campaign_action_edit", cancel_url=self.obj.parent.url_for(), ajax=False)
@route('delete', methods=['GET', 'POST'])
def delete(self):
- return render_delete_sqla(self.obj, db, title=u"Confirm delete",
- message=u"Delete campaign action '%s'?" % self.obj.title,
- success=u"You have deleted campaign action '%s'." % self.obj.title,
+ return render_delete_sqla(self.obj, db, title="Confirm delete",
+ message="Delete campaign action '%s'?" % self.obj.title,
+ success="You have deleted campaign action '%s'." % self.obj.title,
next=self.obj.parent.url_for())
@route('csv', methods=['GET', 'POST'])
@@ -330,7 +330,7 @@ def csv(self):
if self.obj.type not in ('Y', 'N', 'M', 'F'):
abort(403)
outfile = StringIO()
- out = unicodecsv.writer(outfile, 'excel')
+ out = csv.writer(outfile, 'excel')
out.writerow(['fullname', 'username', 'email', 'phone'])
for ua in self.obj.useractions:
out.writerow([ua.user.fullname, ua.user.username, ua.user.email, ua.user.phone])
@@ -416,7 +416,7 @@ def campaign_action(campaign):
db.session.add(cua)
else: # All of the other types require a user (not an anon user; will change when forms are introduced)
return render_template('campaign_action_response.html.jinja2', campaign=campaign,
- redirect=url_for('login', next=request.referrer, message=u"Please login so we can save your preferences"))
+ redirect=url_for('login', next=request.referrer, message="Please login so we can save your preferences"))
if action.is_rsvp_type:
for cua in campaign.useractions(g.user).values():
diff --git a/hasjob/views/helper.py b/hasjob/views/helper.py
index f054923dd..b1c3427a7 100644
--- a/hasjob/views/helper.py
+++ b/hasjob/views/helper.py
@@ -3,7 +3,8 @@
from os import path
from datetime import timedelta
from uuid import uuid4
-from urllib import quote, quote_plus
+from urllib.parse import quote, quote_plus
+from base64 import b64decode
import hashlib
import bleach
from pytz import UTC
@@ -25,8 +26,8 @@
from ..utils import scrubemail, redactemail, cointoss
-gif1x1 = 'R0lGODlhAQABAJAAAP8AAAAAACH5BAUQAAAALAAAAAABAAEAAAICBAEAOw=='.decode('base64')
-MAX_COUNTS_KEY = u'maxcounts'
+gif1x1 = b64decode(b'R0lGODlhAQABAJAAAP8AAAAAACH5BAUQAAAALAAAAAABAAEAAAICBAEAOw==')
+MAX_COUNTS_KEY = 'maxcounts'
@app.route('/_sniffle.gif')
@@ -90,14 +91,14 @@ def load_user_data(user):
if request.endpoint not in ('static', 'baseframe.static'):
# Loading an anon user only if we're not rendering static resources
if user:
- if 'au' in session and session['au'] is not None and not unicode(session['au']).startswith(u'test'):
+ if 'au' in session and session['au'] is not None and not str(session['au']).startswith('test'):
anon_user = AnonUser.query.get(session['au'])
if anon_user:
anon_user.user = user
session.pop('au', None)
else:
if not session.get('au'):
- session['au'] = u'test-' + unicode(uuid4())
+ session['au'] = 'test-' + str(uuid4())
g.esession = EventSessionBase.new_from_request(request)
g.event_data['anon_cookie_test'] = session['au']
# elif session['au'] == 'test': # Legacy test cookie, original request now lost
@@ -121,7 +122,7 @@ def load_user_data(user):
if not anon_user:
# XXX: We got a fake value? This shouldn't happen
g.event_data['anon_cookie_test'] = session['au']
- session['au'] = u'test-' + unicode(uuid4()) # Try again
+ session['au'] = 'test-' + str(uuid4()) # Try again
g.esession = EventSessionBase.new_from_request(request)
else:
g.anon_user = anon_user
@@ -143,7 +144,7 @@ def load_user_data(user):
if 'impressions' in session:
# Run this in the foreground since we need this later in the request for A/B display consistency.
# This is most likely being called from the UI-non-blocking sniffle.gif anyway.
- save_impressions(g.esession.id, session.pop('impressions').values(), now)
+ save_impressions(g.esession.id, list(session.pop('impressions').values()), now)
# We have a user, now look up everything else
@@ -253,7 +254,7 @@ def record_views_and_events(response):
g.event_data['user_geonameids'] = g.user_geonameids
if g.impressions:
- g.event_data['impressions'] = g.impressions.values()
+ g.event_data['impressions'] = list(g.impressions.values())
if g.user:
for campaign in g.campaign_views:
@@ -294,7 +295,7 @@ def record_views_and_events(response):
db.session.rollback()
if g.impressions:
- save_impressions.queue(g.esession.id, g.impressions.values(), now)
+ save_impressions.queue(g.esession.id, list(g.impressions.values()), now)
if g.jobpost_viewed != (None, None):
save_jobview.queue(
@@ -383,7 +384,7 @@ def get_post_viewcounts(jobpost_id):
redis_store.hset(cache_key, 'pay_label', values['pay_label'].encode('utf-8'))
redis_store.expire(cache_key, 86400)
elif isinstance(values['pay_label'], str): # Redis appears to return bytestrings, not Unicode
- values['pay_label'] = unicode(values['pay_label'], 'utf-8')
+ values['pay_label'] = str(values['pay_label'], 'utf-8')
return values
@@ -434,7 +435,7 @@ def load_viewcounts(posts):
values = redis_pipe.execute()
viewcounts_values = values[:-1]
maxcounts_values = values[-1]
- g.viewcounts = dict(zip(viewcounts_keys, viewcounts_values))
+ g.viewcounts = dict(list(zip(viewcounts_keys, viewcounts_values)))
g.maxcounts = maxcounts_values
@@ -514,15 +515,15 @@ def gettags(alltime=False):
pay_graph_buckets = {
'INR': (
- range(0, 200000, 25000)
- + range(200000, 2000000, 50000)
- + range(2000000, 10000000, 100000)
- + range(10000000, 100000000, 1000000)
+ list(range(0, 200000, 25000))
+ + list(range(200000, 2000000, 50000))
+ + list(range(2000000, 10000000, 100000))
+ + list(range(10000000, 100000000, 1000000))
+ [100000000]),
'USD': (
- range(0, 200000, 5000)
- + range(200000, 1000000, 50000)
- + range(1000000, 10000000, 100000)
+ list(range(0, 200000, 5000))
+ + list(range(200000, 1000000, 50000))
+ + list(range(1000000, 10000000, 100000))
+ [10000000])
}
pay_graph_buckets['EUR'] = pay_graph_buckets['USD']
@@ -741,7 +742,7 @@ def cleanurl(url):
@app.template_filter('urlquote')
def urlquote(data):
- if isinstance(data, unicode):
+ if isinstance(data, str):
return quote(data.encode('utf-8'))
else:
return quote(data)
@@ -749,7 +750,7 @@ def urlquote(data):
@app.template_filter('urlquoteplus')
def urlquoteplus(data):
- if isinstance(data, unicode):
+ if isinstance(data, str):
return quote_plus(data.encode('utf-8'))
else:
return quote_plus(data)
@@ -757,7 +758,7 @@ def urlquoteplus(data):
@app.template_filter('scrubemail')
def scrubemail_filter(data, css_junk=''):
- return Markup(scrubemail(unicode(bleach.linkify(bleach.clean(data))), rot13=True, css_junk=css_junk))
+ return Markup(scrubemail(str(bleach.linkify(bleach.clean(data))), rot13=True, css_junk=css_junk))
@app.template_filter('hideemail')
@@ -874,7 +875,7 @@ def format_job_category(job_category):
def inject_filter_options():
def get_job_filters():
filters = g.get('event_data', {}).get('filters', {})
- cache_key = 'jobfilters/' + (g.board.name + '/' if g.board else '') + hashlib.sha1(repr(filters)).hexdigest()
+ cache_key = 'jobfilters/' + (g.board.name + '/' if g.board else '') + hashlib.sha1(repr(filters).encode('utf-8')).hexdigest()
result = cache.get(cache_key)
if not result:
basequery = getposts(showall=True, order=False, limit=False)
diff --git a/hasjob/views/index.py b/hasjob/views/index.py
index cc8687031..05dc01ae0 100644
--- a/hasjob/views/index.py
+++ b/hasjob/views/index.py
@@ -226,7 +226,7 @@ def fetch_jobposts(request_args, request_values, filters, is_index, board, board
if posts:
employer_name = posts[0].company_name
else:
- employer_name = u'a single employer'
+ employer_name = 'a single employer'
jobpost_ab = session_jobpost_ab()
if is_index and posts and not gkiosk and not embed:
@@ -302,7 +302,7 @@ def fetch_jobposts(request_args, request_values, filters, is_index, board, board
if (row[0][1].hashid not in pinned_hashids and row[0][1].datetime < startdate):
break
- batch = grouped.items()[startindex:startindex + batchsize]
+ batch = list(grouped.items())[startindex:startindex + batchsize]
if startindex + batchsize < len(grouped):
# Get the datetime of the last group's first item
# batch = [((type, domain), [(pinned, post, bgroup), ...])]
@@ -395,7 +395,7 @@ def index(basequery=None, filters={}, md5sum=None, tag=None, domain=None, locati
db.session.rollback()
g.event_data['search_syntax_error'] = (query_string, search_query)
if not request.is_xhr:
- flash(_(u"Search terms ignored because this didn’t parse: {query}").format(query=search_query), 'danger')
+ flash(_("Search terms ignored because this didn’t parse: {query}").format(query=search_query), 'danger')
search_query = None
else:
search_query = None
@@ -422,7 +422,7 @@ def index(basequery=None, filters={}, md5sum=None, tag=None, domain=None, locati
if data['grouped']:
g.impressions = {post.id: (pinflag, post.id, is_bgroup)
- for group in data['grouped'].itervalues()
+ for group in data['grouped'].values()
for pinflag, post, is_bgroup in group}
elif data['pinsandposts']:
g.impressions = {post.id: (pinflag, post.id, is_bgroup) for pinflag, post, is_bgroup in data['pinsandposts']}
@@ -608,11 +608,11 @@ def feed(basequery=None, type=None, category=None, md5sum=None, domain=None, loc
elif category:
title = category.title
elif md5sum or domain:
- title = u"Jobs at a single employer"
+ title = "Jobs at a single employer"
elif location:
- title = u"Jobs in {location}".format(location=location['use_title'])
+ title = "Jobs in {location}".format(location=location['use_title'])
elif tag:
- title = u"Jobs tagged {tag}".format(tag=title)
+ title = "Jobs tagged {tag}".format(tag=title)
posts = list(getposts(basequery, showall=True, limit=100))
if posts: # Can't do this unless posts is a list
updated = posts[0].datetime.isoformat() + 'Z'
@@ -889,8 +889,8 @@ def oembed(url):
if endpoint not in embed_index_views:
return jsonify({})
- board = Board.get(view_args.get('subdomain', u'www'))
- iframeid = 'hasjob-iframe-' + unicode(uuid4())
+ board = Board.get(view_args.get('subdomain', 'www'))
+ iframeid = 'hasjob-iframe-' + str(uuid4())
parsed_url = urlsplit(url)
embed_url = SplitResult(
diff --git a/hasjob/views/kiosk.py b/hasjob/views/kiosk.py
index 83a0d90d7..2313198cd 100644
--- a/hasjob/views/kiosk.py
+++ b/hasjob/views/kiosk.py
@@ -65,7 +65,7 @@ def kiosk_manifest():
lines.append('NETWORK:')
lines.append('*')
- return Response(u'\r\n'.join(lines),
+ return Response('\r\n'.join(lines),
content_type='text/cache-manifest')
else:
abort(410)
diff --git a/hasjob/views/listing.py b/hasjob/views/listing.py
index 20084c19d..d3fd23905 100644
--- a/hasjob/views/listing.py
+++ b/hasjob/views/listing.py
@@ -297,7 +297,7 @@ def applyjob(domain, hashid):
if job_application:
flashmsg = "You have already applied to this job. You may not apply again"
if request.is_xhr:
- return u'{}
'.format(flashmsg)
+ return '{}
'.format(flashmsg)
else:
flash(flashmsg, 'interactive')
return redirect(post.url_for(), 303)
@@ -337,7 +337,7 @@ def applyjob(domain, hashid):
email_text = html2text(email_html)
flashmsg = "Your application has been sent to the employer"
- msg = Message(subject=u"Job application: {fullname}".format(fullname=job_application.fullname),
+ msg = Message(subject="Job application: {fullname}".format(fullname=job_application.fullname),
recipients=[post.email])
if not job_application.user:
# Also BCC the candidate (for kiosk mode)
@@ -349,7 +349,7 @@ def applyjob(domain, hashid):
mail.send(msg)
if request.is_xhr:
- return u'{}
'.format(flashmsg)
+ return '{}
'.format(flashmsg)
else:
flash(flashmsg, 'interactive')
return redirect(post.url_for(), 303)
@@ -418,7 +418,7 @@ def view_application(domain, hashid, application):
# Transition code until we force all employers to login before posting
if post.user and not (post.admin_is(g.user) or lastuser.has_permission('siteadmin')):
if not g.user:
- return redirect(url_for('login', message=u"You need to be logged in to view candidate applications on Hasjob."))
+ return redirect(url_for('login', message="You need to be logged in to view candidate applications on Hasjob."))
else:
abort(403)
job_application = JobApplication.query.filter_by(hashid=application, jobpost=post).first_or_404()
@@ -489,13 +489,13 @@ def process_application(domain, hashid, application):
email_text = html2text(email_html)
sender_name = g.user.fullname if post.admin_is(g.user) else post.fullname or post.company_name
- sender_formatted = u'{sender} (via {site})'.format(
+ sender_formatted = '{sender} (via {site})'.format(
sender=sender_name,
site=app.config['SITE_TITLE'])
if job_application.response.REPLIED:
msg = Message(
- subject=u"{candidate}: {headline}".format(
+ subject="{candidate}: {headline}".format(
candidate=job_application.user.fullname, headline=post.headline),
sender=(sender_formatted, app.config['MAIL_SENDER']),
reply_to=(sender_name, post.email),
@@ -503,7 +503,7 @@ def process_application(domain, hashid, application):
bcc=[post.email])
flashmsg = "We sent your message to the candidate and copied you. Their email and phone number are below"
else:
- msg = Message(subject=u"Job declined: {headline}".format(headline=post.headline),
+ msg = Message(subject="Job declined: {headline}".format(headline=post.headline),
sender=(sender_formatted, app.config['MAIL_SENDER']),
bcc=[job_application.email, post.email])
flashmsg = "We sent your message to the candidate and copied you"
@@ -523,7 +523,7 @@ def process_application(domain, hashid, application):
if flashmsg:
if request.is_xhr:
- return u'{}
'.format(flashmsg)
+ return '{}
'.format(flashmsg)
else:
flash(flashmsg, 'interactive')
@@ -732,7 +732,7 @@ def confirm_email(domain, hashid, key):
return redirect(post.url_for('confirm'), code=302)
elif post.state.PENDING:
if key != post.email_verify_key:
- return render_template('403.html.jinja2', description=u"This link has expired or is malformed. Check if you have received a newer email from us.")
+ return render_template('403.html.jinja2', description="This link has expired or is malformed. Check if you have received a newer email from us.")
else:
if app.config.get('THROTTLE_LIMIT', 0) > 0:
post_count = JobPost.query.filter(
@@ -741,9 +741,9 @@ def confirm_email(domain, hashid, key):
JobPost.datetime > utcnow() - timedelta(days=1)
).count()
if post_count > app.config['THROTTLE_LIMIT']:
- flash(u"We have received too many posts with %s addresses in the last 24 hours. "
- u"Posts are rate-limited per domain, so yours was not confirmed for now. "
- u"Please try confirming again in a few hours."
+ flash("We have received too many posts with %s addresses in the last 24 hours. "
+ "Posts are rate-limited per domain, so yours was not confirmed for now. "
+ "Please try confirming again in a few hours."
% post.email_domain, category='info')
return redirect(url_for('index'))
post.confirm()
@@ -805,7 +805,7 @@ def editjob(hashid, key, domain=None, form=None, validated=False, newpost=None):
form.job_type.choices = JobType.choices(g.board)
form.job_category.choices = JobCategory.choices(g.board)
if g.board and not g.board.require_pay:
- form.job_pay_type.choices = [(-1, u'Confidential')] + PAY_TYPE.items()
+ form.job_pay_type.choices = [(-1, 'Confidential')] + PAY_TYPE.items()
post = None
no_email = False
@@ -841,7 +841,7 @@ def editjob(hashid, key, domain=None, form=None, validated=False, newpost=None):
form_perks = bleach.linkify(bleach.clean(form.job_perks_description.data, tags=ALLOWED_TAGS)) if form.job_perks.data else ''
form_how_to_apply = form.job_how_to_apply.data
form_email_domain = get_email_domain(form.poster_email.data)
- form_words = get_word_bag(u' '.join((form_description, form_perks, form_how_to_apply)))
+ form_words = get_word_bag(' '.join((form_description, form_perks, form_how_to_apply)))
similar = False
with db.session.no_autoflush:
@@ -922,10 +922,10 @@ def editjob(hashid, key, domain=None, form=None, validated=False, newpost=None):
# To protect from gaming, don't allow words to be removed in edited posts once the post
# has been confirmed. Just add the new words.
if not post.state.UNPUBLISHED:
- prev_words = post.words or u''
+ prev_words = post.words or ''
else:
- prev_words = u''
- post.words = get_word_bag(u' '.join((prev_words, form_description, form_perks, form_how_to_apply)))
+ prev_words = ''
+ post.words = get_word_bag(' '.join((prev_words, form_description, form_perks, form_how_to_apply)))
post.language, post.language_confidence = identify_language(post)
@@ -964,8 +964,8 @@ def newjob():
archived_post = None
if not g.user:
return redirect(url_for('login', next=url_for('newjob'),
- message=u"Hasjob now requires you to login before posting a job. Please login as yourself."
- u" We'll add details about your company later"))
+ message="Hasjob now requires you to login before posting a job. Please login as yourself."
+ " We'll add details about your company later"))
else:
if g.user.blocked:
flash("Your account has been blocked from posting jobs", category='info')
@@ -976,7 +976,7 @@ def newjob():
abort(403)
if g.board and not g.board.require_pay:
- form.job_pay_type.choices = [(-1, u'Confidential')] + PAY_TYPE.items()
+ form.job_pay_type.choices = [(-1, 'Confidential')] + PAY_TYPE.items()
form.job_type.choices = JobType.choices(g.board)
form.job_category.choices = JobCategory.choices(g.board)
diff --git a/hasjob/views/location.py b/hasjob/views/location.py
index 354d62118..d6bdde0bd 100644
--- a/hasjob/views/location.py
+++ b/hasjob/views/location.py
@@ -22,7 +22,7 @@ def location_new():
JobPost.state.LISTED,
~JobLocation.geonameid.in_(db.session.query(Location.id).filter(Location.board == g.board))
).group_by(JobLocation.geonameid).order_by(db.text('count DESC')).limit(100)])
- data = location_geodata(geonames.keys())
+ data = location_geodata(list(geonames.keys()))
for row in data.values():
geonames[row['geonameid']] = row
choices = [('%s/%s' % (row['geonameid'], row['name']), row['picker_title']) for row in geonames.values()]
@@ -72,6 +72,6 @@ def location_delete(name):
abort(404)
return render_delete_sqla(location, db, title=_("Confirm delete"),
- message=_(u"Delete location ‘{title}’?").format(title=location.title),
- success=_(u"You have deleted location ‘{title}’.").format(title=location.title),
+ message=_("Delete location ‘{title}’?").format(title=location.title),
+ success=_("You have deleted location ‘{title}’.").format(title=location.title),
next=url_for('index'), cancel_url=location.url_for())
diff --git a/hasjob/views/login.py b/hasjob/views/login.py
index c01292c5b..4909c4b36 100644
--- a/hasjob/views/login.py
+++ b/hasjob/views/login.py
@@ -24,7 +24,7 @@ def login():
@app.route('/logout')
@lastuser.logout_handler
def logout():
- flash(u"You are now logged out", category='info')
+ flash("You are now logged out", category='info')
signal_logout.send(app, user=g.user)
return get_next_url()
@@ -48,9 +48,9 @@ def lastuser_error(error, error_description=None, error_uri=None):
if error == 'access_denied':
flash("You denied the request to login", category='error')
return redirect(get_next_url())
- return Response(u"Error: %s\n"
- u"Description: %s\n"
- u"URI: %s" % (error, error_description, error_uri),
+ return Response("Error: %s\n"
+ "Description: %s\n"
+ "URI: %s" % (error, error_description, error_uri),
mimetype="text/plain")
diff --git a/instance/settings.py b/instance/settings.py
index 17960ac7d..f9073491b 100644
--- a/instance/settings.py
+++ b/instance/settings.py
@@ -69,18 +69,18 @@
SUPPORT_EMAIL = 'support@hasgeek.com'
#: Words banned in the title and their error messages
BANNED_WORDS = [
- [['awesome'], u'We’ve had a bit too much awesome around here lately. Got another adjective?'],
- [['rockstar', 'rock star', 'rock-star'], u'You are not rich enough to hire a rockstar. Got another adjective?'],
- [['superstar', 'super star'], u'Everyone around here is a superstar. The term is redundant.'],
- [['kickass', 'kick ass', 'kick-ass', 'kicka$$', 'kick-a$$', 'kick a$$'], u'We don’t condone kicking asses around here. Got another adjective?'],
- [['ninja'], u'Ninjas kill people. We can’t allow that. Got another adjective?'],
- [['urgent', 'immediate', 'emergency'], u'Sorry, we can’t help with urgent or immediate requirements. Geeks don’t grow on trees'],
- [['passionate'], u'Passion is implicit. Why even ask? Try another adjective?'],
- [['amazing'], u'Everybody’s amazing around here. The adjective is redundant.'],
- [['fodu'], u'We don’t know what you mean, but that sounds like a dirty word. Got another adjective?'],
- [['sick'], u'Need an ambulance? Call 102, 108, 112 or 1298. One of those should work.'],
- [['killer'], u'Murder is illegal. Don’t make us call the cops.'],
- [['iit', 'iitian', 'iit-ian', 'iim', 'bits', 'bitsian'], u'Q: How do you know someone is from IIT/IIM/BITS? A: They remind you all the time. Don’t be that person.'],
+ [['awesome'], 'We’ve had a bit too much awesome around here lately. Got another adjective?'],
+ [['rockstar', 'rock star', 'rock-star'], 'You are not rich enough to hire a rockstar. Got another adjective?'],
+ [['superstar', 'super star'], 'Everyone around here is a superstar. The term is redundant.'],
+ [['kickass', 'kick ass', 'kick-ass', 'kicka$$', 'kick-a$$', 'kick a$$'], 'We don’t condone kicking asses around here. Got another adjective?'],
+ [['ninja'], 'Ninjas kill people. We can’t allow that. Got another adjective?'],
+ [['urgent', 'immediate', 'emergency'], 'Sorry, we can’t help with urgent or immediate requirements. Geeks don’t grow on trees'],
+ [['passionate'], 'Passion is implicit. Why even ask? Try another adjective?'],
+ [['amazing'], 'Everybody’s amazing around here. The adjective is redundant.'],
+ [['fodu'], 'We don’t know what you mean, but that sounds like a dirty word. Got another adjective?'],
+ [['sick'], 'Need an ambulance? Call 102, 108, 112 or 1298. One of those should work.'],
+ [['killer'], 'Murder is illegal. Don’t make us call the cops.'],
+ [['iit', 'iitian', 'iit-ian', 'iim', 'bits', 'bitsian'], 'Q: How do you know someone is from IIT/IIM/BITS? A: They remind you all the time. Don’t be that person.'],
]
#: URLs we don't accept, with accompanying error messages
INVALID_URLS = [
@@ -93,7 +93,7 @@
re.compile(r'hirist\.com/j/'),
re.compile(r'iimjobs\.com/j/'),
re.compile(r'.*\.workable.com/jobs'),
- ], u"Candidates must apply via Hasjob")
+ ], "Candidates must apply via Hasjob")
]
#: LastUser server
LASTUSER_SERVER = 'https://auth.hasgeek.com/'
diff --git a/instance/testing.py b/instance/testing.py
index 36fe3abc6..8076c31dc 100644
--- a/instance/testing.py
+++ b/instance/testing.py
@@ -4,11 +4,18 @@
#: The title of this site
SITE_TITLE = 'Job Board'
#: Database backend
-SQLALCHEMY_DATABASE_URI = 'postgres://127.0.0.1/hasjob'
+SQLALCHEMY_DATABASE_URI = 'postgres:///hasjob_testing'
SERVER_NAME = 'hasjob.travis.local:5000'
#: LastUser server
LASTUSER_SERVER = 'https://auth.hasgeek.com/'
#: LastUser client id
-LASTUSER_CLIENT_ID = os.environ.get('LASTUSER_CLIENT_ID')
+LASTUSER_CLIENT_ID = os.environ.get('LASTUSER_CLIENT_ID', '')
#: LastUser client secret
-LASTUSER_CLIENT_SECRET = os.environ.get('LASTUSER_CLIENT_SECRET')
+LASTUSER_CLIENT_SECRET = os.environ.get('LASTUSER_CLIENT_SECRET', '')
+
+STATIC_SUBDOMAIN = 'static'
+
+ASSET_SERVER = 'https://static.hasgeek.co.in/'
+ASSET_MANIFEST_PATH = "static/build/manifest.json"
+# no trailing slash
+ASSET_BASE_PATH = '/static/build'
diff --git a/migrations/env.py b/migrations/env.py
index 46a8e201d..e936a859f 100755
--- a/migrations/env.py
+++ b/migrations/env.py
@@ -1,4 +1,4 @@
-from __future__ import with_statement
+
from alembic import context
from sqlalchemy import engine_from_config, pool
from logging.config import fileConfig
diff --git a/migrations/versions/05e807853572_switch_to_timestamptz.py b/migrations/versions/05e807853572_switch_to_timestamptz.py
index 8706bfa7e..c48fd6963 100644
--- a/migrations/versions/05e807853572_switch_to_timestamptz.py
+++ b/migrations/versions/05e807853572_switch_to_timestamptz.py
@@ -5,7 +5,7 @@
Create Date: 2019-05-10 12:52:53.016791
"""
-from __future__ import print_function
+
# revision identifiers, used by Alembic.
revision = '05e807853572'
diff --git a/migrations/versions/140e56666d9e_a_b_tests_for_headlines.py b/migrations/versions/140e56666d9e_a_b_tests_for_headlines.py
index fc646c992..e9a456072 100644
--- a/migrations/versions/140e56666d9e_a_b_tests_for_headlines.py
+++ b/migrations/versions/140e56666d9e_a_b_tests_for_headlines.py
@@ -26,12 +26,12 @@ def upgrade():
sa.PrimaryKeyConstraint('event_session_id', 'jobpost_id')
)
op.create_index(op.f('ix_job_view_session_jobpost_id'), 'job_view_session', ['jobpost_id'], unique=False)
- op.add_column(u'job_impression', sa.Column('bgroup', sa.Boolean(), nullable=True))
- op.add_column(u'jobpost', sa.Column('headlineb', sa.Unicode(length=100), nullable=True))
+ op.add_column('job_impression', sa.Column('bgroup', sa.Boolean(), nullable=True))
+ op.add_column('jobpost', sa.Column('headlineb', sa.Unicode(length=100), nullable=True))
def downgrade():
- op.drop_column(u'jobpost', 'headlineb')
- op.drop_column(u'job_impression', 'bgroup')
+ op.drop_column('jobpost', 'headlineb')
+ op.drop_column('job_impression', 'bgroup')
op.drop_index(op.f('ix_job_view_session_event_session_id'), table_name='job_view_session')
op.drop_table('job_view_session')
diff --git a/migrations/versions/1710bfac281a_discard_unused_org_and_team_models.py b/migrations/versions/1710bfac281a_discard_unused_org_and_team_models.py
index 50b44ed28..0e083beca 100644
--- a/migrations/versions/1710bfac281a_discard_unused_org_and_team_models.py
+++ b/migrations/versions/1710bfac281a_discard_unused_org_and_team_models.py
@@ -17,7 +17,7 @@
def upgrade():
op.drop_index('ix_jobpost_org_id', table_name='jobpost')
- op.drop_constraint(u'jobpost_org_id_fkey', 'jobpost', type_='foreignkey')
+ op.drop_constraint('jobpost_org_id_fkey', 'jobpost', type_='foreignkey')
op.drop_column('jobpost', 'org_id')
op.drop_table('users_teams')
op.drop_table('org_location')
@@ -27,7 +27,7 @@ def upgrade():
def downgrade():
op.create_table('team',
- sa.Column('id', sa.INTEGER(), server_default=sa.text(u"nextval('team_id_seq'::regclass)"), nullable=False),
+ sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('team_id_seq'::regclass)"), nullable=False),
sa.Column('created_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=False),
sa.Column('updated_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=False),
sa.Column('title', sa.VARCHAR(length=250), autoincrement=False, nullable=False),
@@ -35,11 +35,11 @@ def downgrade():
sa.Column('owners', sa.BOOLEAN(), autoincrement=False, nullable=False),
sa.Column('orgid', sa.VARCHAR(length=22), autoincrement=False, nullable=False),
sa.Column('members', sa.BOOLEAN(), autoincrement=False, nullable=False),
- sa.PrimaryKeyConstraint('id', name=u'team_pkey'),
+ sa.PrimaryKeyConstraint('id', name='team_pkey'),
postgresql_ignore_search_path=False
)
op.create_table('organization',
- sa.Column('id', sa.INTEGER(), server_default=sa.text(u"nextval('organization_id_seq'::regclass)"), nullable=False),
+ sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('organization_id_seq'::regclass)"), nullable=False),
sa.Column('created_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=False),
sa.Column('updated_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=False),
sa.Column('domain', sa.VARCHAR(length=253), autoincrement=False, nullable=True),
@@ -52,9 +52,9 @@ def downgrade():
sa.Column('title', sa.VARCHAR(length=250), autoincrement=False, nullable=False),
sa.Column('admin_team_id', sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column('hiring_team_id', sa.INTEGER(), autoincrement=False, nullable=True),
- sa.ForeignKeyConstraint(['admin_team_id'], [u'team.id'], name=u'organization_admin_team_id_fkey', ondelete=u'SET NULL'),
- sa.ForeignKeyConstraint(['hiring_team_id'], [u'team.id'], name=u'organization_hiring_team_id_fkey', ondelete=u'SET NULL'),
- sa.PrimaryKeyConstraint('id', name=u'organization_pkey'),
+ sa.ForeignKeyConstraint(['admin_team_id'], ['team.id'], name='organization_admin_team_id_fkey', ondelete='SET NULL'),
+ sa.ForeignKeyConstraint(['hiring_team_id'], ['team.id'], name='organization_hiring_team_id_fkey', ondelete='SET NULL'),
+ sa.PrimaryKeyConstraint('id', name='organization_pkey'),
postgresql_ignore_search_path=False
)
op.create_table('org_location',
@@ -73,18 +73,18 @@ def downgrade():
sa.Column('country', sa.VARCHAR(length=80), autoincrement=False, nullable=True),
sa.Column('geonameid', sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column('url_id', sa.INTEGER(), autoincrement=False, nullable=False),
- sa.ForeignKeyConstraint(['org_id'], [u'organization.id'], name=u'org_location_org_id_fkey'),
- sa.PrimaryKeyConstraint('id', name=u'org_location_pkey')
+ sa.ForeignKeyConstraint(['org_id'], ['organization.id'], name='org_location_org_id_fkey'),
+ sa.PrimaryKeyConstraint('id', name='org_location_pkey')
)
op.create_table('users_teams',
sa.Column('created_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=False),
sa.Column('updated_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=False),
sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('team_id', sa.INTEGER(), autoincrement=False, nullable=False),
- sa.ForeignKeyConstraint(['team_id'], [u'team.id'], name=u'users_teams_team_id_fkey'),
- sa.ForeignKeyConstraint(['user_id'], [u'user.id'], name=u'users_teams_user_id_fkey'),
- sa.PrimaryKeyConstraint('user_id', 'team_id', name=u'users_teams_pkey')
+ sa.ForeignKeyConstraint(['team_id'], ['team.id'], name='users_teams_team_id_fkey'),
+ sa.ForeignKeyConstraint(['user_id'], ['user.id'], name='users_teams_user_id_fkey'),
+ sa.PrimaryKeyConstraint('user_id', 'team_id', name='users_teams_pkey')
)
op.add_column('jobpost', sa.Column('org_id', sa.INTEGER(), autoincrement=False, nullable=True))
- op.create_foreign_key(u'jobpost_org_id_fkey', 'jobpost', 'organization', ['org_id'], ['id'])
+ op.create_foreign_key('jobpost_org_id_fkey', 'jobpost', 'organization', ['org_id'], ['id'])
op.create_index('ix_jobpost_org_id', 'jobpost', ['org_id'], unique=False)
diff --git a/migrations/versions/17869f3e044c_event_sessions.py b/migrations/versions/17869f3e044c_event_sessions.py
index 55361183a..430cc6316 100644
--- a/migrations/versions/17869f3e044c_event_sessions.py
+++ b/migrations/versions/17869f3e044c_event_sessions.py
@@ -41,7 +41,7 @@ def upgrade():
sa.Column('gclid', sa.Unicode(length=250), nullable=False),
sa.Column('active_at', sa.DateTime(), nullable=False),
sa.Column('ended_at', sa.DateTime(), nullable=True),
- sa.CheckConstraint(u'CASE WHEN (event_session.user_id IS NOT NULL) THEN 1 ELSE 0 END + CASE WHEN (event_session.anon_user_id IS NOT NULL) THEN 1 ELSE 0 END = 1', name='user_event_session_user_id_or_anon_user_id'),
+ sa.CheckConstraint('CASE WHEN (event_session.user_id IS NOT NULL) THEN 1 ELSE 0 END + CASE WHEN (event_session.anon_user_id IS NOT NULL) THEN 1 ELSE 0 END = 1', name='user_event_session_user_id_or_anon_user_id'),
sa.ForeignKeyConstraint(['anon_user_id'], ['anon_user.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
diff --git a/migrations/versions/29fd6847a8e2_org_teams.py b/migrations/versions/29fd6847a8e2_org_teams.py
index 3cf3ec28c..932f75bb5 100644
--- a/migrations/versions/29fd6847a8e2_org_teams.py
+++ b/migrations/versions/29fd6847a8e2_org_teams.py
@@ -37,8 +37,8 @@ def upgrade():
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('user_id', 'team_id')
)
- op.add_column(u'organization', sa.Column('admin_team_id', sa.Integer(), nullable=True))
- op.add_column(u'organization', sa.Column('hiring_team_id', sa.Integer(), nullable=True))
+ op.add_column('organization', sa.Column('admin_team_id', sa.Integer(), nullable=True))
+ op.add_column('organization', sa.Column('hiring_team_id', sa.Integer(), nullable=True))
op.create_foreign_key('organization_admin_team_id_fkey', 'organization', 'team', ['admin_team_id'], ['id'],
ondelete='SET NULL')
op.create_foreign_key('organization_hiring_team_id_fkey', 'organization', 'team', ['hiring_team_id'], ['id'],
@@ -48,8 +48,8 @@ def upgrade():
def downgrade():
op.drop_constraint('organization_hiring_team_id_fkey', 'organization', type_='foreignkey')
op.drop_constraint('organization_admin_team_id_fkey', 'organization', type_='foreignkey')
- op.drop_column(u'organization', 'hiring_team_id')
- op.drop_column(u'organization', 'admin_team_id')
+ op.drop_column('organization', 'hiring_team_id')
+ op.drop_column('organization', 'admin_team_id')
op.drop_table('users_teams')
op.drop_index(op.f('ix_team_orgid'), table_name='team')
op.drop_table('team')
diff --git a/migrations/versions/476608367f85_dns_domains.py b/migrations/versions/476608367f85_dns_domains.py
index 527a8ac33..79ab84760 100644
--- a/migrations/versions/476608367f85_dns_domains.py
+++ b/migrations/versions/476608367f85_dns_domains.py
@@ -54,7 +54,7 @@ def upgrade():
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
- op.add_column(u'jobpost', sa.Column('domain_id', sa.Integer(), nullable=True))
+ op.add_column('jobpost', sa.Column('domain_id', sa.Integer(), nullable=True))
op.create_foreign_key('jobpost_domain_id_fkey', 'jobpost', 'domain', ['domain_id'], ['id'])
op.execute(sa.text(
'''INSERT INTO domain (created_at, updated_at, name, is_webmail, is_banned)
@@ -70,5 +70,5 @@ def upgrade():
def downgrade():
op.drop_constraint('jobpost_domain_id_fkey', 'jobpost', type_='foreignkey')
- op.drop_column(u'jobpost', 'domain_id')
+ op.drop_column('jobpost', 'domain_id')
op.drop_table('domain')
diff --git a/migrations/versions/a8f1e1c55a57_switch_to_timestamptz_part_two.py b/migrations/versions/a8f1e1c55a57_switch_to_timestamptz_part_two.py
index 7097c3619..5ea8cdc95 100644
--- a/migrations/versions/a8f1e1c55a57_switch_to_timestamptz_part_two.py
+++ b/migrations/versions/a8f1e1c55a57_switch_to_timestamptz_part_two.py
@@ -5,7 +5,7 @@
Create Date: 2019-05-15 20:46:26.193463
"""
-from __future__ import print_function
+
# revision identifiers, used by Alembic.
revision = 'a8f1e1c55a57'
diff --git a/migrations/versions/da1dfcda8b3_location_scoped_to_board.py b/migrations/versions/da1dfcda8b3_location_scoped_to_board.py
index 5922c3842..7cc4da911 100644
--- a/migrations/versions/da1dfcda8b3_location_scoped_to_board.py
+++ b/migrations/versions/da1dfcda8b3_location_scoped_to_board.py
@@ -18,7 +18,7 @@ def upgrade():
op.add_column('location', sa.Column('board_id', sa.Integer(), nullable=False, server_default=sa.text('1')))
op.alter_column('location', 'board_id', server_default=None)
op.create_index(op.f('ix_location_board_id'), 'location', ['board_id'], unique=False)
- op.drop_constraint(u'location_name_key', 'location', type_='unique')
+ op.drop_constraint('location_name_key', 'location', type_='unique')
op.create_unique_constraint('location_board_id_name_key', 'location', ['board_id', 'name'])
op.create_foreign_key('location_board_id_fkey', 'location', 'board', ['board_id'], ['id'])
op.drop_constraint('location_pkey', 'location', type_='primary')
diff --git a/requirements.txt b/requirements.txt
index 9f9fc492e..d83fa8539 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -20,14 +20,14 @@ ordereddict==1.1
jsmin==2.2.2
psycopg2==2.8.4
dnspython==1.16.0
-html2text==2019.9.26
+html2text==2020.1.16
premailer==3.6.1
langid>=1.1.4dev
tldextract==2.2.2
-unicodecsv==0.14.1
geoip2==2.9.0
-git+https://github.com/hasgeek/coaster
-git+https://github.com/hasgeek/flask-lastuser
-git+https://github.com/hasgeek/baseframe
+git+https://github.com/hasgeek/flask-babel2.git
+git+https://github.com/hasgeek/coaster.git
+git+https://github.com/hasgeek/flask-lastuser.git
+git+https://github.com/hasgeek/baseframe.git
Flask-Migrate==2.5.2
progressbar2==3.47.0
diff --git a/rqinit.py b/rqinit.py
index 00a6e60cd..1fa41d029 100644
--- a/rqinit.py
+++ b/rqinit.py
@@ -1,4 +1,4 @@
-from urlparse import urlparse
+from urllib.parse import urlparse
from hasjob import app
diff --git a/runtests.sh b/runtests.sh
index 2e9cb98b5..594fb3abc 100755
--- a/runtests.sh
+++ b/runtests.sh
@@ -1,5 +1,6 @@
#!/bin/sh
set -e
+export PYTHONIOENCODING="UTF-8"
export FLASK_ENV="TESTING"
-coverage run `which nosetests` "$@"
+coverage run -m pytest "$@"
coverage report -m
diff --git a/setup.cfg b/setup.cfg
index 7593f0158..784e2babf 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -2,3 +2,6 @@
ignore = E501, E128, E124, E402, W503
hang-closing = True
exclude = hasjob/assets/node_modules
+
+[tool:pytest]
+testpaths = tests
diff --git a/test_requirements.txt b/test_requirements.txt
index 39ee417cd..eb237bfef 100644
--- a/test_requirements.txt
+++ b/test_requirements.txt
@@ -1,2 +1,4 @@
coverage==4.5.4
-nose-progressive==1.5.2
\ No newline at end of file
+nose-progressive==1.5.2
+pytest>=4.6.3
+coveralls==1.8.2
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 000000000..0319d8048
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+
+
+import pytest
+
+from hasjob import app
+from hasjob.models import db
+
+
+@pytest.fixture(scope='session')
+def test_client():
+ # Flask provides a way to test your application by exposing the Werkzeug test Client
+ # and handling the context locals for you.
+ testing_client = app.test_client()
+
+ # Establish an application context before running the tests.
+ ctx = app.app_context()
+ ctx.push()
+
+ yield testing_client # this is where the testing happens!
+
+ # anything after yield is teardown code
+ ctx.pop()
+
+
+@pytest.fixture(scope='session')
+def test_db(test_client):
+ # Create the database and the database table
+ db.create_all()
+
+ yield db # this is where the testing happens!
+
+ # anything after yield is teardown code
+ db.session.rollback()
+ db.session.remove()
+ db.drop_all()
diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/integration/test_index.py b/tests/integration/test_index.py
new file mode 100644
index 000000000..86775fb12
--- /dev/null
+++ b/tests/integration/test_index.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+
+from flask import url_for
+
+
+class TestIndexView:
+ def test_index(self, test_client, test_db):
+ with test_client as c:
+ resp = c.get(url_for('index', subdomain=None))
+ assert "Hasjob" in resp.data.decode('utf-8')
diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
new file mode 100644
index 000000000..d0b171f07
--- /dev/null
+++ b/tests/unit/test_utils.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+
+from hasjob.utils import redactemail
+
+
+class TestUtils:
+ def test_redactemail(self):
+ message = "Send email to test@example.com and you are all set."
+ expected_message = "Send email to [redacted] and you are all set."
+ assert redactemail(message) == expected_message
diff --git a/tools/metarefresh.py b/tools/metarefresh.py
index 55d4c7772..1b58fd22a 100644
--- a/tools/metarefresh.py
+++ b/tools/metarefresh.py
@@ -41,4 +41,4 @@ def final_url(url):
if __name__ == '__main__':
- print final_url('http://localhost:8000/')
+ print(final_url('http://localhost:8000/'))