teh publish button again
authorRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Tue, 11 Oct 2011 10:29:05 +0000 (12:29 +0200)
committerRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Tue, 11 Oct 2011 10:31:09 +0000 (12:31 +0200)
apps/apiclient/__init__.py
apps/apiclient/urls.py
apps/apiclient/views.py
apps/catalogue/models/book.py
apps/catalogue/models/chunk.py
apps/catalogue/templates/catalogue/book_detail.html
apps/catalogue/tests.py [new file with mode: 0755]
apps/catalogue/views.py
redakcja/localsettings.sample
redakcja/settings/test.py
requirements-test.txt

index 7913ac3..d44e016 100644 (file)
@@ -22,25 +22,29 @@ class NotAuthorizedError(BaseException):
 
 
 def api_call(user, path, data=None):
 
 
 def api_call(user, path, data=None):
-    # what if not verified?
     conn = OAuthConnection.get(user)
     if not conn.access:
         raise NotAuthorizedError("No WL authorization for user %s." % user)
     token = oauth2.Token(conn.token, conn.token_secret)
     client = oauth2.Client(wl_consumer, token)
     if data is not None:
     conn = OAuthConnection.get(user)
     if not conn.access:
         raise NotAuthorizedError("No WL authorization for user %s." % user)
     token = oauth2.Token(conn.token, conn.token_secret)
     client = oauth2.Client(wl_consumer, token)
     if data is not None:
+        data = simplejson.dumps(data)
+        data = urllib.urlencode({"data": data})
         resp, content = client.request(
         resp, content = client.request(
-                "%s%s.json" % (WL_API_URL, path),
+                "%s%s" % (WL_API_URL, path),
                 method="POST",
                 method="POST",
-                body=urllib.urlencode(data))
+                body=data)
     else:
         resp, content = client.request(
     else:
         resp, content = client.request(
-                "%s%s.json" % (WL_API_URL, path))
+                "%s%s" % (WL_API_URL, path))
     status = resp['status']
     status = resp['status']
+
     if status == '200':
         return simplejson.loads(content)
     elif status.startswith('2'):
         return
     if status == '200':
         return simplejson.loads(content)
     elif status.startswith('2'):
         return
+    elif status == '401':
+        raise ApiError('User not authorized for publishing.')
     else:
         raise ApiError("WL API call error")
 
     else:
         raise ApiError("WL API call error")
 
index 5e54965..87d9997 100755 (executable)
@@ -1,6 +1,6 @@
 from django.conf.urls.defaults import *
 
 urlpatterns = patterns('apiclient.views',
 from django.conf.urls.defaults import *
 
 urlpatterns = patterns('apiclient.views',
-    url(r'^oauth/$', 'oauth', name='users_oauth'),
-    url(r'^oauth_callback/$', 'oauth_callback', name='users_oauth_callback'),
+    url(r'^oauth/$', 'oauth', name='apiclient_oauth'),
+    url(r'^oauth_callback/$', 'oauth_callback', name='apiclient_oauth_callback'),
 )
 )
index f851590..d496014 100644 (file)
@@ -33,7 +33,7 @@ def oauth(request):
     url = "%s?oauth_token=%s&oauth_callback=%s" % (
             WL_AUTHORIZE_URL, 
             request_token['oauth_token'],
     url = "%s?oauth_token=%s&oauth_callback=%s" % (
             WL_AUTHORIZE_URL, 
             request_token['oauth_token'],
-            request.build_absolute_uri(reverse("users_oauth_callback")),
+            request.build_absolute_uri(reverse("apiclient_oauth_callback")),
             )
 
     return HttpResponseRedirect(url)
             )
 
     return HttpResponseRedirect(url)
index fb59388..f4e025e 100755 (executable)
@@ -3,10 +3,15 @@
 # This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 # This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
+from django.contrib.sites.models import Site
 from django.db import models
 from django.template.loader import render_to_string
 from django.utils.translation import ugettext_lazy as _
 from slughifi import slughifi
 from django.db import models
 from django.template.loader import render_to_string
 from django.utils.translation import ugettext_lazy as _
 from slughifi import slughifi
