diff --git a/ployst/core/blips/__init__.py b/ployst/core/blips/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ployst/core/blips/admin.py b/ployst/core/blips/admin.py new file mode 100644 index 0000000..bdd4630 --- /dev/null +++ b/ployst/core/blips/admin.py @@ -0,0 +1,15 @@ +from django.contrib import admin + +from .models import Blip, Stream, Tag + + +class StreamInline(admin.TabularInline): + model = Blip.streams.through + + +class BlipAdmin(admin.ModelAdmin): + inlines = (StreamInline,) + +admin.site.register(Blip, BlipAdmin) +admin.site.register(Stream) +admin.site.register(Tag) diff --git a/ployst/core/blips/blip.py b/ployst/core/blips/blip.py new file mode 100644 index 0000000..ec00dc5 --- /dev/null +++ b/ployst/core/blips/blip.py @@ -0,0 +1,55 @@ +import re + +# import yaml + + +LEXEMS = { + 'name': r'[\w\-_]+', + 'key_separator': ':', +} + +PATTERNS = { + 'streams': r'\>(?P{name})', + 'mentions': r'@(?P{name})', + 'privates': r'!(?P{name})', + 'tags': r'#(?P{name}(?:{key_separator}{name})?)', +} + +COMPILED_PATTERNS = { + key: re.compile(value.format(**LEXEMS), flags=re.UNICODE) + for key, value in PATTERNS.items() +} + + +class Blip(object): + + @classmethod + def from_text(cls, text): + blip = cls() + blip.text = text + for attr, pattern in COMPILED_PATTERNS.items(): + setattr(blip, attr, pattern.findall(text)) + return blip.populate_metadata() + + """ + @classmethod + def from_yaml(cls, yaml_text): + parsed = yaml.load(yaml_text) + blip = cls() + blip.__dict__ = parsed + return blip.populate_metadata() + """ + + def populate_metadata(self): + self.metadata = {} + for tag in self.tags: + try: + key, value = tag.split(LEXEMS['key_separator']) + except: + pass + else: + self.metadata[key] = value + return self + + def __getitem__(self, key): + return self.metadata[key] diff --git a/ployst/core/blips/migrations/0001_initial.py b/ployst/core/blips/migrations/0001_initial.py new file mode 100644 index 0000000..a9ec37e --- /dev/null +++ b/ployst/core/blips/migrations/0001_initial.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Blip', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('title', models.CharField(max_length=256)), + ('text', models.TextField()), + ('as_persona', models.ForeignKey(to='blips.Blip', null=True)), + ('author', models.ForeignKey(related_name='blips', to=settings.AUTH_USER_MODEL)), + ('mentions', models.ManyToManyField(related_name='mentioned_in', to=settings.AUTH_USER_MODEL)), + ('privates', models.ManyToManyField(related_name='private_in', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Metadata', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('value', models.CharField(max_length=256)), + ('blip', models.ForeignKey(to='blips.Blip')), + ], + ), + migrations.CreateModel( + name='Stream', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=256)), + ('owner', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=256)), + ], + ), + migrations.AddField( + model_name='metadata', + name='tag', + field=models.ForeignKey(to='blips.Tag'), + ), + migrations.AddField( + model_name='blip', + name='streams', + field=models.ManyToManyField(to='blips.Stream'), + ), + migrations.AddField( + model_name='blip', + name='tags', + field=models.ManyToManyField(to='blips.Tag'), + ), + ] diff --git a/ployst/core/blips/migrations/__init__.py b/ployst/core/blips/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ployst/core/blips/models.py b/ployst/core/blips/models.py new file mode 100644 index 0000000..4a2fbff --- /dev/null +++ b/ployst/core/blips/models.py @@ -0,0 +1,39 @@ +from django.db import models + +from ployst.core.accounts.models import User + + +class Stream(models.Model): + owner = models.ForeignKey(User) + name = models.CharField(max_length=256) + + def __unicode__(self): + return u'>{}.{}'.format(self.owner.username, self.name) + + +class Tag(models.Model): + name = models.CharField(max_length=256) + + def __unicode__(self): + return u'#{}'.format(self.name) + + +class Metadata(models.Model): + tag = models.ForeignKey(Tag) + blip = models.ForeignKey('blips.Blip') + value = models.CharField(max_length=256) + + +class Blip(models.Model): + author = models.ForeignKey(User, related_name='blips') + as_persona = models.ForeignKey('blips.Blip', null=True) + title = models.CharField(max_length=256) + text = models.TextField() + streams = models.ManyToManyField(Stream) + mentions = models.ManyToManyField(User, related_name='mentioned_in') + privates = models.ManyToManyField(User, related_name='private_in') + tags = models.ManyToManyField(Tag) + # metadata = models.ManyToManyField(Tag, through=Metadata) + + def __unicode__(self): + return self.text diff --git a/ployst/core/blips/parsers.py b/ployst/core/blips/parsers.py new file mode 100644 index 0000000..fdfa96a --- /dev/null +++ b/ployst/core/blips/parsers.py @@ -0,0 +1,15 @@ +from rest_framework.parsers import BaseParser + + +class PlainTextParser(BaseParser): + """ + Plain text parser. + """ + + media_type = 'text/plain' + + def parse(self, stream, media_type=None, parser_context=None): + """ + Simply return a string representing the body of the request. + """ + return {'text': stream.read()} diff --git a/ployst/core/blips/serializers.py b/ployst/core/blips/serializers.py new file mode 100644 index 0000000..2b850b5 --- /dev/null +++ b/ployst/core/blips/serializers.py @@ -0,0 +1,47 @@ +from rest_framework import serializers + +from ployst.core.accounts.models import User + +from .blip import Blip as BlipParser +from .models import Blip, Stream, Tag + + +class BlipSerializer(serializers.ModelSerializer): + text = serializers.CharField() + streams = serializers.SlugRelatedField( + many=True, read_only=True, slug_field='name') + tags = serializers.SlugRelatedField( + many=True, read_only=True, slug_field='name') + mentions = serializers.SlugRelatedField( + many=True, read_only=True, slug_field='username') + privates = serializers.SlugRelatedField( + many=True, read_only=True, slug_field='username') + + class Meta: + model = Blip + fields = ('id', 'text', 'streams', 'tags', 'mentions', 'privates') + + def create(self, validated_data): + request = self.context.get('request', None) + user = request.user + parsed = BlipParser.from_text(**validated_data) + blip = Blip.objects.create( + author=user, + text=parsed.text, + ) + + for name in parsed.tags: + tag, _ = Tag.objects.get_or_create(name=name) + blip.tags.add(tag) + + for name in parsed.streams: + stream, _ = Stream.objects.get_or_create(name=name, owner=user) + blip.streams.add(stream) + + for attr in ['privates', 'mentions']: + collection = getattr(blip, attr) + for name in getattr(parsed, attr): + user_ref = User.objects.get(username=name) + collection.add(user_ref) + + return blip diff --git a/ployst/core/blips/test/__init__.py b/ployst/core/blips/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ployst/core/blips/test/test_blip_parser.py b/ployst/core/blips/test/test_blip_parser.py new file mode 100644 index 0000000..2ebbdfd --- /dev/null +++ b/ployst/core/blips/test/test_blip_parser.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +from unittest import TestCase +from ..blip import Blip + +FULL = u""" +Something to consider. + +Hey here is a link https://python.org from @orne #language:python !alex +>barrobés >read-later +""".strip() + +TAGS = u"Python rules #language:python #programming #rating:5 I love it" + + +class TestBlipParseText(TestCase): + + def test_parse_full(self): + blip = Blip.from_text(FULL) + self.assertEqual(blip.mentions, [u'orne']) + self.assertEqual(blip.privates, [u'alex']) + self.assertEqual(blip.streams, [u'barrobés', u'read-later']) + self.assertEqual(blip.tags, [u'language:python']) + + def test_parse_tags(self): + blip = Blip.from_text(TAGS) + self.assertEqual(blip.mentions, []) + self.assertEqual(blip.privates, []) + self.assertEqual(blip.streams, []) + self.assertEqual(blip.tags, + [u'language:python', 'programming', 'rating:5']) + + def test_parse_metadata(self): + blip = Blip.from_text(TAGS) + self.assertEqual(blip.metadata, {'language': 'python', 'rating': '5'}) + self.assertEqual(blip['language'], 'python') + self.assertEqual(blip['rating'], '5') + + +YAML_FULL = u""" +title: My blip +text: "Here is a blip about a nice programming language" +mentions: + - orne +privates: + - alex +tags: + - language:python + - rating:5 + - programming +streams: + - barrobés + - read-later +""".strip() + + +""" +class TestBlipParseYaml(TestCase): + + def test_parse_full(self): + blip = Blip.from_yaml(YAML_FULL) + self.assertEqual(blip['language'], 'python') + self.assertEqual(blip['rating'], '5') + self.assertEqual(blip.mentions, [u'orne']) + self.assertEqual(blip.privates, [u'alex']) + self.assertEqual(blip.streams, [u'barrobés', u'read-later']) +""" diff --git a/ployst/core/blips/urls.py b/ployst/core/blips/urls.py new file mode 100644 index 0000000..ff822f9 --- /dev/null +++ b/ployst/core/blips/urls.py @@ -0,0 +1,13 @@ +from django.conf.urls import url, patterns, include + +from rest_framework import routers +from . import views + +router = routers.SimpleRouter(trailing_slash=False) +router.register(r'blip', views.BlipViewSet, base_name='blip') + + +urlpatterns = patterns( + '', + url(r'^', include(router.urls)), +) diff --git a/ployst/core/blips/views.py b/ployst/core/blips/views.py new file mode 100644 index 0000000..fe3661b --- /dev/null +++ b/ployst/core/blips/views.py @@ -0,0 +1,28 @@ +import django_filters +from rest_framework.viewsets import ModelViewSet +from rest_framework.parsers import JSONParser + +from .models import Blip +from .parsers import PlainTextParser +from .serializers import BlipSerializer + + +class BlipFilter(django_filters.FilterSet): + stream = django_filters.CharFilter(name="streams__name") + tag = django_filters.CharFilter(name="tags__name") + mention = django_filters.CharFilter(name="mentions__username") + private = django_filters.CharFilter(name="privates__username") + + class Meta: + model = Blip + fields = ['author', 'stream', 'tag', 'mention', 'private'] + + +class BlipViewSet(ModelViewSet): + """ + A simple ViewSet for blips. + """ + queryset = Blip.objects.all() + serializer_class = BlipSerializer + parser_classes = (JSONParser, PlainTextParser) + filter_class = BlipFilter diff --git a/ployst/core/urls.py b/ployst/core/urls.py index 9d8159c..57b7d0b 100644 --- a/ployst/core/urls.py +++ b/ployst/core/urls.py @@ -8,6 +8,8 @@ url(r'^accounts/', include('ployst.core.accounts.urls', namespace='accounts')), + url(r'^blips/', + include('ployst.core.blips.urls', namespace='blips')), url(r'^builds/', include('ployst.core.builds.urls', namespace='builds')), url(r'^features/', diff --git a/ployst/settings/base.py b/ployst/settings/base.py index 0ddd0fb..9e8e175 100644 --- a/ployst/settings/base.py +++ b/ployst/settings/base.py @@ -50,6 +50,7 @@ 'ployst.core.features', # planning: stories, bugs, features... 'ployst.core.repos', # version control: repos, branches... 'ployst.core.builds', # continuous integration: build results... + 'ployst.core.blips', # ployst2 prototyping 'ployst.ui', # The main Ployst UI # Providers @@ -73,7 +74,7 @@ # Database -# https://docs.djangoproject.com/en/1.6/ref/settings/#databases +# https://docs.djangoproject.com/en/1.8/ref/settings/#databases DATABASES = { 'default': {