Commits

Jens Diemer committed 03ff782

add poll feature ref #13 as squash commit see also http://support.djangobb.org/topic/333/ (This is a combination of 9 commits.)

Comments (0)

Files changed (13)

+*.py[co]
+*.egg-info
+.pydevproject
+.project
+.settings
+*.db
+*.json
+*.log
+*~
+local_settings.py
+.env
+build/
+dist/

djangobb_forum/admin.py

 from django.utils.translation import ugettext_lazy as _
 
 from djangobb_forum.models import Category, Forum, Topic, Post, Profile, Reputation, \
-    Report, Ban, Attachment
+    Report, Ban, Attachment, Poll, PollChoice
 
 
 class BaseModelAdmin(admin.ModelAdmin):
     list_filter = ("content_type",)
 
 
+class PollChoiceInline(admin.TabularInline):
+    model = PollChoice
+    extra = 3
+
+class PollAdmin(admin.ModelAdmin):
+    list_display = ("question", "active",)
+    list_display_links = ("question",)
+    list_editable = ("active",)
+    list_filter = ("active",)
+    inlines = [PollChoiceInline]
+
+
 admin.site.unregister(User)
+admin.site.register(User, UserAdmin)
 
-admin.site.register(User, UserAdmin)
 admin.site.register(Category, CategoryAdmin)
 admin.site.register(Forum, ForumAdmin)
 admin.site.register(Topic, TopicAdmin)
 admin.site.register(Report, ReportAdmin)
 admin.site.register(Ban, BanAdmin)
 admin.site.register(Attachment, AttachmentAdmin)
+admin.site.register(Poll, PollAdmin)
 

djangobb_forum/forms.py

-# -*- coding: utf-8 -*-
+# coding: utf-8
+
 import os.path
-from datetime import datetime
+from datetime import datetime, timedelta
 
 from django import forms
 from django.conf import settings
 from django.contrib.auth.models import User
+from django.db.models.expressions import F
 from django.utils.translation import ugettext_lazy as _
 
 from djangobb_forum.models import Topic, Post, Profile, Reputation, Report, \
-    Attachment
+    Attachment, Poll, PollChoice
 from djangobb_forum import settings as forum_settings
 from djangobb_forum.util import convert_text_to_html, set_language
 
 
 
 class AddPostForm(forms.ModelForm):
+    FORM_NAME = "AddPostForm" # used in view and template submit button
+
     name = forms.CharField(label=_('Subject'), max_length=255,
                            widget=forms.TextInput(attrs={'size':'115'}))
     attachment = forms.FileField(label=_('Attachment'), required=False)
     class Meta:
         model = Profile
         fields = ['show_avatar', 'signature']
-        
+
     def __init__(self, *args, **kwargs):
         extra_args = kwargs.pop('extra_args', {})
         self.profile = kwargs['instance']
     def __init__(self, *args, **kwargs):
         extra_args = kwargs.pop('extra_args', {})
         super(PrivacyProfileForm, self).__init__(*args, **kwargs)
