diff --git a/alembic/versions/11fb55be6e62_added_campaign_model.py b/alembic/versions/11fb55be6e62_added_campaign_model.py new file mode 100644 index 0000000..fbdd7fa --- /dev/null +++ b/alembic/versions/11fb55be6e62_added_campaign_model.py @@ -0,0 +1,56 @@ +"""Added campaign model + +Revision ID: 11fb55be6e62 +Revises: 155bdd6d893d +Create Date: 2013-12-23 15:37:54.560976 + +""" + +# revision identifiers, used by Alembic. +revision = '11fb55be6e62' +down_revision = '155bdd6d893d' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.add_column('user', sa.Column('send_newsletter', sa.Boolean(), nullable=False, server_default=sa.text(u"'t'"))) + + op.alter_column('user', 'send_newsletter', server_default=None) + + # Create a new table for email_campaign + op.create_table('email_campaign', + sa.Column('id', sa.Integer, nullable=False, primary_key=True), + sa.Column('event_id', sa.Integer, nullable=False), + sa.Column('status', sa.Integer, nullable=False), + sa.Column('start_datetime', sa.DateTime, nullable=False), + sa.Column('end_datetime', sa.DateTime, nullable=True), + sa.Column('name', sa.Unicode(250), nullable=False, unique=True), + sa.Column('title', sa.Unicode(250), nullable=False), + sa.Column('created_at', sa.DateTime, nullable=False), + sa.Column('updated_at', sa.DateTime, nullable=False), + ) + op.create_foreign_key("fk_email_campaign_event_id", "email_campaign", "event", ["event_id"], ["id"], ondelete="CASCADE") + + # Create a new table for email_campaign_user + op.create_table('email_campaign_user', + sa.Column('id', sa.Integer, nullable=False, primary_key=True), + sa.Column('user_id', sa.Integer, nullable=False), + sa.Column('email_campaign_id', sa.Integer, nullable=False), + sa.Column('created_at', sa.DateTime, nullable=False), + sa.Column('updated_at', sa.DateTime, nullable=False), + ) + op.create_foreign_key("fk_email_campaign_user_user_id", "email_campaign_user", "user", ["user_id"], ["id"], ondelete="CASCADE") + op.create_foreign_key("fk_email_campaign_user_email_campaign_id", "email_campaign_user", "email_campaign", ["email_campaign_id"], ["id"], ondelete="CASCADE") + + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_column('user', 'send_newsletter') + op.drop_table('email_campaign_user') + op.drop_table('email_campaign') + ### end Alembic commands ### diff --git a/hacknight/forms/profile.py b/hacknight/forms/profile.py index 7217874..10169aa 100644 --- a/hacknight/forms/profile.py +++ b/hacknight/forms/profile.py @@ -3,10 +3,14 @@ import wtforms from baseframe.forms import Form, RichTextField -__all__ = ['ProfileForm'] +__all__ = ['ProfileForm', 'NewsLetterForm'] class ProfileForm(Form): type = wtforms.SelectField(u"Profile type", coerce=int, validators=[wtforms.validators.Required()]) description = RichTextField(u"Description/Bio", content_css="/static/css/editor.css") + + +class NewsLetterForm(Form): + send_newsletter = wtforms.BooleanField("Receive NewsLetter", description="Do you like to receive notification about new hacknight?") diff --git a/hacknight/models/__init__.py b/hacknight/models/__init__.py index daebcb6..76f7aeb 100644 --- a/hacknight/models/__init__.py +++ b/hacknight/models/__init__.py @@ -12,3 +12,4 @@ from hacknight.models.project import * from hacknight.models.participant import * from hacknight.models.sponsor import * +from hacknight.models.campaign import * diff --git a/hacknight/models/campaign.py b/hacknight/models/campaign.py new file mode 100644 index 0000000..dc12095 --- /dev/null +++ b/hacknight/models/campaign.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- + +import datetime +from hacknight.models import db, BaseNameMixin, BaseMixin, Event, User + + +class EMAIL_CAMPAIGN_STATUS: + COMPLETED = 1 + PROGRESS = 2 + + +class EmailCampaign(BaseNameMixin, db.Model): + __tablename__ = "email_campaign" + + event_id = db.Column(None, db.ForeignKey('event.id'), nullable=False) + event = db.relationship(Event) + start_datetime = db.Column(db.DateTime, default=datetime.datetime.utcnow, nullable=False) + end_datetime = db.Column(db.DateTime, nullable=True) + status = db.Column(db.Integer, default=EMAIL_CAMPAIGN_STATUS.PROGRESS, nullable=False) + + @classmethod + def get(cls, event): + return cls.query.filter_by(event=event).first() + + @classmethod + def sent_for(cls, event): + email_campaign = cls.get(event=event) + if email_campaign: + if email_campaign.status == EMAIL_CAMPAIGN_STATUS.COMPLETED: + return True + return False + + def yet_to_send(self): + return set(User.subscribed_to_newsletter()) - set([user.user for user in self.users]) + + +class EmailCampaignUser(BaseMixin, db.Model): + __tablename__ = "email_campaign_user" + + user_id = db.Column(None, db.ForeignKey('user.id'), nullable=False) + user = db.relationship(User) + email_campaign_id = db.Column(None, db.ForeignKey('email_campaign.id'), nullable=False) + email_campaign = db.relationship(EmailCampaign, backref=db.backref('users', cascade='all, delete-orphan')) diff --git a/hacknight/models/event.py b/hacknight/models/event.py index 37093ff..0b6182c 100644 --- a/hacknight/models/event.py +++ b/hacknight/models/event.py @@ -64,6 +64,10 @@ def url_for(self, action='view', _external=True): return url_for('profile_view', profile=self.name, _external=_external) elif action == 'new-event': return url_for('event_new', profile=self.name, _external=_external) + elif action == 'settings': + return url_for('profile_settings', profile=self.name, _external=_external) + elif action == 'unsubscribe': + return url_for('profile_settings', action='unsubscribe', profile=self.name, _external=_external) class Event(BaseScopedNameMixin, db.Model): diff --git a/hacknight/models/user.py b/hacknight/models/user.py index b757a22..d8a6be5 100644 --- a/hacknight/models/user.py +++ b/hacknight/models/user.py @@ -14,6 +14,7 @@ class User(UserBase, db.Model): phone_no = db.Column(db.Unicode(15), default=u'', nullable=True) job_title = db.Column(db.Unicode(120), default=u'', nullable=True) company = db.Column(db.Unicode(1200), default=u'', nullable=True) + send_newsletter = db.Column(db.Boolean, default=True, nullable=False) @property def profile_url(self): @@ -28,6 +29,10 @@ def profiles(self): return [self.profile] + Profile.query.filter( Profile.userid.in_(self.organizations_owned_ids())).order_by('title').all() + @classmethod + def subscribed_to_newsletter(cls): + return cls.query.filter_by(send_newsletter=True).all() + def projects_in(self, event): return [member.project for member in self.project_memberships if member.project.event == event] diff --git a/hacknight/templates/send_newsletter.html b/hacknight/templates/send_newsletter.html new file mode 100644 index 0000000..544f4a8 --- /dev/null +++ b/hacknight/templates/send_newsletter.html @@ -0,0 +1,7 @@ +

