def get_or_create_tracker(self, user, forum): """ Correctly create tracker in mysql db on default REPEATABLE READ transaction mode It's known problem when standrard get_or_create method return can raise exception with correct data in mysql database. See http://stackoverflow.com/questions/2235318/how-do-i-deal-with-this-race-condition-in-django/2235624 """ is_new = True sid = transaction.savepoint(using=self.db) try: with get_atomic_func()(): obj = ForumReadTracker.objects.create(user=user, forum=forum) transaction.savepoint_commit(sid) except DatabaseError: transaction.savepoint_rollback(sid) is_new = False obj = ForumReadTracker.objects.get(user=user, forum=forum) return obj, is_new
class PostEditMixin(PybbFormsMixin): @method_decorator(get_atomic_func()) def post(self, request, *args, **kwargs): return super(PostEditMixin, self).post(request, *args, **kwargs) def get_form_class(self): if defaults.PYBB_ENABLE_ADMIN_POST_FORM and \ perms.may_post_as_admin(self.request.user): return self.get_admin_post_form_class() else: return self.get_post_form_class() def get_context_data(self, **kwargs): ctx = super(PostEditMixin, self).get_context_data(**kwargs) if perms.may_attach_files( self.request.user) and 'aformset' not in kwargs: ctx['aformset'] = self.get_attachment_formset_class()( instance=getattr(self, 'object', None)) if perms.may_create_poll( self.request.user) and 'pollformset' not in kwargs: ctx['pollformset'] = self.get_poll_answer_formset_class()( instance=self.object.topic if getattr(self, 'object', None ) else None) return ctx def form_valid(self, form): success = True save_attachments = False save_poll_answers = False self.object, topic = form.save(commit=False) if perms.may_attach_files(self.request.user): aformset = self.get_attachment_formset_class()( self.request.POST, self.request.FILES, instance=self.object) if aformset.is_valid(): save_attachments = True else: success = False else: aformset = None if perms.may_create_poll(self.request.user): pollformset = self.get_poll_answer_formset_class()() if getattr(self, 'forum', None) or topic.head == self.object: if topic.poll_type != Topic.POLL_TYPE_NONE: pollformset = self.get_poll_answer_formset_class()( self.request.POST, instance=topic) if pollformset.is_valid(): save_poll_answers = True else: success = False else: topic.poll_question = None topic.poll_answers.all().delete() else: pollformset = None if success: try: topic.save() except ValidationError as e: success = False errors = form._errors.setdefault('name', ErrorList()) errors += e.error_list else: self.object.topic = topic self.object.save() if save_attachments: aformset.save() if self.object.attachments.count(): # re-parse the body to replace attachment's references by URLs self.object.save() if save_poll_answers: pollformset.save() return HttpResponseRedirect(self.get_success_url()) return self.render_to_response( self.get_context_data(form=form, aformset=aformset, pollformset=pollformset))
class TopicView(RedirectToLoginMixin, PaginatorMixin, PybbFormsMixin, generic.ListView): paginate_by = defaults.PYBB_TOPIC_PAGE_SIZE template_object_name = 'post_list' template_name = 'pybb/topic.html' def get(self, request, *args, **kwargs): if defaults.PYBB_NICE_URL and 'pk' in kwargs: return redirect( self.topic, permanent=defaults.PYBB_NICE_URL_PERMANENT_REDIRECT) response = super(TopicView, self).get(request, *args, **kwargs) self.mark_read() return response def get_login_redirect_url(self): return self.topic.get_absolute_url() @method_decorator(csrf_protect) def dispatch(self, request, *args, **kwargs): self.topic = self.get_topic(**kwargs) if request.GET.get('first-unread'): if request.user.is_authenticated(): read_dates = [] try: read_dates.append( TopicReadTracker.objects.get( user=request.user, topic=self.topic).time_stamp) except TopicReadTracker.DoesNotExist: pass try: read_dates.append( ForumReadTracker.objects.get( user=request.user, forum=self.topic.forum).time_stamp) except ForumReadTracker.DoesNotExist: pass read_date = read_dates and max(read_dates) if read_date: try: first_unread_topic = self.topic.posts.filter( created__gt=read_date).order_by('created', 'id')[0] except IndexError: first_unread_topic = self.topic.last_post else: first_unread_topic = self.topic.head return HttpResponseRedirect( reverse('pybb:post', kwargs={'pk': first_unread_topic.id})) return super(TopicView, self).dispatch(request, *args, **kwargs) def get_queryset(self): if not perms.may_view_topic(self.request.user, self.topic): raise PermissionDenied if self.request.user.is_authenticated( ) or not defaults.PYBB_ANONYMOUS_VIEWS_CACHE_BUFFER: Topic.objects.filter(id=self.topic.id).update(views=F('views') + 1) else: cache_key = util.build_cache_key('anonymous_topic_views', topic_id=self.topic.id) cache.add(cache_key, 0) if cache.incr(cache_key ) % defaults.PYBB_ANONYMOUS_VIEWS_CACHE_BUFFER == 0: Topic.objects.filter(id=self.topic.id).update( views=F('views') + defaults.PYBB_ANONYMOUS_VIEWS_CACHE_BUFFER) cache.set(cache_key, 0) qs = self.topic.posts.all().select_related('user') if defaults.PYBB_PROFILE_RELATED_NAME: qs = qs.select_related('user__%s' % defaults.PYBB_PROFILE_RELATED_NAME) if not perms.may_moderate_topic(self.request.user, self.topic): qs = perms.filter_posts(self.request.user, qs) return qs def get_context_data(self, **kwargs): ctx = super(TopicView, self).get_context_data(**kwargs) if self.request.user.is_authenticated(): self.request.user.is_moderator = perms.may_moderate_topic( self.request.user, self.topic) self.request.user.is_subscribed = self.request.user in self.topic.subscribers.all( ) if defaults.PYBB_ENABLE_ADMIN_POST_FORM and \ perms.may_post_as_admin(self.request.user): ctx['form'] = self.get_admin_post_form_class()( initial={ 'login': getattr(self.request.user, username_field) }, topic=self.topic) else: ctx['form'] = self.get_post_form_class()(topic=self.topic) elif defaults.PYBB_ENABLE_ANONYMOUS_POST: ctx['form'] = self.get_post_form_class()(topic=self.topic) else: ctx['form'] = None ctx['next'] = self.get_login_redirect_url() if perms.may_attach_files(self.request.user): aformset = self.get_attachment_formset_class()() ctx['aformset'] = aformset ctx['attachment_max_size'] = defaults.PYBB_ATTACHMENT_SIZE_LIMIT if defaults.PYBB_FREEZE_FIRST_POST: ctx['first_post'] = self.topic.head else: ctx['first_post'] = None ctx['topic'] = self.topic if perms.may_vote_in_topic(self.request.user, self.topic) and \ pybb_topic_poll_not_voted(self.topic, self.request.user): ctx['poll_form'] = self.get_poll_form_class()(self.topic) return ctx @method_decorator(get_atomic_func()) def mark_read(self): if not self.request.user.is_authenticated(): return try: forum_mark = ForumReadTracker.objects.get(forum=self.topic.forum, user=self.request.user) except ForumReadTracker.DoesNotExist: forum_mark = None if (forum_mark is None) or (forum_mark.time_stamp < self.topic.updated): topic_mark, new = TopicReadTracker.objects.get_or_create_tracker( topic=self.topic, user=self.request.user) if not new and topic_mark.time_stamp > self.topic.updated: # Bail early if we already read this thread. return # Check, if there are any unread topics in forum readed_trackers = TopicReadTracker.objects.filter( user=self.request.user, topic__forum=self.topic.forum, time_stamp__gte=F('topic__updated')) unread = self.topic.forum.topics.exclude( topicreadtracker__in=readed_trackers) if forum_mark is not None: unread = unread.filter(updated__gte=forum_mark.time_stamp) if not unread.exists(): # Clear all topic marks for this forum, mark forum as read TopicReadTracker.objects.filter( user=self.request.user, topic__forum=self.topic.forum).delete() forum_mark, new = ForumReadTracker.objects.get_or_create_tracker( forum=self.topic.forum, user=self.request.user) forum_mark.save() def get_topic(self, **kwargs): if 'pk' in kwargs: topic = get_object_or_404(Topic, pk=kwargs['pk'], post_count__gt=0) elif ('slug' and 'forum_slug' and 'category_slug') in kwargs: topic = get_object_or_404( Topic, slug=kwargs['slug'], forum__slug=kwargs['forum_slug'], forum__category__slug=kwargs['category_slug'], post_count__gt=0) else: raise Http404(_('This topic does not exists')) return topic
class MovePostForm(forms.Form): def __init__(self, instance, user, *args, **kwargs): super(MovePostForm, self).__init__(*args, **kwargs) self.instance = instance self.user = user self.post = self.instance self.category, self.forum, self.topic = self.post.get_parents() if not self.post.is_topic_head: # we do not move an entire topic but a part of it's posts. Let's select those posts. self.posts_to_move = Post.objects.filter( created__gte=self.post.created, topic=self.topic).order_by('created', 'pk') # if multiple posts exists with the same created datetime, it's important to keep the # same order and do not move some posts which could be "before" our post. # We can not just filter by adding `pk__gt=self.post.pk` because we could exclude # some posts if for some reasons, a lesser pk has a greater "created" datetime # Most of the time, we just do one extra request to be sure the first post is # the wanted one first_pk = self.posts_to_move.values_list('pk', flat=True)[0] while first_pk != self.post.pk: self.posts_to_move = self.posts_to_move.exclude(pk=first_pk) first_pk = self.posts_to_move.values_list('pk', flat=True)[0] i = 0 choices = [] for post in self.posts_to_move[1:]: # all except the current one i += 1 bvars = { 'author': util.get_pybb_profile(post.user).get_display_name(), 'abstract': Truncator(post.body_text).words(8), 'i': i } label = _('%(i)d (%(author)s: "%(abstract)s")') % bvars choices.append((i, label)) choices.insert(0, (0, _('None'))) choices.insert(0, (-1, _('All'))) self.fields['number'] = forms.TypedChoiceField( label=ugettext_lazy('Number of following posts to move with'), choices=choices, required=True, coerce=int, ) # we move the entire topic, so we want to change it's forum. # So, let's exclude the current one # get all forum where we can move this post (and the others) move_to_forums = permissions.perms.filter_forums( self.user, Forum.objects.all()) if self.post.is_topic_head: # we move the entire topic, so we want to change it's forum. # So, let's exclude the current one move_to_forums = move_to_forums.exclude(pk=self.forum.pk) last_cat_pk = None choices = [] for forum in move_to_forums.order_by('category__position', 'position', 'name'): if not permissions.perms.may_create_topic(self.user, forum): continue if last_cat_pk != forum.category.pk: last_cat_pk = forum.category.pk choices.append(('%s' % forum.category, [])) if self.forum.pk == forum.pk: name = '%(forum)s (forum of the current post)' % { 'forum': self.forum } else: name = '%s' % forum choices[-1][1].append((forum.pk, name)) self.fields['move_to'] = forms.ChoiceField( label=ugettext_lazy('Move to forum'), initial=self.forum.pk, choices=choices, required=True, ) self.fields['name'] = forms.CharField(label=_('New subject'), initial=self.topic.name, max_length=255, required=True) if permissions.perms.may_edit_topic_slug(self.user): self.fields['slug'] = forms.CharField(label=_('New topic slug'), initial=self.topic.slug, max_length=255, required=False) def get_new_topic(self): if hasattr(self, '_new_topic'): return self._new_topic if self.post.is_topic_head: topic = self.topic else: topic = Topic(user=self.post.user) if topic.name != self.cleaned_data['name']: topic.name = self.cleaned_data['name'] # force slug auto-rebuild if slug is not speficied and topic is renamed topic.slug = self.cleaned_data.get('slug', None) elif self.cleaned_data.get('slug', None): topic.slug = self.cleaned_data['slug'] topic.forum = Forum.objects.get(pk=self.cleaned_data['move_to']) topic.slug = create_or_check_slug(topic, Topic, forum=topic.forum) topic.save() return topic @method_decorator(compat.get_atomic_func()) def save(self): data = self.cleaned_data topic = self.get_new_topic() if not self.post.is_topic_head: # we move some posts posts = self.posts_to_move if data['number'] != -1: number = data[ 'number'] + 1 # we want to move at least the current post ;-) posts = posts[0:number] # update posts # we can not update with subqueries on same table with mysql 5.5 # it raises: You can't specify target table 'pybb_post' for update in FROM clause # so we need to get all pks... It's bad for perfs, but posts are not often splited... posts_pks = [p.pk for p in posts] Post.objects.filter(pk__in=posts_pks).update(topic_id=topic.pk) topic.update_counters() topic.forum.update_counters() if topic.pk != self.topic.pk: # we just created a new topic. let's update the counters self.topic.update_counters() if self.forum.pk != topic.forum.pk: self.forum.update_counters() return Post.objects.get(pk=self.post.pk)
class PostEditMixin(PybbFormsMixin): @method_decorator(get_atomic_func()) def post(self, request, *args, **kwargs): return super(PostEditMixin, self).post(request, *args, **kwargs) def get_form_class(self): if perms.may_post_as_admin(self.request.user): return self.get_admin_post_form_class() else: return self.get_post_form_class() def get_context_data(self, **kwargs): ctx = super(PostEditMixin, self).get_context_data(**kwargs) if perms.may_attach_files( self.request.user) and (not 'aformset' in kwargs): ctx['aformset'] = self.get_attachment_formset_class()( instance=self.object if getattr(self, 'object') else None) if perms.may_create_poll(self.request.user) and ('pollformset' not in kwargs): ctx['pollformset'] = self.get_poll_answer_formset_class( )(instance=self.object.topic if getattr(self, 'object') else None) return ctx def form_valid(self, form): success = True save_attachments = False save_poll_answers = False self.object = form.save(commit=False) if perms.may_attach_files(self.request.user): aformset = self.get_attachment_formset_class()( self.request.POST, self.request.FILES, instance=self.object) if aformset.is_valid(): save_attachments = True else: success = False else: aformset = None if perms.may_create_poll(self.request.user): pollformset = self.get_poll_answer_formset_class()() if getattr(self, 'forum', None) or self.object.topic.head == self.object: if self.object.topic.poll_type != Topic.POLL_TYPE_NONE: pollformset = self.get_poll_answer_formset_class()( self.request.POST, instance=self.object.topic) if pollformset.is_valid(): save_poll_answers = True else: success = False else: self.object.topic.poll_question = None self.object.topic.poll_answers.all().delete() else: pollformset = None if success: self.object.topic.save() self.object.topic = self.object.topic self.object.save() if save_attachments: aformset.save() if save_poll_answers: pollformset.save() return HttpResponseRedirect(self.get_success_url()) else: return self.render_to_response( self.get_context_data(form=form, aformset=aformset, pollformset=pollformset))