plone.restapi/plone-only-login: Support content reording on site root + support setting layo

Roel Bruggink <jenkins-z4DKO/[email protected]>
Newsgroups gmane.comp.web.zope.plone.cvs
Message-ID <[email protected]>
Repository: plone.restapi
Branch: refs/heads/plone-only-login
Date: 2017-07-01T14:28:26+02:00
Author: Roel Bruggink (jaroel) <roel-Ttk/PUy0oENmR6Xm/[email protected]>
Commit: https://github.com/plone/plone.restapi/commit/f27564f4ace3872a539042c5ae0c61749a460343

Support content reording on site root + support setting layout on site root

Files changed:
A src/plone/restapi/deserializer/site.py
A src/plone/restapi/tests/mixin_ordering.py
A src/plone/restapi/tests/test_site_deserializer.py
M CHANGES.rst
M src/plone/restapi/deserializer/configure.zcml
M src/plone/restapi/deserializer/mixins.py
M src/plone/restapi/services/content/configure.zcml
M src/plone/restapi/tests/test_atcontent_deserializer.py
M src/plone/restapi/tests/test_dxcontent_deserializer.py

diff --git a/CHANGES.rst b/CHANGES.rst
index 66f57a27..f99d1992 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -8,6 +8,12 @@ Changelog
   and breadcrumbs as components.
   [timo]
 
+- Support content reordering on the site root.
+  [jaroel]
+
+- Support setting Layout on the site root.
+  [jaroel]
+
 
 1.0a19 (2017-06-25)
 -------------------
diff --git a/src/plone/restapi/deserializer/configure.zcml b/src/plone/restapi/deserializer/configure.zcml
index 006d4482..21fcf196 100644
--- a/src/plone/restapi/deserializer/configure.zcml
+++ b/src/plone/restapi/deserializer/configure.zcml
@@ -3,6 +3,8 @@
     xmlns:zcml="http://namespaces.zope.org/zcml"
     i18n_domain="plone.restapi">
 
+  <adapter factory=".site.DeserializeSiteRootFromJson" />
+
   <adapter factory=".dxcontent.DeserializeFromJson" />
   <adapter factory=".dxfields.DefaultFieldDeserializer" />
   <adapter factory=".dxfields.DatetimeFieldDeserializer" />
diff --git a/src/plone/restapi/deserializer/mixins.py b/src/plone/restapi/deserializer/mixins.py
index c72f7ef5..98739ed3 100644
--- a/src/plone/restapi/deserializer/mixins.py
+++ b/src/plone/restapi/deserializer/mixins.py
@@ -12,11 +12,20 @@ def handle_ordering(self, data):
             delta = data['ordering']['delta']
             subset_ids = data['ordering'].get('subset_ids')
 
+            # The REST api returns only content items and a Zope resource
+            # may contain non-content items. We need to set the subset_ids
+            # so we'll move items relative to each other.
+            if not subset_ids:
+                subset_ids = self.context.contentIds()
+
             self.reorderItems(obj_id, delta, subset_ids)
 
-    def reorderItems(self, obj_id, delta, subset_ids=None):
+    def reorderItems(self, obj_id, delta, subset_ids):
         # Based on wildcard.foldercontents.viewsItemOrder
         ordering = self.getOrdering()
+        if ordering is None:
+            msg = 'Content ordering is not supported by this resource'
+            raise BadRequest(msg)
 
         # Make sure we're seeing the same order as the client is.
         if subset_ids:
@@ -38,7 +47,7 @@ def reorderItems(self, obj_id, delta, subset_ids=None):
     def getOrdering(self):
         if IPloneSiteRoot.providedBy(self.context):
             return self.context
-        else:
+        elif getattr(self.context, 'getOrdering', None):
             ordering = self.context.getOrdering()
             if not IExplicitOrdering.providedBy(ordering):
                 return None
