diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0ab606f --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +*.py[co] +*.egg-info +.pydevproject +.project +.settings +*.db +*.json +*.log +*~ +local_settings.py +.env +build/ +dist/ \ No newline at end of file diff --git a/djangobb_forum/admin.py b/djangobb_forum/admin.py index 4d9f6d7..8ccb384 100644 --- a/djangobb_forum/admin.py +++ b/djangobb_forum/admin.py @@ -6,7 +6,7 @@ from django.contrib.auth.models import User 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): @@ -71,9 +71,21 @@ class AttachmentAdmin(BaseModelAdmin): list_filter = ("content_type",) -admin.site.unregister(User) +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(Category, CategoryAdmin) admin.site.register(Forum, ForumAdmin) admin.site.register(Topic, TopicAdmin) @@ -83,4 +95,5 @@ admin.site.register(Reputation, ReputationAdmin) admin.site.register(Report, ReportAdmin) admin.site.register(Ban, BanAdmin) admin.site.register(Attachment, AttachmentAdmin) +admin.site.register(Poll, PollAdmin) diff --git a/djangobb_forum/forms.py b/djangobb_forum/forms.py index 626530c..de445a4 100644 --- a/djangobb_forum/forms.py +++ b/djangobb_forum/forms.py @@ -1,14 +1,16 @@ -# -*- 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 @@ -44,6 +46,8 @@ SEARCH_IN_CHOICES = ( 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) @@ -232,7 +236,7 @@ class PersonalityProfileForm(forms.ModelForm): class Meta: model = Profile fields = ['show_avatar', 'signature'] - + def __init__(self, *args, **kwargs): extra_args = kwargs.pop('extra_args', {}) self.profile = kwargs['instance'] @@ -265,7 +269,7 @@ class PrivacyProfileForm(forms.ModelForm): 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 ) @@ -293,27 +297,27 @@ class UserSearchForm(forms.Form): 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'})) @@ -357,7 +361,7 @@ class ReputationForm(forms.ModelForm): 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')) @@ -376,7 +380,7 @@ class ReputationForm(forms.ModelForm): 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'})) @@ -401,3 +405,83 @@ class ReportForm(forms.ModelForm): 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) + diff --git a/djangobb_forum/migrations/0005_auto__add_pollchoice__add_poll.py b/djangobb_forum/migrations/0005_auto__add_pollchoice__add_poll.py new file mode 100644 index 0000000..eeb4fc4 --- /dev/null +++ b/djangobb_forum/migrations/0005_auto__add_pollchoice__add_poll.py @@ -0,0 +1,226 @@ +# -*- 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'] \ No newline at end of file diff --git a/djangobb_forum/models.py b/djangobb_forum/models.py index 1b6ef0b..e969461 100644 --- a/djangobb_forum/models.py +++ b/djangobb_forum/models.py @@ -1,13 +1,15 @@ -from datetime import datetime -import os -import os.path -from hashlib import sha1 +# coding: utf-8 + +from datetime import datetime +from hashlib import sha1 +import os -from django.db import models -from django.contrib.auth.models import User, Group from django.conf import settings -from django.utils.translation import ugettext_lazy as _ +from django.contrib.auth.models import User, Group +from django.db import models +from django.db.models import aggregates from django.db.models.signals import post_save +from django.utils.translation import ugettext_lazy as _ from djangobb_forum.fields import AutoOneToOneField, ExtendedImageField, JSONField from djangobb_forum.util import smiles, convert_text_to_html @@ -410,6 +412,54 @@ class Attachment(models.Model): 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') diff --git a/djangobb_forum/static/djangobb_forum/js/markup/bbcode/board.js b/djangobb_forum/static/djangobb_forum/js/markup/bbcode/board.js index db1d679..53fccd5 100644 --- a/djangobb_forum/static/djangobb_forum/js/markup/bbcode/board.js +++ b/djangobb_forum/static/djangobb_forum/js/markup/bbcode/board.js @@ -73,4 +73,24 @@ $(document).ready(function() { 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; + }); }); \ No newline at end of file diff --git a/djangobb_forum/static/djangobb_forum/themes/default/style.css b/djangobb_forum/static/djangobb_forum/themes/default/style.css index 5925f91..31f2ddf 100644 --- a/djangobb_forum/static/djangobb_forum/themes/default/style.css +++ b/djangobb_forum/static/djangobb_forum/themes/default/style.css @@ -315,3 +315,13 @@ TD DIV.tclcon {MARGIN-LEFT: 2.3em} color: #8A1F11; } +/****************************************************************/ +/* for poll results */ +/****************************************************************/ +#poll .bar { + height: 4px; + background-color: #46586A; +} +#poll li { + height: 2.5em; +} diff --git a/djangobb_forum/templates/djangobb_forum/add_post.html b/djangobb_forum/templates/djangobb_forum/add_post.html deleted file mode 100644 index 2408789..0000000 --- a/djangobb_forum/templates/djangobb_forum/add_post.html +++ /dev/null @@ -1,46 +0,0 @@ -{% 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>» {% link forum %}</li></ul> - {% else %} - <ul><li><a href="{% url djangobb:index %}">{% trans "Root" %}</a> </li><li>» {% link topic.forum %}</li><li>» {{ 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 %} diff --git a/djangobb_forum/templates/djangobb_forum/add_topic.html b/djangobb_forum/templates/djangobb_forum/add_topic.html new file mode 100644 index 0000000..da0b5b7 --- /dev/null +++ b/djangobb_forum/templates/djangobb_forum/add_topic.html @@ -0,0 +1,17 @@ +{% 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>» {% link forum %}</li></ul> + {% else %} + <ul><li><a href="{% url djangobb:index %}">{% trans "Root" %}</a> </li><li>» {% link topic.forum %}</li><li>» {{ topic }}</li></ul> + {% endif %} + <div class="clearer"></div> + </div> +</div> +{% include "djangobb_forum/includes/post_form.html" %} +{% endblock %} diff --git a/djangobb_forum/templates/djangobb_forum/includes/post_form.html b/djangobb_forum/templates/djangobb_forum/includes/post_form.html index 3138dd6..13c7ab5 100644 --- a/djangobb_forum/templates/djangobb_forum/includes/post_form.html +++ b/djangobb_forum/templates/djangobb_forum/includes/post_form.html @@ -2,8 +2,30 @@ <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> @@ -33,7 +55,7 @@ </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> \ No newline at end of file diff --git a/djangobb_forum/templates/djangobb_forum/topic.html b/djangobb_forum/templates/djangobb_forum/topic.html index 753a43a..414f8ee 100644 --- a/djangobb_forum/templates/djangobb_forum/topic.html +++ b/djangobb_forum/templates/djangobb_forum/topic.html @@ -18,6 +18,37 @@ <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 }}"> </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> @@ -152,10 +183,14 @@ <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"> diff --git a/djangobb_forum/urls.py b/djangobb_forum/urls.py index 78681fb..b5ce6b7 100644 --- a/djangobb_forum/urls.py +++ b/djangobb_forum/urls.py @@ -2,12 +2,12 @@ from django.conf.urls.defaults import * 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('', @@ -54,16 +54,13 @@ 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'), @@ -73,7 +70,7 @@ urlpatterns = patterns('', # 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'), diff --git a/djangobb_forum/views.py b/djangobb_forum/views.py index c1772f3..09bb437 100644 --- a/djangobb_forum/views.py +++ b/djangobb_forum/views.py @@ -1,33 +1,35 @@ +# coding: utf-8 + import math from datetime import datetime, timedelta -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 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.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.core.urlresolvers import reverse from django.db import transaction -from django.views.decorators.csrf import csrf_exempt +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.utils.encoding import smart_str from django.utils.translation import ugettext as _ +from django.views.decorators.csrf import csrf_exempt + +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): @@ -312,6 +314,19 @@ def show_forum(request, forum_id, 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() @@ -323,82 +338,147 @@ def show_topic(request, topic_id, full=True): 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