Skip to content

Commit

Permalink
Progress on moderation/flagging: admin ui mostly works, but widget ne…
Browse files Browse the repository at this point in the history
…eds to be redone so we don't end up 404 after delete.
  • Loading branch information
slinkp committed Dec 20, 2011
1 parent 25fd65f commit f0d8d9b
Show file tree
Hide file tree
Showing 9 changed files with 643 additions and 2 deletions.
2 changes: 2 additions & 0 deletions docs/changes/release_notes.rst
Expand Up @@ -48,6 +48,8 @@ Backward Incompatibilities
New Features in 1.2
-------------------

* Added ``ebpub.moderation`` app that allows users to flag

* User-uploaded images now supported for NewsItems, and enabled for
the NeighborNews user-contributed content.

Expand Down

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions ebpub/ebpub/db/models.py
Expand Up @@ -183,6 +183,11 @@ class Schema(models.Model):
help_text="Whether to allow users to add comments to NewsItems of the schema. Only applies to items with detail page."
)

allow_flagging = models.BooleanField(
default=False,
help_text="Whether to allow uses to flag NewsItems of this schema as spam or inappropriate."
)

allow_charting = models.BooleanField(
default=False,
help_text="Whether aggregate charts are displayed on the home page of this Schema")
Expand Down Expand Up @@ -1233,7 +1238,6 @@ class Meta(object):
def __unicode__(self):
return u'%s - %s' % (self.news_item, self.image.name)


###########################################
# Signals #
###########################################
Expand All @@ -1252,4 +1256,3 @@ def clear_allowed_schema_ids_cache(sender, **kwargs):
post_update.connect(clear_allowed_schema_ids_cache, sender=Schema)
post_save.connect(clear_allowed_schema_ids_cache, sender=Schema)
post_delete.connect(clear_allowed_schema_ids_cache, sender=Schema)

22 changes: 22 additions & 0 deletions ebpub/ebpub/moderation/__init__.py
@@ -0,0 +1,22 @@
# Copyright 2011 OpenPlans, and contributors
#
# This file is part of ebpub
#
# ebpub is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ebpub is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ebpub. If not, see <http://www.gnu.org/licenses/>.
#

"""
Models and admin views to support users flagging and administrators
removing flagged content.
"""
126 changes: 126 additions & 0 deletions ebpub/ebpub/moderation/admin.py
@@ -0,0 +1,126 @@
# Copyright 2011 OpenPlans and contributors
#
# This file is part of ebpub
#
# ebpub is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ebpub is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ebpub. If not, see <http://www.gnu.org/licenses/>.
#

from django.contrib.gis import admin
from ebpub.geoadmin import OSMModelAdmin
from .models import NewsItemFlag

from django.forms.widgets import Input, MultiWidget
from django.utils.safestring import mark_safe

from django import forms

class SubmitInput(Input):
input_type = 'submit'

APPROVE=u'approve item'
REJECT=u'delete item'

class ModerationWidget(MultiWidget):

# Two buttons whose output is a single string.
# There's probably a cleaner/shorter way to do this?

def __init__(self, attrs=None):
widgets=[SubmitInput(attrs={'value': APPROVE}),
SubmitInput(attrs={'value': REJECT})]
MultiWidget.__init__(self, widgets, attrs=attrs)

def decompress(self, value):
# We don't actually care about this, but get
# NotImplementedError without it.
return [value, value]

def format_output(self, rendered_widgets):
"""
Given a list of rendered widgets (as strings), returns a Unicode string
representing the HTML for the whole lot.
This hook allows you to format the HTML design of the widgets, if
needed.
"""
return u'&nbsp;'.join(rendered_widgets)

def value_from_datadict(self, data, files, name):
result = MultiWidget.value_from_datadict(self, data, files, name)
# What we have is a list of the strings that were submitted to
# our sub-widgets and Nones for the others.
# Since input is destined for a single CharField, merge it.
result = u' '.join([s for s in result if s])
return result

