Merge lp:~acoconut/systers/flexible_essays into lp:systers

Proposed by Ana Cutillas
Status: Superseded
Proposed branch: lp:~acoconut/systers/flexible_essays
Merge into: lp:systers
Diff against target: 286 lines (+131/-28)
5 files modified
Mailman/Cgi/admindb.py (+10/-7)
Mailman/Cgi/listinfo.py (+20/-7)
Mailman/Cgi/subscribe.py (+46/-7)
Mailman/DlistUtils.py (+54/-1)
bin/rmlist (+1/-6)
To merge this branch: bzr merge lp:~acoconut/systers/flexible_essays
Reviewer Review Type Date Requested Status
Robin J Pending
beachbrake Pending
Review via email: mp+118224@code.launchpad.net

Description of the change

I added the functionality to show the pending subscriptions with the new types of essays (in admindb.py)

I fixed the bug found when users tried to write an answer longer than what the database holds (and changed the database schema so it holds up to 1500 characters for the essay answers)

I fixed what happens when a user tries to subscribe twice while their application is being reviewed by making changes to the storm function and counting the number of answers with that question ID and email address. We will have to go through this approach again when we have different form versions, because it will probably not work. I also had to modify subscribe.py, check the return value of the function that writes to the database and give an appropriate error message.

To post a comment you must log in.
Revision history for this message
Robin J (robin-jeffries) wrote :

Ahh, now I understand why we can never make sense of the order the questions come in. Could you change it so they are ordered by subscribe time? (maybe we have to add that field? Or does the database know when the item is entered?) I have always considered the ordering to be random, and when you have "saved" some items to process later, they get reordered and you can't tell the old ones from the new ones.

Also change the comment about modifying this to add essays, since it's not an essay any more. Something like "the answers to the questions asked on the subscribe page?

I think you need to rethink the error checking in subscribe.py First, it's going to confuse the next person who reads your code when you say if len(answer)<1500, since that only applies to text boxes - a checkbox can't be that big. So a branch that has different logic for textboxes than checkboxes will make it clearer. Also, this error message

 results.append(_('At least one of your answers is too long. Your answers must be less than 1500 characters long.'))

is not very use friendly. Tell them which question is too long. You already have info about the questions -- use the long text. (Suppose there are 2 checkboxes and one is under 1500 chars, but not by a lot and the other is over. That will drive the subscriber crazy trying to figure out which ones needs to change)

For results = _("""There is a pending subscription with this email.""")

how about adding "You may not subscribe a second time. The moderator will contact you if there are any questions about the answers you gave originally."

Can you look up and see if there is a way to set a given change (e.g., one that has been approved) as the baseline, so that I can look at only new changes. It's hard to review this stuff when the diffs don't give me much information about what you changed since I last looked at it.

Revision history for this message
Ana Cutillas (acoconut) wrote :

On Mon, Aug 6, 2012 at 3:23 AM, Robin J <email address hidden> wrote:

> Ahh, now I understand why we can never make sense of the order the
> questions come in. Could you change it so they are ordered by subscribe
> time? (maybe we have to add that field? Or does the database know when
> the item is entered?) I have always considered the ordering to be random,
> and when you have "saved" some items to process later, they get reordered
> and you can't tell the old ones from the new ones.
>

I don't understand what you mean here...

Right now the database stores the form_question_id (PK), the answer, the
email, the question version, the question id (these look like "talk",
"woman", etc.) and the status (accepted, pending, rejected).

And they are stored in the database in the same order they have in the
form. So I think the cgi module puts the answers in a list in the same
order they have in the form. Then, I read them in subscribe.py and put them
in a new list, in the same order. And then, a function in DlistUtils.py is
called for every answer (still in order) and this functions stores the
answers in the database. I have a few mock subscriptions in the database
and they all have the same order.