+from librarian import NoDublinCore, ParseError, ValidationError
+from librarian.dcparser import BookInfo
+
+import apiclient
 from catalogue.helpers import cached_in_field
 from catalogue.models import BookPublishRecord, ChunkPublishRecord
 from catalogue.signals import post_publish
 from catalogue.helpers import cached_in_field
 from catalogue.models import BookPublishRecord, ChunkPublishRecord
 from catalogue.signals import post_publish
@@ -113,7 +118,7 @@ class Book(models.Model):
                 chunk.title = title[:255]
                 chunk.save()
             else:
                 chunk.title = title[:255]
                 chunk.save()
             else:
-                chunk = instance.add(slug, title, adjust_slug=True)
+                chunk = instance.add(slug, title)
 
             chunk.commit(text, **commit_args)
 
 
             chunk.commit(text, **commit_args)
 
@@ -186,13 +191,25 @@ class Book(models.Model):
         except IndexError:
             return None
 
         except IndexError:
             return None
 
-    def publishable(self):
-        if not self.chunk_set.exists():
-            return False
-        for chunk in self:
-            if not chunk.publishable():
-                return False
-        return True
+    def assert_publishable(self):
+        assert self.chunk_set.exists(), _('No chunks in the book.')
+        try:
+            changes = self.get_current_changes(publishable=True)
+        except self.NoTextError:
+            raise AssertionError(_('Not all chunks have publishable revisions.'))
+        book_xml = self.materialize(changes=changes)
+
+        try:
+            bi = BookInfo.from_string(book_xml.encode('utf-8'))
+        except ParseError, e:
+            raise AssertionError(_('Invalid XML') + ': ' + str(e))
+        except NoDublinCore:
+            raise AssertionError(_('No Dublin Core found.'))
+        except ValidationError, e:
+            raise AssertionError(_('Invalid Dublin Core') + ': ' + str(e))
+
+        valid_about = "http://%s%s" % (Site.objects.get_current().domain, self.get_absolute_url())
+        assert bi.about == valid_about, _("rdf:about is not") + " " + valid_about
 
     def hidden(self):
         return self.slug.startswith('.')
 
     def hidden(self):
         return self.slug.startswith('.')
@@ -277,13 +294,10 @@ class Book(models.Model):
         """
             Publishes a book on behalf of a (local) user.
         """
         """
             Publishes a book on behalf of a (local) user.
         """
-        raise NotImplementedError("Publishing not possible yet.")
-
-        from apiclient import api_call
-
+        self.assert_publishable()
         changes = self.get_current_changes(publishable=True)
         book_xml = self.materialize(changes=changes)
         changes = self.get_current_changes(publishable=True)
         book_xml = self.materialize(changes=changes)
-        #api_call(user, "books", {"book_xml": book_xml})
+        apiclient.api_call(user, "books/", {"book_xml": book_xml})
         # record the publish
         br = BookPublishRecord.objects.create(book=self, user=user)
         for c in changes:
         # record the publish
         br = BookPublishRecord.objects.create(book=self, user=user)
         for c in changes:
index d373e04..8b8f56b 100755 (executable)
@@ -63,7 +63,7 @@ class Chunk(dvcs_models.Document):
     # Creating and manipulation
     # =========================
 
     # Creating and manipulation
     # =========================
 
-    def split(self, slug, title='', adjust_slug=False, **kwargs):
+    def split(self, slug, title='', **kwargs):
         """ Create an empty chunk after this one """
         self.book.chunk_set.filter(number__gt=self.number).update(
                 number=models.F('number')+1)
         """ Create an empty chunk after this one """
         self.book.chunk_set.filter(number__gt=self.number).update(
                 number=models.F('number')+1)
index 8e9c1f1..19e673c 100755 (executable)
@@ -68,7 +68,7 @@
 
 <p>{% trans "Last published" %}: {{ book.last_published }}</p>
 
 
 <p>{% trans "Last published" %}: {{ book.last_published }}</p>
 
