changeset 147:152d77265da6

Implement #38: add function to mark user as a spammer. Display only active members on member list. Display login form as table (not sure why wasn't doing this before).
author Brian Neal <bgneal@gmail.com>
date Sun, 13 Dec 2009 08:11:16 +0000
parents 023132c90021
children 35a0e6345815
files gpp/bio/admin.py gpp/bio/models.py gpp/bio/views.py gpp/forums/tools.py gpp/forums/views.py gpp/templates/accounts/login.html gpp/templates/bio/members.html
diffstat 7 files changed, 190 insertions(+), 16 deletions(-) [+]
line wrap: on
line diff
--- a/gpp/bio/admin.py	Wed Dec 09 22:58:05 2009 +0000
+++ b/gpp/bio/admin.py	Sun Dec 13 08:11:16 2009 +0000
@@ -1,15 +1,92 @@
 """
 This file contains the admin definitions for the bio application.
 """
+import datetime
 
 from django.contrib import admin
 from bio.models import UserProfile
 from bio.models import UserProfileFlag
+import bio.models
+from comments.models import Comment
+from forums.tools import delete_user_posts
+
 
 class UserProfileAdmin(admin.ModelAdmin):
     search_fields = ('user__username', 'user__first_name', 'user__last_name',
             'user__email')
     exclude = ('profile_html', 'signature_html')
+    list_display = ('__unicode__', 'get_status_display', 'status_date')
+    list_filter = ('status', )
+    date_hierarchy = 'status_date'
+    actions = (
+        'mark_active', 
+        'mark_resigned',
+        'mark_removed',
+        'mark_suspended',
+        'mark_spammer',
+    )
+
+    def get_status_display(self, obj):
+        return obj.get_status_display()
+    get_status_display.short_description = 'Status'
+
+    def mark_user_status(self, request, qs, status):
+        """
+        Common code for the admin actions. Updates the status field in the 
+        profiles to 'status'. Updates the status_date.  Sets the is_active 
+        field to True if the status is STA_ACTIVE and False otherwise.
+        """
+        now = datetime.datetime.now()
+        for profile in qs:
+            profile.user.is_active = status == bio.models.STA_ACTIVE
+            profile.user.save()
+            profile.status = status
+            profile.status_date = now
+            profile.save()
+
+        count = qs.count()
+        msg = "1 user" if count == 1 else "%d users" % count
+        self.message_user(request, "%s successfully marked as %s." % (msg,
+            bio.models.USER_STATUS_CHOICES[status][1]))
+
+    def mark_active(self, request, qs):
+        """
+        Marks users as active. Updates their profile status to STA_ACTIVE.
+        """
+        self.mark_user_status(request, qs, bio.models.STA_ACTIVE)
+    mark_active.short_description = "Mark selected users as active"
+
+    def mark_resigned(self, request, qs):
+        """
+        Marks users as inactive. Updates their profile status to STA_RESIGNED.
+        """
+        self.mark_user_status(request, qs, bio.models.STA_RESIGNED)
+    mark_resigned.short_description = "Mark selected users as resigned"
+
+    def mark_removed(self, request, qs):
+        """
+        Marks users as inactive. Updates their profile status to STA_REMOVED.
+        """
+        self.mark_user_status(request, qs, bio.models.STA_REMOVED)
+    mark_removed.short_description = "Mark selected users as removed"
+
+    def mark_suspended(self, request, qs):
+        """
+        Marks users as inactive. Updates their profile status to STA_SUSPENDED.
+        """
+        self.mark_user_status(request, qs, bio.models.STA_SUSPENDED)
+    mark_suspended.short_description = "Mark selected users as suspended"
+
+    def mark_spammer(self, request, qs):
+        """
+        Marks users as inactive. Updates their profile status to STA_SPAMMER.
+        Deletes all their comments and forum posts.
+        """
+        self.mark_user_status(request, qs, bio.models.STA_SPAMMER)
+        for profile in qs:
+            Comment.objects.filter(user=profile.user).delete()
+            delete_user_posts(profile.user)
+    mark_spammer.short_description = "Mark selected users as spammers"
 
 
 class UserProfileFlagAdmin(admin.ModelAdmin):
