-
Notifications
You must be signed in to change notification settings - Fork 23
Recurring meetup #91
base: master
Are you sure you want to change the base?
Recurring meetup #91
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
from datetime import datetime, timedelta | ||
import json | ||
import calendar | ||
|
||
from mako.template import Template | ||
from pylons.i18n import _ | ||
|
@@ -10,10 +11,10 @@ | |
from r2.lib.filters import python_websafe | ||
from r2.lib.jsonresponse import Json | ||
from r2.lib.menus import CommentSortMenu,NumCommentsMenu | ||
from r2.lib.pages import BoringPage, ShowMeetup, NewMeetup, EditMeetup, PaneStack, CommentListing, LinkInfoPage, CommentReplyBox, NotEnoughKarmaToPost | ||
from r2.models import Meetup,Link,Subreddit,CommentBuilder,PendingJob | ||
from r2.lib.pages import BoringPage, ShowMeetup, NewMeetup, EditMeetup, PaneStack, CommentListing, LinkInfoPage, CommentReplyBox, NotEnoughKarmaToPost, CancelMeetup, UnfoundPage | ||
from r2.models import Meetup,Link,Subreddit,CommentBuilder,PendingJob,Account | ||
from r2.models.listing import NestedListing | ||
from validator import validate, VUser, VModhash, VRequired, VMeetup, VEditMeetup, VFloat, ValueOrBlank, ValidIP, VMenu, VCreateMeetup, VTimestamp | ||
from validator import validate, VUser, VModhash, VRequired, VMeetup, VEditMeetup, VFloat, ValueOrBlank, ValidIP, VMenu, VCreateMeetup, VTimestamp, nop | ||
from routes.util import url_for | ||
|
||
|
||
|
@@ -29,6 +30,28 @@ def meetup_article_text(meetup): | |
def meetup_article_title(meetup): | ||
return "Meetup : %s"%meetup.title | ||
|
||
def calculate_month_interval(date): | ||
"""Calculates the number of days between a date | ||
and the same day (ie 3rd Wednesday) in the | ||
following month""" | ||
|
||
day_of_week = date.weekday() | ||
week_of_month = (date.day-1)/7 | ||
this_month = (date.year, date.month) | ||
if this_month[1] != 12: | ||
next_month = (this_month[0], this_month[1]+1) | ||
else: | ||
next_month = (this_month[0] + 1, 1) | ||
this_month = calendar.monthrange(*this_month) | ||
next_month = calendar.monthrange(*next_month) | ||
remaining_days_in_month = this_month[1] - date.day | ||
"""this line calculates the nth day of the next month, | ||
if that exceeds the number of days in that month, it | ||
backs off a week""" | ||
days_into_next_month = (day_of_week - next_month[0])%7 + (week_of_month * 7 + 1) if (day_of_week - next_month[0])%7 + week_of_month * 7 + 1 < next_month[1] else (day_of_week - next_month[0])%7 + (week_of_month-1) * 7 + 1 | ||
offset = remaining_days_in_month + days_into_next_month | ||
return offset | ||
|
||
class MeetupsController(RedditController): | ||
def response_func(self, **kw): | ||
return self.sendstring(json.dumps(kw)) | ||
|
@@ -56,21 +79,45 @@ def GET_new(self, *a, **kw): | |
latitude = VFloat('latitude', error=errors.NO_LOCATION), | ||
longitude = VFloat('longitude', error=errors.NO_LOCATION), | ||
timestamp = VTimestamp('timestamp'), | ||
tzoffset = VFloat('tzoffset', error=errors.INVALID_DATE)) | ||
def POST_create(self, res, title, description, location, latitude, longitude, timestamp, tzoffset, ip): | ||
tzoffset = VFloat('tzoffset', error=errors.INVALID_DATE), | ||
recurring = nop('recurring')) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be nice if you defined a I would make a class called |
||
def POST_create(self, res, title, description, location, latitude, longitude, timestamp, tzoffset, ip, recurring): | ||
if res._chk_error(errors.NO_TITLE): | ||
res._chk_error(errors.TITLE_TOO_LONG) | ||
res._focus('title') | ||
|
||
if recurring != 'never': | ||
try: | ||
Account._byID(c.user._id).email | ||
except: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This won't work, because the final version of the wiki-account-creation code set a default email of |
||
res._set_error(errors.CANT_RECUR) | ||
|
||
res._chk_errors((errors.NO_LOCATION, | ||
errors.NO_DESCRIPTION, | ||
errors.INVALID_DATE, | ||
errors.NO_DATE)) | ||
errors.NO_DATE, | ||
errors.CANT_RECUR)) | ||
|
||
if res.error: return | ||
|
||
meetup = self.create(c.user._id, title, description, location, latitude, longitude, timestamp, tzoffset, ip, recurring) | ||
|
||
res._redirect(url_for(action='show', id=meetup._id36)) | ||
|
||
@validate(key = nop('key')) | ||
def GET_stopmeetup(self, key): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should at least be a Can you also add additional checking? Ideally only the user who created the meetup (or an admin) should be able to stop the recurrence. Is there a page somewhere that lists a user's upcoming meetups? That would be a good place to put a "cancel recurring meetup" button, and the "your meetup is about to be reposted" email can just link to that page. |
||
try: | ||
pj = list(PendingJob._query(PendingJob.c._id == key, data=True))[0] | ||
pj._delete_from_db() | ||
return BoringPage(_("Cancel Meetup"), | ||
content=CancelMeetup()).render() | ||
except: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I worry about the catch-all |
||
return BoringPage(_("Page not found"), | ||
content=UnfoundPage()).render() | ||
|
||
def create(self, author_id, title, description, location, latitude, longitude, timestamp, tzoffset, ip, recurring): | ||
meetup = Meetup( | ||
author_id = c.user._id, | ||
author_id = author_id, | ||
|
||
title = title, | ||
description = description, | ||
|
@@ -89,22 +136,39 @@ def POST_create(self, res, title, description, location, latitude, longitude, ti | |
meetup._commit() | ||
|
||
l = Link._submit(meetup_article_title(meetup), meetup_article_text(meetup), | ||
c.user, Subreddit._by_name('discussion'),ip, []) | ||
Account._byID(author_id, data=True), Subreddit._by_name('discussion'),ip, []) | ||
|
||
l.meetup = meetup._id36 | ||
l._commit() | ||
meetup.assoc_link = l._id | ||
meetup._commit() | ||
|
||
when = datetime.now(g.tz) + timedelta(0, 3600) # Leave a short window of time before notification, in case | ||
when = datetime.now(g.tz) + timedelta(0, 60) # Leave a short window of time before notification, in case | ||
# the meetup is edited/deleted soon after its creation | ||
PendingJob.store(when, 'process_new_meetup', {'meetup_id': meetup._id}) | ||
|
||
if recurring != 'never': | ||
if recurring == 'weekly': | ||
offset = 7 | ||
elif recurring == 'biweekly': | ||
offset = 14 | ||
elif recurring == 'monthly': | ||
offset = calculate_month_interval(meetup.datetime()) | ||
|
||
data = {'author_id':author_id,'title':title,'description':description,'location':location, | ||
'latitude':latitude,'longitude':longitude,'timestamp':timestamp+offset*86400, | ||
'tzoffset':tzoffset,'ip':ip,'recurring':recurring} | ||
|
||
when = datetime.now(g.tz) + timedelta(offset-15) | ||
|
||
PendingJob.store(when, 'meetup_repost_emailer', data) | ||
|
||
|
||
#update the queries | ||
if g.write_query_queue: | ||
queries.new_link(l) | ||
|
||
res._redirect(url_for(action='show', id=meetup._id36)) | ||
return meetup | ||
|
||
@Json | ||
@validate(VUser(), | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,7 +22,7 @@ | |
from email.MIMEText import MIMEText | ||
from pylons.i18n import _ | ||
from pylons import c, g, request | ||
from r2.lib.pages import PasswordReset, MeetupNotification, Share, Mail_Opt, EmailVerify | ||
from r2.lib.pages import PasswordReset, MeetupNotification, Share, Mail_Opt, EmailVerify, RepostEmail | ||
from r2.lib.utils import timeago | ||
from r2.models import passhash, Email, Default, has_opted_out | ||
from r2.config import cache | ||
|
@@ -56,6 +56,12 @@ def password_email(user): | |
'lesswrong.com password reset', | ||
PasswordReset(user=user, passlink=passlink).render(style='email')) | ||
|
||
def repost_email(user, pendingjob): | ||
stoplink = 'http://' + g.domain + '/cancelmeetup/' + str(pendingjob) | ||
simple_email(user.email, '[email protected]', | ||
'lesswrong.com meetup repost', | ||
RepostEmail(user=user, stoplink=stoplink).render(style='email')) | ||
|
||
def confirmation_email(user): | ||
simple_email(user.email, '[email protected]', | ||
'lesswrong.com email verification', | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -85,6 +85,7 @@ | |
('NOT_ENOUGH_KARMA', _('You do not have enough karma')), | ||
('BAD_POLL_SYNTAX', _('Error in poll syntax')), | ||
('BAD_POLL_BALLOT', _('Error in poll ballot')), | ||
('CANT_RECUR', _('You need to register an email address to create a recurring meetup')) | ||
)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We seem to be adding a lot of features that need the user to have a verified email. I think |
||
errors = Storage([(e, e) for e in error_list.keys()]) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -695,15 +695,20 @@ def _commit(self, *a, **kw): | |
Thing._commit(self, *a, **kw) | ||
|
||
if should_invalidate: | ||
g.rendercache.delete('side-posts' + '-' + c.site.name) | ||
g.rendercache.delete('side-comments' + '-' + c.site.name) | ||
try: | ||
name = c.site.name | ||
except: | ||
name = Subreddit._byID(self.sr_id).name | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please investigate here also and find out what type of exception you're expecting to catch. |
||
|
||
g.rendercache.delete('side-posts' + '-' + name) | ||
g.rendercache.delete('side-comments' + '-' + name) | ||
tags = self.tag_names() | ||
if 'open_thread' in tags: | ||
g.rendercache.delete('side-open' + '-' + c.site.name) | ||
g.rendercache.delete('side-open' + '-' + name) | ||
if 'quotes' in tags: | ||
g.rendercache.delete('side-quote' + '-' + c.site.name) | ||
g.rendercache.delete('side-quote' + '-' + name) | ||
if 'group_rationality_diary' in tags: | ||
g.rendercache.delete('side-diary' + '-' + c.site.name) | ||
g.rendercache.delete('side-diary' + '-' + name) | ||
|
||
# Note that there are no instances of PromotedLink or LinkCompressed, | ||
# so overriding their methods here will not change their behaviour | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
## The contents of this file are subject to the Common Public Attribution | ||
## License Version 1.0. (the "License"); you may not use this file except in | ||
## compliance with the License. You may obtain a copy of the License at | ||
## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public | ||
## License Version 1.1, but Sections 14 and 15 have been added to cover use of | ||
## software over a computer network and provide for limited attribution for the | ||
## Original Developer. In addition, Exhibit A has been modified to be consistent | ||
## with Exhibit B. | ||
## | ||
## Software distributed under the License is distributed on an "AS IS" basis, | ||
## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for | ||
## the specific language governing rights and limitations under the License. | ||
## | ||
## The Original Code is Reddit. | ||
## | ||
## The Original Developer is the Initial Developer. The Initial Developer of | ||
## the Original Code is CondeNet, Inc. | ||
## | ||
## All portions of the code written by CondeNet are Copyright (c) 2006-2008 | ||
## CondeNet, Inc. All Rights Reserved. | ||
################################################################################ | ||
|
||
<h2>Your meetup has been canceled.<h2> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
<%! | ||
from r2.lib.template_helpers import static | ||
from r2.lib.utils import usformat | ||
from routes.util import url_for | ||
%> | ||
<%namespace name="utils" file="utils.html" import="error_field, submit_form"/> | ||
|
||
<%utils:submit_form action="/meetups/create" onsubmit="return post_form(this, 'meetups/create', null, null, true, '/')" _id="newmeetup" _class="meetup" tzoffset="${thing.tzoffset}" latitude="${thing.latitude}" longitude="${thing.longitude}"> | ||
<h1>New Meetup</h1> | ||
${form_fields()} | ||
</%utils:submit_form> | ||
|
||
<script src="${static('meetups.js')}" type="text/javascript" charset="utf-8"></script> | ||
|
||
<%def name="form_fields()"> | ||
<fieldset> | ||
<label for="title">${_("Title")}:</label> | ||
<input id="title" name="title" value="${thing.title}" onfocus="clearTitle(this)" type="text" /> | ||
${error_field("NO_TITLE", "span")} | ||
${error_field("TITLE_TOO_LONG", "span")} | ||
</fieldset> | ||
|
||
<fieldset> | ||
<label for="location">${_("Location")}:</label> | ||
<input id="location" name="location" value="${thing.location}" type="text" /> | ||
${error_field("NO_LOCATION", "p", cls="form-info-line")} | ||
</fieldset> | ||
|
||
<fieldset> | ||
<label for="description">${_("Description")}:</label> | ||
<textarea id="description" name="description" rows="5" cols="35"> | ||
${thing.description} | ||
</textarea> | ||
${error_field("NO_DESCRIPTION", "span")} | ||
</fieldset> | ||
|
||
<fieldset> | ||
<label for="date">${_("Date")}:</label> | ||
<input id="date" class="date" name="timestamp" value="${thing.timestamp}" type="text" /> | ||
${error_field("NO_DATE", "span")} | ||
${error_field("INVALID_DATE", "span")} | ||
</fieldset> | ||
|
||
<button class="btn" type="submit">${_("Submit Meetup")}</button> | ||
<span id="status" class="error"></span> | ||
</%def> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
## The contents of this file are subject to the Common Public Attribution | ||
## License Version 1.0. (the "License"); you may not use this file except in | ||
## compliance with the License. You may obtain a copy of the License at | ||
## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public | ||
## License Version 1.1, but Sections 14 and 15 have been added to cover use of | ||
## software over a computer network and provide for limited attribution for the | ||
## Original Developer. In addition, Exhibit A has been modified to be consistent | ||
## with Exhibit B. | ||
## | ||
## Software distributed under the License is distributed on an "AS IS" basis, | ||
## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for | ||
## the specific language governing rights and limitations under the License. | ||
## | ||
## The Original Code is Reddit. | ||
## | ||
## The Original Developer is the Initial Developer. The Initial Developer of | ||
## the Original Code is CondeNet, Inc. | ||
## | ||
## All portions of the code written by CondeNet are Copyright (c) 2006-2008 | ||
## CondeNet, Inc. All Rights Reserved. | ||
################################################################################ | ||
|
||
Hello ${thing.user.name}: | ||
|
||
Your recurring meetup is about to repost, to take place 2 weeks from now. | ||
|
||
If you would like to cancel your recurring meetup please go to: ${thing.stoplink}. | ||
|
||
Thank you for hosting meetups! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like having to write and maintain date/time code if I don't have to: it's always really fiddly and yuck. I feel like this sort of thing should exist in a library somewhere - maybe dateutil?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possibly something from here? http://labix.org/python-dateutil#head-ba5ffd4df8111d1b83fc194b97ebecf837add454