diff --git a/src/plone/restapi/deserializer/site.py b/src/plone/restapi/deserializer/site.py
new file mode 100644
index 00000000..e1336ded
--- /dev/null
+++ b/src/plone/restapi/deserializer/site.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+from Products.CMFPlone.interfaces import IPloneSiteRoot
+from plone.restapi.deserializer import json_body
+from plone.restapi.interfaces import IDeserializeFromJson
+from zope.component import adapter
+from zope.interface import implementer, Interface
+
+from plone.restapi.deserializer.mixins import OrderingMixin
+from zope.publisher.interfaces import IRequest
+
+
+@implementer(IDeserializeFromJson)
+@adapter(IPloneSiteRoot, Interface)
+class DeserializeSiteRootFromJson(OrderingMixin, object):
+    """JSON deserializer for the Plone site root
+    """
+
+    def __init__(self, context, request):
+        self.context = context
+        self.request = request
+
+    def __call__(self, validate_all=False):
+        # Currently we only do layout and ordering, as the plone site root
+        # has no schema or something like that.
+        data = json_body(self.request)
+
+        if 'layout' in data:
+            layout = data['layout']
+            self.context.setLayout(layout)
+
+        # OrderingMixin
+        if 'ordering' in data and not 'subset_ids' in data['ordering']:
+            data['ordering']['subset_ids'] = self.context.contentIds()
+        self.handle_ordering(data)
+
+        return self.context
diff --git a/src/plone/restapi/services/content/configure.zcml b/src/plone/restapi/services/content/configure.zcml
index 9e338f5f..f3cbde14 100644
--- a/src/plone/restapi/services/content/configure.zcml
+++ b/src/plone/restapi/services/content/configure.zcml
@@ -19,6 +19,13 @@
 
   <plone:service
     method="PATCH"
+    for="Products.CMFPlone.interfaces.IPloneSiteRoot"
+    factory=".update.ContentPatch"
+    permission="cmf.ModifyPortalContent"
+    />
+
+  <plone:service
+    method="PATCH"
     for="Products.CMFCore.interfaces.IContentish"
     factory=".update.ContentPatch"
     permission="cmf.ModifyPortalContent"
