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/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/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..c76fcf90d --- /dev/null +++ b/hasjob/jobs/job_alerts.py @@ -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() diff --git a/hasjob/models/__init__.py b/hasjob/models/__init__.py index 034867e3e..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")) @@ -88,3 +93,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..b09838437 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'] @@ -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) @@ -71,6 +74,39 @@ class Filterset(BaseScopedNameMixin, db.Model): def __repr__(self): return '' % (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() 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/models/jobpost_alert.py b/hasjob/models/jobpost_alert.py new file mode 100644 index 000000000..ee0e0ba51 --- /dev/null +++ b/hasjob/models/jobpost_alert.py @@ -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 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/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/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 @@