-        self.fields['privacy_permission'].widget = forms.RadioSelect(  
+        self.fields['privacy_permission'].widget = forms.RadioSelect(
                                                     choices=self.fields['privacy_permission'].choices
                                                     )
 
             sort_by = self.cleaned_data['sort_by']
             sort_dir = self.cleaned_data['sort_dir']
             qs = qs.filter(username__contains=username, forum_profile__post_count__gte=forum_settings.POST_USER_SEARCH)
-            if sort_by=='username':
-                if sort_dir=='ASC':
+            if sort_by == 'username':
+                if sort_dir == 'ASC':
                     return qs.order_by('username')
-                elif sort_dir=='DESC':
+                elif sort_dir == 'DESC':
                     return qs.order_by('-username')
-            elif sort_by=='registered':
-                if sort_dir=='ASC':
+            elif sort_by == 'registered':
+                if sort_dir == 'ASC':
                     return qs.order_by('date_joined')
-                elif sort_dir=='DESC':
+                elif sort_dir == 'DESC':
                     return qs.order_by('-date_joined')
-            elif sort_by=='num_posts':
-                if sort_dir=='ASC':
+            elif sort_by == 'num_posts':
+                if sort_dir == 'ASC':
                     return qs.order_by('forum_profile__post_count')
-                elif sort_dir=='DESC':
+                elif sort_dir == 'DESC':
                     return qs.order_by('-forum_profile__post_count')
         else:
             return qs
 
 
 class PostSearchForm(forms.Form):
-    keywords = forms.CharField(required=False, label=_('Keyword search'), 
+    keywords = forms.CharField(required=False, label=_('Keyword search'),
                                widget=forms.TextInput(attrs={'size':'40', 'maxlength':'100'}))
     author = forms.CharField(required=False, label=_('Author search'),
                              widget=forms.TextInput(attrs={'size':'25', 'maxlength':'25'}))
             pass
         else:
             raise forms.ValidationError(_('You already voted for this post'))
-        
+
         # check if this post really belong to `from_user`
         if not Post.objects.filter(pk=self.cleaned_data['post'].id, user=self.to_user).exists():
             raise forms.ValidationError(_('This post does\'t belong to this user'))
 class MailToForm(forms.Form):
     subject = forms.CharField(label=_('Subject'),
                               widget=forms.TextInput(attrs={'size':'75', 'maxlength':'70', 'class':'longinput'}))
-    body = forms.CharField(required=False, label=_('Message'), 
+    body = forms.CharField(required=False, label=_('Message'),
                                widget=forms.Textarea(attrs={'rows':'10', 'cols':'75'}))
 
 
         if commit:
             report.save()
         return report
+
+
+class VotePollForm(forms.Form):
+    """
+    Dynamic form for the poll.
+    """
+    FORM_NAME = "VotePollForm" # used in view and template submit button
+
+    choice = forms.MultipleChoiceField()
+    def __init__(self, poll, *args, **kwargs):
+        self.poll = poll
+        super(VotePollForm, self).__init__(*args, **kwargs)
+
+        choices = self.poll.choices.all().values_list("id", "choice")
+        if self.poll.choice_count == 1:
+            self.fields["choice"] = forms.ChoiceField(
+                choices=choices, widget=forms.RadioSelect
+            )
+        else:
+            self.fields["choice"] = forms.MultipleChoiceField(
+                choices=choices, widget=forms.CheckboxSelectMultiple
+            )
+
+    def clean_choice(self):
+        ids = self.cleaned_data["choice"]
+        count = len(ids)
+        if count > self.poll.choice_count:
+            raise forms.ValidationError(
+                _(u'You have selected too many choices! (Only %i allowed.)') % self.poll.choice_count
+            )
+        return ids
+
+
+class PollForm(forms.ModelForm):
+    answers = forms.CharField(min_length=2, widget=forms.Textarea,
+        help_text=_("Write each answer on a new line.")
+    )
+    days = forms.IntegerField(required=False, min_value=1,
+        help_text=_("Number of days for this poll to run. Leave empty for never ending poll.")
+    )
+    class Meta:
+        model = Poll
+        fields = ['question', 'choice_count']
+
+    def create_poll(self):
+        """
+        return True if one field filled with data -> the user wants to create a poll
+        """
+        return any(self.data.get(key) for key in ('question', 'answers', 'days'))
+
+    def clean_answers(self):
+        # validate if there is more than whitespaces ;)
+        raw_answers = self.cleaned_data["answers"]
+        answers = [answer.strip() for answer in raw_answers.splitlines() if answer.strip()]
+        if len(answers) == 0:
+            raise forms.ValidationError(_(u"There is no valid answer!"))
+
+        # validate length of all answers
+        is_max_length = max([len(answer) for answer in answers])
+        should_max_length = PollChoice._meta.get_field("choice").max_length
+        if is_max_length > should_max_length:
+            raise forms.ValidationError(_(u"One of this answers are too long!"))
+
+        return answers
+
+    def save(self, post):
+        """
+        Create poll and all answers in PollChoice model.
+        """
+        poll = super(PollForm, self).save(commit=False)
+        poll.topic = post.topic
+        days = self.cleaned_data["days"]
+        if days:
+            now = datetime.now()
+            poll.deactivate_date = now + timedelta(days=days)
+        poll.save()
+        answers = self.cleaned_data["answers"]
+        for answer in answers:
+            PollChoice.objects.create(poll=poll, choice=answer)
+

djangobb_forum/migrations/0005_auto__add_pollchoice__add_poll.py

+# -*- coding: 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 'PollChoice'
+        db.create_table('djangobb_forum_pollchoice', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('poll', self.gf('django.db.models.fields.related.ForeignKey')(related_name='choices', to=orm['djangobb_forum.Poll'])),
+            ('choice', self.gf('django.db.models.fields.CharField')(max_length=200)),
+            ('votes', self.gf('django.db.models.fields.IntegerField')(default=0)),
+        ))
+        db.send_create_signal('djangobb_forum', ['PollChoice'])
+
+        # Adding model 'Poll'
+        db.create_table('djangobb_forum_poll', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('topic', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['djangobb_forum.Topic'])),
+            ('question', self.gf('django.db.models.fields.CharField')(max_length=200)),
+            ('choice_count', self.gf('django.db.models.fields.PositiveSmallIntegerField')(default=1)),
+            ('active', self.gf('django.db.models.fields.BooleanField')(default=True)),
+            ('deactivate_date', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)),
+        ))
+        db.send_create_signal('djangobb_forum', ['Poll'])
+
+        # Adding M2M table for field users on 'Poll'
+        db.create_table('djangobb_forum_poll_users', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('poll', models.ForeignKey(orm['djangobb_forum.poll'], null=False)),
+            ('user', models.ForeignKey(orm['auth.user'], null=False))
+        ))
+        db.create_unique('djangobb_forum_poll_users', ['poll_id', 'user_id'])
+
+
+    def backwards(self, orm):
+        # Deleting model 'PollChoice'
+        db.delete_table('djangobb_forum_pollchoice')
+
+        # Deleting model 'Poll'
+        db.delete_table('djangobb_forum_poll')
+
+        # Removing M2M table for field users on 'Poll'
+        db.delete_table('djangobb_forum_poll_users')
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'djangobb_forum.attachment': {
+            'Meta': {'object_name': 'Attachment'},
+            'content_type': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'hash': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '40', 'db_index': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.TextField', [], {}),
+            'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attachments'", 'to': "orm['djangobb_forum.Post']"}),
+            'size': ('django.db.models.fields.IntegerField', [], {})
+        },
+        'djangobb_forum.ban': {
+            'Meta': {'object_name': 'Ban'},
+            'ban_end': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'ban_start': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'reason': ('django.db.models.fields.TextField', [], {}),
+            'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'ban_users'", 'unique': 'True', 'to': "orm['auth.User']"})
+        },
+        'djangobb_forum.category': {
+            'Meta': {'ordering': "['position']", 'object_name': 'Category'},
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['auth.Group']", 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}),
+            'position': ('django.db.models.fields.IntegerField', [], {'default': '0', 'blank': 'True'})
+        },
+        'djangobb_forum.forum': {
+            'Meta': {'ordering': "['position']", 'object_name': 'Forum'},
+            'category': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'forums'", 'to': "orm['djangobb_forum.Category']"}),
+            'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_forum_post'", 'null': 'True', 'to': "orm['djangobb_forum.Post']"}),
+            'moderators': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}),
+            'position': ('django.db.models.fields.IntegerField', [], {'default': '0', 'blank': 'True'}),
+            'post_count': ('django.db.models.fields.IntegerField', [], {'default': '0', 'blank': 'True'}),
+            'topic_count': ('django.db.models.fields.IntegerField', [], {'default': '0', 'blank': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
+        },
+        'djangobb_forum.poll': {
+            'Meta': {'object_name': 'Poll'},
+            'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'choice_count': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '1'}),
+            'deactivate_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'question': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
+            'topic': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['djangobb_forum.Topic']"}),
+            'users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
+        },
+        'djangobb_forum.pollchoice': {
+            'Meta': {'object_name': 'PollChoice'},
+            'choice': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'poll': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'choices'", 'to': "orm['djangobb_forum.Poll']"}),
+            'votes': ('django.db.models.fields.IntegerField', [], {'default': '0'})
+        },
+        'djangobb_forum.post': {
+            'Meta': {'ordering': "['created']", 'object_name': 'Post'},
+            'body': ('django.db.models.fields.TextField', [], {}),
+            'body_html': ('django.db.models.fields.TextField', [], {}),
+            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'markup': ('django.db.models.fields.CharField', [], {'default': "'bbcode'", 'max_length': '15'}),
+            'topic': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'to': "orm['djangobb_forum.Topic']"}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'updated_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'to': "orm['auth.User']"}),
+            'user_ip': ('django.db.models.fields.IPAddressField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'})
+        },
+        'djangobb_forum.posttracking': {
+            'Meta': {'object_name': 'PostTracking'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_read': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+            'topics': ('djangobb_forum.fields.JSONField', [], {'null': 'True'}),
+            'user': ('djangobb_forum.fields.AutoOneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
+        },
+        'djangobb_forum.profile': {
+            'Meta': {'object_name': 'Profile'},
+            'aim': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
+            'auto_subscribe': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'avatar': ('djangobb_forum.fields.ExtendedImageField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}),
+            'icq': ('django.db.models.fields.CharField', [], {'max_length': '12', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'jabber': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
+            'language': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '5'}),
+            'location': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'markup': ('django.db.models.fields.CharField', [], {'default': "'bbcode'", 'max_length': '15'}),
+            'msn': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
+            'post_count': ('django.db.models.fields.IntegerField', [], {'default': '0', 'blank': 'True'}),
+            'privacy_permission': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+            'show_avatar': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'show_signatures': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'show_smilies': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'signature': ('django.db.models.fields.TextField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
+            'signature_html': ('django.db.models.fields.TextField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
+            'site': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
+            'status': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'theme': ('django.db.models.fields.CharField', [], {'default': "'default'", 'max_length': '80'}),
+            'time_zone': ('django.db.models.fields.FloatField', [], {'default': '3.0'}),
+            'user': ('djangobb_forum.fields.AutoOneToOneField', [], {'related_name': "'forum_profile'", 'unique': 'True', 'to': "orm['auth.User']"}),
+            'yahoo': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'})
+        },
+        'djangobb_forum.report': {
+            'Meta': {'object_name': 'Report'},
+            'created': ('django.db.models.fields.DateTimeField', [], {'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['djangobb_forum.Post']"}),
+            'reason': ('django.db.models.fields.TextField', [], {'default': "''", 'max_length': "'1000'", 'blank': 'True'}),
+            'reported_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reported_by'", 'to': "orm['auth.User']"}),
+            'zapped': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'zapped_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'zapped_by'", 'null': 'True', 'to': "orm['auth.User']"})
+        },
+        'djangobb_forum.reputation': {
+            'Meta': {'unique_together': "(('from_user', 'post'),)", 'object_name': 'Reputation'},
+            'from_user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reputations_from'", 'to': "orm['auth.User']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'post'", 'to': "orm['djangobb_forum.Post']"}),
+            'reason': ('django.db.models.fields.TextField', [], {'max_length': '1000'}),
+            'sign': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'time': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'to_user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reputations_to'", 'to': "orm['auth.User']"})
+        },
+        'djangobb_forum.topic': {
+            'Meta': {'ordering': "['-updated']", 'object_name': 'Topic'},
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'topics'", 'to': "orm['djangobb_forum.Forum']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_topic_post'", 'null': 'True', 'to': "orm['djangobb_forum.Post']"}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'post_count': ('django.db.models.fields.IntegerField', [], {'default': '0', 'blank': 'True'}),
+            'sticky': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'subscribers': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'subscriptions'", 'blank': 'True', 'to': "orm['auth.User']"}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+            'views': ('django.db.models.fields.IntegerField', [], {'default': '0', 'blank': 'True'})
+        }
+    }
+
+    complete_apps = ['djangobb_forum']

djangobb_forum/models.py

+# coding: utf-8
+
 from datetime import datetime
+from hashlib import sha1
 import os
-import os.path
-from hashlib import sha1
 
+from django.conf import settings
+from django.contrib.auth.models import User, Group
 from django.db import models
-from django.contrib.auth.models import User, Group
-from django.conf import settings
+from django.db.models import aggregates
+from django.db.models.signals import post_save
 from django.utils.translation import ugettext_lazy as _
-from django.db.models.signals import post_save
 
 from djangobb_forum.fields import AutoOneToOneField, ExtendedImageField, JSONField
 from djangobb_forum.util import smiles, convert_text_to_html
                             self.path)
 
 
+#------------------------------------------------------------------------------
+
+
+class Poll(models.Model):
+    topic = models.ForeignKey(Topic)
+    question = models.CharField(max_length=200)
+    choice_count = models.PositiveSmallIntegerField(default=1,
+        help_text=_("How many choices are allowed simultaneously."),
+    )
+    active = models.BooleanField(default=True,
+        help_text=_("Can users vote to this poll or just see the result?"),
+    )
+    deactivate_date = models.DateTimeField(null=True, blank=True,
+        help_text=_("Point of time after this poll would be automatic deactivated"),
+    )
+    users = models.ManyToManyField(User, blank=True, null=True,
+        help_text=_("Users who has voted this poll."),
+    )
+    def auto_deactivate(self):
+        if self.active and self.deactivate_date:
+            now = datetime.now()
+            if now > self.deactivate_date:
+                self.active = False
+                self.save()
+
+    def __unicode__(self):
+        return self.question
+
+
+class PollChoice(models.Model):
+    poll = models.ForeignKey(Poll, related_name="choices")
+    choice = models.CharField(max_length=200)
+    votes = models.IntegerField(default=0, editable=False)
+
+    def percent(self):
+        if not self.votes:
+            return 0.0
+        result = PollChoice.objects.filter(poll=self.poll).aggregate(aggregates.Sum("votes"))
+        votes_sum = result["votes__sum"]
+        return float(self.votes) / votes_sum * 100
+
+    def __unicode__(self):
+        return self.choice
+
+
+#------------------------------------------------------------------------------
+
+
 from .signals import post_saved, topic_saved
 
 post_save.connect(post_saved, sender=Post, dispatch_uid='djangobb_post_save')

djangobb_forum/static/djangobb_forum/js/markup/bbcode/board.js

         paste("[b]"+nick+"[/b]\n");
     });
     $(".username").attr('title', 'Click to paste nick name in reply form.');
+    
+    window.onbeforeunload = function() {
+        var obj = $("textarea#id_body");
+        if (obj.length != 1) {
+            // object not found in page -> do nothing
+            return
+        }
+        var text = obj.val().trim();
+        //log("onbeforeunload text:" + text);
+        if (text.length > 3) {
+            // Firefox will not use the string. IE use it
+            // TODO: Translate string
+            return "Leave page with unsaved content?";
+        }
+        // if nothing returned, browser leave the page without any message
+    };
+    $("form#post").bind("submit", function() {
+        //log("unbind onbeforeunload");
+        window.onbeforeunload = null;
+    });
 });

djangobb_forum/static/djangobb_forum/themes/default/style.css

     color: #8A1F11;
 }
 
+/****************************************************************/
+/* for poll results                                             */
+/****************************************************************/
+#poll .bar {
+    height: 4px;
+    background-color: #46586A;
+}
+#poll li {
+    height: 2.5em;
+}

djangobb_forum/templates/djangobb_forum/add_post.html

-{% extends 'djangobb_forum/base.html' %}
-{% load forum_extras %}
-{% load i18n %}
-
-{% block content %}
-<div class="linkst">
-	<div class="inbox">
-		{% if forum %}
-		<ul class="start"><li><a href="{% url djangobb:index %}">{% trans "Root" %}</a> </li><li>&raquo; {% link forum %}</li></ul>
-		{% else %}
-			<ul><li><a href="{% url djangobb:index %}">{% trans "Root" %}</a> </li><li>&raquo; {% link topic.forum %}</li><li>&raquo; {{ topic }}</li></ul>
-		{% endif %}
-		<div class="clearer"></div>
-	</div>
-</div>
-
-{% include "djangobb_forum/includes/post_form.html" %}
-
-{% if not forum %}
-<div id="postreview" class="blockpost">
-
-	<h2><span>{% trans "Topic review (newest first)" %}</span></h2>
-	{% for post in posts reversed %}
-	<div class="box rowodd">
-		<div class="inbox">
-			<div class="postleft">
-				<dl>
-					<dt><strong><a href="javascript:pasteN('{{ post.user.username }}');">{{ post.user.username }}</a></strong></dt>
-					<dd>{% forum_time post.created %}</dd>
-				</dl>
-			</div>
-			<div class="postright">
-				<div class="postmsg">
-					{{ post.body_html|safe }}
-				</div>
-			</div>
-			<div class="clearer"></div>
-						<div class="postfootright"><ul><li class="postquote"><a onmouseover="copyQ('{{ post.user.username }}');" href="javascript:pasteQ();">{% trans "Quote" %}</a></li></ul></div>
-		</div>
-	</div>
-	{% endfor %}
-	
-</div>
-{% endif %}
-
-{% endblock %}