class ModerationForm(forms.ModelForm):
class Meta:
model = NewsItemFlag

def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
super(ModerationForm, self).__init__(*args, **kwargs)

moderate = forms.CharField(widget=ModerationWidget(), required=False)

def clean(self):
moderation = self.cleaned_data.get('moderate')
if moderation == APPROVE:
self.cleaned_data['state'] = u'approved'
elif moderation == REJECT:
self.cleaned_data['state'] = u'deleted'
if self.instance:
item = self.instance.news_item
if item is not None:
msg = u'Deleted news item %d' % item.id
item.delete()
# XXX self.instance will get auto-deleted anyway
# XXX can i somehow trigger a redirect?? argh i don't
# have a response and we don't have redirects as exceptions!
self.instance.news_item = None
if self.request is not None:
from django.contrib import messages
messages.add_message(self.request, messages.INFO, msg)


class NewsItemFlagAdmin(OSMModelAdmin):

form = ModerationForm

def get_form(self, request, obj=None, **kwargs):
# Jumping through hoops to make request available
# to the form instance, so it can be accessed during clean().
# See eg http://stackoverflow.com/questions/1057252/django-how-do-i-access-the-request-object-or-any-other-variable-in-a-forms-cle
_base = super(NewsItemFlagAdmin, self).get_form(request, obj, **kwargs)
class ModelFormMetaClass(_base):
def __new__(cls, *args, **kwargs):
kwargs['request'] = request
return _base(*args, **kwargs)
return ModelFormMetaClass

list_display = ('item_title', 'item_schema', 'state', 'reason', 'submitted', 'updated')
search_fields = ('item_title', 'item_description', 'comment',)

list_filter = ('reason', 'state', 'news_item__schema__slug',)
# XXX TODO: Allow deleting the NewsItem directly from our form.

raw_id_fields = ('news_item',)

date_hierarchy = 'submitted'
readonly_fields = ('item_title', 'item_schema', 'item_description', 'item_url',
'item_pub_date',
'submitted',
)

admin.site.register(NewsItemFlag, NewsItemFlagAdmin)
155 changes: 155 additions & 0 deletions ebpub/ebpub/moderation/migrations/0001_initial.py
@@ -0,0 +1,155 @@
# encoding: utf-8
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models

class Migration(SchemaMigration):

def forwards(self, orm):

# Adding model 'NewsItemFlag'
db.create_table('moderation_newsitemflag', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('news_item', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='moderation_newsitemflag_related', null=True, to=orm['db.NewsItem'])),
('reason', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)),
('email', self.gf('django.db.models.fields.CharField')(default='anonymous', max_length=128, null=True, blank=True)),
('comment', self.gf('django.db.models.fields.CharField')(max_length=512)),
('submitted', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
('updated', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
('state', self.gf('django.db.models.fields.CharField')(default='new', max_length=64, db_index=True, blank=True)),
))
db.send_create_signal('moderation', ['NewsItemFlag'])

# Adding model 'CommentFlag'
db.create_table('moderation_commentflag', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('news_item', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='moderation_commentflag_related', null=True, to=orm['db.NewsItem'])),
('reason', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)),
('email', self.gf('django.db.models.fields.CharField')(default='anonymous', max_length=128, null=True, blank=True)),
('comment', self.gf('django.db.models.fields.CharField')(max_length=512)),
('submitted', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
('updated', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
('state', self.gf('django.db.models.fields.CharField')(default='new', max_length=64, db_index=True, blank=True)),
))
db.send_create_signal('moderation', ['CommentFlag'])


def backwards(self, orm):

# Deleting model 'NewsItemFlag'
db.delete_table('moderation_newsitemflag')

# Deleting model 'CommentFlag'
db.delete_table('moderation_commentflag')