--- a/gpp/bio/models.py	Wed Dec 09 22:58:05 2009 +0000
+++ b/gpp/bio/models.py	Sun Dec 13 08:11:16 2009 +0000
@@ -13,6 +13,16 @@
 from core.markup import SiteMarkup
 
 
+(STA_ACTIVE, STA_RESIGNED, STA_REMOVED, STA_SUSPENDED, STA_SPAMMER) = range(5)
+
+USER_STATUS_CHOICES = (
+    (STA_ACTIVE, "Active"),
+    (STA_RESIGNED, "Resigned"),
+    (STA_REMOVED, "Removed"),
+    (STA_SUSPENDED, "Suspended"),
+    (STA_SPAMMER, "Spammer"),
+)
+
 def avatar_file_path_for_user(username, filename):
     return os.path.join(settings.AVATAR_DIR, 'users', username, filename)
 
@@ -39,6 +49,9 @@
             default='US/Pacific')
     use_24_time = models.BooleanField(default=False)
     forum_post_count = models.IntegerField(default=0)
+    status = models.IntegerField(default=STA_ACTIVE,
+            choices=USER_STATUS_CHOICES)
+    status_date = models.DateTimeField(auto_now_add=True)
 
     def __unicode__(self):
         return self.user.username
--- a/gpp/bio/views.py	Wed Dec 09 22:58:05 2009 +0000
+++ b/gpp/bio/views.py	Sun Dec 13 08:11:16 2009 +0000
@@ -30,12 +30,17 @@
 
 @login_required
 def member_list(request, type='user', page=1):
+    """
+    This view displays the member list. Only active members are displayed.
+    """
+    qs = auth.models.User.objects.filter(is_active=True)
     if type == 'user':
-        users = auth.models.User.objects.all().order_by('username')
+        qs = qs.order_by('username')
     else:
-        users = auth.models.User.objects.all().order_by('date_joined')
+        qs = qs.order_by('date_joined')
+    num_members = qs.count()
 
-    paginator = DiggPaginator(users, 10, body=5, tail=3, margin=3, padding=2)
+    paginator = DiggPaginator(qs, 20, body=5, tail=3, margin=3, padding=2)
     try:
         the_page = paginator.page(int(page))
     except InvalidPage:
@@ -54,6 +59,7 @@
     return render_to_response('bio/members.html', {
         'page': the_page,
         'type': type,
+        'num_members': num_members,
         }, 
         context_instance = RequestContext(request))
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/forums/tools.py	Sun Dec 13 08:11:16 2009 +0000
@@ -0,0 +1,69 @@
+"""
+This module contains misc. utility functions for forum management.
+"""
+from forums.models import Post, Topic, Forum, ForumLastVisit, TopicLastVisit
+
+
+def delete_user_posts(user):
+    """
+    This function deletes all the posts for a given user.
+    It also cleans up any last visit database records for the user.
+    This function adjusts the last post foreign keys before deleting
+    the posts to avoid the cascading delete behavior.
+    """
+    posts = Post.objects.filter(user=user).select_related()
+
+    # build a set of topics and forums affected by the post deletions
+
+    topics = set(post.topic for post in posts)
+    forums = set(topic.forum for topic in topics)
+
+    post_ids = [post.pk for post in posts]
+    pending_delete = []
+
+    for topic in topics:
+        if topic.last_post.pk in post_ids:
+            topic_posts = Post.objects.filter(topic=topic).exclude(
+                    pk__in=post_ids)
+            topic.post_count = topic_posts.count()
+            if topic.post_count > 0:
+                topic.last_post = topic_posts.latest()
+                topic.update_date = topic.last_post.creation_date
+                topic.save()
+            else:
+                # Topic should be deleted, it has no posts;
+                # We can't delete it now as it could cascade and take out a 
+                # forum. Remember it for later deletion.
+                pending_delete.append(topic)
+
+    for forum in forums:
+        if forum.last_post.pk in post_ids:
+            forum_posts = Post.objects.filter(topic__forum=forum).exclude(
+                    pk__in=post_ids)
+            forum.post_count = forum_posts.count()
+            if forum.post_count > 0:
+                forum.last_post = forum_posts.latest()
+            else:
+                forum.last_post = None
+            forum.save()
+    
+    # Delete pending topics now because forums have just adjusted their
+    # foreign keys into Post
+    if pending_delete:
+        topic_ids = [topic.pk for topic in pending_delete]
+        Topic.objects.filter(pk__in=topic_ids).delete()
+
+        # Topics have been deleted, re-compute topic counts for forums
+        for forum in forums:
+            forum.topic_count = Topic.objects.filter(forum=forum).count()
+            forum.save()
+            
+    # All foreign keys are accounted for, we can now delete the posts in bulk.
+    # Since some posts in our original queryset may have been deleted already,
+    # run a new query (although it may be ok)
+    Post.objects.filter(pk__in=post_ids).delete()
+
+    # delete all the last visit records for this user
+    TopicLastVisit.objects.filter(user=user).delete()
+    ForumLastVisit.objects.filter(user=user).delete()
+
--- a/gpp/forums/views.py	Wed Dec 09 22:58:05 2009 +0000
+++ b/gpp/forums/views.py	Sun Dec 13 08:11:16 2009 +0000
@@ -325,13 +325,22 @@
     if not can_delete:
         return HttpResponseForbidden("You don't have permission to delete that post.")
 
