Commit a7327547 authored by cynddl's avatar cynddl

Add finite state logic for robust status changes 🎩

parent 361504f4
...@@ -17,12 +17,20 @@ class ArticleViewSet(viewsets.ModelViewSet): ...@@ -17,12 +17,20 @@ class ArticleViewSet(viewsets.ModelViewSet):
def publish(self, request, pk=None): def publish(self, request, pk=None):
article = self.get_object() article = self.get_object()
article.publish() article.publish()
article.save()
return self.response_serialized_object(article) return self.response_serialized_object(article)
@detail_route(methods=["post"], url_path="reject") @detail_route(methods=["post"], url_path="reject")
def reject(self, request, pk=None): def reject(self, request, pk=None):
article = self.get_object() article = self.get_object()
article.status = "REJECTED" article.reject()
article.save()
return self.response_serialized_object(article)
@detail_route(methods=["post"], url_path="recover")
def recover(self, request, pk=None):
article = self.get_object()
article.recover()
article.save() article.save()
return self.response_serialized_object(article) return self.response_serialized_object(article)
...@@ -41,13 +49,13 @@ class ArticleViewSet(viewsets.ModelViewSet): ...@@ -41,13 +49,13 @@ class ArticleViewSet(viewsets.ModelViewSet):
@detail_route(methods=["post"], url_path="priority/on") @detail_route(methods=["post"], url_path="priority/on")
def priority_on(self, request, pk=None, priority=True): def priority_on(self, request, pk=None, priority=True):
article = self.get_object() article = self.get_object()
article.priority = True article.set_priority(True)
article.save() article.save()
return self.response_serialized_object(article) return self.response_serialized_object(article)
@detail_route(methods=["post"], url_path="priority/off") @detail_route(methods=["post"], url_path="priority/off")
def priority_off(self, request, pk=None, priority=True): def priority_off(self, request, pk=None, priority=True):
article = self.get_object() article = self.get_object()
article.priority = False article.set_priority(False)
article.save() article.save()
return self.response_serialized_object(article) return self.response_serialized_object(article)
...@@ -25,4 +25,4 @@ class ArticleFactory(factory.django.DjangoModelFactory): ...@@ -25,4 +25,4 @@ class ArticleFactory(factory.django.DjangoModelFactory):
published_at = FuzzyDateTime( published_at = FuzzyDateTime(
datetime.datetime(2014, 1, 1, tzinfo=pytz.UTC)) datetime.datetime(2014, 1, 1, tzinfo=pytz.UTC))
status = "PENDING" status = "NEW"
...@@ -4,12 +4,16 @@ from django.utils.translation import ugettext_lazy as _ ...@@ -4,12 +4,16 @@ from django.utils.translation import ugettext_lazy as _
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
from newspaper import Article as ArticleParser from newspaper import Article as ArticleParser
from django_und.models import VoteMixin from django_und.models import VoteMixin
from django_fsm import FSMField, transition, RETURN_VALUE
from datetime import datetime from datetime import datetime
ARTICLE_SCORE_THRESHOLD = 3
STATUS_CHOICES = ( STATUS_CHOICES = (
("PENDING", _("Pending")), ("NEW", _("New")),
("DRAFT", _("Draft")),
("PUBLISHED", _("Published")), ("PUBLISHED", _("Published")),
("REJECTED", _("Rejected")) ("REJECTED", _("Rejected"))
) )
...@@ -32,6 +36,8 @@ article content. You should aim at around 500 characters. Use bracket ellipsis ...@@ -32,6 +36,8 @@ article content. You should aim at around 500 characters. Use bracket ellipsis
class Article(VoteMixin): class Article(VoteMixin):
status = FSMField(default='NEW', choices=STATUS_CHOICES, protected=True)
url = models.URLField("URL", help_text=URL_HELP_TEXT) url = models.URLField("URL", help_text=URL_HELP_TEXT)
lang = models.CharField( lang = models.CharField(
_("Language"), choices=LANG_CHOICES, default="NA", max_length=50) _("Language"), choices=LANG_CHOICES, default="NA", max_length=50)
...@@ -51,8 +57,6 @@ class Article(VoteMixin): ...@@ -51,8 +57,6 @@ class Article(VoteMixin):
updated_at = models.DateTimeField(_("Last update"), auto_now=True) updated_at = models.DateTimeField(_("Last update"), auto_now=True)
published_at = models.DateTimeField( published_at = models.DateTimeField(
_("Publication date"), blank=True, null=True) _("Publication date"), blank=True, null=True)
status = models.CharField(
_("Status"), choices=STATUS_CHOICES, default="PENDING", max_length=20)
#: priority: True if article have priority #: priority: True if article have priority
priority = models.BooleanField(default=False) priority = models.BooleanField(default=False)
...@@ -71,12 +75,44 @@ class Article(VoteMixin): ...@@ -71,12 +75,44 @@ class Article(VoteMixin):
def __str__(self): def __str__(self):
return self.title return self.title
def publish(self): # Finite state logic
if not self.published_at:
self.published_at = datetime.now()
self.status = "PUBLISHED" @transition(field=status, source='DRAFT', target='PUBLISHED')
self.save() def publish(self):
self.published_at = datetime.now()
@transition(field=status, source='NEW', target='DRAFT',
permission="can_change_status")
def recover(self):
pass
@transition(field=status, source=['NEW', 'DRAFT'], target='REJECTED',
permission="can_change_status")
def reject(self):
pass
@transition(field=status, source='DRAFT', target='DRAFT',
permission="can_change_priority")
def set_priority(self, value):
self.priority = value
@transition(field=status, source='DRAFT', target='DRAFT')
@transition(field=status, source='NEW',
target=RETURN_VALUE('NEW', 'DRAFT'), permission="can_vote")
def upvote(self, user_object):
super(Article, self).upvote(user_object)
if self.und_score >= ARTICLE_SCORE_THRESHOLD:
return 'DRAFT'
else:
return self.status
@transition(field=status, source='NEW', target='NEW', permission="can_vote")
@transition(field=status, source='DRAFT', target='DRAFT',
permission="can_vote")
def downvote(self, user_object):
super(Article, self).downvote(user_object)
# Content extraction
def parse(self): def parse(self):
lang_lower = self.lang.lower() if self.lang != "NA" else None lang_lower = self.lang.lower() if self.lang != "NA" else None
......
...@@ -119,8 +119,8 @@ ...@@ -119,8 +119,8 @@
<td class="actions-cell"> <td class="actions-cell">
<ul class="actions-list"> <ul class="actions-list">
{% if filter_view == 'flux' %} {% if filter_view == 'flux' %}
<li class="actions-item actions-item-draft"><a href="#"> <li class="actions-item actions-item-draft" onclick="javascript:call_recover({{article.id}})"><a href="#">
<i class="fa fa-chevron-right" aria-hidden="true"></i> Mettre en attente</a> <i class="fa fa-chevron-right" aria-hidden="true"></i> Forcer</a>
</li> </li>
<li class="actions-item actions-item-reject" onclick="javascript:call_reject({{article.id}})"> <li class="actions-item actions-item-reject" onclick="javascript:call_reject({{article.id}})">
<i class="fa fa-times" aria-hidden="true"></i> Rejeter <i class="fa fa-times" aria-hidden="true"></i> Rejeter
...@@ -194,19 +194,27 @@ ...@@ -194,19 +194,27 @@
}); });
} }
function clean_row(id) {
$("#row_" + id).hide();
$("#row_empty_" + id).hide();
$("#row_tags_" + id).hide();
}
function call_recover(id) {
$.post("/api/articles/" + id + "/recover/", function response(data) {
clean_row(id);
});
}
function call_reject(id) { function call_reject(id) {
$.post("/api/articles/" + id + "/reject/", function response(data) { $.post("/api/articles/" + id + "/reject/", function response(data) {
$("#row_" + id).hide(); clean_row(id);
$("#row_empty_" + id).hide();
$("#row_tags_" + id).hide();
}); });
} }
function call_publish(id) { function call_publish(id) {
$.post("/api/articles/" + id + "/publish/", function response(data) { $.post("/api/articles/" + id + "/publish/", function response(data) {
$("#row_" + id).hide(); clean_row(id);
$("#row_empty_" + id).hide();
$("#row_tags_" + id).hide();
}); });
} }
......
...@@ -105,19 +105,22 @@ class ArticleListTestCase(TestCase): ...@@ -105,19 +105,22 @@ class ArticleListTestCase(TestCase):
assert self._test_filter_view("flux") == sorted( assert self._test_filter_view("flux") == sorted(
[self.article.pk, self.article2.pk] [self.article.pk, self.article2.pk]
) )
self.article.save()
assert self._test_filter_view("published") == [] assert self._test_filter_view("published") == []
assert self._test_filter_view("draft") == [self.article.id] assert self._test_filter_view("draft") == [self.article.id]
assert self._test_filter_view("rejected") == [] assert self._test_filter_view("rejected") == []
self.article2.status = "REJECTED" self.article2.reject()
self.article2.save() self.article2.save()
assert self._test_filter_view("published") == [] assert self._test_filter_view("published") == []
assert self._test_filter_view("flux") == [self.article.id] assert self._test_filter_view("flux") == []
assert self._test_filter_view("draft") == [self.article.id] assert self._test_filter_view("draft") == [self.article.id]
assert self._test_filter_view("rejected") == [self.article2.id] assert self._test_filter_view("rejected") == [self.article2.id]
self.article.status = "PUBLISHED" self.article.publish()
self.article.save() self.article.save()
assert self._test_filter_view("published") == [self.article.id] assert self._test_filter_view("published") == [self.article.id]
......
...@@ -9,6 +9,8 @@ from rp.models import Article ...@@ -9,6 +9,8 @@ from rp.models import Article
class VoteViewTestCase(TestCase): class VoteViewTestCase(TestCase):
def setUp(self): def setUp(self):
self.article = ArticleFactory() self.article = ArticleFactory()
self.article_draft = ArticleFactory(status="DRAFT")
self.profile = ProfileFactory() self.profile = ProfileFactory()
self.user = self.profile.user self.user = self.profile.user
self.client.force_login(self.user) self.client.force_login(self.user)
...@@ -34,18 +36,20 @@ class VoteViewTestCase(TestCase): ...@@ -34,18 +36,20 @@ class VoteViewTestCase(TestCase):
def test_publish(self): def test_publish(self):
url_publish = reverse("api:article-publish", kwargs={ url_publish = reverse("api:article-publish", kwargs={
"pk": self.article.id "pk": self.article_draft.id
})
url_reject = reverse("api:article-reject", kwargs={
"pk": self.article.id
}) })
response = self.client.post(url_publish) response = self.client.post(url_publish)
assert response.status_code == 200 assert response.status_code == 200
article_db = Article.objects.get(id=self.article.id) article_db = Article.objects.get(id=self.article_draft.id)
assert article_db.status == "PUBLISHED" assert article_db.status == "PUBLISHED"
def test_reject(self):
url_reject = reverse("api:article-reject", kwargs={
"pk": self.article.id
})
response = self.client.post(url_reject) response = self.client.post(url_reject)
assert response.status_code == 200 assert response.status_code == 200
article_db = Article.objects.get(id=self.article.id) article_db = Article.objects.get(id=self.article.id)
......
...@@ -23,27 +23,17 @@ class ArticleListFlux(UDList): ...@@ -23,27 +23,17 @@ class ArticleListFlux(UDList):
def get_queryset(self): def get_queryset(self):
filter_view = self.kwargs.get("filter_view", "draft") filter_view = self.kwargs.get("filter_view", "draft")
if filter_view == "published": if filter_view in ["published", "draft", "rejected"]:
qs = Article.objects.filter(status="PUBLISHED") qs = Article.objects.filter(status=filter_view.upper())
elif filter_view == "draft":
qs = Article.objects.extra(where=[
"(und_score_up + und_score_down >= 3) or priority",
"status='PENDING'"
])
elif filter_view == "rejected":
qs = Article.objects.filter(status="REJECTED")
else: else:
qs = Article.objects.filter(status="PENDING") qs = Article.objects.filter(status="NEW")
return qs.order_by('-created_at') return qs.order_by('-created_at')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["filter_view"] = self.kwargs.get("filter_view", "draft") context["filter_view"] = self.kwargs.get("filter_view", "draft")
context["nb_draft"] = Article.objects.extra(where=[ context["nb_draft"] = Article.objects.filter(status="DRAFT").count()
"(und_score_up + und_score_down >= 3) or priority",
"status='PENDING'"
]).count()
return context return context
......
...@@ -26,6 +26,8 @@ CONTRIB_APPS = [ ...@@ -26,6 +26,8 @@ CONTRIB_APPS = [
"allauth", "allauth",
"allauth.account", "allauth.account",
"allauth.socialaccount", "allauth.socialaccount",
"django_fsm"
] ]
if DEBUG: if DEBUG:
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment