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