diff --git a/src/plone/restapi/tests/mixin_ordering.py b/src/plone/restapi/tests/mixin_ordering.py
new file mode 100644
index 00000000..dcd9c532
--- /dev/null
+++ b/src/plone/restapi/tests/mixin_ordering.py
@@ -0,0 +1,98 @@
+import json
+
+from zExceptions import BadRequest
+
+
+class OrderingMixin():
+    # This assumes there is a self.folder with 9 documents in it.
+
+    def test_reorder(self):
+        # We run all this in one test, because of dependend ordering.
+        # initial situation
+        self.assertEquals(
+            ['doc1', 'doc2', 'doc3', 'doc4', 'doc5', 'doc6', 'doc7', 'doc8', 'doc9', ],  # noqa
+            self.folder.contentIds()
+        )
+
+        # Normal
+        # Move to top
+        data = {'ordering': {'delta': 'top', 'obj_id': 'doc9'}}
+        self.deserialize(body=json.dumps(data), context=self.folder)
+        self.assertEquals(
+            ['doc9', 'doc1', 'doc2', 'doc3', 'doc4', 'doc5', 'doc6', 'doc7', 'doc8', ],  # noqa
+            self.folder.contentIds()
+        )
+
+        # Move to bottom
+        data = {'ordering': {'delta': 'bottom', 'obj_id': 'doc9'}}
+        self.deserialize(body=json.dumps(data), context=self.folder)
+        self.assertEquals(
+            ['doc1', 'doc2', 'doc3', 'doc4', 'doc5', 'doc6', 'doc7', 'doc8', 'doc9', ],  # noqa
+            self.folder.contentIds()
+        )
+
+        # Delta up
+        data = {'ordering': {'delta': -2, 'obj_id': 'doc5'}}
+        self.deserialize(body=json.dumps(data), context=self.folder)
+        self.assertEquals(
+            ['doc1', 'doc2', 'doc5', 'doc3', 'doc4', 'doc6', 'doc7', 'doc8', 'doc9', ],  # noqa
+            self.folder.contentIds()
+        )
+
+        # Delta down
+        data = {'ordering': {'delta': 2, 'obj_id': 'doc6'}}
+        self.deserialize(body=json.dumps(data), context=self.folder)
+        self.assertEquals(
+            ['doc1', 'doc2', 'doc5', 'doc3', 'doc4', 'doc7', 'doc8', 'doc6', 'doc9', ],  # noqa
+            self.folder.contentIds()
+        )
+
+        # subset ids
+        # Move to top
+        data = {'ordering': {'delta': 'top', 'obj_id': 'doc8', 'subset_ids': ['doc2', 'doc3', 'doc8']}}  # noqa
+        self.deserialize(body=json.dumps(data), context=self.folder)
+        self.assertEquals(
+            ['doc1', 'doc8', 'doc5', 'doc2', 'doc4', 'doc7', 'doc3', 'doc6', 'doc9'],  # noqa
+            self.folder.contentIds()
+        )
+
+        # Move to bottom
+        data = {'ordering': {'delta': 'bottom', 'obj_id': 'doc8', 'subset_ids': ['doc8', 'doc2', 'doc3']}}  # noqa
+        self.deserialize(body=json.dumps(data), context=self.folder)
+        self.assertEquals(
+            ['doc1', 'doc2', 'doc5', 'doc3', 'doc4', 'doc7', 'doc8', 'doc6', 'doc9'],  # noqa
+            self.folder.contentIds()
+        )
+
+        # Delta up
+        data = {'ordering': {'delta': -1, 'obj_id': 'doc8', 'subset_ids': ['doc2', 'doc3', 'doc8']}}  # noqa
+        self.deserialize(body=json.dumps(data), context=self.folder)
+        self.assertEquals(
+            ['doc1', 'doc2', 'doc5', 'doc8', 'doc4', 'doc7', 'doc3', 'doc6', 'doc9'],  # noqa
+            self.folder.contentIds()
+        )
+
+        # Delta down
+        data = {'ordering': {'delta': 1, 'obj_id': 'doc2', 'subset_ids': ['doc2', 'doc8', 'doc3']}}  # noqa
+        self.deserialize(body=json.dumps(data), context=self.folder)
+        self.assertEquals(
+            ['doc1', 'doc8', 'doc5', 'doc2', 'doc4', 'doc7', 'doc3', 'doc6', 'doc9'],  # noqa
+            self.folder.contentIds()
+        )
+
+    def test_reorder_subsetids(self):
+        # sanity check, initial situation
+        self.assertEquals(
+            ['doc1', 'doc2', 'doc3', 'doc4', 'doc5', 'doc6', 'doc7', 'doc8', 'doc9'],  # noqa
+            self.folder.contentIds()
+        )
+
+        data = {'ordering': {'delta': 1, 'obj_id': 'doc8', 'subset_ids': ['doc2', 'doc8', 'doc6']}}  # noqa
+
+        with self.assertRaises(BadRequest) as cm:
+            self.deserialize(body=json.dumps(data), context=self.folder)
+
+        self.assertEquals(
+            u'Client/server ordering mismatch',
+            cm.exception.message
+        )
\ No newline at end of file
diff --git a/src/plone/restapi/tests/test_atcontent_deserializer.py b/src/plone/restapi/tests/test_atcontent_deserializer.py
index 2a7d5a2d..1a8fe094 100644
--- a/src/plone/restapi/tests/test_atcontent_deserializer.py
+++ b/src/plone/restapi/tests/test_atcontent_deserializer.py
@@ -7,6 +7,7 @@
 from plone.restapi.deserializer.atcontent import ValidationRequest
 from plone.restapi.interfaces import IDeserializeFromJson
 from plone.restapi.testing import PLONE_RESTAPI_AT_INTEGRATION_TESTING