+ You are receiving this mail because you registered for Hacknight newsletter. + Unsubscribe
+

{{ event.title }}

+ {{ event.description|safe }}
+ Join Hacknight +

diff --git a/hacknight/views/profile.py b/hacknight/views/profile.py index fbf821a..d19e92d 100644 --- a/hacknight/views/profile.py +++ b/hacknight/views/profile.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- -from flask import render_template, g, abort, flash +from flask import render_template, g, abort, flash, request from coaster.views import load_model from baseframe.forms import render_redirect, render_form from hacknight import app -from hacknight.models import db, Profile, User, Event +from hacknight.models import db, Profile, User, Event, PROFILE_TYPE from hacknight.models.event import profile_types -from hacknight.forms.profile import ProfileForm +from hacknight.forms.profile import ProfileForm, NewsLetterForm from hacknight.views.login import lastuser from hacknight.models.participant import Participant @@ -46,3 +46,23 @@ def profile_edit(profile): return render_redirect(profile.url_for(), code=303) return render_form(form=form, title=u"Edit profile", submit=u"Save", cancel_url=profile.url_for(), ajax=True) + + +@app.route('//settings', methods=['POST', 'GET']) +@lastuser.requires_login +@load_model(Profile, {'name': 'profile'}, 'profile') +def profile_settings(profile): + user = g.user + if not user.profile == profile: + return render_redirect(user.profile.url_for('settings')) + form = NewsLetterForm(obj=user) + action = request.args.get('action') + if action == "unsubscribe": + form.send_newsletter.data = False + if form.validate_on_submit(): + form.populate_obj(user) + db.session.commit() + flash(u"Newsletter preference for '{fullname}' is saved".format(fullname=user.fullname), 'success') + return render_redirect(profile.url_for(), code=303) + return render_form(form=form, title=u"Settings", submit=u"Save", + cancel_url=profile.url_for(), ajax=False) diff --git a/send_newsletter.py b/send_newsletter.py new file mode 100644 index 0000000..9c0d7c3 --- /dev/null +++ b/send_newsletter.py @@ -0,0 +1,92 @@ +#! /usr/bin/env python + +import datetime +import sys +import logging + +from jinja2 import Environment, PackageLoader, TemplateNotFound +from html2text import html2text + +from hacknight import app, init_for +from hacknight.models import EmailCampaign, EmailCampaignUser, EMAIL_CAMPAIGN_STATUS, Event, User, db +from hacknight.views.event import send_email + + +formatter = logging.Formatter(u'%(asctime)s - %(name)s - %(levelname)s - %(message)s') + +logger = logging.getLogger('send_newsletter') +logger.setLevel(logging.INFO) + +fh = logging.FileHandler('send_newsletter.log') +fh.setLevel(logging.INFO) +fh.setFormatter(formatter) +logger.addHandler(fh) + + +# Jinja2 template for newsletter +env = Environment(loader=PackageLoader('hacknight', 'templates')) + + +def get_template(name='send_newsletter.html'): + try: + template = env.get_template(name) + return template + except TemplateNotFound, e: + logger.error(e) + return None + + +def send_emails(event, email_campaign): + sender = User.query.filter_by(userid=event.profile.userid).first() + # We need request context to generate event url. + ctx = app.test_request_context('/') + ctx.push() + count = 0 + for user in email_campaign.yet_to_send(): + if user.email: + subject = u"New Hacknight {0}".format(event.title) + template = get_template() + html = template.render(user=user, event=event) + if html: + text = html2text(html) + send_email(sender=(sender.fullname, sender.email), to=user.email, + subject=subject, body=text, html=html) + email_campaign_user = EmailCampaignUser(user=user, email_campaign=email_campaign) + db.session.add(email_campaign_user) + db.session.commit() + count += 1 + else: + logger.error(u"No HMTL found for {title}.".format(event.title)) + break + logger.info(u"Email campaign completed for {0} users.".format(count)) + ctx.pop() + + +def main(): + try: + future_events = Event.upcoming_events() + for event in future_events: + email_campaign = EmailCampaign.get(event) + if not EmailCampaign.sent_for(event): + email_campaign = EmailCampaign.get(event) + if not email_campaign: + name = u'-'.join(["Newsletter campaign", event.title]) + start_datetime = datetime.datetime.now() + email_campaign = EmailCampaign(name=name, title=name, start_datetime=start_datetime, event=event) + db.session.add(email_campaign) + db.session.commit() + send_emails(event, email_campaign) + email_campaign.end_datetime = datetime.datetime.now() + email_campaign.status = EMAIL_CAMPAIGN_STATUS.COMPLETED + db.session.commit() + except Exception, e: + logger.exception(e) + + +if __name__ == "__main__": + if len(sys.argv) >= 2: + init_for(sys.argv[1]) + main() + else: + print("Missing parameter") + print("Syntax: python send_newsletter.py [development|production]")