djangobb_forum/templates/djangobb_forum/add_topic.html

+{% extends 'djangobb_forum/base.html' %}
+{% load forum_extras %}
+{% load i18n %}
+
+{% block content %}
+<div class="linkst">
+	<div class="inbox">
+		{% if forum %}
+		<ul class="start"><li><a href="{% url djangobb:index %}">{% trans "Root" %}</a> </li><li>&raquo; {% link forum %}</li></ul>
+		{% else %}
+			<ul><li><a href="{% url djangobb:index %}">{% trans "Root" %}</a> </li><li>&raquo; {% link topic.forum %}</li><li>&raquo; {{ topic }}</li></ul>
+		{% endif %}
+		<div class="clearer"></div>
+	</div>
+</div>
+{% include "djangobb_forum/includes/post_form.html" %}
+{% endblock %}

djangobb_forum/templates/djangobb_forum/includes/post_form.html

 <div class="blockform" id="reply">
     <h2><span>{% if forum %}{% trans "New topic" %}{% else %}{% trans "New reply" %}{% endif %}</span></h2>
     <div class="box">
-        <form id="post" action="{% if forum %}{% url djangobb:add_topic forum.id %}{% else %}{% url djangobb:add_post topic.id %}{% endif %}" method="post" enctype="multipart/form-data">
+        <form id="post" action="{{ form_url|default_if_none:"." }}" method="post" enctype="multipart/form-data">
             {% csrf_token %}