+from plone.restapi.tests.mixin_ordering import OrderingMixin
 from zExceptions import BadRequest
 from zope.component import getMultiAdapter
 from zope.component import provideHandler
@@ -21,7 +22,7 @@
 import unittest
 
 
-class TestATContentDeserializer(unittest.TestCase):
+class TestATContentDeserializer(unittest.TestCase, OrderingMixin):
 
     layer = PLONE_RESTAPI_AT_INTEGRATION_TESTING
 
@@ -152,96 +153,7 @@ def test_set_layout(self):
         self.deserialize(body='{"layout": "my_new_layout"}')
         self.assertEquals('my_new_layout', self.doc1.getLayout())
 
-    def test_reorder(self):
-        # We run all this in one test, because of dependend ordering.
-        # initial situation
-        self.assertEquals(
-            ['doc1', 'doc2', 'doc3', 'doc4', 'doc5', 'doc6', 'doc7', 'doc8', 'doc9', ],  # noqa
-            self.folder.objectIds()
-        )
-
-        # Normal
-        # Move to top
-        data = {'ordering': {'delta': 'top', 'obj_id': 'doc9'}}
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc9', 'doc1', 'doc2', 'doc3', 'doc4', 'doc5', 'doc6', 'doc7', 'doc8', ],  # noqa
-            self.folder.objectIds()
-        )
-
-        # Move to bottom
-        data = {'ordering': {'delta': 'bottom', 'obj_id': 'doc9'}}
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc1', 'doc2', 'doc3', 'doc4', 'doc5', 'doc6', 'doc7', 'doc8', 'doc9', ],  # noqa
-            self.folder.objectIds()
-        )
-
-        # Delta up
-        data = {'ordering': {'delta': -2, 'obj_id': 'doc5'}}
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc1', 'doc2', 'doc5', 'doc3', 'doc4', 'doc6', 'doc7', 'doc8', 'doc9', ],  # noqa
-            self.folder.objectIds()
-        )
-
-        # Delta down
-        data = {'ordering': {'delta': 2, 'obj_id': 'doc6'}}
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc1', 'doc2', 'doc5', 'doc3', 'doc4', 'doc7', 'doc8', 'doc6', 'doc9', ],  # noqa
-            self.folder.objectIds()
-        )
-
-        # subset ids
-        # Move to top
-        data = {'ordering': {'delta': 'top', 'obj_id': 'doc8', 'subset_ids': ['doc2', 'doc3', 'doc8']}}  # noqa
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc1', 'doc8', 'doc5', 'doc2', 'doc4', 'doc7', 'doc3', 'doc6', 'doc9'],  # noqa
-            self.folder.objectIds()
-        )
-
-        # Move to bottom
-        data = {'ordering': {'delta': 'bottom', 'obj_id': 'doc8', 'subset_ids': ['doc8', 'doc2', 'doc3']}}  # noqa
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc1', 'doc2', 'doc5', 'doc3', 'doc4', 'doc7', 'doc8', 'doc6', 'doc9'],  # noqa
-            self.folder.objectIds()
-        )
 
-        # Delta up
-        data = {'ordering': {'delta': -1, 'obj_id': 'doc8', 'subset_ids': ['doc2', 'doc3', 'doc8']}}  # noqa
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc1', 'doc2', 'doc5', 'doc8', 'doc4', 'doc7', 'doc3', 'doc6', 'doc9'],  # noqa
-            self.folder.objectIds()
-        )
-
-        # Delta down
-        data = {'ordering': {'delta': 1, 'obj_id': 'doc2', 'subset_ids': ['doc2', 'doc8', 'doc3']}}  # noqa
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc1', 'doc8', 'doc5', 'doc2', 'doc4', 'doc7', 'doc3', 'doc6', 'doc9'],  # noqa
-            self.folder.objectIds()
-        )
-
-    def test_reorder_subsetids(self):
-        # sanity check, initial situation
-        self.assertEquals(
-            ['doc1', 'doc2', 'doc3', 'doc4', 'doc5', 'doc6', 'doc7', 'doc8', 'doc9'],  # noqa
-            self.folder.objectIds()
-        )
-
-        data = {'ordering': {'delta': 1, 'obj_id': 'doc8', 'subset_ids': ['doc2', 'doc8', 'doc6']}}  # noqa
-
-        with self.assertRaises(BadRequest) as cm:
-            self.deserialize(body=json.dumps(data), context=self.folder)
-
-        self.assertEquals(
-            u'Client/server ordering mismatch',
-            cm.exception.message
-        )
 
 
 class TestValidationRequest(unittest.TestCase):
