-
Notifications
You must be signed in to change notification settings - Fork 80
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'jobalerts' into master-jobalerts
- Loading branch information
Showing
16 changed files
with
427 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
from . import job_alerts # NOQA |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
# -*- 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 | ||
|
||
if not subscription.is_right_time_to_send_alert(): | ||
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
from datetime import datetime, timedelta | ||
from coaster.sqlalchemy import StateManager | ||
from ..utils import random_long_key | ||
from . import db, BaseMixin, LabeledEnum, User, AnonUser | ||
|
||
__all__ = ['JobPostSubscription', 'JobPostAlert', 'jobpost_alert_table'] | ||
|
||
|
||
class EMAIL_FREQUENCY(LabeledEnum): | ||
DAILY = (1, 'Daily') | ||
WEEKLY = (7, 'Weekly') | ||
|
||
|
||
class JobPostSubscription(BaseMixin, db.Model): | ||
__tablename__ = 'jobpost_subscription' | ||
|
||
filterset_id = db.Column(None, db.ForeignKey('filterset.id'), nullable=False) | ||
filterset = db.relationship('Filterset', backref=db.backref('subscriptions', lazy='dynamic')) | ||
email = db.Column(db.Unicode(254), nullable=True) | ||
user_id = db.Column(None, db.ForeignKey('user.id'), nullable=True, index=True) | ||
user = db.relationship(User) | ||
anon_user_id = db.Column(None, db.ForeignKey('anon_user.id'), nullable=True, index=True) | ||
anon_user = db.relationship(AnonUser) | ||
|
||
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) | ||
subscribed_at = db.Column(db.DateTime, nullable=False, default=db.func.utcnow()) | ||
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") | ||
|
||
__table_args__ = (db.UniqueConstraint('filterset_id', 'email'), | ||
db.CheckConstraint( | ||
db.case([(user_id != None, 1)], else_=0) + db.case([(anon_user_id != None, 1)], else_=0) <= 1, # NOQA | ||
name='jobpost_subscription_user_id_or_anon_user_id')) | ||
|
||
@classmethod | ||
def get(cls, filterset, email): | ||
return cls.query.filter(cls.filterset == filterset, cls.email == email).one_or_none() | ||
|
||
def verify_email(self): | ||
self.active = True | ||
self.email_verified_at = db.func.utcnow() | ||
|
||
def unsubscribe(self): | ||
self.active = False | ||
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 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() | ||
|
||
def is_right_time_to_send_alert(self): | ||
""" | ||
Checks if it's the right time to send this subscriber an alert. | ||
For now, the time at which the subscription was initiated is taken as the "preferred time" and | ||
uses a tolerance of 30 minutes | ||
""" | ||
return ((datetime.utcnow() - self.subscribed_at.time()).total_seconds()/60) <= 30 | ||
|
||
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=True) | ||
failed_at = db.Column(db.DateTime, nullable=True) | ||
fail_reason = db.Column(db.Unicode(255), nullable=True) | ||
|
||
def register_delivery(self): | ||
self.sent_at = db.func.utcnow() | ||
|
||
def register_failure(self, fail_reason): | ||
self.failed_at = db.func.utcnow() | ||
self.fail_reason = fail_reason |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{% extends "inc/email_layout_lite.html.jinja2" %} | ||
|
||
{% block content %} | ||
<div itemscope itemtype="http://schema.org/EmailMessage"> | ||
<div itemprop="action" itemscope itemtype="http://schema.org/ViewAction"> | ||
<meta itemprop="name" content="Confirm your email for job alerts"/> | ||
</div> | ||
<meta itemprop="description" content="Job alerts"/> | ||
<div itemprop="publisher" itemscope itemtype="http://schema.org/Organization"> | ||
<meta itemprop="name" content="Hasjob"/> | ||
<link itemprop="url" href="{{ url_for('index', subdomain=none, _external=true) }}"/> | ||
</div> | ||
</div> | ||
Please click <a href="{{url_for('confirm_subscription_to_job_alerts', token=token, _external=True)}}">here</a> to confirm your subscription. | ||
<hr> | ||
<p><a href="{{ url_for('index', subdomain=none, _external=true) }}">Hasjob</a> is a service of <a href="https://hasgeek.com/">HasGeek</a>. Write to us at <a href="mailto:{{ config['SUPPORT_EMAIL'] }}">{{ config['SUPPORT_EMAIL'] }}</a> if you have suggestions or questions on this service.</p> | ||
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
{% extends "inc/email_layout_lite.html.jinja2" %} | ||
|
||
{% block content %} | ||
<div itemscope itemtype="http://schema.org/EmailMessage"> | ||
<div itemprop="action" itemscope itemtype="http://schema.org/ViewAction"> | ||
<meta itemprop="name" content="Job Posts"/> | ||
</div> | ||
<meta itemprop="description" content="Job alerts"/> | ||
<div itemprop="publisher" itemscope itemtype="http://schema.org/Organization"> | ||
<meta itemprop="name" content="Hasjob"/> | ||
<link itemprop="url" href="{{ url_for('index', subdomain=none, _external=true) }}"/> | ||
</div> | ||
</div> | ||
<ul> | ||
{%- for post in posts %} | ||
<li><a href={{post.url_for(_external=True)}}>{{post.headline}}</a></li> | ||
{%- endfor %} | ||
</ul> | ||
<hr> | ||
<p><a href="{{ url_for('index', subdomain=none, _external=true) }}">Hasjob</a> is a service of <a href="https://hasgeek.com/">HasGeek</a>. Write to us at <a href="mailto:{{ config['SUPPORT_EMAIL'] }}">{{ config['SUPPORT_EMAIL'] }}</a> if you have suggestions or questions on this service.</p> | ||
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.