{"action":"created","comment":{"url":"https://api.github.com/repos/zestedesavoir/zds-site/pulls/comments/30413042","id":30413042,"diff_hunk":"@@ -1,1143 +1,576 @@\n #!/usr/bin/python\n # -*- coding: utf-8 -*-\n-from datetime import datetime\n import json\n \n from django.conf import settings\n-from django.db.models import Q\n from django.contrib import messages\n from django.contrib.auth.decorators import login_required\n from django.contrib.auth.models import User\n from django.core.exceptions import PermissionDenied\n-from django.core.mail import EmailMultiAlternatives\n from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage\n from django.core.urlresolvers import reverse\n from django.db import transaction\n from django.http import Http404, HttpResponse, StreamingHttpResponse\n from django.shortcuts import redirect, get_object_or_404, render, render_to_response\n-from django.template.loader import render_to_string\n-from django.views.decorators.http import require_POST\n+from django.utils.decorators import method_decorator\n from django.utils.translation import ugettext_lazy as _\n-\n+from django.views.decorators.http import require_POST, require_GET\n+from django.views.generic import ListView, DetailView, CreateView, UpdateView\n+from django.views.generic.detail import SingleObjectMixin\n from haystack.inputs import AutoQuery\n from haystack.query import SearchQuerySet\n \n-from forms import TopicForm, PostForm, MoveTopicForm\n-from models import Category, Forum, Topic, Post, follow, follow_by_email, never_read, \\\n-    mark_read, TopicFollowed, get_topics\n-from zds.forum.models import TopicRead\n+from zds.forum.forms import TopicForm, PostForm, MoveTopicForm\n+from zds.forum.models import Category, Forum, Topic, Post, never_read, mark_read\n+from zds.forum.commons import TopicEditMixin, PostEditMixin, SinglePostObjectMixin\n from zds.member.decorator import can_write_and_read_now\n-from zds.member.views import get_client_ip\n from zds.utils import slugify\n-from zds.utils.models import Alert, CommentLike, CommentDislike, Tag\n+from zds.utils.forums import create_topic, send_post, CreatePostView\n+from zds.utils.mixins import FilterMixin\n+from zds.utils.models import Alert, Tag\n from zds.utils.mps import send_mp\n-from zds.utils.paginator import paginator_range\n-from zds.utils.templatetags.emarkdown import emarkdown\n-from zds.utils.templatetags.topbar import top_categories\n+from zds.utils.paginator import paginator_range, ZdSPagingListView\n \n \n-def index(request):\n-    \"\"\"\n-    The forum index. Displays the category list with all their forums the current user can read.\n-    \"\"\"\n+class CategoriesForumsListView(ListView):\n \n-    categories = top_categories(request.user)\n+    context_object_name = 'categories'\n+    template_name = 'forum/index.html'\n+    queryset = Category.objects.all()\n \n-    return render(request, \"forum/index.html\", {\"categories\": categories,\n-                                                \"user\": request.user,\n-                                                \"nb\": settings.ZDS_APP['forum']['topics_per_page'],\n-                                                \"page\": 1})\n+    def get_context_data(self, **kwargs):\n+        context = super(CategoriesForumsListView, self).get_context_data(**kwargs)\n+        for category in context.get('categories'):\n+            category.forums = category.get_forums(self.request.user)\n+        return context\n \n \n-def details(request, cat_slug, forum_slug):\n-    \"\"\"\n-    Displays a forum with all its topic, if the current use is allowed to read it.\n-    The topic list is paginated (`settings.ZDS_APP['forum']['topics_per_page']` topics per page) and can be filtered.\n-    :param cat_slug: As a forum slug is unique through the database, this is unused.\n-    :param forum_slug: The slug of the forum to display.\n-    \"\"\"\n+class CategoryForumsDetailView(DetailView):\n \n-    forum = get_object_or_404(Forum, slug=forum_slug)\n-    if not forum.can_read(request.user):\n-        raise PermissionDenied\n-    if 'filter' in request.GET:\n-        filter = request.GET['filter']\n-    else:\n-        filter = None\n-    sticky_topics = get_topics(forum_pk=forum.pk, is_sticky=True, filter=filter)\n-    topics = get_topics(forum_pk=forum.pk, is_sticky=False, filter=filter)\n+    context_object_name = 'category'\n+    template_name = 'forum/category/index.html'\n+    queryset = Category.objects.all()\n \n-    # Paginator\n+    def get_context_data(self, **kwargs):\n+        context = super(CategoryForumsDetailView, self).get_context_data(**kwargs)\n+        context['forums'] = context.get('category').get_forums(self.request.user)\n+        return context\n \n-    paginator = Paginator(topics, settings.ZDS_APP['forum']['topics_per_page'])\n-    page = request.GET.get(\"page\")\n-    try:\n-        shown_topics = paginator.page(page)\n-        page = int(page)\n-    except PageNotAnInteger:\n-        shown_topics = paginator.page(1)\n-        page = 1\n-    except EmptyPage:\n-        shown_topics = paginator.page(paginator.num_pages)\n-        page = paginator.num_pages\n \n-    return render(request, \"forum/category/forum.html\", {\n-        \"forum\": forum,\n-        \"sticky_topics\": sticky_topics,\n-        \"topics\": shown_topics,\n-        \"page\": 1,\n-        \"pages\": paginator_range(page, paginator.num_pages),\n-        \"nb\": page,\n-        \"filter\": filter,\n-    })\n+class ForumTopicsListView(FilterMixin, ZdSPagingListView, SingleObjectMixin):\n \n+    context_object_name = 'topics'\n+    paginate_by = settings.ZDS_APP['forum']['topics_per_page']\n+    template_name = 'forum/category/forum.html'\n+    filter_url_kwarg = 'filter'\n+    default_filter_param = 'all'\n+    object = None\n \n-def cat_details(request, cat_slug):\n-    \"\"\"\n-    Displays a category and all its forums the current user can read.\n-    :param cat_slug: The slug of the category to display.\n-    \"\"\"\n+    def get(self, request, *args, **kwargs):\n+        self.object = self.get_object()\n+        if not self.object.can_read(request.user):\n+            raise PermissionDenied\n+        return super(ForumTopicsListView, self).get(request, *args, **kwargs)\n+\n+    def get_context_data(self, **kwargs):\n+        context = super(ForumTopicsListView, self).get_context_data(**kwargs)\n+        context.update({\n+            'forum': self.object,\n+            'sticky_topics': self.filter_queryset(\n+                Topic.objects.get_all_topics_of_a_forum(self.object.pk, is_sticky=True),\n+                context['filter']),\n+        })\n+        return context\n \n-    category = get_object_or_404(Category, slug=cat_slug)\n-\n-    forums_pub = Forum.objects\\\n-        .filter(group__isnull=True, category__pk=category.pk)\\\n-        .select_related(\"category\").all()\n-    if request.user.is_authenticated():\n-        forums_prv = Forum.objects\\\n-            .filter(group__isnull=False,\n-                    group__in=request.user.groups.all(),\n-                    category__pk=category.pk)\\\n-            .select_related(\"category\")\\\n-            .all()\n-        forums = forums_pub | forums_prv\n-    else:\n-        forums = forums_pub\n-\n-    return render(request, \"forum/category/index.html\", {\"category\": category,\n-                                                         \"forums\": forums,\n-                                                         \"nb\": settings.ZDS_APP['forum']['topics_per_page'],\n-                                                         \"page\": 1})\n-\n-\n-def topic(request, topic_pk, topic_slug):\n-    \"\"\"\n-    Displays a topic (if the user can read it) and all its posts, paginated with\n-    `settings.ZDS_APP['forum']['posts_per_page']` posts per page.\n-    If the page to display is not the first one, the last post of the previous page is displayed as the first post of\n-    the current page.\n-    :param topic_pk: The primary key of the topic to display\n-    :param topic_slug: The slug of the topic to display. If the slug doesn't match the topic slug, the user is\n-    redirected to the proper topic URL.\n-    \"\"\"\n+    def get_object(self, queryset=None):\n+        return get_object_or_404(Forum, slug=self.kwargs.get('forum_slug'))\n \n-    topic = get_object_or_404(Topic, pk=topic_pk)\n-    if not topic.forum.can_read(request.user):\n-        raise PermissionDenied\n+    def get_queryset(self):\n+        self.queryset = Topic.objects.get_all_topics_of_a_forum(self.object.pk)\n+        return super(ForumTopicsListView, self).get_queryset()\n \n-    # Check link\n+    def filter_queryset(self, queryset, filter_param):\n+        if filter_param == 'solve':\n+            queryset = queryset.filter(is_solved=True)\n+        elif filter_param == 'unsolve':\n+            queryset = queryset.filter(is_solved=False)\n+        elif filter_param == 'noanswer':\n+            queryset = queryset.filter(last_message__position=1)\n+        return queryset\n \n-    if not topic_slug == slugify(topic.title):\n-        return redirect(topic.get_absolute_url())\n \n-    # If the user is authenticated and has never read topic, we mark it as\n-    # read.\n+class TopicPostsListView(ZdSPagingListView, SingleObjectMixin):\n \n-    if request.user.is_authenticated():\n-        if never_read(topic):\n-            mark_read(topic)\n+    context_object_name = 'posts'\n+    paginate_by = settings.ZDS_APP['forum']['posts_per_page']\n+    template_name = 'forum/topic/index.html'\n+    object = None\n \n-    # Retrieves all posts of the topic and use paginator with them.\n+    def get(self, request, *args, **kwargs):\n+        self.object = self.get_object()\n+        if not self.object.forum.can_read(request.user):\n+            raise PermissionDenied\n+        if not self.kwargs.get('topic_slug') == slugify(self.object.title):\n+            return redirect(self.object.get_absolute_url())\n+        return super(TopicPostsListView, self).get(request, *args, **kwargs)\n+\n+    def get_context_data(self, **kwargs):\n+        context = super(TopicPostsListView, self).get_context_data(**kwargs)\n+        form = PostForm(self.object, self.request.user)\n+        form.helper.form_action = reverse('post-new') + \"?sujet=\" + str(self.object.pk)\n+        context.update({\n+            'topic': self.object,\n+            'posts': self.build_list_with_previous_item(context['object_list']),\n+            'last_post_pk': self.object.last_message.pk,\n+            'form': form,\n+            'form_move': MoveTopicForm(topic=self.object),\n+        })\n+        if self.request.user.is_authenticated():\n+            if never_read(self.object):\n+                mark_read(self.object)\n+        return context\n \n-    posts = \\\n-        Post.objects.filter(topic__pk=topic.pk) \\\n-        .select_related(\"author__profile\") \\\n-        .order_by(\"position\").all()\n-    last_post_pk = topic.last_message.pk\n+    def get_object(self, queryset=None):\n+        return get_object_or_404(Topic, pk=self.kwargs.get('topic_pk'))\n \n-    # Handle pagination\n+    def get_queryset(self):\n+        return Post.objects.get_messages_of_a_topic(self.object.pk)\n \n-    paginator = Paginator(posts, settings.ZDS_APP['forum']['posts_per_page'])\n \n-    # The category list is needed to move threads\n+class TopicNew(CreateView, SingleObjectMixin):\n \n-    categories = Category.objects.all()\n-    if \"page\" in request.GET:\n-        try:\n-            page_nbr = int(request.GET[\"page\"])\n-        except (KeyError, ValueError):\n-            # problem in variable format\n-            raise Http404\n-    else:\n-        page_nbr = 1\n-    try:\n-        posts = paginator.page(page_nbr)\n-    except PageNotAnInteger:\n-        posts = paginator.page(1)\n-    except EmptyPage:\n-        raise Http404\n-    res = []\n-    if page_nbr != 1:\n-\n-        # Show the last post of the previous page\n-\n-        last_page = paginator.page(page_nbr - 1).object_list\n-        last_post = last_page[len(last_page) - 1]\n-        res.append(last_post)\n-    for post in posts:\n-        res.append(post)\n-\n-    # Build form to send a post for the current topic.\n-\n-    form = PostForm(topic, request.user)\n-    form.helper.form_action = reverse(\"zds.forum.views.answer\") + \"?sujet=\" \\\n-        + str(topic.pk)\n-    form_move = MoveTopicForm(topic=topic)\n-\n-    return render(request, \"forum/topic/index.html\", {\n-        \"topic\": topic,\n-        \"posts\": res,\n-        \"categories\": categories,\n-        \"page\": 1,\n-        \"pages\": paginator_range(page_nbr, paginator.num_pages),\n-        \"nb\": page_nbr,\n-        \"last_post_pk\": last_post_pk,\n-        \"form\": form,\n-        \"form_move\": form_move,\n-    })\n-\n-\n-def get_tag_by_title(title):\n-    \"\"\"\n-    Extract tags from title.\n-    In a title, tags can be set this way:\n-    > [Tag 1][Tag 2] There is the real title\n-    Rules to detect tags:\n-    - Tags are enclosed in square brackets. This allows multi-word tags instead of hashtags.\n-    - Tags can embed square brackets: [Tag] is a valid tag and must be written [[Tag]] in the raw title\n-    - All tags must be declared at the beginning of the title. Example: _\"Title [tag]\"_ will not create a tag.\n-    - Tags and title correctness (example: empty tag/title detection) is **not** checked here\n-    :param title: The raw title\n-    :return: A tuple: (the tag list, the title without the tags).\n-    \"\"\"\n-    nb_bracket = 0\n-    current_tag = u\"\"\n-    current_title = u\"\"\n-    tags = []\n-    continue_parsing_tags = True\n-    original_title = title\n-    for char in title:\n-\n-        if char == u\"[\" and nb_bracket == 0 and continue_parsing_tags:\n-            nb_bracket += 1\n-        elif nb_bracket > 0 and char != u\"]\" and continue_parsing_tags:\n-            current_tag = current_tag + char\n-            if char == u\"[\":\n-                nb_bracket += 1\n-        elif char == u\"]\" and nb_bracket > 0 and continue_parsing_tags:\n-            nb_bracket -= 1\n-            if nb_bracket == 0 and current_tag.strip() != u\"\":\n-                tags.append(current_tag.strip())\n-                current_tag = u\"\"\n-            elif current_tag.strip() != u\"\" and nb_bracket > 0:\n-                current_tag = current_tag + char\n-\n-        elif (char != u\"[\" and char.strip() != \"\") or not continue_parsing_tags:\n-            continue_parsing_tags = False\n-            current_title = current_title + char\n-    title = current_title\n-    # if we did not succed in parsing the tags\n-    if nb_bracket != 0:\n-        return ([], original_title)\n-\n-    return (tags, title.strip())\n+    template_name = 'forum/topic/new.html'\n+    form_class = TopicForm\n+    object = None\n \n+    @method_decorator(login_required)\n+    @method_decorator(can_write_and_read_now)\n+    @method_decorator(transaction.atomic)\n+    def dispatch(self, request, *args, **kwargs):\n+        self.object = self.get_object()\n+        if not self.object.can_read(request.user):\n+            raise PermissionDenied\n+        return super(TopicNew, self).dispatch(request, *args, **kwargs)\n \n-@can_write_and_read_now\n-@login_required\n-@transaction.atomic\n-def new(request):\n-    \"\"\"\n-    Creates a new topic in a forum.\n-    \"\"\"\n+    def get_object(self, queryset=None):\n+        try:\n+            forum_pk = int(self.request.GET.get('forum'))\n+        except (KeyError, ValueError, TypeError):\n+            raise Http404\n+        return get_object_or_404(Forum, pk=forum_pk)\n \n-    try:\n-        forum_pk = int(request.GET[\"forum\"])\n-    except (KeyError, ValueError):\n-        # problem in variable format\n-        raise Http404\n-    forum = get_object_or_404(Forum, pk=forum_pk)\n-    if not forum.can_read(request.user):\n-        raise PermissionDenied\n-    if request.method == \"POST\":\n+    def get(self, request, *args, **kwargs):\n+        return render(request, self.template_name, {'forum': self.object, 'form': self.form_class()})\n \n-        # If the client is using the \"preview\" button\n+    def post(self, request, *args, **kwargs):\n+        form = self.get_form(self.form_class)\n \n         if \"preview\" in request.POST:\n             if request.is_ajax():\n                 content = render_to_response('misc/previsualization.part.html', {'text': request.POST['text']})\n                 return StreamingHttpResponse(content)\n             else:\n-                form = TopicForm(initial={\"title\": request.POST[\"title\"],\n-                                          \"subtitle\": request.POST[\"subtitle\"],\n-                                          \"text\": request.POST[\"text\"]})\n-\n-                return render(request, \"forum/topic/new.html\",\n-                                       {\"forum\": forum,\n-                                        \"form\": form,\n-                                        \"text\": request.POST[\"text\"]})\n-        form = TopicForm(request.POST)\n-        data = form.data\n-        if form.is_valid():\n-\n-            # Treat title\n-\n-            (tags, title) = get_tag_by_title(data[\"title\"])\n-\n-            # Creating the thread\n-            n_topic = Topic()\n-            n_topic.forum = forum\n-            n_topic.title = title\n-            n_topic.subtitle = data[\"subtitle\"]\n-            n_topic.pubdate = datetime.now()\n-            n_topic.author = request.user\n-            n_topic.save()\n-            # add tags\n-\n-            n_topic.add_tags(tags)\n-            n_topic.save()\n-            # Adding the first message\n-\n-            post = Post()\n-            post.topic = n_topic\n-            post.author = request.user\n-            post.update_content(request.POST[\"text\"])\n-            post.pubdate = datetime.now()\n-            post.position = 1\n-            post.ip_address = get_client_ip(request)\n-            post.save()\n-            n_topic.last_message = post\n-            n_topic.save()\n-\n-            # Follow the topic\n-\n-            follow(n_topic)\n-            return redirect(n_topic.get_absolute_url())\n-    else:\n-        form = TopicForm()\n-\n-    return render(request, \"forum/topic/new.html\", {\"forum\": forum, \"form\": form})\n-\n-\n-@can_write_and_read_now\n-@login_required\n-@require_POST\n-@transaction.atomic\n-def solve_alert(request):\n-    \"\"\"\n-    Solves an alert (i.e. delete it from alert list) and sends an email to the user that created the alert, if the\n-    resolver leaves a comment.\n-    This can only be done by staff.\n-    \"\"\"\n-\n-    if not request.user.has_perm(\"forum.change_post\"):\n-        raise PermissionDenied\n-\n-    alert = get_object_or_404(Alert, pk=request.POST[\"alert_pk\"])\n-    post = Post.objects.get(pk=alert.comment.id)\n-\n-    if \"text\" in request.POST and request.POST[\"text\"] != \"\":\n-        bot = get_object_or_404(User, username=settings.ZDS_APP['member']['bot_account'])\n-        msg = \\\n-            (u'Bonjour {0},'\n-             u'Vous recevez ce message car vous avez signalé le message de *{1}*, '\n-             u'dans le sujet [{2}]({3}). Votre alerte a été traitée par **{4}** '\n-             u'et il vous a laissé le message suivant :'\n-             u'\\n\\n> {5}\\n\\nToute l\\'équipe de la modération vous remercie !'.format(\n-                 alert.author.username,\n-                 post.author.username,\n-                 post.topic.title,\n-                 settings.ZDS_APP['site']['url'] + post.get_absolute_url(),\n-                 request.user.username,\n-                 request.POST[\"text\"],))\n-        send_mp(\n-            bot,\n-            [alert.author],\n-            u\"Résolution d'alerte : {0}\".format(post.topic.title),\n-            \"\",\n-            msg,\n-            False,\n+                initial = {\n+                    \"title\": request.POST[\"title\"],\n+                    \"subtitle\": request.POST[\"subtitle\"],\n+                    \"text\": request.POST[\"text\"]\n+                }\n+                form = self.form_class(initial=initial)\n+        elif form.is_valid():\n+            return self.form_valid(form)\n+        return render(request, self.template_name, {'forum': self.object, 'form': form})\n+\n+    def get_form(self, form_class):\n+        return form_class(self.request.POST)\n+\n+    def form_valid(self, form):\n+        topic = create_topic(\n+            self.request,\n+            self.request.user,\n+            self.object,\n+            form.data['title'],\n+            form.data['subtitle'],\n+            form.data['text'],\n+            None\n         )\n-\n-    alert.delete()\n-    messages.success(request, u\"L'alerte a bien été résolue.\")\n-    return redirect(post.get_absolute_url())\n+        return redirect(topic.get_absolute_url())\n \n \n-@can_write_and_read_now\n-@login_required\n-@require_POST\n-@transaction.atomic\n-def move_topic(request):\n-    \"\"\"\n-    Moves a topic to another forum.\n-    This can only be done by staff.\n-    \"\"\"\n+class TopicEdit(UpdateView, SingleObjectMixin, TopicEditMixin):\n \n-    if not request.user.has_perm(\"forum.change_topic\"):\n-        raise PermissionDenied\n-    try:\n-        topic_pk = int(request.GET[\"sujet\"])\n-    except (KeyError, ValueError):\n-        # problem in variable format\n-        raise Http404\n-    forum = get_object_or_404(Forum, pk=request.POST[\"forum\"])\n-    if not forum.can_read(request.user):\n-        raise PermissionDenied\n-    topic = get_object_or_404(Topic, pk=topic_pk)\n-    topic.forum = forum\n-    topic.save()\n-\n-    # If the topic is moved in a restricted forum, users that cannot read this topic any more un-follow it.\n-    # This avoids unreachable notifications.\n-    followers = TopicFollowed.objects.filter(topic=topic)\n-    for follower in followers:\n-        if not forum.can_read(follower.user):\n-            follower.delete()\n-    messages.success(request,\n-                     u\"Le sujet {0} a bien été déplacé dans {1}.\"\n-                     .format(topic.title,\n-                             forum.title))\n-    return redirect(topic.get_absolute_url())\n+    template_name = 'forum/topic/edit.html'\n+    form_class = TopicForm\n+    object = None\n+    page = 1\n \n+    @method_decorator(login_required)\n+    @method_decorator(can_write_and_read_now)\n+    @method_decorator(transaction.atomic)\n+    def dispatch(self, request, *args, **kwargs):\n+        self.object = self.get_object()\n+        if not self.object.forum.can_read(request.user):\n+            return redirect(reverse('cats-forums-list'))\n+        if ('text' in request.POST or request.method == 'GET') \\\n+                and self.object.author != request.user and not request.user.has_perm('forum.change_topic'):\n+            raise PermissionDenied\n+        if 'page' in request.POST:\n+            try:\n+                self.page = int(request.POST.get('page'))\n+            except (KeyError, ValueError, TypeError):\n+                self.page = 1\n+        return super(TopicEdit, self).dispatch(request, *args, **kwargs)\n+\n+    def get(self, request, *args, **kwargs):\n+        if self.object.author != request.user and request.user.has_perm(\"forum.change_topic\"):\n+            messages.warning(request, _(\n+                u'Vous éditez un topic en tant que modérateur (auteur : {}). Soyez encore plus '\n+                u'prudent lors de l\\'édition de celui-ci !').format(self.object.author.username))\n+        prefix = ''\n+        for tag in self.object.tags.all():\n+            prefix += u'[{0}]'.format(tag.title)\n+\n+        form = self.create_form(self.form_class, **{\n+            'title': u'{0} {1}'.format(prefix, self.object.title).strip(),\n+            'subtitle': self.object.subtitle,\n+            'text': self.object.first_post().text\n+        })\n+        return render(request, self.template_name, {'topic': self.object, 'form': form})\n \n-@can_write_and_read_now\n-@login_required\n-@require_POST\n-def edit(request):\n-    \"\"\"\n-    Edit the given topic metadata: follow, follow by email, solved, locked, sticky.\n-    \"\"\"\n+    def post(self, request, *args, **kwargs):\n+        if 'text' in request.POST:\n+            form = self.get_form(self.form_class)\n \n-    try:\n-        topic_pk = request.POST['topic']\n-    except (KeyError, ValueError):\n-        # problem in variable format\n-        raise Http404\n-    if \"page\" in request.POST:\n+            if \"preview\" in request.POST:\n+                if request.is_ajax():\n+                    content = render_to_response('misc/previsualization.part.html', {'text': request.POST['text']})\n+                    return StreamingHttpResponse(content)\n+                else:\n+                    form = self.create_form(self.form_class, **{\n+                        'title': request.POST.get('title'),\n+                        'subtitle': request.POST.get('subtitle'),\n+                        'text': request.POST.get('text')\n+                    })\n+            elif form.is_valid():\n+                return self.form_valid(form)\n+            return render(request, self.template_name, {'topic': self.object, 'form': form})\n+\n+        response = {}\n+        if 'follow' in request.POST:\n+            response['follow'] = self.perform_follow(self.object, request.user)\n+        if 'email' in request.POST:","path":"zds/forum/views.py","position":625,"original_position":625,"commit_id":"3e0e46d798ec152a5f55a5016c02563c79ab6958","original_commit_id":"3e0e46d798ec152a5f55a5016c02563c79ab6958","user":{"login":"GerardPaligot","id":1913901,"avatar_url":"https://avatars.githubusercontent.com/u/1913901?v=3","gravatar_id":"","url":"https://api.github.com/users/GerardPaligot","html_url":"https://github.com/GerardPaligot","followers_url":"https://api.github.com/users/GerardPaligot/followers","following_url":"https://api.github.com/users/GerardPaligot/following{/other_user}","gists_url":"https://api.github.com/users/GerardPaligot/gists{/gist_id}","starred_url":"https://api.github.com/users/GerardPaligot/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/GerardPaligot/subscriptions","organizations_url":"https://api.github.com/users/GerardPaligot/orgs","repos_url":"https://api.github.com/users/GerardPaligot/repos","events_url":"https://api.github.com/users/GerardPaligot/events{/privacy}","received_events_url":"https://api.github.com/users/GerardPaligot/received_events","type":"User","site_admin":false},"body":"Tout à fait.","created_at":"2015-05-15T14:33:06Z","updated_at":"2015-05-15T14:33:06Z","html_url":"https://github.com/zestedesavoir/zds-site/pull/2697#discussion_r30413042","pull_request_url":"https://api.github.com/repos/zestedesavoir/zds-site/pulls/2697","_links":{"self":{"href":"https://api.github.com/repos/zestedesavoir/zds-site/pulls/comments/30413042"},"html":{"href":"https://github.com/zestedesavoir/zds-site/pull/2697#discussion_r30413042"},"pull_request":{"href":"https://api.github.com/repos/zestedesavoir/zds-site/pulls/2697"}}},"pull_request":{"url":"https://api.github.com/repos/zestedesavoir/zds-site/pulls/2697","id":35370502,"html_url":"https://github.com/zestedesavoir/zds-site/pull/2697","diff_url":"https://github.com/zestedesavoir/zds-site/pull/2697.diff","patch_url":"https://github.com/zestedesavoir/zds-site/pull/2697.patch","issue_url":"https://api.github.com/repos/zestedesavoir/zds-site/issues/2697","number":2697,"state":"open","locked":false,"title":"Refactoring du module des forums","user":{"login":"GerardPaligot","id":1913901,"avatar_url":"https://avatars.githubusercontent.com/u/1913901?v=3","gravatar_id":"","url":"https://api.github.com/users/GerardPaligot","html_url":"https://github.com/GerardPaligot","followers_url":"https://api.github.com/users/GerardPaligot/followers","following_url":"https://api.github.com/users/GerardPaligot/following{/other_user}","gists_url":"https://api.github.com/users/GerardPaligot/gists{/gist_id}","starred_url":"https://api.github.com/users/GerardPaligot/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/GerardPaligot/subscriptions","organizations_url":"https://api.github.com/users/GerardPaligot/orgs","repos_url":"https://api.github.com/users/GerardPaligot/repos","events_url":"https://api.github.com/users/GerardPaligot/events{/privacy}","received_events_url":"https://api.github.com/users/GerardPaligot/received_events","type":"User","site_admin":false},"body":"| Q                             | R\r\n| ----------------------------- | -------------------------------------------\r\n| Correction de bugs ?          | Non\r\n| Nouvelle Fonctionnalité ?     | Oui\r\n| Tickets (_issues_) concernés  | ZEP-30\r\n\r\nQue dois faire la QA :\r\n\r\n- Lister les catégories avec ses forums\r\n- Lister les forums d'une catégorie\r\n- Lister les topics d'un forum\r\n- Lister les topics d'un membre\r\n- Lister les topics d'un tag\r\n- Créer un topic\r\n- Editer un topic (inclus marquer résolu, suivre, notifié par e-mail et les actions de modération)\r\n- Lister les messages d'un topic\r\n- Lister les messages d'un membre\r\n- Créer une réponse\r\n- Editer une réponse\r\n- Mentionner utile une réponse\r\n- Marquer un message comme non lu\r\n- Aimer ou non un message\r\n\r\nVoilà, n'hésitez pas à chercher les cas tordus et have fun!","created_at":"2015-05-13T21:03:46Z","updated_at":"2015-05-15T14:33:06Z","closed_at":null,"merged_at":null,"merge_commit_sha":"bce03c5689350c16e5604da95ab0ddf879fd2775","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/zestedesavoir/zds-site/pulls/2697/commits","review_comments_url":"https://api.github.com/repos/zestedesavoir/zds-site/pulls/2697/comments","review_comment_url":"https://api.github.com/repos/zestedesavoir/zds-site/pulls/comments{/number}","comments_url":"https://api.github.com/repos/zestedesavoir/zds-site/issues/2697/comments","statuses_url":"https://api.github.com/repos/zestedesavoir/zds-site/statuses/3e0e46d798ec152a5f55a5016c02563c79ab6958","head":{"label":"GerardPaligot:refacto_forum","ref":"refacto_forum","sha":"3e0e46d798ec152a5f55a5016c02563c79ab6958","user":{"login":"GerardPaligot","id":1913901,"avatar_url":"https://avatars.githubusercontent.com/u/1913901?v=3","gravatar_id":"","url":"https://api.github.com/users/GerardPaligot","html_url":"https://github.com/GerardPaligot","followers_url":"https://api.github.com/users/GerardPaligot/followers","following_url":"https://api.github.com/users/GerardPaligot/following{/other_user}","gists_url":"https://api.github.com/users/GerardPaligot/gists{/gist_id}","starred_url":"https://api.github.com/users/GerardPaligot/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/GerardPaligot/subscriptions","organizations_url":"https://api.github.com/users/GerardPaligot/orgs","repos_url":"https://api.github.com/users/GerardPaligot/repos","events_url":"https://api.github.com/users/GerardPaligot/events{/privacy}","received_events_url":"https://api.github.com/users/GerardPaligot/received_events","type":"User","site_admin":false},"repo":{"id":20162464,"name":"zds-site","full_name":"GerardPaligot/zds-site","owner":{"login":"GerardPaligot","id":1913901,"avatar_url":"https://avatars.githubusercontent.com/u/1913901?v=3","gravatar_id":"","url":"https://api.github.com/users/GerardPaligot","html_url":"https://github.com/GerardPaligot","followers_url":"https://api.github.com/users/GerardPaligot/followers","following_url":"https://api.github.com/users/GerardPaligot/following{/other_user}","gists_url":"https://api.github.com/users/GerardPaligot/gists{/gist_id}","starred_url":"https://api.github.com/users/GerardPaligot/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/GerardPaligot/subscriptions","organizations_url":"https://api.github.com/users/GerardPaligot/orgs","repos_url":"https://api.github.com/users/GerardPaligot/repos","events_url":"https://api.github.com/users/GerardPaligot/events{/privacy}","received_events_url":"https://api.github.com/users/GerardPaligot/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/GerardPaligot/zds-site","description":"Dépot ZDS","fork":true,"url":"https://api.github.com/repos/GerardPaligot/zds-site","forks_url":"https://api.github.com/repos/GerardPaligot/zds-site/forks","keys_url":"https://api.github.com/repos/GerardPaligot/zds-site/keys{/key_id}","collaborators_url":"https://api.github.com/repos/GerardPaligot/zds-site/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/GerardPaligot/zds-site/teams","hooks_url":"https://api.github.com/repos/GerardPaligot/zds-site/hooks","issue_events_url":"https://api.github.com/repos/GerardPaligot/zds-site/issues/events{/number}","events_url":"https://api.github.com/repos/GerardPaligot/zds-site/events","assignees_url":"https://api.github.com/repos/GerardPaligot/zds-site/assignees{/user}","branches_url":"https://api.github.com/repos/GerardPaligot/zds-site/branches{/branch}","tags_url":"https://api.github.com/repos/GerardPaligot/zds-site/tags","blobs_url":"https://api.github.com/repos/GerardPaligot/zds-site/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/GerardPaligot/zds-site/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/GerardPaligot/zds-site/git/refs{/sha}","trees_url":"https://api.github.com/repos/GerardPaligot/zds-site/git/trees{/sha}","statuses_url":"https://api.github.com/repos/GerardPaligot/zds-site/statuses/{sha}","languages_url":"https://api.github.com/repos/GerardPaligot/zds-site/languages","stargazers_url":"https://api.github.com/repos/GerardPaligot/zds-site/stargazers","contributors_url":"https://api.github.com/repos/GerardPaligot/zds-site/contributors","subscribers_url":"https://api.github.com/repos/GerardPaligot/zds-site/subscribers","subscription_url":"https://api.github.com/repos/GerardPaligot/zds-site/subscription","commits_url":"https://api.github.com/repos/GerardPaligot/zds-site/commits{/sha}","git_commits_url":"https://api.github.com/repos/GerardPaligot/zds-site/git/commits{/sha}","comments_url":"https://api.github.com/repos/GerardPaligot/zds-site/comments{/number}","issue_comment_url":"https://api.github.com/repos/GerardPaligot/zds-site/issues/comments{/number}","contents_url":"https://api.github.com/repos/GerardPaligot/zds-site/contents/{+path}","compare_url":"https://api.github.com/repos/GerardPaligot/zds-site/compare/{base}...{head}","merges_url":"https://api.github.com/repos/GerardPaligot/zds-site/merges","archive_url":"https://api.github.com/repos/GerardPaligot/zds-site/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/GerardPaligot/zds-site/downloads","issues_url":"https://api.github.com/repos/GerardPaligot/zds-site/issues{/number}","pulls_url":"https://api.github.com/repos/GerardPaligot/zds-site/pulls{/number}","milestones_url":"https://api.github.com/repos/GerardPaligot/zds-site/milestones{/number}","notifications_url":"https://api.github.com/repos/GerardPaligot/zds-site/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/GerardPaligot/zds-site/labels{/name}","releases_url":"https://api.github.com/repos/GerardPaligot/zds-site/releases{/id}","created_at":"2014-05-25T18:43:41Z","updated_at":"2015-05-07T22:01:32Z","pushed_at":"2015-05-15T13:56:27Z","git_url":"git://github.com/GerardPaligot/zds-site.git","ssh_url":"git@github.com:GerardPaligot/zds-site.git","clone_url":"https://github.com/GerardPaligot/zds-site.git","svn_url":"https://github.com/GerardPaligot/zds-site","homepage":null,"size":83784,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":2,"forks":0,"open_issues":2,"watchers":0,"default_branch":"refacto_mp"}},"base":{"label":"zestedesavoir:dev","ref":"dev","sha":"93559e3a308098946987b593fda781a03d9c487a","user":{"login":"zestedesavoir","id":7406239,"avatar_url":"https://avatars.githubusercontent.com/u/7406239?v=3","gravatar_id":"","url":"https://api.github.com/users/zestedesavoir","html_url":"https://github.com/zestedesavoir","followers_url":"https://api.github.com/users/zestedesavoir/followers","following_url":"https://api.github.com/users/zestedesavoir/following{/other_user}","gists_url":"https://api.github.com/users/zestedesavoir/gists{/gist_id}","starred_url":"https://api.github.com/users/zestedesavoir/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/zestedesavoir/subscriptions","organizations_url":"https://api.github.com/users/zestedesavoir/orgs","repos_url":"https://api.github.com/users/zestedesavoir/repos","events_url":"https://api.github.com/users/zestedesavoir/events{/privacy}","received_events_url":"https://api.github.com/users/zestedesavoir/received_events","type":"Organization","site_admin":false},"repo":{"id":14778920,"name":"zds-site","full_name":"zestedesavoir/zds-site","owner":{"login":"zestedesavoir","id":7406239,"avatar_url":"https://avatars.githubusercontent.com/u/7406239?v=3","gravatar_id":"","url":"https://api.github.com/users/zestedesavoir","html_url":"https://github.com/zestedesavoir","followers_url":"https://api.github.com/users/zestedesavoir/followers","following_url":"https://api.github.com/users/zestedesavoir/following{/other_user}","gists_url":"https://api.github.com/users/zestedesavoir/gists{/gist_id}","starred_url":"https://api.github.com/users/zestedesavoir/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/zestedesavoir/subscriptions","organizations_url":"https://api.github.com/users/zestedesavoir/orgs","repos_url":"https://api.github.com/users/zestedesavoir/repos","events_url":"https://api.github.com/users/zestedesavoir/events{/privacy}","received_events_url":"https://api.github.com/users/zestedesavoir/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/zestedesavoir/zds-site","description":"Dépot ZDS","fork":false,"url":"https://api.github.com/repos/zestedesavoir/zds-site","forks_url":"https://api.github.com/repos/zestedesavoir/zds-site/forks","keys_url":"https://api.github.com/repos/zestedesavoir/zds-site/keys{/key_id}","collaborators_url":"https://api.github.com/repos/zestedesavoir/zds-site/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/zestedesavoir/zds-site/teams","hooks_url":"https://api.github.com/repos/zestedesavoir/zds-site/hooks","issue_events_url":"https://api.github.com/repos/zestedesavoir/zds-site/issues/events{/number}","events_url":"https://api.github.com/repos/zestedesavoir/zds-site/events","assignees_url":"https://api.github.com/repos/zestedesavoir/zds-site/assignees{/user}","branches_url":"https://api.github.com/repos/zestedesavoir/zds-site/branches{/branch}","tags_url":"https://api.github.com/repos/zestedesavoir/zds-site/tags","blobs_url":"https://api.github.com/repos/zestedesavoir/zds-site/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/zestedesavoir/zds-site/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/zestedesavoir/zds-site/git/refs{/sha}","trees_url":"https://api.github.com/repos/zestedesavoir/zds-site/git/trees{/sha}","statuses_url":"https://api.github.com/repos/zestedesavoir/zds-site/statuses/{sha}","languages_url":"https://api.github.com/repos/zestedesavoir/zds-site/languages","stargazers_url":"https://api.github.com/repos/zestedesavoir/zds-site/stargazers","contributors_url":"https://api.github.com/repos/zestedesavoir/zds-site/contributors","subscribers_url":"https://api.github.com/repos/zestedesavoir/zds-site/subscribers","subscription_url":"https://api.github.com/repos/zestedesavoir/zds-site/subscription","commits_url":"https://api.github.com/repos/zestedesavoir/zds-site/commits{/sha}","git_commits_url":"https://api.github.com/repos/zestedesavoir/zds-site/git/commits{/sha}","comments_url":"https://api.github.com/repos/zestedesavoir/zds-site/comments{/number}","issue_comment_url":"https://api.github.com/repos/zestedesavoir/zds-site/issues/comments{/number}","contents_url":"https://api.github.com/repos/zestedesavoir/zds-site/contents/{+path}","compare_url":"https://api.github.com/repos/zestedesavoir/zds-site/compare/{base}...{head}","merges_url":"https://api.github.com/repos/zestedesavoir/zds-site/merges","archive_url":"https://api.github.com/repos/zestedesavoir/zds-site/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/zestedesavoir/zds-site/downloads","issues_url":"https://api.github.com/repos/zestedesavoir/zds-site/issues{/number}","pulls_url":"https://api.github.com/repos/zestedesavoir/zds-site/pulls{/number}","milestones_url":"https://api.github.com/repos/zestedesavoir/zds-site/milestones{/number}","notifications_url":"https://api.github.com/repos/zestedesavoir/zds-site/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/zestedesavoir/zds-site/labels{/name}","releases_url":"https://api.github.com/repos/zestedesavoir/zds-site/releases{/id}","created_at":"2013-11-28T14:23:48Z","updated_at":"2015-05-15T06:34:45Z","pushed_at":"2015-05-15T13:56:27Z","git_url":"git://github.com/zestedesavoir/zds-site.git","ssh_url":"git@github.com:zestedesavoir/zds-site.git","clone_url":"https://github.com/zestedesavoir/zds-site.git","svn_url":"https://github.com/zestedesavoir/zds-site","homepage":null,"size":166375,"stargazers_count":92,"watchers_count":92,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":true,"forks_count":85,"mirror_url":null,"open_issues_count":230,"forks":85,"open_issues":230,"watchers":92,"default_branch":"dev"}},"_links":{"self":{"href":"https://api.github.com/repos/zestedesavoir/zds-site/pulls/2697"},"html":{"href":"https://github.com/zestedesavoir/zds-site/pull/2697"},"issue":{"href":"https://api.github.com/repos/zestedesavoir/zds-site/issues/2697"},"comments":{"href":"https://api.github.com/repos/zestedesavoir/zds-site/issues/2697/comments"},"review_comments":{"href":"https://api.github.com/repos/zestedesavoir/zds-site/pulls/2697/comments"},"review_comment":{"href":"https://api.github.com/repos/zestedesavoir/zds-site/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/zestedesavoir/zds-site/pulls/2697/commits"},"statuses":{"href":"https://api.github.com/repos/zestedesavoir/zds-site/statuses/3e0e46d798ec152a5f55a5016c02563c79ab6958"}}}}