diff --git a/src/plone/restapi/tests/test_dxcontent_deserializer.py b/src/plone/restapi/tests/test_dxcontent_deserializer.py
index 7e8dd906..264691c4 100644
--- a/src/plone/restapi/tests/test_dxcontent_deserializer.py
+++ b/src/plone/restapi/tests/test_dxcontent_deserializer.py
@@ -6,6 +6,7 @@
 from plone.restapi.interfaces import IDeserializeFromJson
 from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING
 from plone.restapi.tests.dxtypes import ITestAnnotationsBehavior
+from plone.restapi.tests.mixin_ordering import OrderingMixin
 from zExceptions import BadRequest
 from zope.component import getMultiAdapter
 from zope.component import provideHandler
@@ -15,7 +16,7 @@
 import unittest
 
 
-class TestDXContentDeserializer(unittest.TestCase):
+class TestDXContentDeserializer(unittest.TestCase, OrderingMixin):
 
     layer = PLONE_RESTAPI_DX_INTEGRATION_TESTING
 
@@ -147,93 +148,3 @@ def test_set_layout(self):
         self.assertNotEquals(current_layout, "my_new_layout")
         self.deserialize(body='{"layout": "my_new_layout"}')
         self.assertEquals('my_new_layout', self.portal.doc1.getLayout())
