From 23519c078fe3f8ea10b982c73a294469fe3c962a Mon Sep 17 00:00:00 2001 From: Shreyas Satish Date: Thu, 5 Apr 2018 21:22:16 +0530 Subject: [PATCH 01/17] subscriptions [wip] --- hasjob/models/user.py | 33 +++++++++++++++++++++++++ hasjob/views/index.py | 57 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/hasjob/models/user.py b/hasjob/models/user.py index 91086ec35..7786d5ec9 100644 --- a/hasjob/models/user.py +++ b/hasjob/models/user.py @@ -228,3 +228,36 @@ class UserEvent(UserEventBase, BaseMixin, db.Model): name = db.Column(db.Unicode(80), nullable=False) #: Custom event data (null = no data saved) data = db.Column(JsonDict, nullable=True) + + +# class JobPostSubscription(BaseMixin, db.Model): +# __tablename__ = 'jobpost_subscription' + +# user_id = db.Column(None, db.ForeignKey('user.id'), primary_key=True, index=True) +# user = db.relationship(User, backref=db.backref('subscriptions', +# lazy='dynamic', cascade='all, delete-orphan')) + +# filterset_id = db.Column(None, db.ForeignKey('filterset.id'), primary_key=True, index=True) +# filterset = db.relationship('Filterset', backref=db.backref('subscriptions', +# lazy='dynamic', cascade='all, delete-orphan')) +# active = db.Column(db.Boolean, nullable=False, default=True) +# email = db.Column(db.Boolean, nullable=False, default=False) +# email_frequency = db.Column(db.SmallInteger, nullable=True) +# email_preferred_time = db.Column(db.Time, nullable=False, default=db.func.utcnow(), primary_key=True) + + +# jobpost_alert_table = db.Table('jobpost_alert', db.Model.metadata, +# db.Column('jobpost_id', None, db.ForeignKey('jobpost.id'), primary_key=True, index=True), +# db.Column('jobpost_alert', None, db.ForeignKey('jobpost_alert.id'), primary_key=True, index=True), +# db.Column('created_at', db.DateTime, nullable=False, default=db.func.utcnow()) +# ) + + +# class JobPostAlert(BaseMixin, db.Model): +# __tablename__ = 'jobpost_alert' + +# jobpost_subscription_id = db.Column(None, db.ForeignKey('jobpost_subscription.id'), +# primary_key=True, index=True) +# jobpost_subscription = db.relationship(JobPostSubscription, backref=db.backref('alerts', +# lazy='dynamic', cascade='all, delete-orphan')) +# sent_at = db.Column(db.DateTime, nullable=False, default=db.func.utcnow(), primary_key=True) diff --git a/hasjob/views/index.py b/hasjob/views/index.py index 892f4cd10..c1dcc20fa 100644 --- a/hasjob/views/index.py +++ b/hasjob/views/index.py @@ -90,7 +90,7 @@ def json_index(data): return jsonify(result) -def fetch_jobposts(request_args, request_values, filters, is_index, board, board_jobs, gkiosk, basequery, md5sum, domain, location, title, showall, statusfilter, batched, ageless, template_vars, search_query=None, query_string=None): +def fetch_jobposts(request_args, request_values, filters, is_index, board, board_jobs, gkiosk, basequery, md5sum, domain, location, title, showall, statusfilter, batched, ageless, template_vars, search_query=None, query_string=None, posts_only=False): if basequery is None: basequery = JobPost.query @@ -201,12 +201,14 @@ def fetch_jobposts(request_args, request_values, filters, is_index, board, board data_filters['query_string'] = query_string basequery = basequery.filter(JobPost.search_vector.match(search_query, postgresql_regconfig='english')) + posts = getposts(basequery, pinned=True, showall=showall, statusfilter=statusfilter, ageless=ageless).all() + if posts_only: + return posts + if data_filters: showall = True batched = True - posts = getposts(basequery, pinned=True, showall=showall, statusfilter=statusfilter, ageless=ageless).all() - if posts: employer_name = posts[0].company_name else: @@ -569,6 +571,55 @@ def filterset_view(name): filterset=filterset) +# @app.route('/subscribe', subdomain='', methods=['POST']) +# @app.route('/subscribe', methods=['POST']) +# def subscribe_to_jobposts(): +# filterset = Filterset.from_filters(g.board, filters) +# if not filterset: +# form = FiltersetForm(parent=g.board) +# if form.validate_on_submit(): +# filterset = Filterset(board=g.board, title=u"") +# form.populate_obj(filterset) +# db.session.add(filterset) + +# if g.user not in filterset.users: +# filterset.users.append(g.user) +# db.session.commit() +# return index(filters=filterset.to_filters(translate_geonameids=True), +# query_string=filterset.keywords, +# filterset=filterset) + + +# def send_alerts_to_subscribers(): +# subscriptions = JobPostSubscription.query.filter_by(active=True).all() +# for sub in subscriptions: +# posts = get_filtered_posts(filters=sub.filterset.to_filters()) +# now = datetime.utcnow() +# if sub.daily: +# days_delta = 1 +# elif sub.weekly: +# days_delta = 7 +# elif sub.monthly: +# days_delta = 30 + +# last_reference_date = now - timedelta(days=days_delta) +# recent_alert = JobPostAlert.query.filter( +# JobPostAlert.jobpost_subscription == sub, JobPostAlert.sent_at >= last_reference_date +# ).order_by('created_at desc').first() +# if recent_alert: +# break + +# sent_jobpostids = [jobpost.id for jobpost in recent_alert.jobposts] +# unsent_posts = [post for post in posts if post.id not in sent_jobpostids] +# if not unsent_posts: +# break + +# jobpost_alert = JobPostAlert(jobpost_subscription=sub, sent_at=now) +# jobpost_alert.jobposts = unsent_posts +# db.session.add(jobpost_alert) +# db.session.commit() + + @app.route('/opensearch.xml', subdomain='') @app.route('/opensearch.xml') def opensearch(): From 620d0453d07b7512aa6dfd21ba9fa3c30d10f746 Mon Sep 17 00:00:00 2001 From: Shreyas Satish Date: Mon, 9 Apr 2018 18:40:19 +0530 Subject: [PATCH 02/17] working backend for job alerts --- hasjob/__init__.py | 2 +- hasjob/jobs/__init__.py | 3 + hasjob/jobs/job_alerts.py | 41 +++++++++++ hasjob/models/__init__.py | 1 + hasjob/models/filterset.py | 33 +++++++++ hasjob/models/jobpost_alert.py | 67 +++++++++++++++++ hasjob/models/user.py | 33 --------- .../job_alert_email_confirmation.html.jinja2 | 17 +++++ hasjob/templates/job_alert_mailer.html.jinja2 | 21 ++++++ hasjob/views/board.py | 2 +- hasjob/views/helper.py | 10 +-- hasjob/views/index.py | 65 +++-------------- hasjob/views/job_alerts.py | 71 +++++++++++++++++++ manage.py | 7 ++ .../efdbaaf67b26_add_tables_for_job_alerts.py | 70 ++++++++++++++++++ 15 files changed, 347 insertions(+), 96 deletions(-) create mode 100644 hasjob/jobs/__init__.py create mode 100644 hasjob/jobs/job_alerts.py create mode 100644 hasjob/models/jobpost_alert.py create mode 100644 hasjob/templates/job_alert_email_confirmation.html.jinja2 create mode 100644 hasjob/templates/job_alert_mailer.html.jinja2 create mode 100644 hasjob/views/job_alerts.py create mode 100644 migrations/versions/efdbaaf67b26_add_tables_for_job_alerts.py diff --git a/hasjob/__init__.py b/hasjob/__init__.py index 952bfa3ca..8b311f722 100644 --- a/hasjob/__init__.py +++ b/hasjob/__init__.py @@ -30,7 +30,7 @@ # Third, after config, import the models and views -from . import models, views # NOQA +from . import models, views, jobs # NOQA from .models import db # NOQA # Configure the app diff --git a/hasjob/jobs/__init__.py b/hasjob/jobs/__init__.py new file mode 100644 index 000000000..347e0a89c --- /dev/null +++ b/hasjob/jobs/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import job_alerts # NOQA diff --git a/hasjob/jobs/job_alerts.py b/hasjob/jobs/job_alerts.py new file mode 100644 index 000000000..a6e89215d --- /dev/null +++ b/hasjob/jobs/job_alerts.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- + +from datetime import datetime, timedelta +from flask_mail import Message +from flask import render_template +from flask_rq import job +from html2text import html2text +from premailer import transform as email_transform +from hasjob import mail +from hasjob.models import db, JobPost, JobPostSubscription, JobPostAlert, jobpost_alert_table +from hasjob.views.index import fetch_jobposts + + +@job('hasjob') +def send_email_alerts(): + subscriptions = JobPostSubscription.query.filter(JobPostSubscription.active == True, + JobPostSubscription.email_verified_at != None).all() + for sub in subscriptions: + if JobPostAlert.query.filter( + JobPostAlert.jobpost_subscription == sub, + JobPostAlert.sent_at >= datetime.utcnow() - timedelta(days=sub.email_frequency.value) + ).order_by('created_at desc').notempty(): + print 'alert was recently sent so skipping' + break + + posts = fetch_jobposts(filters=sub.filterset.to_filters(), posts_only=True) + sent_jobpostids = JobPost.query.join(jobpost_alert_table).join(JobPostAlert).filter(JobPostAlert.jobpost_subscription == sub).options(db.load_only('id')).all() + unseen_posts = [post for post in posts if post.id not in sent_jobpostids] + if not unseen_posts: + print "no unseen posts" + break + + jobpost_alert = JobPostAlert(jobpost_subscription=sub) + jobpost_alert.jobposts = unseen_posts + db.session.commit() + + msg = Message(subject=u"Job alerts", recipients=[sub.user.email]) + html = email_transform(render_template('job_alert_mailer.html.jinja2', posts=jobpost_alert.jobposts)) + msg.html = html + msg.body = html2text(html) + mail.send(msg) diff --git a/hasjob/models/__init__.py b/hasjob/models/__init__.py index 034867e3e..06942a38a 100644 --- a/hasjob/models/__init__.py +++ b/hasjob/models/__init__.py @@ -88,3 +88,4 @@ class CANDIDATE_FEEDBACK(LabeledEnum): from .flags import * from .campaign import * from .filterset import * +from .jobpost_alert import * diff --git a/hasjob/models/filterset.py b/hasjob/models/filterset.py index 2efbadcfc..15ac360a6 100644 --- a/hasjob/models/filterset.py +++ b/hasjob/models/filterset.py @@ -5,6 +5,7 @@ from sqlalchemy.ext.associationproxy import association_proxy from . import db, BaseScopedNameMixin, JobType, JobCategory, Tag, Domain, Board from ..extapi import location_geodata +from coaster.utils import buid, getbool __all__ = ['Filterset'] @@ -79,6 +80,38 @@ def url_for(self, action='view', _external=True, **kwargs): kwargs.setdefault('subdomain', self.board.name if self.board.not_root else None) return super(Filterset, self).url_for(action, name=self.name, _external=_external, **kwargs) + @classmethod + def init_from_filters(cls, board, filters): + obj = cls(parent=board, title=buid()) + if filters.get('t'): + obj.types = JobType.query.filter(JobType.name.in_(filters['t'])).group_by(JobType.id).having( + db.func.count(JobType.name) == len(filters['t'])).all() + + if filters.get('c'): + obj.categories = JobCategory.query.filter(JobCategory.name.in_(filters['c'])).group_by(JobCategory.id).having( + db.func.count(JobCategory.name) == len(filters['c'])).all() + + if filters.get('l'): + geonameids = [] + for loc in filters.get('l'): + geonameids.append(location_geodata(loc)['geonameid']) + obj.geonameids = geonameids + + if getbool(filters.get('anywhere')): + obj.remote_location = True + + if getbool(filters.get('equity')): + obj.equity = True + + if filters.get('currency') and filters.get('pay'): + obj.pay_currency = filters.get('currency') + obj.pay_cash = filters.get('pay') + + if filters.get('q'): + obj.keywords = filters.get('q') + + return obj + def to_filters(self, translate_geonameids=False): location_names = [] if translate_geonameids and self.geonameids: diff --git a/hasjob/models/jobpost_alert.py b/hasjob/models/jobpost_alert.py new file mode 100644 index 000000000..0f0375e52 --- /dev/null +++ b/hasjob/models/jobpost_alert.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- + +from coaster.sqlalchemy import StateManager +from ..utils import random_long_key +from . import db, BaseMixin, LabeledEnum + +__all__ = ['JobPostSubscription', 'JobPostAlert', 'jobpost_alert_table'] + + +class EMAIL_FREQUENCY(LabeledEnum): + DAILY = (1, 'Daily') + WEEKLY = (7, 'Weekly') + MONTHLY = (30, 'Monthly') + + +class JobPostSubscription(BaseMixin, db.Model): + __tablename__ = 'jobpost_subscription' + __table_args__ = (db.UniqueConstraint('user_id', 'user_type', 'filterset_id'),) + + user_id = db.Column(None, db.ForeignKey('user.id')) + user = db.relationship('User', backref=db.backref('subscriptions', + lazy='dynamic', cascade='all, delete-orphan')) + user_type = db.Column(db.Unicode(8), nullable=False, default=u'User') + filterset_id = db.Column(None, db.ForeignKey('filterset.id')) + filterset = db.relationship('Filterset', backref=db.backref('subscriptions', + lazy='dynamic')) + active = db.Column(db.Boolean, nullable=False, default=True, index=True) + email = db.Column(db.Boolean, nullable=True, default=True, index=True) + _email_frequency = db.Column('email_frequency', + db.Integer, StateManager.check_constraint('email_frequency', EMAIL_FREQUENCY), + default=EMAIL_FREQUENCY.DAILY, nullable=True) + email_frequency = StateManager('_email_frequency', EMAIL_FREQUENCY, doc="Email frequency") + email_verify_key = db.Column(db.String(40), nullable=True, default=random_long_key) + email_verified_at = db.Column(db.DateTime, nullable=True, index=True) + deactivated_at = db.Column(db.DateTime, nullable=True) + reactivated_at = db.Column(db.DateTime, nullable=True) + + def verify_email(self): + self.email_verified_at = db.func.utcnow() + + def deactivate(self): + self.active = False + self.deactivated = db.func.utcnow() + + def reactivate(self): + if self.email_verified: + self.active = True + self.reactivated_at = db.func.utcnow() + + +jobpost_alert_table = db.Table('jobpost_jobpost_alert', db.Model.metadata, + db.Column('jobpost_id', None, db.ForeignKey('jobpost.id'), primary_key=True), + db.Column('jobpost_alert_id', None, db.ForeignKey('jobpost_alert.id'), primary_key=True), + db.Column('created_at', db.DateTime, nullable=False, default=db.func.utcnow()) +) + + +class JobPostAlert(BaseMixin, db.Model): + __tablename__ = 'jobpost_alert' + + jobpost_subscription_id = db.Column(None, db.ForeignKey('jobpost_subscription.id'), + index=True) + jobpost_subscription = db.relationship(JobPostSubscription, backref=db.backref('alerts', + lazy='dynamic', cascade='all, delete-orphan')) + jobposts = db.relationship('JobPost', lazy='dynamic', secondary=jobpost_alert_table, + backref=db.backref('alerts', lazy='dynamic')) + sent_at = db.Column(db.DateTime, nullable=False, default=db.func.utcnow()) diff --git a/hasjob/models/user.py b/hasjob/models/user.py index 7786d5ec9..91086ec35 100644 --- a/hasjob/models/user.py +++ b/hasjob/models/user.py @@ -228,36 +228,3 @@ class UserEvent(UserEventBase, BaseMixin, db.Model): name = db.Column(db.Unicode(80), nullable=False) #: Custom event data (null = no data saved) data = db.Column(JsonDict, nullable=True) - - -# class JobPostSubscription(BaseMixin, db.Model): -# __tablename__ = 'jobpost_subscription' - -# user_id = db.Column(None, db.ForeignKey('user.id'), primary_key=True, index=True) -# user = db.relationship(User, backref=db.backref('subscriptions', -# lazy='dynamic', cascade='all, delete-orphan')) - -# filterset_id = db.Column(None, db.ForeignKey('filterset.id'), primary_key=True, index=True) -# filterset = db.relationship('Filterset', backref=db.backref('subscriptions', -# lazy='dynamic', cascade='all, delete-orphan')) -# active = db.Column(db.Boolean, nullable=False, default=True) -# email = db.Column(db.Boolean, nullable=False, default=False) -# email_frequency = db.Column(db.SmallInteger, nullable=True) -# email_preferred_time = db.Column(db.Time, nullable=False, default=db.func.utcnow(), primary_key=True) - - -# jobpost_alert_table = db.Table('jobpost_alert', db.Model.metadata, -# db.Column('jobpost_id', None, db.ForeignKey('jobpost.id'), primary_key=True, index=True), -# db.Column('jobpost_alert', None, db.ForeignKey('jobpost_alert.id'), primary_key=True, index=True), -# db.Column('created_at', db.DateTime, nullable=False, default=db.func.utcnow()) -# ) - - -# class JobPostAlert(BaseMixin, db.Model): -# __tablename__ = 'jobpost_alert' - -# jobpost_subscription_id = db.Column(None, db.ForeignKey('jobpost_subscription.id'), -# primary_key=True, index=True) -# jobpost_subscription = db.relationship(JobPostSubscription, backref=db.backref('alerts', -# lazy='dynamic', cascade='all, delete-orphan')) -# sent_at = db.Column(db.DateTime, nullable=False, default=db.func.utcnow(), primary_key=True) diff --git a/hasjob/templates/job_alert_email_confirmation.html.jinja2 b/hasjob/templates/job_alert_email_confirmation.html.jinja2 new file mode 100644 index 000000000..62e379a8c --- /dev/null +++ b/hasjob/templates/job_alert_email_confirmation.html.jinja2 @@ -0,0 +1,17 @@ +{% extends "inc/email_layout_lite.html.jinja2" %} + +{% block content %} +
+
+ +
+ +
+ + +
+
+ Please click here to confirm your subscription. +
+