models = {
'db.location': {
'Meta': {'ordering': "('slug',)", 'unique_together': "(('slug', 'location_type'),)", 'object_name': 'Location'},
'area': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
'city': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'creation_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'null': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'display_order': ('django.db.models.fields.SmallIntegerField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_mod_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'null': 'True', 'blank': 'True'}),
'location': ('django.contrib.gis.db.models.fields.GeometryField', [], {'null': 'True'}),
'location_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.LocationType']"}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'normalized_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'population': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '32', 'db_index': 'True'}),
'source': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
'user_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'})
},
'db.locationtype': {
'Meta': {'ordering': "('name',)", 'object_name': 'LocationType'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_browsable': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_significant': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'plural_name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
'scope': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'})
},
'db.newsitem': {
'Meta': {'ordering': "('title',)", 'object_name': 'NewsItem'},
'description': ('django.db.models.fields.TextField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'item_date': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today', 'db_index': 'True'}),
'last_modification': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
'location': ('django.contrib.gis.db.models.fields.GeometryField', [], {'null': 'True', 'blank': 'True'}),
'location_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'location_object': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['db.Location']"}),
'location_set': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['db.Location']", 'null': 'True', 'through': "orm['db.NewsItemLocation']", 'blank': 'True'}),
'pub_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
'schema': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.Schema']"}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'url': ('django.db.models.fields.TextField', [], {'blank': 'True'})
},
'db.newsitemlocation': {
'Meta': {'unique_together': "(('news_item', 'location'),)", 'object_name': 'NewsItemLocation'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'location': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.Location']"}),
'news_item': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.NewsItem']"})
},
'db.schema': {
'Meta': {'ordering': "('name',)", 'object_name': 'Schema'},
'allow_charting': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'allow_comments': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'allow_flagging': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'can_collapse': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'date_name': ('django.db.models.fields.CharField', [], {'default': "'Date'", 'max_length': '32'}),
'date_name_plural': ('django.db.models.fields.CharField', [], {'default': "'Dates'", 'max_length': '32'}),
'grab_bag': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
'grab_bag_headline': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'blank': 'True'}),
'has_newsitem_detail': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'importance': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'indefinite_article': ('django.db.models.fields.CharField', [], {'max_length': '2'}),
'intro': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
'is_event': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
'is_special_report': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_updated': ('django.db.models.fields.DateField', [], {}),
'map_color': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'map_icon_url': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'min_date': ('django.db.models.fields.DateField', [], {'default': 'datetime.date(1970, 1, 1)'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'number_in_overview': ('django.db.models.fields.SmallIntegerField', [], {'default': '5'}),
'plural_name': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'short_description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
'short_source': ('django.db.models.fields.CharField', [], {'default': "'One-line description of where this information came from.'", 'max_length': '128', 'blank': 'True'}),
'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
'source': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
'summary': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
'update_frequency': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64', 'blank': 'True'}),
'uses_attributes_in_list': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
},
'moderation.commentflag': {
'Meta': {'ordering': "('news_item',)", 'object_name': 'CommentFlag'},
'comment': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
'email': ('django.db.models.fields.CharField', [], {'default': "'anonymous'", 'max_length': '128', 'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'news_item': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'moderation_commentflag_related'", 'null': 'True', 'to': "orm['db.NewsItem']"}),
'reason': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
'state': ('django.db.models.fields.CharField', [], {'default': "'new'", 'max_length': '64', 'db_index': 'True', 'blank': 'True'}),
'submitted': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
},
'moderation.newsitemflag': {
'Meta': {'ordering': "('news_item',)", 'object_name': 'NewsItemFlag'},
'comment': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
'email': ('django.db.models.fields.CharField', [], {'default': "'anonymous'", 'max_length': '128', 'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'news_item': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'moderation_newsitemflag_related'", 'null': 'True', 'to': "orm['db.NewsItem']"}),
'reason': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
'state': ('django.db.models.fields.CharField', [], {'default': "'new'", 'max_length': '64', 'db_index': 'True', 'blank': 'True'}),
'submitted': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
}
}

complete_apps = ['moderation']
Empty file.

0 comments on commit f0d8d9b

Please sign in to comment.