-
-    def test_reorder(self):  # noqa: ignore=E501
-        # We run all this in one test, because of dependend ordering.
-        # initial situation
-        self.assertEquals(
-            ['doc1', 'doc2', 'doc3', 'doc4', 'doc5', 'doc6', 'doc7', 'doc8', 'doc9', ],  # noqa
-            self.folder.objectIds()
-        )
-
-        # Normal
-        # Move to top
-        data = {'ordering': {'delta': 'top', 'obj_id': 'doc9'}}
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc9', 'doc1', 'doc2', 'doc3', 'doc4', 'doc5', 'doc6', 'doc7', 'doc8', ],  # noqa
-            self.folder.objectIds())
-
-        # Move to bottom
-        data = {'ordering': {'delta': 'bottom', 'obj_id': 'doc9'}}
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc1', 'doc2', 'doc3', 'doc4', 'doc5', 'doc6', 'doc7', 'doc8', 'doc9', ],  # noqa
-            self.folder.objectIds()
-        )
-
-        # Delta up
-        data = {'ordering': {'delta': -2, 'obj_id': 'doc5'}}
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc1', 'doc2', 'doc5', 'doc3', 'doc4', 'doc6', 'doc7', 'doc8', 'doc9', ],  # noqa
-            self.folder.objectIds()
-        )
-
-        # Delta down
-        data = {'ordering': {'delta': 2, 'obj_id': 'doc6'}}
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc1', 'doc2', 'doc5', 'doc3', 'doc4', 'doc7', 'doc8', 'doc6', 'doc9', ],  # noqa
-            self.folder.objectIds()
-        )
-
-        # subset ids
-        # Move to top
-        data = {'ordering': {'delta': 'top', 'obj_id': 'doc8', 'subset_ids': ['doc2', 'doc3', 'doc8']}}  # noqa
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc1', 'doc8', 'doc5', 'doc2', 'doc4', 'doc7', 'doc3', 'doc6', 'doc9'],  # noqa
-            self.folder.objectIds()
-        )
-
-        # Move to bottom
-        data = {'ordering': {'delta': 'bottom', 'obj_id': 'doc8', 'subset_ids': ['doc8', 'doc2', 'doc3']}}  # noqa
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc1', 'doc2', 'doc5', 'doc3', 'doc4', 'doc7', 'doc8', 'doc6', 'doc9'],  # noqa
-            self.folder.objectIds()
-        )
-
-        # Delta up
-        data = {'ordering': {'delta': -1, 'obj_id': 'doc8', 'subset_ids': ['doc2', 'doc3', 'doc8']}}  # noqa
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc1', 'doc2', 'doc5', 'doc8', 'doc4', 'doc7', 'doc3', 'doc6', 'doc9'],  # noqa
-            self.folder.objectIds()
-        )
-
-        # Delta down
-        data = {'ordering': {'delta': 1, 'obj_id': 'doc2', 'subset_ids': ['doc2', 'doc8', 'doc3']}}  # noqa
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc1', 'doc8', 'doc5', 'doc2', 'doc4', 'doc7', 'doc3', 'doc6', 'doc9'],  # noqa
-            self.folder.objectIds()
-        )
-
-    def test_reorder_subsetids(self):
-        # sanity check, initial situation
-        self.assertEquals(
-            ['doc1', 'doc2', 'doc3', 'doc4', 'doc5', 'doc6', 'doc7', 'doc8', 'doc9'],  # noqa
-            self.folder.objectIds()
-        )
-
-        data = {'ordering': {'delta': 1, 'obj_id': 'doc8', 'subset_ids': ['doc2', 'doc8', 'doc6']}}  # noqa
-
-        with self.assertRaises(BadRequest) as cm:
-            self.deserialize(body=json.dumps(data), context=self.folder)
-
-        self.assertEquals(
-            u'Client/server ordering mismatch',
-            cm.exception.message
-        )
diff --git a/src/plone/restapi/tests/test_site_deserializer.py b/src/plone/restapi/tests/test_site_deserializer.py
new file mode 100644
index 00000000..ad17514d
--- /dev/null
+++ b/src/plone/restapi/tests/test_site_deserializer.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+from plone.app.testing import TEST_USER_ID
+from plone.app.testing import setRoles
+from plone.dexterity.interfaces import IDexterityItem
+from plone.restapi.exceptions import DeserializationError
+from plone.restapi.interfaces import IDeserializeFromJson
+from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING
+from plone.restapi.tests.dxtypes import ITestAnnotationsBehavior
+from plone.restapi.tests.mixin_ordering import OrderingMixin
+from zExceptions import BadRequest
+from zope.component import getMultiAdapter
+from zope.component import provideHandler
+from zope.lifecycleevent.interfaces import IObjectModifiedEvent
+
+import json
+import unittest
+
+
+class TestDXContentDeserializer(unittest.TestCase, OrderingMixin):
+
+    layer = PLONE_RESTAPI_DX_INTEGRATION_TESTING
+
+    def setUp(self):
+        self.portal = self.layer['portal']
+        self.request = self.layer['request']
+
+        # ordering setup
+        self.folder = self.portal
+
+        for x in range(1, 10):
+            self.folder.invokeFactory(
+                'Document',
+                id='doc' + str(x),
+                title='Test doc ' + str(x)
+            )
+
+    def deserialize(self, body='{}', validate_all=False, context=None):
+        context = context or self.portal
+        self.request['BODY'] = body
+        deserializer = getMultiAdapter((context, self.request),
+                                       IDeserializeFromJson)
+        return deserializer(validate_all=validate_all)
+
+    def test_set_layout(self):
+        current_layout = self.portal.getLayout()
+        self.assertNotEquals(current_layout, "my_new_layout")
+        self.deserialize(body='{"layout": "my_new_layout"}')
+        self.assertEquals('my_new_layout', self.portal.getLayout())



------------------------------------------------------------------------------
Check out the vibrant tech community on one of the world's most
engaging tech sites, Slashdot.org! http://sdm.link/slashdot
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.