+    delete_single_post(post)
+    return HttpResponse("The post has been deleted.")
+
+
+def delete_single_post(post):
+    """
+    This function deletes a single post. It handles the case of where
+    a post is the sole post in a topic by deleting the topic also. It
+    adjusts any foreign keys in Topic or Forum objects that might be pointing
+    to this post before deleting the post to avoid a cascading delete.
+    """
     if post.topic.post_count == 1 and post == post.topic.last_post:
         _delete_topic(post.topic)
     else:
         _delete_post(post)
 
-    return HttpResponse("The post has been deleted.")
-
 
 def _delete_post(post):
     """
@@ -371,7 +380,7 @@
     Note that we don't bother adjusting all the users'
     post counts as that doesn't seem to be worth the effort.
     """
-    if topic.forum.last_post.topic == topic:
+    if topic.forum.last_post and topic.forum.last_post.topic == topic:
         topic.forum.last_post_pre_delete()
         topic.forum.save()
 
--- a/gpp/templates/accounts/login.html	Wed Dec 09 22:58:05 2009 +0000
+++ b/gpp/templates/accounts/login.html	Sun Dec 13 08:11:16 2009 +0000
@@ -2,19 +2,19 @@
 {% block title %}Login{% endblock %}
 {% block content %}
 <h2>Login</h2>
-{% if form.errors %}
-<p>Your username and password didn't match. Please try again.</p>
-{% endif %}
 
 <form method="post" action=".">
 <table>
-<tr><td>{{ form.username.label_tag }}:</td><td>{{ form.username }}</td></tr>
-<tr><td>{{ form.password.label_tag }}:</td><td>{{ form.password }}</td></tr>
-<tr><td>&nbsp;</td><td><input type="submit" value="Login" />
-      <input type="hidden" name="next" value="{{ next }}" /></td></tr>
+{{ form.as_table }}
+<tr><td>&nbsp;</td><td><input type="submit" value="Login" /></td></tr>
 </table>
+<input type="hidden" name="next" value="{{ next }}" />
 </form>
-<p>Forgot your password? You can reset it <a href="{% url accounts-password_reset %}">here</a>.</p>
-<p>Don't have an account? Why don't you <a href="{% url accounts-register %}">register</a>?</p>
+
+<ul>
+<li>Forgot your password? You can reset it <a href="{% url accounts-password_reset %}">here</a>.</li>
+<li>Don't have an account? Why don't you <a href="{% url accounts-register %}">register</a>?</li>
+<li>Having problems? Please <a href="{% url contact-form %}">contact us</a>.</li>
+</ul>
 
 {% endblock %}
--- a/gpp/templates/bio/members.html	Wed Dec 09 22:58:05 2009 +0000
+++ b/gpp/templates/bio/members.html	Sun Dec 13 08:11:16 2009 +0000
@@ -7,7 +7,7 @@
 {% endblock %}
 {% block content %}
 <h2>Member List</h2>
-
+<p>Surfguitar101.com currently has {{ num_members }} active members.</p>
 {% if page.object_list %}
 <ul class="tab-nav">
    <li><a href="{% url bio-members_full type="user",page="1" %}"