Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Email alerts for new job posts [WIP] #434

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion hasjob/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions hasjob/forms/jobpost.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions hasjob/jobs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-

from . import job_alerts # NOQA
45 changes: 45 additions & 0 deletions hasjob/jobs/job_alerts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-

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


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():
for subscription in JobPostSubscription.get_active_subscriptions():
if subscription.has_recent_alert():
# Alert was sent recently, break out of loop
break

unseen_posts = get_unseen_posts(subscription)
if not unseen_posts:
# Nothing new to see, break out of loop
break

jobpost_alert = JobPostAlert(jobpost_subscription=subscription)
jobpost_alert.jobposts = unseen_posts

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)
try:
mail.send(msg)
jobpost_alert.register_delivery()
except Exception as exc:
jobpost_alert.register_failure(unicode(exc))
db.session.add(jobpost_alert)
db.session.commit()
22 changes: 14 additions & 8 deletions hasjob/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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"))
Expand Down Expand Up @@ -88,3 +93,4 @@ class CANDIDATE_FEEDBACK(LabeledEnum):
from .flags import *
from .campaign import *
from .filterset import *
from .jobpost_alert import *
36 changes: 36 additions & 0 deletions hasjob/models/filterset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down Expand Up @@ -52,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)
Expand All @@ -71,6 +74,39 @@ class Filterset(BaseScopedNameMixin, db.Model):
def __repr__(self):
return '<Filterset %s "%s">' % (self.board.title, self.title)

def __init__(self, **kwargs):
filters = kwargs.pop('filters') if kwargs.get('filters') else {}
super(Filterset, self).__init__(**kwargs)

if not self.title:
self.title = buid()

if filters:
if filters.get('t'):
self.types = JobType.query.filter(JobType.name.in_(filters['t'])).all()

if filters.get('c'):
self.categories = JobCategory.query.filter(JobCategory.name.in_(filters['c'])).all()

if filters.get('l'):
geonameids = []
for loc in filters.get('l'):
geonameids.append(location_geodata(loc)['geonameid'])
self.geonameids = geonameids

if getbool(filters.get('anywhere')):
self.remote_location = True

if getbool(filters.get('equity')):
self.equity = True

if filters.get('currency') and filters.get('pay'):
self.pay_currency = filters.get('currency')
self.pay_cash = filters.get('pay')

if filters.get('q'):
self.keywords = filters.get('q')

@classmethod
def get(cls, board, name):
return cls.query.filter(cls.board == board, cls.name == name).one_or_none()
Expand Down
34 changes: 17 additions & 17 deletions hasjob/models/flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
)
)

Expand All @@ -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
)
)
Expand All @@ -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
)
)
Expand All @@ -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
)
)
Expand Down Expand Up @@ -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
)
)

Expand All @@ -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
)
)
Expand All @@ -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
)
)
Expand All @@ -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
)
)
Expand Down
Loading