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/'))