-{% if book.publishable %}
+{% if publishable %}
     <p>
     <a href="{% url catalogue_book_xml book.slug %}">{% trans "Full XML" %}</a><br/>
     <a target="_blank" href="{% url catalogue_book_html book.slug %}">{% trans "HTML version" %}</a><br/>
     <p>
     <a href="{% url catalogue_book_xml book.slug %}">{% trans "Full XML" %}</a><br/>
     <a target="_blank" href="{% url catalogue_book_html book.slug %}">{% trans "HTML version" %}</a><br/>
     {% endcomment %}
     </p>
 
     {% endcomment %}
     </p>
 
-    {% trans "This book cannot be published yet" %}
-    {% comment %}
-    <!--
-    Angel photos:
-    Angels in Ely Cathedral (http://www.flickr.com/photos/21804434@N02/4483220595/) /
-    mira66 (http://www.flickr.com/photos/21804434@N02/) /
-    CC BY 2.0 (http://creativecommons.org/licenses/by/2.0/)
-    -->
-    <form method="POST" action="{% url catalogue_publish book.slug %}">{% csrf_token %}
-        <img src="{{ STATIC_URL }}img/angel-left.png" style="vertical-align: middle" />
-        <button id="publish-button" type="submit">
-            <span>{% trans "Publish" %}</span></button>
-        <img src="{{ STATIC_URL }}img/angel-right.png" style="vertical-align: middle" />
-        </form>
-    {% endcomment %}
+    {% if user.is_authenticated %}
+        <!--
+        Angel photos:
+        Angels in Ely Cathedral (http://www.flickr.com/photos/21804434@N02/4483220595/) /
+        mira66 (http://www.flickr.com/photos/21804434@N02/) /
+        CC BY 2.0 (http://creativecommons.org/licenses/by/2.0/)
+        -->
+        <form method="POST" action="{% url catalogue_publish book.slug %}">{% csrf_token %}
+            <img src="{{ STATIC_URL }}img/angel-left.png" style="vertical-align: middle" />
+            <button id="publish-button" type="submit">
+                <span>{% trans "Publish" %}</span></button>
+            <img src="{{ STATIC_URL }}img/angel-right.png" style="vertical-align: middle" />
+            </form>
+    {% else %}
+        <a href="{% url login %}">{% trans "Log in to publish." %}</a>
+    {% endif %}
 {% else %}
 {% else %}
-    {% trans "This book cannot be published yet" %}
+    <p>{% trans "This book can't be published yet, because:" %}</p>
+    <ul><li>{{ publishable_error }}</li></ul>
 {% endif %}
 
 {% endblock leftcolumn %}
 {% endif %}
 
 {% endblock leftcolumn %}
diff --git a/apps/catalogue/tests.py b/apps/catalogue/tests.py
new file mode 100755 (executable)
index 0000000..b73ea35
--- /dev/null
@@ -0,0 +1,31 @@
+from nose.tools import *
+from mock import patch
+from django.test import TestCase
+from django.contrib.auth.models import User
+from catalogue.models import Book, BookPublishRecord
+
+class PublishTests(TestCase):
+
+    def setUp(self):
+        self.user = User.objects.create(username='tester')
+        self.book = Book.create(self.user, 'publish me')
+
+    @patch('apiclient.api_call')
+    def test_unpublishable(self, api_call):
+        with self.assertRaises(Book.NoTextError):
+            self.book.publish(self.user)
+
+    @patch('apiclient.api_call')
+    def test_publish(self, api_call):
+        self.book[0].head.set_publishable(True)
+        self.book.publish(self.user)
+        api_call.assert_called_with(self.user, 'books', {"book_xml": 'publish me'})
+
+    @patch('apiclient.api_call')
+    def test_publish_multiple(self, api_call):
+        self.book[0].head.set_publishable(True)
+        self.book[0].split(slug='part-2')
+        self.book[1].commit('take me \n<!-- TRIM_BEGIN -->\n too')
+        self.book[1].head.set_publishable(True)
+        self.book.publish(self.user)
+        api_call.assert_called_with(self.user, 'books', {"book_xml": 'publish me\n too'})
index ef042de..487307d 100644 (file)
@@ -19,6 +19,7 @@ from django.views.generic.simple import direct_to_template
 import librarian.html
 import librarian.text
 
 import librarian.html
 import librarian.text
 
+from apiclient import NotAuthorizedError
 from catalogue import forms
 from catalogue import helpers
 from catalogue.helpers import active_tab
 from catalogue import forms
 from catalogue import helpers
 from catalogue.helpers import active_tab
@@ -294,8 +295,19 @@ def book(request, slug):
     else:
         form = None
 
     else:
         form = None
 
+    try:
+        book.assert_publishable()
+    except AssertionError, e:
+        publishable = False
+        publishable_error = e
+    else:
+        publishable = True
+        publishable_error = None
+
     return direct_to_template(request, "catalogue/book_detail.html", extra_context={
         "book": book,
     return direct_to_template(request, "catalogue/book_detail.html", extra_context={
         "book": book,
+        "publishable": publishable,
+        "publishable_error": publishable_error,
         "chunks": chunks,
         "need_fixing": need_fixing,
         "choose_master": choose_master,
         "chunks": chunks,
         "need_fixing": need_fixing,
         "choose_master": choose_master,
@@ -390,6 +402,8 @@ def publish(request, slug):
     book = get_object_or_404(Book, slug=slug)
     try:
         book.publish(request.user)
     book = get_object_or_404(Book, slug=slug)
     try:
         book.publish(request.user)
+    except NotAuthorizedError:
+        return http.HttpResponseRedirect(reverse('apiclient_oauth'))
     except BaseException, e:
         return http.HttpResponse(e)
     else:
     except BaseException, e:
         return http.HttpResponse(e)
     else:
index c1b1a19..140c52b 100644 (file)
@@ -12,7 +12,7 @@
 from redakcja.settings import *
 
 # Path to repository with managed documents
 from redakcja.settings import *
 
 # Path to repository with managed documents
-WIKI_REPOSITORY_PATH = '/srv/redakcja/books'
+CATALOGUE_REPO_PATH = '/srv/redakcja/books'
 
 LOGGING_CONFIG_FILE = "/srv/redakcja/logging.cfg.dev"
 
 
 LOGGING_CONFIG_FILE = "/srv/redakcja/logging.cfg.dev"
 
@@ -25,4 +25,7 @@ IMAGE_DIR = 'images'
 CAS_SERVER_URL = 'http://logowanie.wolnelektury.pl/cas/'
 REDMINE_URL = 'http://redmine.nowoczesnapolska.org.pl/'
 DEBUG = True
 CAS_SERVER_URL = 'http://logowanie.wolnelektury.pl/cas/'
 REDMINE_URL = 'http://redmine.nowoczesnapolska.org.pl/'
 DEBUG = True
-COMPRESS = False
\ No newline at end of file
+COMPRESS = False
+
+APICLIENT_WL_CONSUMER_KEY = None
+APICLIENT_WL_CONSUMER_SECRET = None
index 1997c96..4762257 100644 (file)
@@ -25,10 +25,10 @@ INSTALLED_APPS += ('django_nose', 'dvcs.tests')
 
 TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
 TEST_MODULES = ('catalogue', 'dvcs.tests', 'wiki', 'toolbar')
 
 TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
 TEST_MODULES = ('catalogue', 'dvcs.tests', 'wiki', 'toolbar')
-#COVER_APPS = ('catalogue', 'dvcs', 'wiki', 'toolbar')
+COVER_APPS = ('catalogue', 'dvcs', 'wiki', 'toolbar')
 NOSE_ARGS = (
     '--tests=' + ','.join(TEST_MODULES),
 NOSE_ARGS = (
     '--tests=' + ','.join(TEST_MODULES),
-    '--cover-package=' + ','.join(TEST_MODULES),
+    '--cover-package=' + ','.join(COVER_APPS),
     '-d',
     '--with-doctest',
     '--with-xunit',
     '-d',
     '--with-doctest',
     '--with-xunit',
index 2ec68c0..3a0f164 100644 (file)
@@ -1,3 +1,4 @@
 django-nose==0.1.3
 nose
 nosexcover
 django-nose==0.1.3
 nose
 nosexcover
+mock