Hasjob is a service of HasGeek. Write to us at {{ config['SUPPORT_EMAIL'] }} if you have suggestions or questions on this service.

+{% endblock %} diff --git a/hasjob/templates/job_alert_mailer.html.jinja2 b/hasjob/templates/job_alert_mailer.html.jinja2 new file mode 100644 index 000000000..00ebdba22 --- /dev/null +++ b/hasjob/templates/job_alert_mailer.html.jinja2 @@ -0,0 +1,21 @@ +{% extends "inc/email_layout_lite.html.jinja2" %} + +{% block content %} +
+
+ +
+ +
+ + +
+
+ +
+

Hasjob is a service of HasGeek. Write to us at {{ config['SUPPORT_EMAIL'] }} if you have suggestions or questions on this service.

+{% endblock %} diff --git a/hasjob/views/board.py b/hasjob/views/board.py index 7a7d3f995..404ff0ba3 100644 --- a/hasjob/views/board.py +++ b/hasjob/views/board.py @@ -33,7 +33,7 @@ def remove_subdomain_parameter(endpoint, values): def add_subdomain_parameter(endpoint, values): if app.url_map.is_endpoint_expecting(endpoint, 'subdomain'): if 'subdomain' not in values: - values['subdomain'] = g.board.name if g.board and g.board.not_root else None + values['subdomain'] = g.board.name if 'board' in g and g.board.not_root else None @app.route('/board', methods=['GET', 'POST']) diff --git a/hasjob/views/helper.py b/hasjob/views/helper.py index ff9802ee7..a04132e98 100644 --- a/hasjob/views/helper.py +++ b/hasjob/views/helper.py @@ -398,7 +398,7 @@ def load_viewcounts(posts): g.viewcounts = dict(zip(viewcounts_keys, viewcounts_values)) -def getposts(basequery=None, pinned=False, showall=False, statusfilter=None, ageless=False, limit=2000, order=True): +def getposts(basequery=None, pinned=False, showall=False, statusfilter=None, ageless=False, limit=2000, board=None, order=True): if ageless: pinned = False # No pinning when browsing archives @@ -410,7 +410,9 @@ def getposts(basequery=None, pinned=False, showall=False, statusfilter=None, age query = basequery.filter(statusfilter).options(*JobPost._defercols).options(db.joinedload('domain')) - if g.board: + if 'board' in g: + board = g.board + if board: query = query.join(JobPost.postboards).filter(BoardJobPost.board == g.board) if not ageless: @@ -418,7 +420,7 @@ def getposts(basequery=None, pinned=False, showall=False, statusfilter=None, age query = query.filter(JobPost.state.LISTED) else: if pinned: - if g.board: + if board: query = query.filter( db.or_( db.and_(BoardJobPost.pinned == True, JobPost.state.LISTED), @@ -432,7 +434,7 @@ def getposts(basequery=None, pinned=False, showall=False, statusfilter=None, age query = query.filter(JobPost.state.NEW) if pinned: - if g.board: + if board: query = query.order_by(db.desc(BoardJobPost.pinned)) else: query = query.order_by(db.desc(JobPost.pinned)) diff --git a/hasjob/views/index.py b/hasjob/views/index.py index c1dcc20fa..9497e01e3 100644 --- a/hasjob/views/index.py +++ b/hasjob/views/index.py @@ -10,7 +10,7 @@ from baseframe import _ # , dogpile from .. import app, lastuser -from ..models import (db, JobCategory, JobPost, JobType, POST_STATE, newlimit, agelimit, JobLocation, Board, Filterset, +from ..models import (db, JobCategory, JobPost, JobType, newlimit, agelimit, JobLocation, Board, Filterset, Domain, Location, Tag, JobPostTag, Campaign, CAMPAIGN_POSITION, CURRENCY, JobApplication, starred_job_table, BoardJobPost) from ..views.helper import (getposts, getallposts, gettags, location_geodata, load_viewcounts, session_jobpost_ab, bgroup, make_pay_graph, index_is_paginated, get_post_viewcounts) @@ -90,38 +90,38 @@ def json_index(data): return jsonify(result) -def fetch_jobposts(request_args, request_values, filters, is_index, board, board_jobs, gkiosk, basequery, md5sum, domain, location, title, showall, statusfilter, batched, ageless, template_vars, search_query=None, query_string=None, posts_only=False): +def fetch_jobposts(request_args={}, request_values={}, filters={}, is_index=False, board=None, board_jobs={}, gkiosk=False, basequery=None, md5sum=None, domain=None, location=None, title=None, showall=True, statusfilter=None, batched=True, ageless=False, template_vars={}, search_query=None, query_string=None, posts_only=False): if basequery is None: basequery = JobPost.query # Apply request.args filters data_filters = {} - f_types = filters.get('t') or request_args.getlist('t') + f_types = filters.get('t') or (request_args and request_args.getlist('t')) while '' in f_types: f_types.remove('') if f_types: data_filters['types'] = f_types basequery = basequery.join(JobType).filter(JobType.name.in_(f_types)) - f_categories = filters.get('c') or request_args.getlist('c') + f_categories = filters.get('c') or (request_args and request_args.getlist('c')) while '' in f_categories: f_categories.remove('') if f_categories: data_filters['categories'] = f_categories basequery = basequery.join(JobCategory).filter(JobCategory.name.in_(f_categories)) - f_domains = filters.get('d') or request_args.getlist('d') + f_domains = filters.get('d') or (request_args and request_args.getlist('d')) while '' in f_domains: f_domains.remove('') if f_domains: basequery = basequery.join(Domain).filter(Domain.name.in_(f_domains)) - f_tags = filters.get('k') or request_args.getlist('k') + f_tags = filters.get('k') or (request_args and request_args.getlist('k')) while '' in f_tags: f_tags.remove('') if f_tags: basequery = basequery.join(JobPostTag).join(Tag).filter(Tag.name.in_(f_tags)) - data_filters['location_names'] = r_locations = filters.get('l') or request_args.getlist('l') + data_filters['location_names'] = r_locations = filters.get('l') or (request_args and request_args.getlist('l')) if location: r_locations.append(location['geonameid']) f_locations = [] @@ -269,7 +269,7 @@ def fetch_jobposts(request_args, request_values, filters, is_index, board, board batchsize = 32 # list of posts that were pinned at the time of first load - pinned_hashids = request_args.getlist('ph') + pinned_hashids = (request_args and request_args.getlist('ph')) # Depending on the display mechanism (grouped or ungrouped), extract the batch if grouped: if not startdate: @@ -571,55 +571,6 @@ def filterset_view(name): filterset=filterset) -# @app.route('/subscribe', subdomain='', methods=['POST']) -# @app.route('/subscribe', methods=['POST']) -# def subscribe_to_jobposts(): -# filterset = Filterset.from_filters(g.board, filters) -# if not filterset: -# form = FiltersetForm(parent=g.board) -# if form.validate_on_submit(): -# filterset = Filterset(board=g.board, title=u"") -# form.populate_obj(filterset) -# db.session.add(filterset) - -# if g.user not in filterset.users: -# filterset.users.append(g.user) -# db.session.commit() -# return index(filters=filterset.to_filters(translate_geonameids=True), -# query_string=filterset.keywords, -# filterset=filterset) - - -# def send_alerts_to_subscribers(): -# subscriptions = JobPostSubscription.query.filter_by(active=True).all() -# for sub in subscriptions: -# posts = get_filtered_posts(filters=sub.filterset.to_filters()) -# now = datetime.utcnow() -# if sub.daily: -# days_delta = 1 -# elif sub.weekly: -# days_delta = 7 -# elif sub.monthly: -# days_delta = 30 - -# last_reference_date = now - timedelta(days=days_delta) -# recent_alert = JobPostAlert.query.filter( -# JobPostAlert.jobpost_subscription == sub, JobPostAlert.sent_at >= last_reference_date -# ).order_by('created_at desc').first() -# if recent_alert: -# break - -# sent_jobpostids = [jobpost.id for jobpost in recent_alert.jobposts] -# unsent_posts = [post for post in posts if post.id not in sent_jobpostids] -# if not unsent_posts: -# break - -# jobpost_alert = JobPostAlert(jobpost_subscription=sub, sent_at=now) -# jobpost_alert.jobposts = unsent_posts -# db.session.add(jobpost_alert) -# db.session.commit() - - @app.route('/opensearch.xml', subdomain='') @app.route('/opensearch.xml') def opensearch(): diff --git a/hasjob/views/job_alerts.py b/hasjob/views/job_alerts.py new file mode 100644 index 000000000..05ed0f1ab --- /dev/null +++ b/hasjob/views/job_alerts.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + + +from flask import abort, redirect, render_template, request, url_for, g +from flask_mail import Message +from flask_rq import job +from premailer import transform as email_transform +from html2text import html2text +from .. import app, mail +from ..models import (db, User, AnonUser, JobPostSubscription, Filterset) + + +@app.route('/confirm_subscription_to_job_alerts', subdomain='') +@app.route('/confirm_subscription_to_job_alerts') +def confirm_subscription_to_job_alerts(): + sub = JobPostSubscription.query.filter_by(email_verify_key=request.args.get('token')).one_or_none() + if not sub: + abort(404) + if sub.email_verified_at: + abort(400) + sub.verify_email() + db.session.commit() + return redirect(url_for('index'), code=302) + + +@job('hasjob') +def send_email_confirmation_for_job_alerts(to_address, token): + msg = Message(subject=u"Job alerts", recipients=[to_address]) + html = email_transform(render_template('job_alert_email_confirmation.html.jinja2', token=token)) + msg.html = html + msg.body = html2text(html) + mail.send(msg) + + +@app.route('/subscribe_to_job_alerts', subdomain='', methods=['POST']) +@app.route('/subscribe_to_job_alerts', methods=['POST']) +def subscribe_to_job_alerts(): + if not request.json or not request.json.get('filters'): + abort(400) + + if g.user and g.user.email: + email = g.user.email + # elif request.json.get('email') and valid_email(request.json.get('email')): + elif request.json.get('email'): + email = request.json.get('email') + else: + abort(400) + + user_type = User.__name__ + if g.user: + user = g.user + if not g.user.email: + # TODO Should we update email on g.user? + pass + else: + user = User.query.filter_by(email=email).one_or_none() + if not user: + user = g.anon_user + user_type = AnonUser.__name__ + + filterset = Filterset.from_filters(g.board, request.json.get('filters')) + if not filterset: + filterset = Filterset.init_from_filters(g.board, request.json.get('filters')) + db.session.add(filterset) + + sub = JobPostSubscription(user=user, user_type=user_type, filterset=filterset) + db.session.add(sub) + db.session.commit() + + send_email_confirmation_for_job_alerts.delay(to_address=email, token=sub.email_verify_key) + return redirect(url_for('index'), code=302) diff --git a/manage.py b/manage.py index 434e28d1b..d326b2537 100755 --- a/manage.py +++ b/manage.py @@ -8,6 +8,7 @@ import hasjob.views as views from hasjob.models import db from hasjob import app +from hasjob.jobs import send_email_alerts from datetime import datetime, timedelta periodic = Manager(usage="Periodic tasks from cron (with recommended intervals)") @@ -36,6 +37,12 @@ def campaignviews(): views.helper.reset_campaign_views() +@periodic.command +def send_job_alerts(): + """Run email alerts very morning at 8 AM""" + send_email_alerts.delay() + + if __name__ == '__main__': db.init_app(app) manager = init_manager(app, db, hasjob=hasjob, models=models, forms=forms, views=views) diff --git a/migrations/versions/efdbaaf67b26_add_tables_for_job_alerts.py b/migrations/versions/efdbaaf67b26_add_tables_for_job_alerts.py new file mode 100644 index 000000000..d693b23f1 --- /dev/null +++ b/migrations/versions/efdbaaf67b26_add_tables_for_job_alerts.py @@ -0,0 +1,70 @@ +"""add_tables_for_job_alerts + +Revision ID: efdbaaf67b26 +Revises: 859f6f33c02d +Create Date: 2018-04-09 14:35:47.960246 + +""" + +# revision identifiers, used by Alembic. +revision = 'efdbaaf67b26' +down_revision = '859f6f33c02d' + +from alembic import op +import sqlalchemy as sa + + + +def upgrade(): + op.create_table('jobpost_subscription', + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('user_type', sa.Unicode(length=8), nullable=False), + sa.Column('filterset_id', sa.Integer(), nullable=True), + sa.Column('active', sa.Boolean(), nullable=False), + sa.Column('email', sa.Boolean(), nullable=True), + sa.Column('email_frequency', sa.Integer(), nullable=True), + sa.Column('email_verify_key', sa.String(length=40), nullable=True), + sa.Column('email_verified_at', sa.DateTime(), nullable=True), + sa.Column('deactivated_at', sa.DateTime(), nullable=True), + sa.Column('reactivated_at', sa.DateTime(), nullable=True), + sa.Column('id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['filterset_id'], ['filterset.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user_id', 'user_type', 'filterset_id') + ) + op.create_index(op.f('ix_jobpost_subscription_active'), 'jobpost_subscription', ['active'], unique=False) + op.create_index(op.f('ix_jobpost_subscription_email'), 'jobpost_subscription', ['email'], unique=False) + op.create_index(op.f('ix_jobpost_subscription_email_verified_at'), 'jobpost_subscription', ['email_verified_at'], unique=False) + + op.create_table('jobpost_alert', + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('jobpost_subscription_id', sa.Integer(), nullable=True), + sa.Column('sent_at', sa.DateTime(), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['jobpost_subscription_id'], ['jobpost_subscription.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_jobpost_alert_jobpost_subscription_id'), 'jobpost_alert', ['jobpost_subscription_id'], unique=False) + + op.create_table('jobpost_jobpost_alert', + sa.Column('jobpost_id', sa.Integer(), nullable=False), + sa.Column('jobpost_alert_id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['jobpost_alert_id'], ['jobpost_alert.id'], ), + sa.ForeignKeyConstraint(['jobpost_id'], ['jobpost.id'], ), + sa.PrimaryKeyConstraint('jobpost_id', 'jobpost_alert_id') + ) + + +def downgrade(): + op.drop_table('jobpost_jobpost_alert') + op.drop_index(op.f('ix_jobpost_alert_jobpost_subscription_id'), table_name='jobpost_alert') + op.drop_table('jobpost_alert') + op.drop_index(op.f('ix_jobpost_subscription_email_verified_at'), table_name='jobpost_subscription') + op.drop_index(op.f('ix_jobpost_subscription_email'), table_name='jobpost_subscription') + op.drop_index(op.f('ix_jobpost_subscription_active'), table_name='jobpost_subscription') + op.drop_table('jobpost_subscription') From 3c457f0f6d51e3bdc856f3a3013a00ad17ceac86 Mon Sep 17 00:00:00 2001 From: Shreyas Satish Date: Mon, 9 Apr 2018 20:50:24 +0530 Subject: [PATCH 03/17] simplify and cleanup --- hasjob/jobs/job_alerts.py | 29 ++++--- hasjob/models/filterset.py | 2 + hasjob/models/jobpost_alert.py | 41 +++++---- hasjob/views/__init__.py | 2 +- hasjob/views/index.py | 2 +- hasjob/views/job_alerts.py | 83 +++++++++++-------- manage.py | 2 +- ...0cdbddf0_schema_changes_for_job_alerts.py} | 31 +++---- 8 files changed, 106 insertions(+), 86 deletions(-) rename migrations/versions/{efdbaaf67b26_add_tables_for_job_alerts.py => 41890cdbddf0_schema_changes_for_job_alerts.py} (75%) diff --git a/hasjob/jobs/job_alerts.py b/hasjob/jobs/job_alerts.py index a6e89215d..11239e21a 100644 --- a/hasjob/jobs/job_alerts.py +++ b/hasjob/jobs/job_alerts.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -from datetime import datetime, timedelta from flask_mail import Message from flask import render_template from flask_rq import job @@ -11,30 +10,30 @@ from hasjob.views.index import fetch_jobposts +def get_unseen_posts(subscription): + posts = fetch_jobposts(filters=subscription.filterset.to_filters(), posts_only=True) + seen_jobpostids = JobPost.query.join(jobpost_alert_table).join(JobPostAlert).filter( + JobPostAlert.jobpost_subscription == subscription).options(db.load_only('id')).all() + return [post for post in posts if post.id not in seen_jobpostids] + + @job('hasjob') def send_email_alerts(): - subscriptions = JobPostSubscription.query.filter(JobPostSubscription.active == True, - JobPostSubscription.email_verified_at != None).all() - for sub in subscriptions: - if JobPostAlert.query.filter( - JobPostAlert.jobpost_subscription == sub, - JobPostAlert.sent_at >= datetime.utcnow() - timedelta(days=sub.email_frequency.value) - ).order_by('created_at desc').notempty(): - print 'alert was recently sent so skipping' + for subscription in JobPostSubscription.get_active_subscriptions: + if subscription.has_recent_alert(): + # Alert was sent recently, break out of loop break - posts = fetch_jobposts(filters=sub.filterset.to_filters(), posts_only=True) - sent_jobpostids = JobPost.query.join(jobpost_alert_table).join(JobPostAlert).filter(JobPostAlert.jobpost_subscription == sub).options(db.load_only('id')).all() - unseen_posts = [post for post in posts if post.id not in sent_jobpostids] + unseen_posts = get_unseen_posts(subscription) if not unseen_posts: - print "no unseen posts" + # Nothing new to see, break out of loop break - jobpost_alert = JobPostAlert(jobpost_subscription=sub) + jobpost_alert = JobPostAlert(jobpost_subscription=subscription) jobpost_alert.jobposts = unseen_posts db.session.commit() - msg = Message(subject=u"Job alerts", recipients=[sub.user.email]) + msg = Message(subject=u"New jobs on Hasjob", recipients=[subscription.email]) html = email_transform(render_template('job_alert_mailer.html.jinja2', posts=jobpost_alert.jobposts)) msg.html = html msg.body = html2text(html) diff --git a/hasjob/models/filterset.py b/hasjob/models/filterset.py index 15ac360a6..493cef67d 100644 --- a/hasjob/models/filterset.py +++ b/hasjob/models/filterset.py @@ -53,6 +53,8 @@ class Filterset(BaseScopedNameMixin, db.Model): #: Welcome text description = db.Column(db.UnicodeText, nullable=False, default=u'') + #: Display on sitemap + sitemap = db.Column(db.Boolean, default=False, nullable=True, index=True) #: Associated job types types = db.relationship(JobType, secondary=filterset_jobtype_table) diff --git a/hasjob/models/jobpost_alert.py b/hasjob/models/jobpost_alert.py index 0f0375e52..2e32aff03 100644 --- a/hasjob/models/jobpost_alert.py +++ b/hasjob/models/jobpost_alert.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +from datetime import datetime, timedelta from coaster.sqlalchemy import StateManager from ..utils import random_long_key from . import db, BaseMixin, LabeledEnum @@ -15,37 +16,41 @@ class EMAIL_FREQUENCY(LabeledEnum): class JobPostSubscription(BaseMixin, db.Model): __tablename__ = 'jobpost_subscription' - __table_args__ = (db.UniqueConstraint('user_id', 'user_type', 'filterset_id'),) + __table_args__ = (db.UniqueConstraint('filterset_id', 'email'),) - user_id = db.Column(None, db.ForeignKey('user.id')) - user = db.relationship('User', backref=db.backref('subscriptions', - lazy='dynamic', cascade='all, delete-orphan')) - user_type = db.Column(db.Unicode(8), nullable=False, default=u'User') - filterset_id = db.Column(None, db.ForeignKey('filterset.id')) + filterset_id = db.Column(None, db.ForeignKey('filterset.id'), nullable=False) filterset = db.relationship('Filterset', backref=db.backref('subscriptions', lazy='dynamic')) - active = db.Column(db.Boolean, nullable=False, default=True, index=True) - email = db.Column(db.Boolean, nullable=True, default=True, index=True) + email = db.Column(db.Unicode(254), nullable=False) + + active = db.Column(db.Boolean, nullable=False, default=False, index=True) + email_verify_key = db.Column(db.String(40), nullable=True, default=random_long_key, unique=True) + unsubscribe_key = db.Column(db.String(40), nullable=True, default=random_long_key, unique=True) + email_verified_at = db.Column(db.DateTime, nullable=True, index=True) + unsubscribed_at = db.Column(db.DateTime, nullable=True) + _email_frequency = db.Column('email_frequency', db.Integer, StateManager.check_constraint('email_frequency', EMAIL_FREQUENCY), default=EMAIL_FREQUENCY.DAILY, nullable=True) email_frequency = StateManager('_email_frequency', EMAIL_FREQUENCY, doc="Email frequency") - email_verify_key = db.Column(db.String(40), nullable=True, default=random_long_key) - email_verified_at = db.Column(db.DateTime, nullable=True, index=True) - deactivated_at = db.Column(db.DateTime, nullable=True) - reactivated_at = db.Column(db.DateTime, nullable=True) def verify_email(self): + self.active = True self.email_verified_at = db.func.utcnow() - def deactivate(self): + def unsubscribe(self): self.active = False - self.deactivated = db.func.utcnow() + self.unsubscribed_at = db.func.utcnow() + + @classmethod + def get_active_subscriptions(cls): + return cls.query.filter(cls.active == True, cls.email_verified_at != None) - def reactivate(self): - if self.email_verified: - self.active = True - self.reactivated_at = db.func.utcnow() + def has_recent_alert(self): + return JobPostAlert.query.filter( + JobPostAlert.jobpost_subscription == self, + JobPostAlert.sent_at >= datetime.utcnow() - timedelta(days=self.email_frequency.value) + ).order_by('created_at desc').notempty() jobpost_alert_table = db.Table('jobpost_jobpost_alert', db.Model.metadata, diff --git a/hasjob/views/__init__.py b/hasjob/views/__init__.py index 46f04ad1a..91d8a8a22 100644 --- a/hasjob/views/__init__.py +++ b/hasjob/views/__init__.py @@ -30,4 +30,4 @@ def root_paths(): ] from . import (index, error_handling, helper, listing, location, static, login, board, kiosk, campaign, # NOQA - admindash, domain, api, admin_filterset) + admindash, domain, api, admin_filterset, job_alerts) diff --git a/hasjob/views/index.py b/hasjob/views/index.py index 9497e01e3..f3353916c 100644 --- a/hasjob/views/index.py +++ b/hasjob/views/index.py @@ -765,7 +765,7 @@ def sitemap(key): ' \n' # Add filtered views to sitemap - for item in Filterset.query.all(): + for item in Filterset.query.filter(Filterset.sitemap == True): sitemapxml += ' \n'\ ' %s\n' % item.url_for(_external=True) + \ ' %s\n' % (item.updated_at.isoformat() + 'Z') + \ diff --git a/hasjob/views/job_alerts.py b/hasjob/views/job_alerts.py index 05ed0f1ab..8790d419c 100644 --- a/hasjob/views/job_alerts.py +++ b/hasjob/views/job_alerts.py @@ -1,31 +1,20 @@ # -*- coding: utf-8 -*- -from flask import abort, redirect, render_template, request, url_for, g +from flask import abort, redirect, render_template, request, url_for, g, flash from flask_mail import Message from flask_rq import job from premailer import transform as email_transform +from pyisemail import is_email from html2text import html2text +from baseframe import _ from .. import app, mail -from ..models import (db, User, AnonUser, JobPostSubscription, Filterset) - - -@app.route('/confirm_subscription_to_job_alerts', subdomain='') -@app.route('/confirm_subscription_to_job_alerts') -def confirm_subscription_to_job_alerts(): - sub = JobPostSubscription.query.filter_by(email_verify_key=request.args.get('token')).one_or_none() - if not sub: - abort(404) - if sub.email_verified_at: - abort(400) - sub.verify_email() - db.session.commit() - return redirect(url_for('index'), code=302) +from ..models import (db, JobPostSubscription, Filterset) @job('hasjob') -def send_email_confirmation_for_job_alerts(to_address, token): - msg = Message(subject=u"Job alerts", recipients=[to_address]) +def send_confirmation_email_for_job_alerts(to_address, token): + msg = Message(subject=u"Please confirm your email to receive alerts on new jobs", recipients=[to_address]) html = email_transform(render_template('job_alert_email_confirmation.html.jinja2', token=token)) msg.html = html msg.body = html2text(html) @@ -40,32 +29,56 @@ def subscribe_to_job_alerts(): if g.user and g.user.email: email = g.user.email - # elif request.json.get('email') and valid_email(request.json.get('email')): - elif request.json.get('email'): + message = _(u"Thank you for signing up to receive job alerts from us! We'll keep you posted.") + verified_user = True + elif request.json.get('email') and is_email(request.json.get('email')): email = request.json.get('email') + message = _(u"Thank you for signing up to receive job alerts from us! We've sent you a confirmation email, please do confirm it so we can keep you posted.") + verified_user = False else: - abort(400) - - user_type = User.__name__ - if g.user: - user = g.user - if not g.user.email: - # TODO Should we update email on g.user? - pass - else: - user = User.query.filter_by(email=email).one_or_none() - if not user: - user = g.anon_user - user_type = AnonUser.__name__ + flash(_(u"Oops! Sorry, we need an email address to send you alerts."), 'danger') + return redirect(url_for('index'), code=302) filterset = Filterset.from_filters(g.board, request.json.get('filters')) if not filterset: filterset = Filterset.init_from_filters(g.board, request.json.get('filters')) db.session.add(filterset) - sub = JobPostSubscription(user=user, user_type=user_type, filterset=filterset) - db.session.add(sub) + subscription = JobPostSubscription(filterset=filterset, email=email) + if verified_user: + subscription.verify_email() + db.session.add(subscription) + db.session.commit() + if not verified_user: + send_confirmation_email_for_job_alerts.delay(to_address=subscription.email, token=subscription.email_verify_key) + + flash(message, 'success') + return redirect(url_for('index'), code=302) + + +@app.route('/confirm_subscription_to_job_alerts', subdomain='') +@app.route('/confirm_subscription_to_job_alerts') +def confirm_subscription_to_job_alerts(): + sub = JobPostSubscription.query.filter_by(email_verify_key=request.args.get('token')).one_or_none() + if not sub: + abort(404) + if sub.email_verified_at: + abort(400) + sub.verify_email() db.session.commit() + flash(_(u"You've just subscribed to receive alerts from us! We'll keep you posted."), 'success') + return redirect(url_for('index'), code=302) + - send_email_confirmation_for_job_alerts.delay(to_address=email, token=sub.email_verify_key) +@app.route('/unsubscribe_from_job_alerts', subdomain='') +@app.route('/unsubscribe_from_job_alerts') +def unsubscribe_from_job_alerts(): + sub = JobPostSubscription.query.filter_by(unsubscribe_key=request.args.get('token')).one_or_none() + if not sub: + abort(404) + if not sub.email_verified_at: + abort(400) + sub.unsubscribe() + db.session.commit() + flash(_(u"You've just unsubscribed from receiving alerts! Hope they were useful."), 'success') return redirect(url_for('index'), code=302) diff --git a/manage.py b/manage.py index d326b2537..ada7dc5a8 100755 --- a/manage.py +++ b/manage.py @@ -8,7 +8,7 @@ import hasjob.views as views from hasjob.models import db from hasjob import app -from hasjob.jobs import send_email_alerts +from hasjob.jobs.job_alerts import send_email_alerts from datetime import datetime, timedelta periodic = Manager(usage="Periodic tasks from cron (with recommended intervals)") diff --git a/migrations/versions/efdbaaf67b26_add_tables_for_job_alerts.py b/migrations/versions/41890cdbddf0_schema_changes_for_job_alerts.py similarity index 75% rename from migrations/versions/efdbaaf67b26_add_tables_for_job_alerts.py rename to migrations/versions/41890cdbddf0_schema_changes_for_job_alerts.py index d693b23f1..383561146 100644 --- a/migrations/versions/efdbaaf67b26_add_tables_for_job_alerts.py +++ b/migrations/versions/41890cdbddf0_schema_changes_for_job_alerts.py @@ -1,13 +1,13 @@ -"""add_tables_for_job_alerts +"""schema_changes_for_job_alerts -Revision ID: efdbaaf67b26 +Revision ID: 41890cdbddf0 Revises: 859f6f33c02d -Create Date: 2018-04-09 14:35:47.960246 +Create Date: 2018-04-09 20:43:57.810337 """ # revision identifiers, used by Alembic. -revision = 'efdbaaf67b26' +revision = '41890cdbddf0' down_revision = '859f6f33c02d' from alembic import op @@ -19,24 +19,22 @@ def upgrade(): op.create_table('jobpost_subscription', sa.Column('created_at', sa.DateTime(), nullable=False), sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('user_type', sa.Unicode(length=8), nullable=False), - sa.Column('filterset_id', sa.Integer(), nullable=True), + sa.Column('filterset_id', sa.Integer(), nullable=False), + sa.Column('email', sa.Unicode(length=254), nullable=False), sa.Column('active', sa.Boolean(), nullable=False), - sa.Column('email', sa.Boolean(), nullable=True), - sa.Column('email_frequency', sa.Integer(), nullable=True), sa.Column('email_verify_key', sa.String(length=40), nullable=True), + sa.Column('unsubscribe_key', sa.String(length=40), nullable=True), sa.Column('email_verified_at', sa.DateTime(), nullable=True), - sa.Column('deactivated_at', sa.DateTime(), nullable=True), - sa.Column('reactivated_at', sa.DateTime(), nullable=True), + sa.Column('unsubscribed_at', sa.DateTime(), nullable=True), + sa.Column('email_frequency', sa.Integer(), nullable=True), sa.Column('id', sa.Integer(), nullable=False), sa.ForeignKeyConstraint(['filterset_id'], ['filterset.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('user_id', 'user_type', 'filterset_id') + sa.UniqueConstraint('email_verify_key'), + sa.UniqueConstraint('filterset_id', 'email'), + sa.UniqueConstraint('unsubscribe_key') ) op.create_index(op.f('ix_jobpost_subscription_active'), 'jobpost_subscription', ['active'], unique=False) - op.create_index(op.f('ix_jobpost_subscription_email'), 'jobpost_subscription', ['email'], unique=False) op.create_index(op.f('ix_jobpost_subscription_email_verified_at'), 'jobpost_subscription', ['email_verified_at'], unique=False) op.create_table('jobpost_alert', @@ -58,13 +56,16 @@ def upgrade(): sa.ForeignKeyConstraint(['jobpost_id'], ['jobpost.id'], ), sa.PrimaryKeyConstraint('jobpost_id', 'jobpost_alert_id') ) + op.add_column(u'filterset', sa.Column('sitemap', sa.Boolean(), nullable=True)) + op.create_index(op.f('ix_filterset_sitemap'), 'filterset', ['sitemap'], unique=False) def downgrade(): + op.drop_index(op.f('ix_filterset_sitemap'), table_name='filterset') + op.drop_column(u'filterset', 'sitemap') op.drop_table('jobpost_jobpost_alert') op.drop_index(op.f('ix_jobpost_alert_jobpost_subscription_id'), table_name='jobpost_alert') op.drop_table('jobpost_alert') op.drop_index(op.f('ix_jobpost_subscription_email_verified_at'), table_name='jobpost_subscription') - op.drop_index(op.f('ix_jobpost_subscription_email'), table_name='jobpost_subscription') op.drop_index(op.f('ix_jobpost_subscription_active'), table_name='jobpost_subscription') op.drop_table('jobpost_subscription') From df699d73046105b4878307a7c8c66d51ea9ea91a Mon Sep 17 00:00:00 2001 From: Bibhas Date: Wed, 11 Apr 2018 13:06:44 +0530 Subject: [PATCH 04/17] Using StateManager for JobApplication model (#432) * initial changes for state manager for job application * added transitions for application processing * more fixes * transition fixes --- hasjob/forms/jobpost.py | 4 +- hasjob/models/__init__.py | 21 +++-- hasjob/models/flags.py | 34 ++++---- hasjob/models/jobpost.py | 86 ++++++++----------- hasjob/templates/application.html.jinja2 | 22 ++--- hasjob/templates/respond_email.html.jinja2 | 8 +- hasjob/views/listing.py | 45 +++++----- ...54_jobapplication_response_statemanager.py | 29 +++++++ 8 files changed, 136 insertions(+), 113 deletions(-) create mode 100644 migrations/versions/625415764254_jobapplication_response_statemanager.py diff --git a/hasjob/forms/jobpost.py b/hasjob/forms/jobpost.py index e64bf3d25..c2a9a974d 100644 --- a/hasjob/forms/jobpost.py +++ b/hasjob/forms/jobpost.py @@ -11,7 +11,7 @@ from coaster.utils import getbool, get_email_domain from flask_lastuser import LastuserResourceException -from ..models import User, JobType, JobApplication, EMPLOYER_RESPONSE, PAY_TYPE, CURRENCY, Domain +from ..models import User, JobType, JobApplication, PAY_TYPE, CURRENCY, Domain from ..uploads import process_image, UploadNotAllowed from .. import app, lastuser @@ -399,7 +399,7 @@ def validate_apply_message(form, field): words = get_word_bag(field.data) form.words = words similar = False - for oldapp in JobApplication.query.filter_by(response=EMPLOYER_RESPONSE.SPAM).all(): + for oldapp in JobApplication.query.filter(JobApplication.response.SPAM).all(): if oldapp.words: s = SequenceMatcher(None, words, oldapp.words) if s.ratio() > 0.8: diff --git a/hasjob/models/__init__.py b/hasjob/models/__init__.py index 06942a38a..d5132a7e9 100644 --- a/hasjob/models/__init__.py +++ b/hasjob/models/__init__.py @@ -26,7 +26,7 @@ class POST_STATE(LabeledEnum): ANNOUNCEMENT = (9, 'announcement', __("Announcement")) # Special announcement CLOSED = (10, 'closed', __("Closed")) # Not accepting applications, but publicly viewable - __order__ = (DRAFT, PENDING, CONFIRMED, REVIEWED, ANNOUNCEMENT, CLOSED, + __order__ = (DRAFT, PENDING, CONFIRMED, REVIEWED, ANNOUNCEMENT, CLOSED, FLAGGED, MODERATED, REJECTED, SPAM, WITHDRAWN) UNPUBLISHED = {DRAFT, PENDING} @@ -46,16 +46,21 @@ class CURRENCY(LabeledEnum): class EMPLOYER_RESPONSE(LabeledEnum): - NEW = (0, __("New")) # New application - PENDING = (1, __("Pending")) # Employer viewed on website - IGNORED = (2, __("Ignored")) # Dismissed as not worth responding to - REPLIED = (3, __("Replied")) # Employer replied to candidate - FLAGGED = (4, __("Flagged")) # Employer reported a spammer - SPAM = (5, __("Spam")) # Admin marked this as spam - REJECTED = (6, __("Rejected")) # Employer rejected candidate with a message + NEW = (0, 'new', __("New")) # New application + PENDING = (1, 'pending', __("Pending")) # Employer viewed on website + IGNORED = (2, 'ignored', __("Ignored")) # Dismissed as not worth responding to + REPLIED = (3, 'replied', __("Replied")) # Employer replied to candidate + FLAGGED = (4, 'flagged', __("Flagged")) # Employer reported a spammer + SPAM = (5, 'spam', __("Spam")) # Admin marked this as spam + REJECTED = (6, 'rejected', __("Rejected")) # Employer rejected candidate with a message __order__ = (NEW, PENDING, IGNORED, REPLIED, FLAGGED, SPAM, REJECTED) + CAN_REPLY = {NEW, PENDING, IGNORED} + CAN_REJECT = CAN_REPLY + CAN_IGNORE = {NEW, PENDING} + CAN_REPORT = {NEW, PENDING, IGNORED, REJECTED} + class PAY_TYPE(LabeledEnum): NOCASH = (0, __("Nothing")) diff --git a/hasjob/models/flags.py b/hasjob/models/flags.py index 79ecdc0e9..898595bc4 100644 --- a/hasjob/models/flags.py +++ b/hasjob/models/flags.py @@ -5,7 +5,7 @@ from sqlalchemy import distinct from werkzeug import cached_property from baseframe import __, cache -from . import db, agelimit, newlimit, POST_STATE, EMPLOYER_RESPONSE +from . import db, agelimit, newlimit from .user import User from .jobpost import JobPost, JobApplication from .board import Board @@ -91,10 +91,10 @@ class UserFlags(object): __("Is a candidate who received a response (at any time)"), lambda user: JobApplication.query.filter( JobApplication.user == user, - JobApplication.response == EMPLOYER_RESPONSE.REPLIED + JobApplication.response.REPLIED ).notempty(), lambda: db.session.query(distinct(JobApplication.user_id).label('id')).filter( - JobApplication.response == EMPLOYER_RESPONSE.REPLIED + JobApplication.response.REPLIED ) ) @@ -103,10 +103,10 @@ class UserFlags(object): lambda user: JobApplication.query.filter( JobApplication.user == user, JobApplication.replied_at >= datetime.utcnow() - newlimit, - JobApplication.response == EMPLOYER_RESPONSE.REPLIED + JobApplication.response.REPLIED ).notempty(), lambda: db.session.query(distinct(JobApplication.user_id).label('id')).filter( - JobApplication.response == EMPLOYER_RESPONSE.REPLIED, + JobApplication.response.REPLIED, JobApplication.replied_at >= datetime.utcnow() - newlimit ) ) @@ -116,10 +116,10 @@ class UserFlags(object): lambda user: JobApplication.query.filter( JobApplication.user == user, JobApplication.replied_at >= datetime.utcnow() - agelimit, - JobApplication.response == EMPLOYER_RESPONSE.REPLIED + JobApplication.response.REPLIED ).notempty(), lambda: db.session.query(distinct(JobApplication.user_id).label('id')).filter( - JobApplication.response == EMPLOYER_RESPONSE.REPLIED, + JobApplication.response.REPLIED, JobApplication.replied_at >= datetime.utcnow() - agelimit ) ) @@ -129,10 +129,10 @@ class UserFlags(object): lambda user: JobApplication.query.filter( JobApplication.user == user, JobApplication.replied_at < datetime.utcnow() - agelimit, - JobApplication.response == EMPLOYER_RESPONSE.REPLIED + JobApplication.response.REPLIED ).notempty(), lambda: db.session.query(distinct(JobApplication.user_id).label('id')).filter( - JobApplication.response == EMPLOYER_RESPONSE.REPLIED, + JobApplication.response.REPLIED, JobApplication.replied_at < datetime.utcnow() - agelimit ) ) @@ -234,10 +234,10 @@ class UserFlags(object): __("Is an employer who responded to a candidate (at any time)"), lambda user: JobApplication.query.filter( JobApplication.replied_by == user, - JobApplication.response == EMPLOYER_RESPONSE.REPLIED + JobApplication.response.REPLIED ).notempty(), lambda: db.session.query(distinct(JobApplication.replied_by_id).label('id')).filter( - JobApplication.response == EMPLOYER_RESPONSE.REPLIED + JobApplication.response.REPLIED ) ) @@ -246,10 +246,10 @@ class UserFlags(object): lambda user: JobApplication.query.filter( JobApplication.replied_by == user, JobApplication.replied_at >= datetime.utcnow() - newlimit, - JobApplication.response == EMPLOYER_RESPONSE.REPLIED + JobApplication.response.REPLIED ).notempty(), lambda: db.session.query(distinct(JobApplication.replied_by_id).label('id')).filter( - JobApplication.response == EMPLOYER_RESPONSE.REPLIED, + JobApplication.response.REPLIED, JobApplication.replied_at >= datetime.utcnow() - newlimit ) ) @@ -259,10 +259,10 @@ class UserFlags(object): lambda user: JobApplication.query.filter( JobApplication.replied_by == user, JobApplication.replied_at >= datetime.utcnow() - agelimit, - JobApplication.response == EMPLOYER_RESPONSE.REPLIED + JobApplication.response.REPLIED ).notempty(), lambda: db.session.query(distinct(JobApplication.replied_by_id).label('id')).filter( - JobApplication.response == EMPLOYER_RESPONSE.REPLIED, + JobApplication.response.REPLIED, JobApplication.replied_at >= datetime.utcnow() - agelimit ) ) @@ -272,10 +272,10 @@ class UserFlags(object): lambda user: JobApplication.query.filter( JobApplication.replied_by == user, JobApplication.replied_at < datetime.utcnow() - agelimit, - JobApplication.response == EMPLOYER_RESPONSE.REPLIED + JobApplication.response.REPLIED ).notempty(), lambda: db.session.query(distinct(JobApplication.replied_by_id).label('id')).filter( - JobApplication.response == EMPLOYER_RESPONSE.REPLIED, + JobApplication.response.REPLIED, JobApplication.replied_at < datetime.utcnow() - agelimit ) ) diff --git a/hasjob/models/jobpost.py b/hasjob/models/jobpost.py index 02fb23d55..07737de0f 100644 --- a/hasjob/models/jobpost.py +++ b/hasjob/models/jobpost.py @@ -753,7 +753,10 @@ class JobApplication(BaseMixin, db.Model): #: User opted-in to experimental features optin = db.Column(db.Boolean, default=False, nullable=False) #: Employer's response code - response = db.Column(db.Integer, nullable=False, default=EMPLOYER_RESPONSE.NEW) + _response = db.Column('response', db.Integer, + StateManager.check_constraint('response', EMPLOYER_RESPONSE), + nullable=False, default=EMPLOYER_RESPONSE.NEW) + response = StateManager('_response', EMPLOYER_RESPONSE, doc="Employer's response") #: Employer's response message response_message = db.Column(db.UnicodeText, nullable=True) #: Bag of words, for spam analysis @@ -771,43 +774,33 @@ def __init__(self, **kwargs): if self.hashid is None: self.hashid = unique_long_hash() - @property - def status(self): - return EMPLOYER_RESPONSE[self.response] - - def is_new(self): - return self.response == EMPLOYER_RESPONSE.NEW - - def is_pending(self): - return self.response == EMPLOYER_RESPONSE.PENDING - - def is_ignored(self): - return self.response == EMPLOYER_RESPONSE.IGNORED - - def is_replied(self): - return self.response == EMPLOYER_RESPONSE.REPLIED - - def is_flagged(self): - return self.response == EMPLOYER_RESPONSE.FLAGGED - - def is_spam(self): - return self.response == EMPLOYER_RESPONSE.SPAM + @response.transition(response.NEW, response.PENDING, title=__("Mark read"), message=__("This job application has been read"), type='success') + def mark_read(self): + pass - def is_rejected(self): - return self.response == EMPLOYER_RESPONSE.REJECTED + @response.transition(response.CAN_REPLY, response.REPLIED, title=__("Reply"), message=__("This job application has been replied to"), type='success') + def reply(self, message, user): + self.response_message = message + self.replied_by = user + self.replied_at = db.func.utcnow() - def can_reply(self): - return self.response in (EMPLOYER_RESPONSE.NEW, EMPLOYER_RESPONSE.PENDING, EMPLOYER_RESPONSE.IGNORED) + @response.transition(response.CAN_REJECT, response.REJECTED, title=__("Reject"), message=__("This job application has been rejected"), type='danger') + def reject(self, message, user): + self.response_message = message + self.replied_by = user + self.replied_at = db.func.utcnow() - def can_reject(self): - return self.response in (EMPLOYER_RESPONSE.NEW, EMPLOYER_RESPONSE.PENDING, EMPLOYER_RESPONSE.IGNORED) + @response.transition(response.CAN_IGNORE, response.IGNORED, title=__("Ignore"), message=__("This job application has been ignored"), type='danger') + def ignore(self): + pass - def can_ignore(self): - return self.response in (EMPLOYER_RESPONSE.NEW, EMPLOYER_RESPONSE.PENDING) + @response.transition(response.CAN_REPORT, response.FLAGGED, title=__("Report"), message=__("This job application has been reported"), type='danger') + def flag(self): + pass - def can_report(self): - return self.response in (EMPLOYER_RESPONSE.NEW, EMPLOYER_RESPONSE.PENDING, - EMPLOYER_RESPONSE.IGNORED, EMPLOYER_RESPONSE.REJECTED) + @response.transition(response.FLAGGED, response.PENDING, title=__("Unflag"), message=__("This job application has been unflagged"), type='success') + def unflag(self): + pass def application_count(self): """Number of jobs candidate has applied to around this one""" @@ -823,19 +816,14 @@ def application_count(self): } date_min = self.created_at - timedelta(days=7) date_max = self.created_at + timedelta(days=7) - counts = defaultdict(int) - for r in db.session.query(JobApplication.response).filter(JobApplication.user == self.user).filter( - JobApplication.created_at > date_min, JobApplication.created_at < date_max): - counts[r.response] += 1 - - return { - 'count': sum(counts.values()), - 'ignored': counts[EMPLOYER_RESPONSE.IGNORED], - 'replied': counts[EMPLOYER_RESPONSE.REPLIED], - 'flagged': counts[EMPLOYER_RESPONSE.FLAGGED], - 'spam': counts[EMPLOYER_RESPONSE.SPAM], - 'rejected': counts[EMPLOYER_RESPONSE.REJECTED], - } + grouped = JobApplication.response.group( + JobApplication.query.filter(JobApplication.user == self.user).filter( + JobApplication.created_at > date_min, JobApplication.created_at < date_max + ).options(db.load_only('id')) + ) + counts = {k.label.name: len(v) for k, v in grouped.items()} + counts['count'] = sum(counts.values()) + return counts def url_for(self, action='view', _external=False, **kwargs): domain = self.jobpost.email_domain @@ -852,7 +840,7 @@ def url_for(self, action='view', _external=False, **kwargs): JobApplication.jobpost = db.relationship(JobPost, backref=db.backref('applications', lazy='dynamic', order_by=( - db.case(value=JobApplication.response, whens={ + db.case(value=JobApplication._response, whens={ EMPLOYER_RESPONSE.NEW: 0, EMPLOYER_RESPONSE.PENDING: 1, EMPLOYER_RESPONSE.IGNORED: 2, @@ -866,12 +854,12 @@ def url_for(self, action='view', _external=False, **kwargs): JobPost.new_applications = db.column_property( db.select([db.func.count(JobApplication.id)]).where( - db.and_(JobApplication.jobpost_id == JobPost.id, JobApplication.response == EMPLOYER_RESPONSE.NEW))) + db.and_(JobApplication.jobpost_id == JobPost.id, JobApplication.response.NEW))) JobPost.replied_applications = db.column_property( db.select([db.func.count(JobApplication.id)]).where( - db.and_(JobApplication.jobpost_id == JobPost.id, JobApplication.response == EMPLOYER_RESPONSE.REPLIED))) + db.and_(JobApplication.jobpost_id == JobPost.id, JobApplication.response.REPLIED))) JobPost.viewcounts_viewed = db.column_property( diff --git a/hasjob/templates/application.html.jinja2 b/hasjob/templates/application.html.jinja2 index 59aa0983c..102275f40 100644 --- a/hasjob/templates/application.html.jinja2 +++ b/hasjob/templates/application.html.jinja2 @@ -29,16 +29,16 @@ {{ response_form.hidden_tag() }} - {%- if job_application.is_new() or job_application.is_pending() or job_application.is_ignored() %} + {%- if job_application.response.CAN_REJECT %}

- - - {% if not job_application.is_ignored() %}{% endif %} + + + {% if not job_application.response.IGNORED %}{% endif %}

- {%- if job_application.is_ignored() %} + {%- if job_application.response.IGNORED %} You have ignored this candidate. {%- endif %} Respond to the candidate to see their contact information. @@ -46,18 +46,18 @@ will not be shared. Spam reports are manually processed.

- {%- elif job_application.is_flagged() %} + {%- elif job_application.response.FLAGGED %}

You have flagged this application as spam.

- {%- elif job_application.is_spam() %} + {%- elif job_application.response.SPAM %}

An administrator flagged this application as spam.

- {%- elif job_application.is_replied() %} + {%- elif job_application.response.REPLIED %}

Email: {{ job_application.email }}
Phone: {{ job_application.phone }} @@ -68,7 +68,7 @@ {%- if job_application.response_message %} {{ job_application.response_message|safe }} {%- endif %} - {%- elif job_application.is_rejected() %} + {%- elif job_application.response.REJECTED %} {%- if job_application.replied_by -%}

Correspondent: {{ job_application.replied_by.pickername }}

{%- endif %} @@ -96,14 +96,14 @@
{%- for appl in post.applications %} - + {%- if appl == job_application -%} {{ appl.fullname }} {%- else -%} {{ appl.fullname }} {%- endif -%}
{%- endfor %} diff --git a/hasjob/templates/respond_email.html.jinja2 b/hasjob/templates/respond_email.html.jinja2 index d90d5a264..5b6f41b13 100644 --- a/hasjob/templates/respond_email.html.jinja2 +++ b/hasjob/templates/respond_email.html.jinja2 @@ -6,9 +6,9 @@
- {%- if job_application.is_replied() %} + {%- if job_application.response.REPLIED %} - {%- elif job_application.is_rejected() %} + {%- elif job_application.response.REJECTED %} {%- endif %}
@@ -17,9 +17,9 @@