> Also change the comment about modifying this to add essays, since it's not
> an essay any more. Something like "the answers to the questions asked on
> the subscribe page?
>
> I think you need to rethink the error checking in subscribe.py First,
> it's going to confuse the next person who reads your code when you say if
> len(answer)<1500, since that only applies to text boxes - a checkbox can't
> be that big. So a branch that has different logic for textboxes than
> checkboxes will make it clearer. Also, this error message
>
> results.append(_('At least one of your answers is too long. Your answers
> must be less than 1500 characters long.'))
>
> is not very use friendly. Tell them which question is too long. You
> already have info about the questions -- use the long text. (Suppose there
> are 2 checkboxes and one is under 1500 chars, but not by a lot and the
> other is over. That will drive the subscriber crazy trying to figure out
> which ones needs to change)
>
> For results = _("""There is a pending subscription with this email.""")
>
> how about adding "You may not subscribe a second time. The moderator will
> contact you if there are any questions about the answers you gave
> originally."
>
> Can you look up and see if there is a way to set a given change (e.g., one
> that has been approved) as the baseline, so that I can look at only new
> changes. It's hard to review this stuff when the diffs don't give me much
> information about what you changed since I last looked at it.
>

Yeah, it's hard for me to understand what part of the code you're talking
about too. I keep clicking resubmit proposal, maybe I should just send a
new one? I'll give it a try and let's see what that gives us.

I think I fixed everything you said.

> --
> https://code.launchpad.net/~acoconut/systers/flexible_essays/+merge/118224
> You are the owner of lp:~acoconut/systers/flexible_essays.
>

lp:~acoconut/systers/flexible_essays updated
97. By Ana Cutillas <email address hidden>

Made the changes suggested by Robin: concerning error messages and
asserting the size of the answers.

Added the code to mark answers as accepted in the database when a user
is accepted.

Revision history for this message
Robin J (robin-jeffries) wrote :

On Tue, Aug 7, 2012 at 3:11 AM, Ana Cutillas <email address hidden> wrote:

> On Mon, Aug 6, 2012 at 3:23 AM, Robin J <email address hidden> wrote:
>
> > Ahh, now I understand why we can never make sense of the order the
> > questions come in. Could you change it so they are ordered by subscribe
> > time? (maybe we have to add that field? Or does the database know when
> > the item is entered?) I have always considered the ordering to be
> random,
> > and when you have "saved" some items to process later, they get reordered
> > and you can't tell the old ones from the new ones.
> >
>
> I don't understand what you mean here...
>

In admindb.py, there is the comment
  # Alphabetical order by email address

This is how the information is shown on the admindb page. So I'm not
talking about the order of the columns but the order of the rows.

It looks like the issue is that the key is the email address (see the sort
statement a bit lower). You would need to keep track of an approximate
submission time and sort by that. Don't worry about that right now, but
put it on your list of things to do after GSOC is over.

lp:~acoconut/systers/flexible_essays updated
98. By Ana Cutillas <email address hidden>

Changed the logic in the checking of answers in subscribe.py
Cleaned up DlistUtils.py

99. By Ana Cutillas <email address hidden>

Added support for setting answers to rejected in the database.

Unmerged revisions

99. By Ana Cutillas <email address hidden>

Added support for setting answers to rejected in the database.

98. By Ana Cutillas <email address hidden>

Changed the logic in the checking of answers in subscribe.py
Cleaned up DlistUtils.py

97. By Ana Cutillas <email address hidden>

Made the changes suggested by Robin: concerning error messages and
asserting the size of the answers.

Added the code to mark answers as accepted in the database when a user
is accepted.

96. By Ana Cutillas <email address hidden>

Added the error message saying that the user hasn't been subscribe to
the list when a user tries to subscribe with a pending subscription.

95. By Ana Cutillas <email address hidden>

Added the functionality to check that the answers aren't longer than
what the database can hold.

94. By Ana Cutillas <email address hidden>

Added changes to show the new essays in the pending subscriptions table.

Handle the possibility of a user trying to subscribe twice before the
subscription is reviewed.

93. By Ana Cutillas <email address hidden>

Removed the unused function get_answer from DlistUtils.py

92. By Ana Cutillas <email address hidden>

Cleaned up DlistUtils and changed the error message in subscribe. Added
comments.

91. By Ana Cutillas <email address hidden>

Cleaned up.

90. By Ana Cutillas <email address hidden>

Cleaned up listinfo.py

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'Mailman/Cgi/admindb.py'
--- Mailman/Cgi/admindb.py 2009-07-31 10:49:01 +0000
+++ Mailman/Cgi/admindb.py 2012-08-07 10:41:20 +0000
@@ -285,11 +285,13 @@
285 form.AddItem(Center(Header(2, _('Subscription Requests'))))285 form.AddItem(Center(Header(2, _('Subscription Requests'))))
286 table = Table(border=2)286 table = Table(border=2)
287 # The Essay is there to support subscription with essay, along with below references to "essay"287 # The Essay is there to support subscription with essay, along with below references to "essay"
288 table.AddRow([Center(Bold(_('Address/name'))),288 table_headers = ([Center(Bold(_('Address/name'))),
289 Center(Bold(_('Your decision'))),289 Center(Bold(_('Your decision'))),
290 Center(Bold(_('Reason for refusal'))),290 Center(Bold(_('Reason for refusal')))
291 Center(Bold(_('Essay')))
292 ])291 ])
292 for question in mlist.questions:
293 table_headers.append(Center(Bold(question[0])))
294 table.AddRow(table_headers)
293 # Alphabetical order by email address295 # Alphabetical order by email address
294 byaddrs = {}296 byaddrs = {}
295 for id in pendingsubs:297 for id in pendingsubs:
@@ -321,11 +323,12 @@
321 # While the address may be a unicode, it must be ascii323 # While the address may be a unicode, it must be ascii
322 paddr = addr.encode('us-ascii', 'replace')324 paddr = addr.encode('us-ascii', 'replace')
323 # Added by Systers to support essay feature. Lets the list admin see the essay the subscriber has entered.325 # Added by Systers to support essay feature. Lets the list admin see the essay the subscriber has entered.
324 table.AddRow(['%s<br><em>%s</em>' % (paddr, Utils.websafe(fullname)),326 content_row = (['%s<br><em>%s</em>' % (paddr, Utils.websafe(fullname)),
325 radio,327 radio,
326 TextBox('comment-%d' % id, size=40),328 TextBox('comment-%d' % id, size=40)])
327 essay329 for answer in essay:
328 ])330 content_row.append(answer[1])
331 table.AddRow(content_row)
329 num += 1332 num += 1
330 if num > 0:333 if num > 0:
331 form.AddItem(table)334 form.AddItem(table)
332335
=== modified file 'Mailman/Cgi/listinfo.py'
--- Mailman/Cgi/listinfo.py 2009-08-14 15:41:17 +0000
+++ Mailman/Cgi/listinfo.py 2012-08-07 10:41:20 +0000
@@ -22,6 +22,7 @@
22import os22import os
23import cgi23import cgi
2424
25from Mailman import DlistUtils
25from Mailman import mm_cfg26from Mailman import mm_cfg
26from Mailman import Utils27from Mailman import Utils
27from Mailman import MailList28from Mailman import MailList
@@ -36,7 +37,6 @@
36i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)37i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
3738
3839
39
4040
41def main():41def main():
42 parts = Utils.GetPathPieces()42 parts = Utils.GetPathPieces()
43 if not parts:43 if not parts:
@@ -62,7 +62,6 @@
62 list_listinfo(mlist, language)62 list_listinfo(mlist, language)
6363
6464
65
6665
67def listinfo_overview(msg=''):66def listinfo_overview(msg=''):
68 # Present the general listinfo overview67 # Present the general listinfo overview
69 hostname = Utils.get_domain()68 hostname = Utils.get_domain()
@@ -152,7 +151,6 @@
152 print doc.Format()151 print doc.Format()
153152
154153
155
156154
157def list_listinfo(mlist, lang):155def list_listinfo(mlist, lang):
158 # Generate list specific listinfo156 # Generate list specific listinfo
159 doc = HeadlessDocument()157 doc = HeadlessDocument()
@@ -189,10 +187,25 @@
189187
190 # If activated, the content and presence of questions and a field to answer them. Added by Systers.188 # If activated, the content and presence of questions and a field to answer them. Added by Systers.
191 try:189 try:
192 if mlist.essay_enabled:190 if mlist.essay_enabled: # Added to support subscribe essays
193 replacements['<mm-questions>'] = """<TR>191 html_questions = []
194 <TD COLSPAN="3">""" + mlist.questions192 for question in mlist.questions:
195 replacements['<mm-essay-box>'] = TextArea('essay', '', rows=8, cols=60).Format() + """ </TD></TR>"""# Added to support subscribe essays193 question_name = "'"+question[1]+"'"
194 html_questions.append("""<TR>
195 <TD COLSPAN="3">""" + question[0] + """</TD>""")
196 if (question[2] == 'smallbox'):
197 html_questions.append("""<TD>""" + TextArea(question_name,\
198 '', rows=1, cols=60).Format() + """</TD></TR>""")
199 elif (question[2] == 'largebox'):
200 html_questions.append("""<TD>""" + TextArea(question_name, \
201 '', rows=8, cols=60).Format() + """</TD></TR>""")
202 elif (question[2] == 'checkbox'):
203 #TODO (Ana Cutillas): consider creating a class like TextArea for checkboxes?
204 html_questions.append("""<TD><input type='checkbox' name='""" \
205 + question[1] + """' value =' """\
206 + question[1] + """'></TD></TR>""")
207 replacements['<mm-questions>'] = ''.join(html_questions)
208 replacements['<mm-essay-box>'] = ''
196 else:209 else:
197 replacements['<mm-questions>'] = ''210 replacements['<mm-questions>'] = ''
198 replacements['<mm-essay-box>'] = ''211 replacements['<mm-essay-box>'] = ''
199212
=== modified file 'Mailman/Cgi/subscribe.py'
--- Mailman/Cgi/subscribe.py 2009-08-14 15:41:17 +0000
+++ Mailman/Cgi/subscribe.py 2012-08-07 10:41:20 +0000
@@ -26,6 +26,7 @@
26from Mailman import MailList26from Mailman import MailList
27from Mailman import Errors27from Mailman import Errors
28from Mailman import i18n28from Mailman import i18n
29from Mailman import DlistUtils # to write to the database
29from Mailman import Message30from Mailman import Message
30from Mailman.DlistUserDesc import DlistUserDesc # to support subscribe essays31from Mailman.DlistUserDesc import DlistUserDesc # to support subscribe essays
31from Mailman.htmlformat import *32from Mailman.htmlformat import *
@@ -135,14 +136,42 @@
135 os.environ.get('REMOTE_ADDR',136 os.environ.get('REMOTE_ADDR',
136 'unidentified origin'))137 'unidentified origin'))
137138
138 # Added to support mailing lists that require essays to join139 # Added to support mailing lists that require answers to questions to join
139 if mlist.essay_enabled:140 if mlist.essay_enabled:
140 essay = cgidata.getvalue('essay', '')141 # We need to retrieve the questions to be able to
141 if not essay: # Check for empty essays.142 # retrieve the content from cgi.
142 if not error_msg:143 essay = []
143 print_error(mlist, doc)144 # This implementation allows for checkboxes to be left unchecked but text boxes must be filled.
144 error_msg = True145 for question in mlist.questions:
145 results.append(_('To subscribe to this list, you must answer the questions asked directly above the large text box.'))146 # A question is a tuple that contains this information:
147 # (question, question_name, type_of_box, format_info, position, version)
148 answer = cgidata.getvalue(question[1], '')
149 if (answer):
150 # The question has been answered
151 if (question[2] != 'checkbox'):
152 if (len(answer) <= 1500):
153 # In the database we have set the essay max length to 1500
154 essay.append((question[1], answer, question[5]))
155 else:
156 if not error_msg:
157 print_error(mlist, doc)
158 error_msg = True
159 results_error = "Your answer to the question \"" + question[0] + "\" is too long. Your answers must be less than 1500 characters long."
160 results.append(_(results_error))
161 else:
162 # The answer is a checked checkbox
163 essay.append((question[1], "True", question[5]))
164 else:
165 if (question[2] != 'checkbox'):
166 # If the answer is empty and it's not a checkbox
167 # the user left fields empty
168 if not error_msg:
169 print_error(mlist, doc)
170 error_msg = True
171 results.append(_('To subscribe to this list, you must answer all the questions asked on this page.'))
172 else:
173 # If the answer is an empty checkbox, the user didn't check it
174 essay.append((question[1], 'False', question[5]))
146 else: # has to be here, else error message when calling DlistUserDesc175 else: # has to be here, else error message when calling DlistUserDesc
147 essay = cgidata.getvalue('essay', 'None')176 essay = cgidata.getvalue('essay', 'None')
148 177
@@ -215,6 +244,11 @@
215may have to be first confirmed by you via email, or approved by the list244may have to be first confirmed by you via email, or approved by the list
216moderator. If confirmation is required, you will soon get a confirmation245moderator. If confirmation is required, you will soon get a confirmation
217email which contains further instructions.""")246email which contains further instructions.""")
247 for answer in essay:
248 form = DlistUtils.FormQuestion(mlist)
249 form_question_id = form.recordAnswer(email, answer[0], answer[1], answer[2])
250 # If form_question_id is -1, the user has a pending subscription
251 # An appropriate message is giving to the user later
218252
219 try:253 try:
220 userdesc = DlistUserDesc(email, fullname, password, digest, lang, essay)254 userdesc = DlistUserDesc(email, fullname, password, digest, lang, essay)
@@ -303,6 +337,11 @@
303 else:337 else:
304 results = _("""\338 results = _("""\
305You have been successfully subscribed to the %(realname)s mailing list.""")339You have been successfully subscribed to the %(realname)s mailing list.""")
340 if (form_question_id == -1):
341 if not error_msg:
342 print_error(mlist, doc)
343 error_msg = True
344 results = _("""There is a pending subscription with this email. You may not subscribe a second time. The moderator will contact you if there are any questions about the answers you gave originally.""")
306 # Show the results345 # Show the results
307 ## Not sure why this is needed346 ## Not sure why this is needed
308 if results == []:347 if results == []:
309348
=== modified file 'Mailman/DlistUtils.py'
--- Mailman/DlistUtils.py 2010-01-31 20:29:48 +0000
+++ Mailman/DlistUtils.py 2012-08-07 10:41:20 +0000
@@ -248,6 +248,12 @@
248 command = "count = (self.store.find(Subscriber,Subscriber.mailman_key == unicode(key,'utf-8'))).count()\n"248 command = "count = (self.store.find(Subscriber,Subscriber.mailman_key == unicode(key,'utf-8'))).count()\n"
249 if DEBUG_MODE:249 if DEBUG_MODE:
250 syslog('info', 'DlistUtils(subscribeToList):Executing query:\n%s', command)250 syslog('info', 'DlistUtils(subscribeToList):Executing query:\n%s', command)
251
252 unicode_key = unicode(key, "utf-8")
253 count_essay = (self.store.find(FormQuestion, FormQuestion.user_email == unicode_key)).count()
254 for i in range(count_essay):
255 self.store.find(FormQuestion, FormQuestion.user_email == unicode_key, FormQuestion.status == u"pending").set(status = u"accepted")
256
251 # First see if member is subscribed with deleted field = false257 # First see if member is subscribed with deleted field = false
252 count = (self.store.find(Subscriber,Subscriber.mailman_key == unicode(key,"utf-8"))).count()258 count = (self.store.find(Subscriber,Subscriber.mailman_key == unicode(key,"utf-8"))).count()
253259
@@ -255,7 +261,7 @@
255 command = "self.store.find(Subscriber,Subscriber.mailman_key == unicode(key,'utf-8')).set(deleted = False)"261 command = "self.store.find(Subscriber,Subscriber.mailman_key == unicode(key,'utf-8')).set(deleted = False)"
256 if DEBUG_MODE:262 if DEBUG_MODE:
257 syslog('info', 'DlistUtils(subscribeToList):Executing query:\n%s', command)263 syslog('info', 'DlistUtils(subscribeToList):Executing query:\n%s', command)
258 self.store.find(Subscriber,Subscriber.mailman_key == unicode(key,"utf-8")).set(deleted = False)264 self.store.find(Subscriber,Subscriber.mailman_key == unicode_key).set(deleted = False)
259 else:265 else:
260 # format is text-only (1) by default266 # format is text-only (1) by default
261 command = "subscriber = self.store.add(Subscriber())\nsubscriber.mailman_key = unicode(key,'utf-8')\nsubscriber.preference = 1\nsubscriber.deleted = False\nsubscriber.format = 1\nsubscriber.suppress = 0"267 command = "subscriber = self.store.add(Subscriber())\nsubscriber.mailman_key = unicode(key,'utf-8')\nsubscriber.preference = 1\nsubscriber.deleted = False\nsubscriber.format = 1\nsubscriber.suppress = 0"
@@ -289,6 +295,53 @@
289 self.store.find(Alias,Alias.subscriber_id == obsolete_id).set(subscriber_id = new_id)295 self.store.find(Alias,Alias.subscriber_id == obsolete_id).set(subscriber_id = new_id)
290 self.store.find(Subscriber,Subscriber.subscriber_id == obsolete_id).remove() 296 self.store.find(Subscriber,Subscriber.subscriber_id == obsolete_id).remove()
291297
298class FormQuestion(object):
299 __storm_table__ = "form_question"
300 form_question_id = Int(primary = True,default = AutoReload)
301 content = Unicode()
302 user_email = Unicode()
303 version = Int()
304 question_id = Unicode()
305 status = Unicode()
306
307 def __init__(self,mlist):
308 self.mlist = mlist
309 self.database = getConn(mlist)
310
311 @decfunc
312 def recordAnswer(self, addr, question, answer, version):
313 """Save the answer for a question of the subscribe form, returning its id."""
314 subscriber = Subscriber(self.mlist)
315 userID = addr.decode('utf-8')
316 question_unicode = question.decode('utf-8')
317 count = (self.store.find(FormQuestion, FormQuestion.user_email == userID, FormQuestion.question_id == question_unicode)).count()
318 if count:
319 # The user has already tried to subscribe.
320 return -1
321 status = u"pending"
322 question_unicode = question.decode('utf-8')
323 answer_unicode = answer.decode('utf-8')
324 #form_question_id has autoreload set, its value will be serially updated in database
325 self.user_email = userID
326 self.content = answer_unicode
327 self.version = version
328 self.question_id = question_unicode
329 self.status = status
330 self.store.add(self)
331 formQuestionID = self.store.find(FormQuestion).max(FormQuestion.form_question_id)
332 if DEBUG_MODE:
333 syslog('info', 'Result of query(formQuestionID) is: %s\n', formQuestionID)
334 return formQuestionID
335
336 @decfunc
337 def acceptAnswer(self, addr):
338 """Sets the status of the answers to accepted"""
339 userID = addr.decode('utf-8')
340 count = (self.store.find(FormQuestion, FormQuestion.user_email == userID).count())
341 for i in range(count):
342 self.store.find(FormQuestion, FormQuestion.user_email == userID).set(status = u"accepted")
343
344
292class Message(object):345class Message(object):
293 __storm_table__ = "message"346 __storm_table__ = "message"
294 message_id = Int(primary = True,default = AutoReload)347 message_id = Int(primary = True,default = AutoReload)
295348
=== modified file 'bin/rmlist'
--- bin/rmlist 2009-07-31 10:49:01 +0000
+++ bin/rmlist 2012-08-07 10:41:20 +0000
@@ -152,12 +152,7 @@
152152
153 # Remove the database in PostgreSQL if it was a dynamic list. Added by Systers.153 # Remove the database in PostgreSQL if it was a dynamic list. Added by Systers.
154 try:154 try:
155 conn = DlistUtils.getConn(None)155 DlistUtils.remove_dlist(listname)
156 old_iso_lvl = conn.isolation_level
157 conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
158 cursor = conn.cursor()
159 cursor.execute('DROP DATABASE "%s"' % listname)
160 conn.set_isolation_level(old_iso_lvl)
161 except:156 except:
162 syslog('error', 'Tried to remove the database for %s, no such database (ok if it was a non dlist). If it was a dlist, the postgreSQL database for this list needs to be removed manually.' % listname)157 syslog('error', 'Tried to remove the database for %s, no such database (ok if it was a non dlist). If it was a dlist, the postgreSQL database for this list needs to be removed manually.' % listname)
163158

Subscribers

People subscribed via source and target branches