+            
+            {% if create_poll_form %}
+            <script>{# TODO: move to html head! #}
+                $(document).ready(function() {
+                    $('.poll .infldset').hide();
+                    $(".poll").click(function() {
+                        $('.poll .infldset').slideDown();
+                    });
+                });
+            </script>
+            <div class="inform poll">
+                <fieldset>
+                    <legend>{% trans "Create a poll" %}</legend>
+                    <div class="infldset">
+                        <div class="rbox">
+                            {{ create_poll_form }}
+                        </div>
+                    </div>
+                </fieldset>
+            </div>
+            {% endif %}
+            
             <div class="inform">
                 <fieldset>
                     <legend>{% trans "Write your message and submit" %}</legend>
                 </fieldset>
             </div>
             {% endif %}
-            <p><input type="submit" value="{% trans "Submit" %}" /><a href="javascript:history.go(-1)">{% trans "Go back" %}</a></p>
+            <p><input type="submit" name="{{ form.FORM_NAME }}" value="{% trans "Submit" %}" /><a href="{{ back_url|default_if_none:"javascript:history.go(-1)" }}">{% trans "Go back" %}</a></p>
         </form>
     </div>
 </div>

djangobb_forum/templates/djangobb_forum/topic.html

 		<div class="clearer"></div>
 	</div>
 </div>
+
+{% if poll %}
+<div id="poll" class="block">
+    <h2><span>{% trans "Poll" %}</span></h2>
+    <div class="box">
+        <div class="inbox"><p><strong>{{ poll.question }}</strong></p>
+        {% if poll_form %}       
+            <form action="." method="post">{% csrf_token %}
+            {{ poll_form }}
+            {% if poll.choice_count > 1 %}
+            <p>
+                {% blocktrans with count=poll.choice_count %}({{ count }} answers allows.){% endblocktrans %}
+            </p>
+            {% endif %}
+            <input type="submit" name="{{ poll_form.FORM_NAME }}" value="{% trans "Vote" %}" />
+            </form>
+        {% else %}
+            <ul>               
+            {% for choice in poll.choices.all %}
+                <li>
+                    <div class="bar" style="width:{{ choice.percent }}%;" title="{{ choice.choice }}">&nbsp;</div>
+                    {{ choice.votes }} vote{{ choice.votes|pluralize }} ({{ choice.percent|floatformat:1 }}%) for: {{ choice.choice }}
+                </li>
+            {% endfor %}
+            </ul>
+        {% endif %}
+        </div>
+    </div>
+</div>
+{% endif %}
+
 {% for post in posts %}
 	<div id="p{{ post.id }}" class="blockpost roweven firstpost">
 		<a name="post-{{ post.id }}"></a>
 		<div class="clearer"></div>
 	</div>
 </div>
-{% if not topic.closed and user.is_authenticated %}
-    {% include "djangobb_forum/includes/post_form.html" %}
+
+{% if reply_form %}
+    {% with form=reply_form %}
+        {% include "djangobb_forum/includes/post_form.html" %}
+    {% endwith %}
 {% endif %}
-{% endblock %}
+
+{% endblock content%}
 
 {% block controls %}
 <div class="conl">

djangobb_forum/urls.py

 
 from djangobb_forum import settings as forum_settings
 from djangobb_forum import views as forum_views
-from djangobb_forum.feeds import LastPosts, LastTopics, LastPostsOnForum,\
+from djangobb_forum.feeds import LastPosts, LastTopics, LastPostsOnForum, \
      LastPostsOnCategory, LastPostsOnTopic
-from djangobb_forum.forms import EssentialsProfileForm,\
-    PersonalProfileForm, MessagingProfileForm, PersonalityProfileForm,\
+from djangobb_forum.forms import EssentialsProfileForm, \
+    PersonalProfileForm, MessagingProfileForm, PersonalityProfileForm, \
     DisplayProfileForm, PrivacyProfileForm, UploadAvatarForm
-     
+
 
 urlpatterns = patterns('',
 
 
     # Topic
     url('^topic/(?P<topic_id>\d+)/$', forum_views.show_topic, name='topic'),
-    url('^(?P<forum_id>\d+)/topic/add/$', forum_views.add_post,
-        {'topic_id': None}, name='add_topic'),
+    url('^(?P<forum_id>\d+)/topic/add/$', forum_views.add_topic, name='add_topic'),
     url('^topic/(?P<topic_id>\d+)/delete_posts/$', forum_views.delete_posts, name='delete_posts'),
     url('^topic/move/$', forum_views.move_topic, name='move_topic'),
     url('^topic/(?P<topic_id>\d+)/stick_unstick/(?P<action>[s|u])/$', forum_views.stick_unstick_topic, name='stick_unstick_topic'),
     url('^topic/(?P<topic_id>\d+)/open_close/(?P<action>[c|o])/$', forum_views.open_close_topic, name='open_close_topic'),
 
     # Post
-    url('^topic/(?P<topic_id>\d+)/post/add/$', forum_views.add_post,
-        {'forum_id': None}, name='add_post'),
     url('^post/(?P<post_id>\d+)/$', forum_views.show_post, name='post'),
     url('^post/(?P<post_id>\d+)/edit/$', forum_views.edit_post, name='edit_post'),
     url('^post/(?P<post_id>\d+)/delete/$', forum_views.delete_post, name='delete_post'),
     # Subscription
     url('^subscription/topic/(?P<topic_id>\d+)/delete/$', forum_views.delete_subscription, name='forum_delete_subscription'),
     url('^subscription/topic/(?P<topic_id>\d+)/add/$', forum_views.add_subscription, name='forum_add_subscription'),
-    
+
     # Feeds
     url(r'^feeds/posts/$', LastPosts(), name='forum_posts_feed'),
     url(r'^feeds/topics/$', LastTopics(), name='forum_topics_feed'),

djangobb_forum/views.py

+# coding: utf-8
+
 import math
 from datetime import datetime, timedelta
 
+from django.contrib import messages
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth.models import User
+from django.contrib.sites.models import Site
+from django.core.cache import cache
+from django.core.urlresolvers import reverse
+from django.db import transaction
+from django.db.models import Q, F
+from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseForbidden
 from django.shortcuts import get_object_or_404, render
-from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseForbidden
-from django.contrib.auth.models import User
-from django.contrib.auth.decorators import login_required
-from django.contrib.sites.models import Site
-from django.core.urlresolvers import reverse
-from django.core.cache import cache
-from django.db.models import Q, F, Sum
 from django.utils.encoding import smart_str
-from django.db import transaction
+from django.utils.translation import ugettext as _
 from django.views.decorators.csrf import csrf_exempt
-from django.utils.translation import ugettext as _
+
+from haystack.query import SearchQuerySet, SQ
 
 from djangobb_forum import settings as forum_settings
 from djangobb_forum.forms import AddPostForm, EditPostForm, UserSearchForm, \
     PostSearchForm, ReputationForm, MailToForm, EssentialsProfileForm, \
-    PersonalProfileForm, MessagingProfileForm, PersonalityProfileForm, \
-    DisplayProfileForm, PrivacyProfileForm, ReportForm, UploadAvatarForm
-from djangobb_forum.models import Category, Forum, Topic, Post, Profile, Reputation, \
+    VotePollForm, ReportForm, VotePollForm, PollForm
+from djangobb_forum.models import Category, Forum, Topic, Post, Reputation, \
     Attachment, PostTracking
 from djangobb_forum.templatetags import forum_extras
 from djangobb_forum.templatetags.forum_extras import forum_moderated_by
 from djangobb_forum.util import build_form, paginate, set_language, smiles, convert_text_to_html
 
-from haystack.query import SearchQuerySet, SQ
-from django.contrib import messages
-from django.core.exceptions import SuspiciousOperation
+
 
 
 def index(request, full=True):
 
 @transaction.commit_on_success
 def show_topic(request, topic_id, full=True):
+    """
+    * Display a topic
+    * save a reply
+    * save a poll vote
+    
+    TODO: Add reply in lofi mode
+    """
+    post_request = request.method == "POST"
+    user_is_authenticated = request.user.is_authenticated()
+    if post_request and not user_is_authenticated:
+        # Info: only user that are logged in should get forms in the page.
+        return HttpResponseForbidden()
+
     topic = get_object_or_404(Topic.objects.select_related(), pk=topic_id)
     if not topic.forum.category.has_access(request.user):
         return HttpResponseForbidden()
         topic.update_read(request.user)
     posts = topic.posts.all().select_related()
 
-    initial = {}
-    if request.user.is_authenticated():
-        initial = {
-            'markup': request.user.forum_profile.markup,
-            'subscribe': request.user.forum_profile.auto_subscribe,
-        }
-    form = AddPostForm(topic=topic, initial=initial)
-
-    moderator = request.user.is_superuser or\
-        request.user in topic.forum.moderators.all()
-    if request.user.is_authenticated() and request.user in topic.subscribers.all():
+    moderator = request.user.is_superuser or request.user in topic.forum.moderators.all()
+    if user_is_authenticated and request.user in topic.subscribers.all():
         subscribed = True
     else:
         subscribed = False
 
+    # reply form
+    reply_form = None
+    form_url = None
+    back_url = None
+    if user_is_authenticated and not topic.closed:
+        form_url = request.path + "#reply" # if form validation failed: browser should scroll down to reply form ;)
+        back_url = request.path
+        ip = request.META.get('REMOTE_ADDR', None)
+        post_form_kwargs = {"topic":topic, "user":request.user, "ip":ip}
+        if post_request and AddPostForm.FORM_NAME in request.POST:
+            reply_form = AddPostForm(request.POST, request.FILES, **post_form_kwargs)
+            if reply_form.is_valid():
+                post = reply_form.save()
+                messages.success(request, _("Your reply saved."))
+                return HttpResponseRedirect(post.get_absolute_url())
+        else:
+            reply_form = AddPostForm(
+                initial={
+                    'markup': request.user.forum_profile.markup,
+                    'subscribe': request.user.forum_profile.auto_subscribe,
+                },
+                **post_form_kwargs
+            )
+
+    # handle poll, if exists
+    poll_form = None
+    polls = topic.poll_set.all()
+    if not polls:
+        poll = None
+    else:
+        poll = polls[0]
+        if user_is_authenticated: # Only logged in users can vote
+            poll.auto_deactivate()
+            has_voted = request.user in poll.users.all()
+            if not post_request or not VotePollForm.FORM_NAME in request.POST:
+                # It's not a POST request or: The reply form was send and not a poll vote
+                if poll.active and not has_voted:
+                    poll_form = VotePollForm(poll)
+            else:
+                if not poll.active:
+                    messages.error(request, _("This poll is not active!"))
+                    return HttpResponseRedirect(topic.get_absolute_url())
+                elif has_voted:
+                    messages.error(request, _("You have already vote to this poll in the past!"))
+                    return HttpResponseRedirect(topic.get_absolute_url())
+
+                poll_form = VotePollForm(poll, request.POST)
+                if poll_form.is_valid():
+                    ids = poll_form.cleaned_data["choice"]
+                    queryset = poll.choices.filter(id__in=ids)
+                    queryset.update(votes=F('votes') + 1)
+                    poll.users.add(request.user) # save that this user has vote
+                    messages.success(request, _("Your votes are saved."))
+                    return HttpResponseRedirect(topic.get_absolute_url())
+
     highlight_word = request.GET.get('hl', '')
     if full:
         return render(request, 'djangobb_forum/topic.html', {'categories': Category.objects.all(),
                 'topic': topic,
                 'last_post': last_post,
-                'form': form,
+                'form_url': form_url,
+                'reply_form': reply_form,
+                'back_url': back_url,
                 'moderator': moderator,
                 'subscribed': subscribed,
                 'posts': posts,
                 'highlight_word': highlight_word,
+                'poll': poll,
+                'poll_form': poll_form,
                 })
     else:
         return render(request, 'djangobb_forum/lofi/topic.html', {'categories': Category.objects.all(),
                 'topic': topic,
                 'posts': posts,
+                'poll': poll,
+                'poll_form': poll_form,
                 })
 
 
 @login_required
 @transaction.commit_on_success
-def add_post(request, forum_id, topic_id):
-    forum = None
-    topic = None
-    posts = None
-
-    if forum_id:
-        forum = get_object_or_404(Forum, pk=forum_id)
-        if not forum.category.has_access(request.user):
-            return HttpResponseForbidden()
-    elif topic_id:
-        topic = get_object_or_404(Topic, pk=topic_id)
-        posts = topic.posts.all().select_related()
-        if not topic.forum.category.has_access(request.user):
-            return HttpResponseForbidden()
-    if topic and topic.closed:
-        messages.error(request, _("This topic is closed."))
-        return HttpResponseRedirect(topic.get_absolute_url())
+def add_topic(request, forum_id):
+    """
+    create a new topic, with or without poll
+    """
+    forum = get_object_or_404(Forum, pk=forum_id)
+    if not forum.category.has_access(request.user):
+        return HttpResponseForbidden()
 
     ip = request.META.get('REMOTE_ADDR', None)
-    form = build_form(AddPostForm, request, topic=topic, forum=forum,
-                      user=request.user, ip=ip,
-                      initial={
-                          'markup': request.user.forum_profile.markup,
-                          'subscribe': request.user.forum_profile.auto_subscribe,
-                          })
+    post_form_kwargs = {"forum":forum, "user":request.user, "ip":ip, }
 
-    if 'post_id' in request.GET:
-        post_id = request.GET['post_id']
-        post = get_object_or_404(Post, pk=post_id)
-        form.fields['body'].initial = u"[quote=%s]%s[/quote]" % (post.user, post.body)
+    if request.method == 'POST':
+        form = AddPostForm(request.POST, request.FILES, **post_form_kwargs)
+        if form.is_valid():
+            all_valid = True
+        else:
+            all_valid = False
 
-    if form.is_valid():
-        post = form.save();
-        messages.success(request, _("Topic saved."))
-        return HttpResponseRedirect(post.get_absolute_url())
+        poll_form = PollForm(request.POST)
+        create_poll = poll_form.create_poll()
+        if not create_poll:
+            # All poll fields are empty: User didn't want to create a poll
+            # Don't run validation and remove all form error messages
+            poll_form = PollForm() # create clean form without form errors
+        elif not poll_form.is_valid():
+            all_valid = False
 
-    return render(request, 'djangobb_forum/add_post.html', {'form': form,
-            'posts': posts,
-            'topic': topic,
-            'forum': forum,
-            })
+        if all_valid:
+            post = form.save()
+            if create_poll:
+                poll_form.save(post)
+                messages.success(request, _("Topic with poll saved."))
+            else:
+                messages.success(request, _("Topic saved."))
+            return HttpResponseRedirect(post.get_absolute_url())
+    else:
+        form = AddPostForm(
+            initial={
+                'markup': request.user.forum_profile.markup,
+                'subscribe': request.user.forum_profile.auto_subscribe,
+            },
+            **post_form_kwargs
+        )
+        if forum_id: # Create a new topic
+            poll_form = PollForm()
+
+    context = {
+        'forum': forum,
+        'create_poll_form': poll_form,
+        'form': form,
+        'form_url': request.path,
+        'back_url': forum.get_absolute_url(),
+    }
+    return render(request, 'djangobb_forum/add_topic.html', context)
 
 
 @transaction.commit_on_success
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.