- {%- if job_application.is_replied() %} + {%- if job_application.response.REPLIED %} {{ g.user.fullname if post.admin_is(g.user) else post.fullname or post.company_name }} has responded to your application for {{ post.headline }}. You can reply to this email to continue the conversation - {%- elif job_application.is_rejected() %} + {%- elif job_application.response.REJECTED %} {{ g.user.fullname if post.admin_is(g.user) else post.fullname or post.company_name }} has declined your application for {{ post.headline }} {%- endif %}

diff --git a/hasjob/views/listing.py b/hasjob/views/listing.py index 63bec2f57..2bb671efb 100644 --- a/hasjob/views/listing.py +++ b/hasjob/views/listing.py @@ -383,8 +383,8 @@ def view_application_email_gif(domain, hashid, application): job_application = None if job_application is not None: - if job_application.response == EMPLOYER_RESPONSE.NEW: - job_application.response = EMPLOYER_RESPONSE.PENDING + if job_application.mark_read.is_available: + job_application.mark_read() db.session.commit() return gif1x1, 200, { 'Content-Type': 'image/gif', @@ -419,16 +419,14 @@ def view_application(domain, hashid, application): if post.email_domain != domain: return redirect(job_application.url_for(), code=301) - if job_application.response == EMPLOYER_RESPONSE.NEW: + if job_application.response.NEW: # If the application is pending, mark it as opened. # However, don't do this if the user is a siteadmin, unless they also own the post. - if post.admin_is(g.user) or not lastuser.has_permission('siteadmin'): - job_application.response = EMPLOYER_RESPONSE.PENDING + if job_application.mark_read.is_available: + job_application.mark_read() db.session.commit() response_form = forms.ApplicationResponseForm() - statuses = set([app.status for app in post.applications]) - if not g.kiosk: if g.preview_campaign: header_campaign = g.preview_campaign @@ -440,7 +438,7 @@ def view_application(domain, hashid, application): return render_template('application.html.jinja2', post=post, job_application=job_application, header_campaign=header_campaign, - response_form=response_form, statuses=statuses, is_siteadmin=lastuser.has_permission('siteadmin')) + response_form=response_form, is_siteadmin=lastuser.has_permission('siteadmin')) @app.route('///appl//process', methods=['POST'], subdomain='') @@ -459,18 +457,21 @@ def process_application(domain, hashid, application): flashmsg = '' if response_form.validate_on_submit(): - if (request.form.get('action') == 'reply' and job_application.can_reply()) or ( - request.form.get('action') == 'reject' and job_application.can_reject()): + if (request.form.get('action') == 'reply' and job_application.response.CAN_REPLY) or ( + request.form.get('action') == 'reject' and job_application.response.CAN_REJECT): if not response_form.response_message.data: flashmsg = "You need to write a message to the candidate." else: if request.form.get('action') == 'reply': - job_application.response = EMPLOYER_RESPONSE.REPLIED + job_application.reply( + message=response_form.response_message.data, + user=g.user + ) else: - job_application.response = EMPLOYER_RESPONSE.REJECTED - job_application.response_message = response_form.response_message.data - job_application.replied_by = g.user - job_application.replied_at = datetime.utcnow() + job_application.reject( + message=response_form.response_message.data, + user=g.user + ) email_html = email_transform( render_template('respond_email.html.jinja2', @@ -484,7 +485,7 @@ def process_application(domain, hashid, application): sender=sender_name, site=app.config['SITE_TITLE']) - if job_application.is_replied(): + if job_application.response.REPLIED: msg = Message( subject=u"{candidate}: {headline}".format( candidate=job_application.user.fullname, headline=post.headline), @@ -502,14 +503,14 @@ def process_application(domain, hashid, application): msg.html = email_html mail.send(msg) db.session.commit() - elif request.form.get('action') == 'ignore' and job_application.can_ignore(): - job_application.response = EMPLOYER_RESPONSE.IGNORED + elif request.form.get('action') == 'ignore' and job_application.response.CAN_IGNORE: + job_application.ignore() db.session.commit() - elif request.form.get('action') == 'flag' and job_application.can_report(): - job_application.response = EMPLOYER_RESPONSE.FLAGGED + elif request.form.get('action') == 'flag' and job_application.response.CAN_REPORT: + job_application.flag() db.session.commit() - elif request.form.get('action') == 'unflag' and job_application.is_flagged(): - job_application.response = EMPLOYER_RESPONSE.NEW + elif request.form.get('action') == 'unflag' and job_application.response.FLAGGED: + job_application.unflag() db.session.commit() if flashmsg: diff --git a/migrations/versions/625415764254_jobapplication_response_statemanager.py b/migrations/versions/625415764254_jobapplication_response_statemanager.py new file mode 100644 index 000000000..5957501da --- /dev/null +++ b/migrations/versions/625415764254_jobapplication_response_statemanager.py @@ -0,0 +1,29 @@ +"""job_application response statemanager + +Revision ID: 625415764254 +Revises: 859f6f33c02d +Create Date: 2018-03-24 03:14:19.250467 + +""" + +# revision identifiers, used by Alembic. +revision = '625415764254' +down_revision = '859f6f33c02d' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_check_constraint( + 'job_application_response_check', + 'job_application', + "response IN (0, 1, 2, 3, 4, 5, 6)" + ) + + +def downgrade(): + op.drop_constraint( + 'job_application_response_check', + 'job_application' + ) From 7e87088657aa0ca7f04df1eb56342fce9ed29de2 Mon Sep 17 00:00:00 2001 From: Shreyas Satish Date: Wed, 11 Apr 2018 13:57:29 +0530 Subject: [PATCH 05/17] turn off autocomplete for the filters form --- hasjob/templates/layout.html.jinja2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hasjob/templates/layout.html.jinja2 b/hasjob/templates/layout.html.jinja2 index 77c9b1ff9..f06b16bdd 100644 --- a/hasjob/templates/layout.html.jinja2 +++ b/hasjob/templates/layout.html.jinja2 @@ -81,7 +81,7 @@ diff --git a/hasjob/views/index.py b/hasjob/views/index.py index 21379b1d8..7e2ef1af8 100644 --- a/hasjob/views/index.py +++ b/hasjob/views/index.py @@ -9,7 +9,7 @@ from coaster.views import render_with from baseframe import _ # , dogpile -from .. import app, lastuser +from .. import app, lastuser, forms from ..models import (db, JobCategory, JobPost, JobType, newlimit, agelimit, JobLocation, Board, Filterset, Domain, Location, Tag, JobPostTag, Campaign, CAMPAIGN_POSITION, CURRENCY, JobApplication, starred_job_table, BoardJobPost) from ..views.helper import (getposts, getallposts, gettags, location_geodata, load_viewcounts, session_jobpost_ab, @@ -447,6 +447,8 @@ def index(basequery=None, filters={}, md5sum=None, tag=None, domain=None, locati data['max_views'] = max_counts['max_views'] data['max_opens'] = max_counts['max_opens'] data['max_applied'] = max_counts['max_applied'] + data['max_applied'] = max_counts['max_applied'] + data['subscription_form'] = forms.JobPostSubscriptionForm() if filterset: data['filterset'] = filterset diff --git a/hasjob/views/job_alerts.py b/hasjob/views/job_alerts.py index 1b94d944d..5d7c75cdd 100644 --- a/hasjob/views/job_alerts.py +++ b/hasjob/views/job_alerts.py @@ -10,6 +10,7 @@ from baseframe import _ from .. import app, mail from ..models import (db, JobPostSubscription, Filterset) +from ..forms import JobPostSubscriptionForm @job('hasjob') @@ -24,29 +25,37 @@ def send_confirmation_email_for_job_alerts(to_address, token): @app.route('/api/1/subscribe_to_job_alerts', subdomain='', methods=['POST']) @app.route('/api/1/subscribe_to_job_alerts', methods=['POST']) def subscribe_to_job_alerts(): - if not request.json or not request.json.get('filters'): - abort(400) + form = JobPostSubscriptionForm() + if not form.validate_on_submit(): + flash(_(u"Oops! Sorry, we need an valid email address to send you alerts."), 'danger') + return redirect(url_for('index'), code=302) if g.user and g.user.email: email = g.user.email message = _(u"Thank you for signing up to receive job alerts from us! We'll keep you posted.") verified_user = True - elif request.json.get('email') and is_email(request.json.get('email')): - email = request.json.get('email') + elif form.email.data: + email = form.email.data message = _(u"Thank you for signing up to receive job alerts from us! We've sent you a confirmation email, please do confirm it so we can keep you posted.") verified_user = False - else: - flash(_(u"Oops! Sorry, we need an email address to send you alerts."), 'danger') - return redirect(url_for('index'), code=302) - filterset = Filterset.from_filters(g.board, request.json.get('filters')) + filters = { + 'l': request.args.getlist('l'), + 't': request.args.getlist('t'), + 'c': request.args.getlist('c'), + 'currency': request.args.get('currency'), + 'pay': request.args.get('pay'), + 'equity': request.args.get('equity'), + 'q': request.args.get('q') + } + filterset = Filterset.from_filters(g.board, filters) if filterset: existing_subscription = JobPostSubscription.get(filterset, email) if existing_subscription: flash(_(u"You've already subscribed to receive alerts for jobs that match this filtering criteria."), 'danger') return redirect(url_for('index'), code=302) else: - filterset = Filterset(board=g.board, filters=request.json.get('filters')) + filterset = Filterset(board=g.board, filters=filters) db.session.add(filterset) subscription = JobPostSubscription(filterset=filterset, email=email, user=g.user, anon_user=g.anon_user) From 27c21041c2efbbd2c900886a70271e381585fe54 Mon Sep 17 00:00:00 2001 From: Shreyas Satish Date: Sat, 21 Apr 2018 17:29:34 +0530 Subject: [PATCH 15/17] use form for email_frequency --- hasjob/views/job_alerts.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/hasjob/views/job_alerts.py b/hasjob/views/job_alerts.py index e6e809247..bf1664d1b 100644 --- a/hasjob/views/job_alerts.py +++ b/hasjob/views/job_alerts.py @@ -58,13 +58,9 @@ def subscribe_to_job_alerts(): filterset = Filterset(board=g.board, filters=filters) db.session.add(filterset) - email_frequency = EMAIL_FREQUENCY.WEEKLY if request.json.get('email_frequency') == EMAIL_FREQUENCY.WEEKLY else EMAIL_FREQUENCY.DAILY - subscription = JobPostSubscription( - filterset=filterset, - email=email, - user=g.user, - email_frequency=email_frequency, - anon_user=g.anon_user) + subscription = JobPostSubscription(filterset=filterset, user=g.user, anon_user=g.anon_user) + form.populate_obj(subscription) + if verified_user: subscription.verify_email() From 06230885d5166667fdb19c732372914931c5b982 Mon Sep 17 00:00:00 2001 From: Vidya Ramakrishnan Date: Mon, 23 Apr 2018 10:30:39 +0530 Subject: [PATCH 16/17] Update UI of job alerts form. --- hasjob/assets/js/app.js | 5 +++ hasjob/assets/sass/_header.sass | 23 ++++++++--- hasjob/assets/sass/_sheet.sass | 4 +- hasjob/assets/sass/_stickie.sass | 16 ++++++- ...ylesheet-app-css.bf0fec291a76efe07fc5.css} | 38 +++++++++++++---- ...d5c.js => app-css.bf0fec291a76efe07fc5.js} | 0 .../build/js/app.3c6dfcd579015e92ad5c.js | 1 - .../build/js/app.bf0fec291a76efe07fc5.js | 1 + ...5c.js => manifest.bf0fec291a76efe07fc5.js} | 0 ...ad5c.js => vendor.bf0fec291a76efe07fc5.js} | 0 hasjob/static/build/manifest.json | 2 +- hasjob/static/img/form-border.png | Bin 0 -> 1183 bytes hasjob/static/service-worker.js | 12 +++--- hasjob/templates/index.html.jinja2 | 39 ++++++++++-------- hasjob/templates/layout.html.jinja2 | 1 + hasjob/templates/macros.html.jinja2 | 2 +- 16 files changed, 100 insertions(+), 44 deletions(-) rename hasjob/static/build/css/{stylesheet-app-css.3c6dfcd579015e92ad5c.css => stylesheet-app-css.bf0fec291a76efe07fc5.css} (97%) rename hasjob/static/build/js/{app-css.3c6dfcd579015e92ad5c.js => app-css.bf0fec291a76efe07fc5.js} (100%) delete mode 100644 hasjob/static/build/js/app.3c6dfcd579015e92ad5c.js create mode 100644 hasjob/static/build/js/app.bf0fec291a76efe07fc5.js rename hasjob/static/build/js/{manifest.3c6dfcd579015e92ad5c.js => manifest.bf0fec291a76efe07fc5.js} (100%) rename hasjob/static/build/js/{vendor.3c6dfcd579015e92ad5c.js => vendor.bf0fec291a76efe07fc5.js} (100%) create mode 100644 hasjob/static/img/form-border.png diff --git a/hasjob/assets/js/app.js b/hasjob/assets/js/app.js index 6b10b6d1e..cfd251f66 100644 --- a/hasjob/assets/js/app.js +++ b/hasjob/assets/js/app.js @@ -41,6 +41,11 @@ window.Hasjob.Subscribe = { submitUrl, redirectOnSubmit, redirectOnSubmit, {} ); + + $('#close-subscribe-form').on('click', function(e) { + e.preventDefault(); + $("#subscribe-jb-form").slideUp(); + }); }, init: function() { this.handleEmailSubscription(); diff --git a/hasjob/assets/sass/_header.sass b/hasjob/assets/sass/_header.sass index 17c1b4bd8..3098c9743 100644 --- a/hasjob/assets/sass/_header.sass +++ b/hasjob/assets/sass/_header.sass @@ -75,9 +75,11 @@ header top: -4px .board-caption + display: block + width: 74% position: relative - left: -50px - top: 14px + left: 44px + top: -14px line-height: 1 .board-caption h1 a @@ -136,7 +138,7 @@ header .filters-wrapper width: 100% background: #ebe7e4 - margin: 8px 0 0 + margin: 0 label font-weight: normal @@ -352,5 +354,16 @@ header border-top-color: $color-title-has border-left-color: $color-title-has -.form-inline .listwidget ul input - top: 6px +#subscribe-jb-form + .subscribeform + background-image: url("~/static/img/form-border.png") + background-position: 100% 0 + background-repeat: repeat-x + background-size: 40px 3px + + .form-inline + .listwidget ul input + top: 6px + + .no-left-padding .controls + padding: 0 diff --git a/hasjob/assets/sass/_sheet.sass b/hasjob/assets/sass/_sheet.sass index 18e7fc5b5..47b2ad21f 100644 --- a/hasjob/assets/sass/_sheet.sass +++ b/hasjob/assets/sass/_sheet.sass @@ -161,10 +161,10 @@ tr > div // Fix for ParsleyJS inserting div tags in table rows in the campaign e color: inherit .list-group-item:first-child - border-top-radius: 0 + border-top-right-radius: 0 .list-group-item:last-child - border-bottom-radius: 0 + border-bottom-right-radius: 0 margin-bottom: -1px .list-group-item.no-bottom-border diff --git a/hasjob/assets/sass/_stickie.sass b/hasjob/assets/sass/_stickie.sass index 090a25474..8af65a7a6 100644 --- a/hasjob/assets/sass/_stickie.sass +++ b/hasjob/assets/sass/_stickie.sass @@ -91,6 +91,9 @@ .count-items.impressions flex: 1.5 + .count-items.applied + flex: 1.2 + .count-arrow float: right @@ -148,6 +151,7 @@ z-index: 2 .applied.background + flex: 1.2 position: relative z-index: 1 @@ -409,9 +413,17 @@ height: 20% max-width: 300px box-shadow: 0 15px 10px $color-shadow - rotate: -3deg + -webkit-transform: rotate(-3deg) + -moz-transform: rotate(-3deg) + -ms-transform: rotate(-3deg) + -o-transform: rotate(-3deg) + transform: rotate(-3deg) &:after right: 10px left: auto - rotate: 3deg + -webkit-transform: rotate(3deg) + -moz-transform: rotate(3deg) + -ms-transform: rotate(3deg) + -o-transform: rotate(3deg) + transform: rotate(3deg) diff --git a/hasjob/static/build/css/stylesheet-app-css.3c6dfcd579015e92ad5c.css b/hasjob/static/build/css/stylesheet-app-css.bf0fec291a76efe07fc5.css similarity index 97% rename from hasjob/static/build/css/stylesheet-app-css.3c6dfcd579015e92ad5c.css rename to hasjob/static/build/css/stylesheet-app-css.bf0fec291a76efe07fc5.css index 966ffe980..6e4653d2b 100644 --- a/hasjob/static/build/css/stylesheet-app-css.3c6dfcd579015e92ad5c.css +++ b/hasjob/static/build/css/stylesheet-app-css.bf0fec291a76efe07fc5.css @@ -61,9 +61,11 @@ header { position: relative; top: -4px; } header #hgnav .board-caption { + display: block; + width: 74%; position: relative; - left: -50px; - top: 14px; + left: 44px; + top: -14px; line-height: 1; } header #hgnav .board-caption h1 a { width: 100%; @@ -121,7 +123,7 @@ header { .filters-wrapper { width: 100%; background: #ebe7e4; - margin: 8px 0 0; } + margin: 0; } .filters-wrapper label { font-weight: normal; font-size: 14px; } @@ -300,9 +302,18 @@ header { border-top-color: #df5e0e; border-left-color: #df5e0e; } -.form-inline .listwidget ul input { +#subscribe-jb-form .subscribeform { + background-image: url("/static/img/form-border.png"); + background-position: 100% 0; + background-repeat: repeat-x; + background-size: 40px 3px; } + +#subscribe-jb-form .form-inline .listwidget ul input { top: 6px; } +#subscribe-jb-form .form-inline .no-left-padding .controls { + padding: 0; } + .input-group-btn > .btn { border-bottom: 1px solid #ccc; } .input-group-btn > .btn:hover, .input-group-btn > .btn:focus, .input-group-btn > .btn:active { @@ -499,9 +510,9 @@ tr > div { .sheet a.list-group-item { color: inherit; } .sheet .list-group-item:first-child { - border-top-radius: 0; } + border-top-right-radius: 0; } .sheet .list-group-item:last-child { - border-bottom-radius: 0; + border-bottom-right-radius: 0; margin-bottom: -1px; } .sheet .list-group-item.no-bottom-border { border-bottom: 0; @@ -789,6 +800,8 @@ tr > div { width: calc(100% - 6px); } .stickie .count-items.impressions { flex: 1.5; } + .stickie .count-items.applied { + flex: 1.2; } .stickie .count-arrow { float: right; } .stickie .count-text, @@ -834,6 +847,7 @@ tr > div { position: relative; z-index: 2; } .stickie .count-background .applied.background { + flex: 1.2; position: relative; z-index: 1; } .stickie .count-background .background.arrow:before { @@ -1072,11 +1086,19 @@ tr > div { height: 20%; max-width: 300px; box-shadow: 0 15px 10px rgba(0, 0, 0, 0.4); - rotate: -3deg; } + -webkit-transform: rotate(-3deg); + -moz-transform: rotate(-3deg); + -ms-transform: rotate(-3deg); + -o-transform: rotate(-3deg); + transform: rotate(-3deg); } .stickie:after { right: 10px; left: auto; - rotate: 3deg; } } + -webkit-transform: rotate(3deg); + -moz-transform: rotate(3deg); + -ms-transform: rotate(3deg); + -o-transform: rotate(3deg); + transform: rotate(3deg); } } #main-40x { text-align: center; } diff --git a/hasjob/static/build/js/app-css.3c6dfcd579015e92ad5c.js b/hasjob/static/build/js/app-css.bf0fec291a76efe07fc5.js similarity index 100% rename from hasjob/static/build/js/app-css.3c6dfcd579015e92ad5c.js rename to hasjob/static/build/js/app-css.bf0fec291a76efe07fc5.js diff --git a/hasjob/static/build/js/app.3c6dfcd579015e92ad5c.js b/hasjob/static/build/js/app.3c6dfcd579015e92ad5c.js deleted file mode 100644 index 86b7fa9aa..000000000 --- a/hasjob/static/build/js/app.3c6dfcd579015e92ad5c.js +++ /dev/null @@ -1 +0,0 @@ -webpackJsonp([2],{yZ5m:function(module,exports,__webpack_require__){"use strict";Hasjob.Util={updateGA:function(){if(window.ga){var path=window.location.href.split(window.location.host)[1];window.ga("set","page",path),window.ga("send","pageview")}},createCustomEvent:function(eventName){if("function"==typeof window.Event)var customEvent=new Event(eventName);else{var customEvent=document.createEvent("Event");customEvent.initEvent(eventName,!0,!0)}return customEvent}},window.Hasjob.Subscribe={handleEmailSubscription:function(){var redirectUrl=window.location.href,redirectOnSubmit=function(){window.location.href=redirectUrl},submitUrl=$("#subscribe-jobalerts").attr("action")+"?"+window.Hasjob.Filters.toParam();window.Baseframe.Forms.handleFormSubmit("subscribe-jobalerts",submitUrl,redirectOnSubmit,redirectOnSubmit,{})},init:function(){this.handleEmailSubscription()}},window.Hasjob.JobPost={handleStarClick:function(){$("#main-content").on("click",".pstar",function(e){var starlink=$(this).find("i"),csrf_token=$('meta[name="csrf-token"]').attr("content");return starlink.addClass("fa-spin"),$.ajax("/star/"+starlink.data("id"),{type:"POST",data:{csrf_token:csrf_token},dataType:"json",complete:function(){starlink.removeClass("fa-spin")},success:function(data){!0===data.is_starred?starlink.removeClass("fa-star-o").addClass("fa-star").parent().find(".pstar-caption").html("Bookmarked"):starlink.removeClass("fa-star").addClass("fa-star-o").parent().find(".pstar-caption").html("Bookmark this")}}),!1})},handleGroupClick:function(){var node,outer,inner,outerTemplate=document.createElement("li"),innerTemplate=document.createElement("a");outerTemplate.setAttribute("class","col-xs-12 col-md-3 col-sm-4 animated shake"),innerTemplate.setAttribute("class","stickie"),innerTemplate.setAttribute("rel","bookmark"),$("#main-content").on("click","#stickie-area li.grouped",function(e){e.preventDefault();for(var group=this,parent=group.parentNode,i=0;i255||rgba[1]>255||rgba[2]>255?"#FFFFFF":0==rgba[0]&&0==rgba[1]&&0==rgba[2]?window.Hasjob.Config[funnelName].maxColour:"#"+("000000"+(rgba[0]<<16)|rgba[1]<<8|rgba[2]).toString(16).slice(-6);var element=document.getElementById(elementId);element.classList.add("funnel-color-set"),element.style.backgroundColor=colourHex},renderGradientColour:function(){$(".js-funnel").each(function(){$(this).hasClass("funnel-color-set")||Hasjob.StickieList.setGradientColour($(this).data("funnel-name"),$(this).data("funnel-value"),$(this).attr("id"))})},createGradientColour:function(){Hasjob.StickieList.createGradientColourScale("impressions",Hasjob.Config.MaxCounts.max_impressions),Hasjob.StickieList.createGradientColourScale("views",Hasjob.Config.MaxCounts.max_views),Hasjob.StickieList.createGradientColourScale("opens",Hasjob.Config.MaxCounts.max_opens),Hasjob.StickieList.createGradientColourScale("applied",Hasjob.Config.MaxCounts.max_applied)},initFunnelViz:function(){window.addEventListener("onStickiesInit",function(e){window.Hasjob.Config.MaxCounts&&(Hasjob.StickieList.createGradientColour(),Hasjob.StickieList.renderGradientColour())},!1),window.addEventListener("onStickiesRefresh",function(e){window.Hasjob.Config.MaxCounts&&Hasjob.StickieList.renderGradientColour()},!1),window.addEventListener("onStickiesPagination",function(e){window.Hasjob.Config.MaxCounts&&Hasjob.StickieList.renderGradientColour()},!1)}},window.Hasjob.Filters={toParam:function(){var sortedFilterParams=this.formatFilterParams($("#js-job-filters").serializeArray());return sortedFilterParams.length?$.param(sortedFilterParams):""},getCurrentState:function(){return Object.keys(window.Hasjob.Config.selectedFilters).length||(window.Hasjob.Config.selectedFilters={selectedLocations:[],selectedTypes:[],selectedCategories:[],selectedQuery:"",selectedCurrency:"",pay:0,equity:""}),{jobLocations:window.Hasjob.Config.allFilters.job_location_filters,jobTypes:window.Hasjob.Config.allFilters.job_type_filters,jobCategories:window.Hasjob.Config.allFilters.job_category_filters,jobsArchive:window.Hasjob.Config.selectedFilters.archive,selectedLocations:window.Hasjob.Config.selectedFilters.location_names,selectedTypes:window.Hasjob.Config.selectedFilters.types,selectedCategories:window.Hasjob.Config.selectedFilters.categories,selectedQuery:window.Hasjob.Config.selectedFilters.query_string,selectedCurrency:window.Hasjob.Config.selectedFilters.currency,pay:window.Hasjob.Config.selectedFilters.pay,equity:window.Hasjob.Config.selectedFilters.equity,isMobile:$(window).width()<768}},init:function(){var keywordTimeout,pageScrollTimerId,filters=this,isFilterDropdownClosed=!0,filterMenuHeight=$("#hgnav").height()-$("#hg-sitenav").height();filters.dropdownMenu=new Ractive({el:"job-filters-ractive-template",template:"#filters-ractive",data:this.getCurrentState(),openOnMobile:function(event){event.original.preventDefault(),filters.dropdownMenu.set("show",!0)},closeOnMobile:function(event){event&&event.original.preventDefault(),filters.dropdownMenu.set("show",!1)},complete:function(){$(window).resize(function(){$(window).width()<768?filters.dropdownMenu.set("isMobile",!0):filters.dropdownMenu.set("isMobile",!1)}),$(document).on("click",function(event){$("#js-job-filters")===event.target||$(event.target).parents("#filter-dropdown").length||filters.dropdownMenu.closeOnMobile()})}});var pageScrollTimer=function(){return setInterval(function(){isFilterDropdownClosed&&($(window).scrollTop()>filterMenuHeight?$("#hg-sitenav").slideUp():$("#hg-sitenav").slideDown())},250)};$(window).width()>767&&(pageScrollTimerId=pageScrollTimer()),$(window).resize(function(){$(window).width()<768?($("#hg-sitenav").show(),pageScrollTimerId&&(clearInterval(pageScrollTimerId),pageScrollTimerId=0)):(filterMenuHeight=$("#hgnav").height()-$("#hg-sitenav").height(),pageScrollTimerId||(pageScrollTimerId=pageScrollTimer()))}),$("#job-filters-keywords").on("change",function(){$(this).val($(this).val().trim())}),$(".js-handle-filter-change").on("change",function(e){window.Hasjob.StickieList.refresh()});var lastKeyword="";$(".js-handle-keyword-update").on("keyup",function(){$(this).val()!==lastKeyword&&(window.clearTimeout(keywordTimeout),lastKeyword=$(this).val(),keywordTimeout=window.setTimeout(window.Hasjob.StickieList.refresh,1e3))}),$("#job-filters-location").multiselect({nonSelectedText:"Location",numberDisplayed:1,buttonWidth:"100%",enableFiltering:!0,enableCaseInsensitiveFiltering:!0,templates:{filter:'
  • ',filterClearBtn:'
  • '},optionClass:function(element){if($(element).hasClass("unavailable"))return"unavailable"},onDropdownShow:function(event,ui){isFilterDropdownClosed=!1},onDropdownHide:function(event,ui){isFilterDropdownClosed=!0}}),$(".job-filter-location-search-clear").click(function(e){$("#job-filter-location-search").val("")}),$("#job-filters-type").multiselect({nonSelectedText:"Job Type",numberDisplayed:1,buttonWidth:"100%",optionClass:function(element){if($(element).hasClass("unavailable"))return"unavailable"},onDropdownShow:function(event,ui){isFilterDropdownClosed=!1},onDropdownHide:function(event,ui){isFilterDropdownClosed=!0}}),$("#job-filters-category").multiselect({nonSelectedText:"Job Category",numberDisplayed:1,buttonWidth:"100%",optionClass:function(element){if($(element).hasClass("unavailable"))return"unavailable"},onDropdownShow:function(event,ui){isFilterDropdownClosed=!1},onDropdownHide:function(event,ui){isFilterDropdownClosed=!0}}),$("#job-filters-pay").on("shown.bs.dropdown",function(){isFilterDropdownClosed=!1}),$("#job-filters-pay").on("hidden.bs.dropdown",function(){isFilterDropdownClosed=!0}),$(document).keydown(function(event){27===event.keyCode&&(event.preventDefault(),filters.dropdownMenu.closeOnMobile())})},formatFilterParams:function(formParams){for(var sortedFilterParams=[],currencyVal="",fpIndex=0;fpIndex0&&(afterPoint=value.substring(value.indexOf("."),value.length)),value=Math.floor(value),value=value.toString();var lastThree=value.substring(value.length-3),otherNumbers=value.substring(0,value.length-3);return""!==otherNumbers&&(lastThree=","+lastThree),"₹"+otherNumbers.replace(/\B(?=(\d{2})+(?!\d))/g,",")+lastThree+afterPoint},window.Hasjob.Currency.prefix=function(currency){var currencyMap={default:"¤",inr:"₹",usd:"$",sgd:"S$",aud:"A$",eur:"€",gbp:"£"};return void 0===currency||"na"==currency.toLowerCase()?currencyMap.default:currencyMap[currency.toLowerCase()]},window.Hasjob.Currency.isRupee=function(currency){return"inr"===currency.toLowerCase()},window.Hasjob.Currency.wNumbFormat=function(currency){var prefix="¤",encoder=null;return currency&&window.Hasjob.Currency.isRupee(currency)&&(encoder=Hasjob.Currency.indian_rupee_encoder),prefix=Hasjob.Currency.prefix(currency),null===encoder?window.wNumb({decimals:0,thousand:",",prefix:prefix}):window.wNumb({decimals:0,thousand:",",prefix:prefix,edit:encoder})},window.Hasjob.Currency.formatTo=function(currency,value){return window.Hasjob.Currency.wNumbFormat(currency).to(value)},window.Hasjob.Currency.formatFrom=function(currency,value){return window.Hasjob.Currency.wNumbFormat(currency).from(value)},window.Hasjob.PaySlider.toNumeric=function(str){return str.slice(1).replace(/,/g,"")},window.Hasjob.PaySlider.range=function(currency){return"INR"===currency?{min:[0,5e3],"15%":[1e5,1e4],"30%":[2e5,5e4],"70%":[2e6,1e5],"85%":[1e7,1e6],max:[1e8]}:{min:[0,5e3],"2%":[2e5,5e4],"10%":[1e6,1e5],max:[1e7,1e5]}},window.Hasjob.PaySlider.prototype.init=function(){return this.slider=$(this.selector).noUiSlider({start:this.start,connect:this.start.constructor===Array,behaviour:"tap",range:{min:[0,5e4],"10%":[1e6,1e5],max:[1e7,1e5]},format:window.wNumb({decimals:0,thousand:",",prefix:"¤"})}),this.slider.Link("lower").to($(this.minField)),void 0!==this.maxField&&this.slider.Link("upper").to($(this.maxField)),this},window.Hasjob.PaySlider.prototype.resetSlider=function(currency){var start,startval=this.slider.val();start=startval.constructor===Array?[Hasjob.PaySlider.toNumeric(startval[0]),Hasjob.PaySlider.toNumeric(startval[1])]:Hasjob.PaySlider.toNumeric(startval),this.slider.noUiSlider({start:start,connect:start.constructor===Array,range:Hasjob.PaySlider.range(currency),format:Hasjob.Currency.wNumbFormat(currency)},!0),this.slider.Link("lower").to($(this.minField)),void 0!==this.maxField&&this.slider.Link("upper").to($(this.maxField))},$(function(){Ractive.DEBUG=!1,$(window).on("popstate",function(event){if(!event.originalEvent.state||!event.originalEvent.state.reloadOnPop)return!1;location.reload(!0)}),window.Hasjob.Filters.init(),window.Hasjob.Subscribe.init(),window.Hasjob.JobPost.handleStarClick(),window.Hasjob.JobPost.handleGroupClick(),window.Hasjob.StickieList.initFunnelViz();var getCurrencyVal=function(){return $("input[type='radio'][name='currency']:checked").val()},setPayTextField=function(){var payFieldLabel,currencyLabel="Pay",equityLabel="";if($("#job-filters-equity").is(":checked")&&(equityLabel+=" + %"),"na"===getCurrencyVal().toLowerCase())currencyLabel="Pay";else{currencyLabel="0"===Hasjob.PaySlider.toNumeric($("#job-filters-payval").val())?"Pay "+getCurrencyVal():$("#job-filters-payval").val()+" per year"}payFieldLabel="Pay"===currencyLabel&&""!==equityLabel?"Equity (%)":currencyLabel+equityLabel,$("#job-filters-pay-text").html(payFieldLabel)};$("#job-filters-equity").on("change",function(){setPayTextField()});var presetCurrency=window.Hasjob.Config&&window.Hasjob.Config.selectedFilters.currency||"NA";$.each($("input[type='radio'][name='currency']"),function(index,currencyRadio){$(currencyRadio).val()===presetCurrency&&$(currencyRadio).attr("checked","checked")}),window.Hasjob.Config&&window.Hasjob.Config.selectedFilters.equity&&$("input[type='checkbox'][name='equity']").attr("checked","checked"),$("input[type='radio'][name='currency']").on("change",function(){setPaySliderVisibility(),paySlider.resetSlider(getCurrencyVal()),setPayTextField()}),$("ul.pay-filter-dropdown").click(function(e){e.stopPropagation()});var setPaySliderVisibility=function(){"na"===getCurrencyVal().toLowerCase()?$(".pay-filter-slider").slideUp():$(".pay-filter-slider").slideDown()},paySlider=new Hasjob.PaySlider({start:window.Hasjob.Config&&window.Hasjob.Config.selectedFilters.pay||0,selector:"#pay-slider",minField:"#job-filters-payval"});$("#pay-slider").on("slide",function(){setPayTextField()}),$("#pay-slider").on("change",function(){window.Hasjob.StickieList.refresh()}),setPaySliderVisibility(),paySlider.resetSlider(getCurrencyVal()),setPayTextField()})}},["yZ5m"]); \ No newline at end of file diff --git a/hasjob/static/build/js/app.bf0fec291a76efe07fc5.js b/hasjob/static/build/js/app.bf0fec291a76efe07fc5.js new file mode 100644 index 000000000..c8e596f7f --- /dev/null +++ b/hasjob/static/build/js/app.bf0fec291a76efe07fc5.js @@ -0,0 +1 @@ +webpackJsonp([2],{yZ5m:function(module,exports,__webpack_require__){"use strict";Hasjob.Util={updateGA:function(){if(window.ga){var path=window.location.href.split(window.location.host)[1];window.ga("set","page",path),window.ga("send","pageview")}},createCustomEvent:function(eventName){if("function"==typeof window.Event)var customEvent=new Event(eventName);else{var customEvent=document.createEvent("Event");customEvent.initEvent(eventName,!0,!0)}return customEvent}},window.Hasjob.Subscribe={handleEmailSubscription:function(){var redirectUrl=window.location.href,redirectOnSubmit=function(){window.location.href=redirectUrl},submitUrl=$("#subscribe-jobalerts").attr("action")+"?"+window.Hasjob.Filters.toParam();window.Baseframe.Forms.handleFormSubmit("subscribe-jobalerts",submitUrl,redirectOnSubmit,redirectOnSubmit,{}),$("#close-subscribe-form").on("click",function(e){e.preventDefault(),$("#subscribe-jb-form").slideUp()})},init:function(){this.handleEmailSubscription()}},window.Hasjob.JobPost={handleStarClick:function(){$("#main-content").on("click",".pstar",function(e){var starlink=$(this).find("i"),csrf_token=$('meta[name="csrf-token"]').attr("content");return starlink.addClass("fa-spin"),$.ajax("/star/"+starlink.data("id"),{type:"POST",data:{csrf_token:csrf_token},dataType:"json",complete:function(){starlink.removeClass("fa-spin")},success:function(data){!0===data.is_starred?starlink.removeClass("fa-star-o").addClass("fa-star").parent().find(".pstar-caption").html("Bookmarked"):starlink.removeClass("fa-star").addClass("fa-star-o").parent().find(".pstar-caption").html("Bookmark this")}}),!1})},handleGroupClick:function(){var node,outer,inner,outerTemplate=document.createElement("li"),innerTemplate=document.createElement("a");outerTemplate.setAttribute("class","col-xs-12 col-md-3 col-sm-4 animated shake"),innerTemplate.setAttribute("class","stickie"),innerTemplate.setAttribute("rel","bookmark"),$("#main-content").on("click","#stickie-area li.grouped",function(e){e.preventDefault();for(var group=this,parent=group.parentNode,i=0;i255||rgba[1]>255||rgba[2]>255?"#FFFFFF":0==rgba[0]&&0==rgba[1]&&0==rgba[2]?window.Hasjob.Config[funnelName].maxColour:"#"+("000000"+(rgba[0]<<16)|rgba[1]<<8|rgba[2]).toString(16).slice(-6);var element=document.getElementById(elementId);element.classList.add("funnel-color-set"),element.style.backgroundColor=colourHex},renderGradientColour:function(){$(".js-funnel").each(function(){$(this).hasClass("funnel-color-set")||Hasjob.StickieList.setGradientColour($(this).data("funnel-name"),$(this).data("funnel-value"),$(this).attr("id"))})},createGradientColour:function(){Hasjob.StickieList.createGradientColourScale("impressions",Hasjob.Config.MaxCounts.max_impressions),Hasjob.StickieList.createGradientColourScale("views",Hasjob.Config.MaxCounts.max_views),Hasjob.StickieList.createGradientColourScale("opens",Hasjob.Config.MaxCounts.max_opens),Hasjob.StickieList.createGradientColourScale("applied",Hasjob.Config.MaxCounts.max_applied)},initFunnelViz:function(){window.addEventListener("onStickiesInit",function(e){window.Hasjob.Config.MaxCounts&&(Hasjob.StickieList.createGradientColour(),Hasjob.StickieList.renderGradientColour())},!1),window.addEventListener("onStickiesRefresh",function(e){window.Hasjob.Config.MaxCounts&&Hasjob.StickieList.renderGradientColour()},!1),window.addEventListener("onStickiesPagination",function(e){window.Hasjob.Config.MaxCounts&&Hasjob.StickieList.renderGradientColour()},!1)}},window.Hasjob.Filters={toParam:function(){var sortedFilterParams=this.formatFilterParams($("#js-job-filters").serializeArray());return sortedFilterParams.length?$.param(sortedFilterParams):""},getCurrentState:function(){return Object.keys(window.Hasjob.Config.selectedFilters).length||(window.Hasjob.Config.selectedFilters={selectedLocations:[],selectedTypes:[],selectedCategories:[],selectedQuery:"",selectedCurrency:"",pay:0,equity:""}),{jobLocations:window.Hasjob.Config.allFilters.job_location_filters,jobTypes:window.Hasjob.Config.allFilters.job_type_filters,jobCategories:window.Hasjob.Config.allFilters.job_category_filters,jobsArchive:window.Hasjob.Config.selectedFilters.archive,selectedLocations:window.Hasjob.Config.selectedFilters.location_names,selectedTypes:window.Hasjob.Config.selectedFilters.types,selectedCategories:window.Hasjob.Config.selectedFilters.categories,selectedQuery:window.Hasjob.Config.selectedFilters.query_string,selectedCurrency:window.Hasjob.Config.selectedFilters.currency,pay:window.Hasjob.Config.selectedFilters.pay,equity:window.Hasjob.Config.selectedFilters.equity,isMobile:$(window).width()<768}},init:function(){var keywordTimeout,pageScrollTimerId,filters=this,isFilterDropdownClosed=!0,filterMenuHeight=$("#hgnav").height()-$("#hg-sitenav").height();filters.dropdownMenu=new Ractive({el:"job-filters-ractive-template",template:"#filters-ractive",data:this.getCurrentState(),openOnMobile:function(event){event.original.preventDefault(),filters.dropdownMenu.set("show",!0)},closeOnMobile:function(event){event&&event.original.preventDefault(),filters.dropdownMenu.set("show",!1)},complete:function(){$(window).resize(function(){$(window).width()<768?filters.dropdownMenu.set("isMobile",!0):filters.dropdownMenu.set("isMobile",!1)}),$(document).on("click",function(event){$("#js-job-filters")===event.target||$(event.target).parents("#filter-dropdown").length||filters.dropdownMenu.closeOnMobile()})}});var pageScrollTimer=function(){return setInterval(function(){isFilterDropdownClosed&&($(window).scrollTop()>filterMenuHeight?$("#hg-sitenav").slideUp():$("#hg-sitenav").slideDown())},250)};$(window).width()>767&&(pageScrollTimerId=pageScrollTimer()),$(window).resize(function(){$(window).width()<768?($("#hg-sitenav").show(),pageScrollTimerId&&(clearInterval(pageScrollTimerId),pageScrollTimerId=0)):(filterMenuHeight=$("#hgnav").height()-$("#hg-sitenav").height(),pageScrollTimerId||(pageScrollTimerId=pageScrollTimer()))}),$("#job-filters-keywords").on("change",function(){$(this).val($(this).val().trim())}),$(".js-handle-filter-change").on("change",function(e){window.Hasjob.StickieList.refresh()});var lastKeyword="";$(".js-handle-keyword-update").on("keyup",function(){$(this).val()!==lastKeyword&&(window.clearTimeout(keywordTimeout),lastKeyword=$(this).val(),keywordTimeout=window.setTimeout(window.Hasjob.StickieList.refresh,1e3))}),$("#job-filters-location").multiselect({nonSelectedText:"Location",numberDisplayed:1,buttonWidth:"100%",enableFiltering:!0,enableCaseInsensitiveFiltering:!0,templates:{filter:'
  • ',filterClearBtn:'
  • '},optionClass:function(element){if($(element).hasClass("unavailable"))return"unavailable"},onDropdownShow:function(event,ui){isFilterDropdownClosed=!1},onDropdownHide:function(event,ui){isFilterDropdownClosed=!0}}),$(".job-filter-location-search-clear").click(function(e){$("#job-filter-location-search").val("")}),$("#job-filters-type").multiselect({nonSelectedText:"Job Type",numberDisplayed:1,buttonWidth:"100%",optionClass:function(element){if($(element).hasClass("unavailable"))return"unavailable"},onDropdownShow:function(event,ui){isFilterDropdownClosed=!1},onDropdownHide:function(event,ui){isFilterDropdownClosed=!0}}),$("#job-filters-category").multiselect({nonSelectedText:"Job Category",numberDisplayed:1,buttonWidth:"100%",optionClass:function(element){if($(element).hasClass("unavailable"))return"unavailable"},onDropdownShow:function(event,ui){isFilterDropdownClosed=!1},onDropdownHide:function(event,ui){isFilterDropdownClosed=!0}}),$("#job-filters-pay").on("shown.bs.dropdown",function(){isFilterDropdownClosed=!1}),$("#job-filters-pay").on("hidden.bs.dropdown",function(){isFilterDropdownClosed=!0}),$(document).keydown(function(event){27===event.keyCode&&(event.preventDefault(),filters.dropdownMenu.closeOnMobile())})},formatFilterParams:function(formParams){for(var sortedFilterParams=[],currencyVal="",fpIndex=0;fpIndex0&&(afterPoint=value.substring(value.indexOf("."),value.length)),value=Math.floor(value),value=value.toString();var lastThree=value.substring(value.length-3),otherNumbers=value.substring(0,value.length-3);return""!==otherNumbers&&(lastThree=","+lastThree),"₹"+otherNumbers.replace(/\B(?=(\d{2})+(?!\d))/g,",")+lastThree+afterPoint},window.Hasjob.Currency.prefix=function(currency){var currencyMap={default:"¤",inr:"₹",usd:"$",sgd:"S$",aud:"A$",eur:"€",gbp:"£"};return void 0===currency||"na"==currency.toLowerCase()?currencyMap.default:currencyMap[currency.toLowerCase()]},window.Hasjob.Currency.isRupee=function(currency){return"inr"===currency.toLowerCase()},window.Hasjob.Currency.wNumbFormat=function(currency){var prefix="¤",encoder=null;return currency&&window.Hasjob.Currency.isRupee(currency)&&(encoder=Hasjob.Currency.indian_rupee_encoder),prefix=Hasjob.Currency.prefix(currency),null===encoder?window.wNumb({decimals:0,thousand:",",prefix:prefix}):window.wNumb({decimals:0,thousand:",",prefix:prefix,edit:encoder})},window.Hasjob.Currency.formatTo=function(currency,value){return window.Hasjob.Currency.wNumbFormat(currency).to(value)},window.Hasjob.Currency.formatFrom=function(currency,value){return window.Hasjob.Currency.wNumbFormat(currency).from(value)},window.Hasjob.PaySlider.toNumeric=function(str){return str.slice(1).replace(/,/g,"")},window.Hasjob.PaySlider.range=function(currency){return"INR"===currency?{min:[0,5e3],"15%":[1e5,1e4],"30%":[2e5,5e4],"70%":[2e6,1e5],"85%":[1e7,1e6],max:[1e8]}:{min:[0,5e3],"2%":[2e5,5e4],"10%":[1e6,1e5],max:[1e7,1e5]}},window.Hasjob.PaySlider.prototype.init=function(){return this.slider=$(this.selector).noUiSlider({start:this.start,connect:this.start.constructor===Array,behaviour:"tap",range:{min:[0,5e4],"10%":[1e6,1e5],max:[1e7,1e5]},format:window.wNumb({decimals:0,thousand:",",prefix:"¤"})}),this.slider.Link("lower").to($(this.minField)),void 0!==this.maxField&&this.slider.Link("upper").to($(this.maxField)),this},window.Hasjob.PaySlider.prototype.resetSlider=function(currency){var start,startval=this.slider.val();start=startval.constructor===Array?[Hasjob.PaySlider.toNumeric(startval[0]),Hasjob.PaySlider.toNumeric(startval[1])]:Hasjob.PaySlider.toNumeric(startval),this.slider.noUiSlider({start:start,connect:start.constructor===Array,range:Hasjob.PaySlider.range(currency),format:Hasjob.Currency.wNumbFormat(currency)},!0),this.slider.Link("lower").to($(this.minField)),void 0!==this.maxField&&this.slider.Link("upper").to($(this.maxField))},$(function(){Ractive.DEBUG=!1,$(window).on("popstate",function(event){if(!event.originalEvent.state||!event.originalEvent.state.reloadOnPop)return!1;location.reload(!0)}),window.Hasjob.Filters.init(),window.Hasjob.Subscribe.init(),window.Hasjob.JobPost.handleStarClick(),window.Hasjob.JobPost.handleGroupClick(),window.Hasjob.StickieList.initFunnelViz();var getCurrencyVal=function(){return $("input[type='radio'][name='currency']:checked").val()},setPayTextField=function(){var payFieldLabel,currencyLabel="Pay",equityLabel="";if($("#job-filters-equity").is(":checked")&&(equityLabel+=" + %"),"na"===getCurrencyVal().toLowerCase())currencyLabel="Pay";else{currencyLabel="0"===Hasjob.PaySlider.toNumeric($("#job-filters-payval").val())?"Pay "+getCurrencyVal():$("#job-filters-payval").val()+" per year"}payFieldLabel="Pay"===currencyLabel&&""!==equityLabel?"Equity (%)":currencyLabel+equityLabel,$("#job-filters-pay-text").html(payFieldLabel)};$("#job-filters-equity").on("change",function(){setPayTextField()});var presetCurrency=window.Hasjob.Config&&window.Hasjob.Config.selectedFilters.currency||"NA";$.each($("input[type='radio'][name='currency']"),function(index,currencyRadio){$(currencyRadio).val()===presetCurrency&&$(currencyRadio).attr("checked","checked")}),window.Hasjob.Config&&window.Hasjob.Config.selectedFilters.equity&&$("input[type='checkbox'][name='equity']").attr("checked","checked"),$("input[type='radio'][name='currency']").on("change",function(){setPaySliderVisibility(),paySlider.resetSlider(getCurrencyVal()),setPayTextField()}),$("ul.pay-filter-dropdown").click(function(e){e.stopPropagation()});var setPaySliderVisibility=function(){"na"===getCurrencyVal().toLowerCase()?$(".pay-filter-slider").slideUp():$(".pay-filter-slider").slideDown()},paySlider=new Hasjob.PaySlider({start:window.Hasjob.Config&&window.Hasjob.Config.selectedFilters.pay||0,selector:"#pay-slider",minField:"#job-filters-payval"});$("#pay-slider").on("slide",function(){setPayTextField()}),$("#pay-slider").on("change",function(){window.Hasjob.StickieList.refresh()}),setPaySliderVisibility(),paySlider.resetSlider(getCurrencyVal()),setPayTextField()})}},["yZ5m"]); \ No newline at end of file diff --git a/hasjob/static/build/js/manifest.3c6dfcd579015e92ad5c.js b/hasjob/static/build/js/manifest.bf0fec291a76efe07fc5.js similarity index 100% rename from hasjob/static/build/js/manifest.3c6dfcd579015e92ad5c.js rename to hasjob/static/build/js/manifest.bf0fec291a76efe07fc5.js diff --git a/hasjob/static/build/js/vendor.3c6dfcd579015e92ad5c.js b/hasjob/static/build/js/vendor.bf0fec291a76efe07fc5.js similarity index 100% rename from hasjob/static/build/js/vendor.3c6dfcd579015e92ad5c.js rename to hasjob/static/build/js/vendor.bf0fec291a76efe07fc5.js diff --git a/hasjob/static/build/manifest.json b/hasjob/static/build/manifest.json index c2a43abca..776086621 100644 --- a/hasjob/static/build/manifest.json +++ b/hasjob/static/build/manifest.json @@ -1 +1 @@ -{"assets":{"vendor":"js/vendor.3c6dfcd579015e92ad5c.js","app-css":"css/stylesheet-app-css.3c6dfcd579015e92ad5c.css","app":"js/app.3c6dfcd579015e92ad5c.js","manifest":"js/manifest.3c6dfcd579015e92ad5c.js"}} \ No newline at end of file +{"assets":{"vendor":"js/vendor.bf0fec291a76efe07fc5.js","app-css":"css/stylesheet-app-css.bf0fec291a76efe07fc5.css","app":"js/app.bf0fec291a76efe07fc5.js","manifest":"js/manifest.bf0fec291a76efe07fc5.js"}} \ No newline at end of file diff --git a/hasjob/static/img/form-border.png b/hasjob/static/img/form-border.png new file mode 100644 index 0000000000000000000000000000000000000000..da2dea39916cde936599de95b313ca6fbb265123 GIT binary patch literal 1183 zcmbVMTWHfz7>?|scws&$xS`u26I3uw(loXS>qggfF0j^83zi9DE@x}laxpn;vw>q( z84N*0AAAt+Ac}Z$)S#cqe&}|h0*;M*r4l4TazQ?eQ zAZk1#UqFR)kEq#ZNC|X8zUg2zL9})Fj-m}f1XS2>SaI_6t-B;J^f}ZqMQp%?NCCb%XnCR^Cui-7cpn_oB$!n}195VWR3V)ON!x{h z5Al@7un`ayLM+clV(eBBW>}79IGW`sHY_rN$ns$RA+a}C?-O%U*Ss$*#mOQoo=xeQlaqj-^C{V;~ z#2v1Ii=qrlwl-u!3&~QP#5W<`&_zY-i*lOIQ=HC3sE8n_RE%YKDx!p=>Nb_p)tFYb zvlu?sB}fvN=wgLvjAc`tAVd?Akc@OPd^nnjq^eli@{nR_uQ)YBU$J1YM&9^)A;o!EBO6V-W)X7W^Du|KH@% zm=qm!${(FmJ;Lo6Y!~JL3k#D3Ej&Iho|B2)c^S{xeOXH6{VDbKNA1q3O6A_cN@?$w z)epaYn*Q1Feodydv9~qbz#X0HZ1}$9_17~i?>y;=lwQdoGhWx+c=kl|bSpPAIjx;P z_u({glV)Z{k6mcIL~rQ+_-tzbVZ#kFH^#$K*ix9s}km2H~ +
    +
    +

    Subscribe to job alerts

    +

    We will send you alerts that match the filtering criteria you've selected above.

    +
    + {{ subscription_form.hidden_tag() }} + {{ renderfield(subscription_form.email_frequency, style='horizlist', nolabel=true, css_class="no-left-padding") }} + {%- if g.user and g.user.email %} + + {%- else %} + {{ renderfield(subscription_form.email, autofocus=true, css_class="clearfix") }} + {%- endif %} + {{ rendersubmit([(None, "Subscribe", 'btn-primary')]) }} + +
    +
    +
    + +{% endblock %} {% block content %} {%- from "macros.html.jinja2" import stickie %} {% with gkiosk=g.kiosk, gboard=g.board, guser=g.user, gstarred_ids=g.starred_ids %} @@ -70,24 +91,6 @@ {{ filterset.description | safe }} {%- endif %} -
    -
    -
    -

    Subscribe to job alerts

    -

    We will send you alerts that match the filtering criteria you've selected above.

    -
    - {{ subscription_form.hidden_tag() }} - {{ renderfield(subscription_form.email_frequency, style='horizlist', nolabel=true) }} - {%- if g.user and g.user.email %} - - {%- else %} - {{ renderfield(subscription_form.email, autofocus=true, css_class="clearfix") }} - {%- endif %} - {{ rendersubmit([(None, "Subscribe", 'btn-primary')]) }} -
    -
    -
    -
    {%- if location and location.use_title and location.description and not paginated %}

    {{ location.use_title }}

    diff --git a/hasjob/templates/layout.html.jinja2 b/hasjob/templates/layout.html.jinja2 index 5240739ef..42a28ef7a 100644 --- a/hasjob/templates/layout.html.jinja2 +++ b/hasjob/templates/layout.html.jinja2 @@ -216,6 +216,7 @@ {% endblock %} {% block basecontent -%} + {% block subscribeform %}{% endblock %} {%- if header_campaign %}
    {% endif %}
    {% block pagecontent %}{% block content %}{% endblock %}{% endblock %} diff --git a/hasjob/templates/macros.html.jinja2 b/hasjob/templates/macros.html.jinja2 index 96f427ce4..9d736f771 100644 --- a/hasjob/templates/macros.html.jinja2 +++ b/hasjob/templates/macros.html.jinja2 @@ -118,7 +118,7 @@ {%- endif %} {%- endif %}{%- endfor %} - +

    From 1219f556398f6b5dd944429e994380db8eca58fd Mon Sep 17 00:00:00 2001 From: Vidya Ramakrishnan Date: Mon, 23 Apr 2018 14:43:50 +0530 Subject: [PATCH 17/17] Update text of subscribe form. Remove the border. --- hasjob/assets/sass/_header.sass | 9 +++------ ...css => stylesheet-app-css.4ae34a71f5396f93a5f7.css} | 9 +++------ ...1a76efe07fc5.js => app-css.4ae34a71f5396f93a5f7.js} | 0 ...ec291a76efe07fc5.js => app.4ae34a71f5396f93a5f7.js} | 0 ...a76efe07fc5.js => manifest.4ae34a71f5396f93a5f7.js} | 0 ...91a76efe07fc5.js => vendor.4ae34a71f5396f93a5f7.js} | 0 hasjob/static/build/manifest.json | 2 +- hasjob/static/service-worker.js | 10 +++++----- hasjob/templates/index.html.jinja2 | 8 ++++---- 9 files changed, 16 insertions(+), 22 deletions(-) rename hasjob/static/build/css/{stylesheet-app-css.bf0fec291a76efe07fc5.css => stylesheet-app-css.4ae34a71f5396f93a5f7.css} (99%) rename hasjob/static/build/js/{app-css.bf0fec291a76efe07fc5.js => app-css.4ae34a71f5396f93a5f7.js} (100%) rename hasjob/static/build/js/{app.bf0fec291a76efe07fc5.js => app.4ae34a71f5396f93a5f7.js} (100%) rename hasjob/static/build/js/{manifest.bf0fec291a76efe07fc5.js => manifest.4ae34a71f5396f93a5f7.js} (100%) rename hasjob/static/build/js/{vendor.bf0fec291a76efe07fc5.js => vendor.4ae34a71f5396f93a5f7.js} (100%) diff --git a/hasjob/assets/sass/_header.sass b/hasjob/assets/sass/_header.sass index 3098c9743..43cdd3eb8 100644 --- a/hasjob/assets/sass/_header.sass +++ b/hasjob/assets/sass/_header.sass @@ -355,15 +355,12 @@ header border-left-color: $color-title-has #subscribe-jb-form - .subscribeform - background-image: url("~/static/img/form-border.png") - background-position: 100% 0 - background-repeat: repeat-x - background-size: 40px 3px - .form-inline .listwidget ul input top: 6px .no-left-padding .controls padding: 0 + + .field-email + width: 100% diff --git a/hasjob/static/build/css/stylesheet-app-css.bf0fec291a76efe07fc5.css b/hasjob/static/build/css/stylesheet-app-css.4ae34a71f5396f93a5f7.css similarity index 99% rename from hasjob/static/build/css/stylesheet-app-css.bf0fec291a76efe07fc5.css rename to hasjob/static/build/css/stylesheet-app-css.4ae34a71f5396f93a5f7.css index 6e4653d2b..d0b5c8ecb 100644 --- a/hasjob/static/build/css/stylesheet-app-css.bf0fec291a76efe07fc5.css +++ b/hasjob/static/build/css/stylesheet-app-css.4ae34a71f5396f93a5f7.css @@ -302,18 +302,15 @@ header { border-top-color: #df5e0e; border-left-color: #df5e0e; } -#subscribe-jb-form .subscribeform { - background-image: url("/static/img/form-border.png"); - background-position: 100% 0; - background-repeat: repeat-x; - background-size: 40px 3px; } - #subscribe-jb-form .form-inline .listwidget ul input { top: 6px; } #subscribe-jb-form .form-inline .no-left-padding .controls { padding: 0; } +#subscribe-jb-form .form-inline .field-email { + width: 100%; } + .input-group-btn > .btn { border-bottom: 1px solid #ccc; } .input-group-btn > .btn:hover, .input-group-btn > .btn:focus, .input-group-btn > .btn:active { diff --git a/hasjob/static/build/js/app-css.bf0fec291a76efe07fc5.js b/hasjob/static/build/js/app-css.4ae34a71f5396f93a5f7.js similarity index 100% rename from hasjob/static/build/js/app-css.bf0fec291a76efe07fc5.js rename to hasjob/static/build/js/app-css.4ae34a71f5396f93a5f7.js diff --git a/hasjob/static/build/js/app.bf0fec291a76efe07fc5.js b/hasjob/static/build/js/app.4ae34a71f5396f93a5f7.js similarity index 100% rename from hasjob/static/build/js/app.bf0fec291a76efe07fc5.js rename to hasjob/static/build/js/app.4ae34a71f5396f93a5f7.js diff --git a/hasjob/static/build/js/manifest.bf0fec291a76efe07fc5.js b/hasjob/static/build/js/manifest.4ae34a71f5396f93a5f7.js similarity index 100% rename from hasjob/static/build/js/manifest.bf0fec291a76efe07fc5.js rename to hasjob/static/build/js/manifest.4ae34a71f5396f93a5f7.js diff --git a/hasjob/static/build/js/vendor.bf0fec291a76efe07fc5.js b/hasjob/static/build/js/vendor.4ae34a71f5396f93a5f7.js similarity index 100% rename from hasjob/static/build/js/vendor.bf0fec291a76efe07fc5.js rename to hasjob/static/build/js/vendor.4ae34a71f5396f93a5f7.js diff --git a/hasjob/static/build/manifest.json b/hasjob/static/build/manifest.json index 776086621..8d6570179 100644 --- a/hasjob/static/build/manifest.json +++ b/hasjob/static/build/manifest.json @@ -1 +1 @@ -{"assets":{"vendor":"js/vendor.bf0fec291a76efe07fc5.js","app-css":"css/stylesheet-app-css.bf0fec291a76efe07fc5.css","app":"js/app.bf0fec291a76efe07fc5.js","manifest":"js/manifest.bf0fec291a76efe07fc5.js"}} \ No newline at end of file +{"assets":{"vendor":"js/vendor.4ae34a71f5396f93a5f7.js","app-css":"css/stylesheet-app-css.4ae34a71f5396f93a5f7.css","app":"js/app.4ae34a71f5396f93a5f7.js","manifest":"js/manifest.4ae34a71f5396f93a5f7.js"}} \ No newline at end of file diff --git a/hasjob/static/service-worker.js b/hasjob/static/service-worker.js index 6e7b892e4..382677aa2 100644 --- a/hasjob/static/service-worker.js +++ b/hasjob/static/service-worker.js @@ -8,19 +8,19 @@ const workboxSW = new self.WorkboxSW({ workboxSW.precache([ { - "url": "/static/build/css/stylesheet-app-css.bf0fec291a76efe07fc5.css", - "revision": "3f712cf67b9405201a4075648af9e04c" + "url": "/static/build/css/stylesheet-app-css.4ae34a71f5396f93a5f7.css", + "revision": "29bf4620ef5a019af29348f4fb6b14d7" }, { - "url": "/static/build/js/app.bf0fec291a76efe07fc5.js", + "url": "/static/build/js/app.4ae34a71f5396f93a5f7.js", "revision": "114869d8de2180fa11f38b774335c944" }, { - "url": "/static/build/js/manifest.bf0fec291a76efe07fc5.js", + "url": "/static/build/js/manifest.4ae34a71f5396f93a5f7.js", "revision": "fe684bf23b9518850a7a3dd90492001d" }, { - "url": "/static/build/js/vendor.bf0fec291a76efe07fc5.js", + "url": "/static/build/js/vendor.4ae34a71f5396f93a5f7.js", "revision": "12a1ca8d2cb2caab35d21438cb595e94" } ]); diff --git a/hasjob/templates/index.html.jinja2 b/hasjob/templates/index.html.jinja2 index d0613282a..bb3564bcd 100644 --- a/hasjob/templates/index.html.jinja2 +++ b/hasjob/templates/index.html.jinja2 @@ -58,16 +58,16 @@

    Subscribe to job alerts

    -

    We will send you alerts that match the filtering criteria you've selected above.

    +

    Get an email alert when a new job matches your criteria:

    {{ subscription_form.hidden_tag() }} - {{ renderfield(subscription_form.email_frequency, style='horizlist', nolabel=true, css_class="no-left-padding") }} + {{ renderfield(subscription_form.email_frequency, style='horizlist', nolabel=true, css_class='no-left-padding') }} {%- if g.user and g.user.email %} {%- else %} - {{ renderfield(subscription_form.email, autofocus=true, css_class="clearfix") }} + {{ renderfield(subscription_form.email, autofocus=true, css_class='clearfix') }} {%- endif %} - {{ rendersubmit([(None, "Subscribe", 'btn-primary')]) }} + {{ rendersubmit([(None, "Subscribe", 'btn-primary')], style='') }}