├── web ├── dbpatterns │ ├── __init__.py │ ├── api │ │ ├── __init__.py │ │ ├── urls.py │ │ ├── auth.py │ │ └── resources.py │ ├── blog │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ └── 0001_initial.py │ │ ├── sitemaps.py │ │ ├── urls.py │ │ ├── admin.py │ │ ├── models.py │ │ └── views.py │ ├── comments │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── signals.py │ │ ├── models.py │ │ └── resources.py │ ├── dbpatterns │ │ ├── __init__.py │ │ ├── templates │ │ │ ├── documents │ │ │ │ ├── base.html │ │ │ │ ├── search_form.html │ │ │ │ ├── fork.html │ │ │ │ ├── stars.html │ │ │ │ ├── preview.html │ │ │ │ ├── new.html │ │ │ │ ├── forks.html │ │ │ │ ├── search.html │ │ │ │ ├── edit.html │ │ │ │ ├── show.html │ │ │ │ └── list.html │ │ │ ├── blog │ │ │ │ ├── pagination.html │ │ │ │ ├── base.html │ │ │ │ ├── detail.html │ │ │ │ ├── index.html │ │ │ │ ├── post.html │ │ │ │ ├── latest.html │ │ │ │ └── donation.html │ │ │ ├── auth │ │ │ │ ├── login_github.html │ │ │ │ ├── complete.html │ │ │ │ ├── register.html │ │ │ │ ├── login.html │ │ │ │ └── profile.html │ │ │ ├── notifications │ │ │ │ ├── list.html │ │ │ │ └── notifications.html │ │ │ ├── newsfeed │ │ │ │ ├── private_newsfeed.html │ │ │ │ ├── public_newsfeed.html │ │ │ │ └── newsfeed.html │ │ │ ├── 500.html │ │ │ ├── 404.html │ │ │ ├── index.html │ │ │ └── base.html │ │ ├── static │ │ │ ├── favicon.ico │ │ │ ├── images │ │ │ │ ├── icons.png │ │ │ │ ├── octocat.png │ │ │ │ ├── background.png │ │ │ │ ├── timeline-icons.png │ │ │ │ └── dbpatterns-logo.png │ │ │ ├── js │ │ │ │ ├── models │ │ │ │ │ ├── assignee.js │ │ │ │ │ ├── comment.js │ │ │ │ │ ├── chat.js │ │ │ │ │ ├── entity.js │ │ │ │ │ ├── document.js │ │ │ │ │ └── attribute.js │ │ │ │ ├── views │ │ │ │ │ ├── notifier.js │ │ │ │ │ ├── connection.js │ │ │ │ │ ├── user.js │ │ │ │ │ ├── comment.js │ │ │ │ │ ├── ui.js │ │ │ │ │ ├── chat.js │ │ │ │ │ ├── entity.js │ │ │ │ │ └── document.js │ │ │ │ ├── libs │ │ │ │ │ ├── jquery │ │ │ │ │ │ ├── jquery.form2json.js │ │ │ │ │ │ ├── jquery.class.min.js │ │ │ │ │ │ └── jquery.class.js │ │ │ │ │ ├── backbone │ │ │ │ │ │ ├── backbone.shortcuts.js │ │ │ │ │ │ └── tastypie.js │ │ │ │ │ ├── hipo │ │ │ │ │ │ ├── hipo.infinityscroll.min.js │ │ │ │ │ │ └── hipo.infinityscroll.js │ │ │ │ │ └── keymaster │ │ │ │ │ │ └── keymaster.min.js │ │ │ │ └── utilities.js │ │ │ └── css │ │ │ │ ├── autocomplete.css │ │ │ │ └── prettify.css │ │ ├── settings_local.py.ex │ │ ├── urls.py │ │ └── wsgi.py │ ├── newsfeed │ │ ├── __init__.py │ │ ├── management │ │ │ ├── __init__.py │ │ │ └── commands │ │ │ │ ├── __init__.py │ │ │ │ └── populate_newsfeed.py │ │ └── constants.py │ ├── profiles │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ └── 0001_initial.py │ │ ├── management │ │ │ ├── __init__.py │ │ │ ├── commands │ │ │ │ ├── __init__.py │ │ │ │ └── create_user_profiles.py │ │ │ └── signals.py │ │ ├── forms.py │ │ ├── models.py │ │ ├── urls.py │ │ ├── admin.py │ │ ├── mixins.py │ │ ├── resources.py │ │ └── views.py │ ├── notifications │ │ ├── __init__.py │ │ ├── urls.py │ │ ├── context_processors.py │ │ ├── constants.py │ │ ├── views.py │ │ └── models.py │ ├── documents │ │ ├── templatetags │ │ │ ├── __init__.py │ │ │ └── document_tags.py │ │ ├── management │ │ │ ├── commands │ │ │ │ ├── __init__.py │ │ │ │ ├── migrate_visibility_options.py │ │ │ │ ├── populate_featured_documents.py │ │ │ │ └── index_keywords.py │ │ │ └── __init__.py │ │ ├── parsers │ │ │ ├── dummy.py │ │ │ └── __init__.py │ │ ├── fields.py │ │ ├── exporters │ │ │ ├── __init__.py │ │ │ └── sql.py │ │ ├── signals.py │ │ ├── mixins.py │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── utils.py │ │ ├── legacy_urls.py │ │ ├── urls.py │ │ ├── forms.py │ │ ├── fixtures │ │ │ └── test_document.json │ │ ├── tests.py │ │ ├── resources.py │ │ └── models.py │ ├── manage.py │ └── terrain.py └── live │ ├── package.json │ └── run.js ├── run_tests.sh ├── .gitignore ├── conf └── requirements.pip ├── tests ├── steps │ ├── __init__.py │ ├── form_steps.py │ ├── page_steps.py │ ├── auth_steps.py │ └── document_steps.py └── scenarios │ ├── documents.feature │ ├── comments.feature │ ├── forks.feature │ ├── stars.feature │ ├── following.feature │ └── private-documents.feature ├── LICENSE └── README.md /web/dbpatterns/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/dbpatterns/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/dbpatterns/blog/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/dbpatterns/comments/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/dbpatterns/newsfeed/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/dbpatterns/profiles/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/dbpatterns/blog/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/dbpatterns/notifications/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/dbpatterns/profiles/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/dbpatterns/documents/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/dbpatterns/documents/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | python web/dbpatterns/manage.py harvest "tests/scenarios/$1" -------------------------------------------------------------------------------- /web/dbpatterns/documents/management/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'fatiherikli' 2 | -------------------------------------------------------------------------------- /web/dbpatterns/newsfeed/management/__init__.py: -------------------------------------------------------------------------------- 1 | __author__='fatiherikli' 2 | -------------------------------------------------------------------------------- /web/dbpatterns/profiles/management/__init__.py: -------------------------------------------------------------------------------- 1 | __author__='fatiherikli' 2 | -------------------------------------------------------------------------------- /web/dbpatterns/newsfeed/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | __author__='fatiherikli' 2 | -------------------------------------------------------------------------------- /web/dbpatterns/profiles/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | __author__='fatiherikli' 2 | -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/documents/base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block header %} 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | settings_local.py 3 | data 4 | *.pyc 5 | *~ 6 | .DS_Store 7 | web/dbpatterns/dbpatterns/static/CACHE/ 8 | node_modules/ -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyistanbul/dbpatterns/HEAD/web/dbpatterns/dbpatterns/static/favicon.ico -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/images/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyistanbul/dbpatterns/HEAD/web/dbpatterns/dbpatterns/static/images/icons.png -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/images/octocat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyistanbul/dbpatterns/HEAD/web/dbpatterns/dbpatterns/static/images/octocat.png -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/images/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyistanbul/dbpatterns/HEAD/web/dbpatterns/dbpatterns/static/images/background.png -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/images/timeline-icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyistanbul/dbpatterns/HEAD/web/dbpatterns/dbpatterns/static/images/timeline-icons.png -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/images/dbpatterns-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyistanbul/dbpatterns/HEAD/web/dbpatterns/dbpatterns/static/images/dbpatterns-logo.png -------------------------------------------------------------------------------- /web/dbpatterns/documents/parsers/dummy.py: -------------------------------------------------------------------------------- 1 | from documents.parsers import BaseParser 2 | 3 | 4 | class DummyParser(BaseParser): 5 | 6 | def parse(self, text): 7 | return text 8 | -------------------------------------------------------------------------------- /web/live/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dbpatterns-live", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "socket.io": "0.9.13", 6 | "underscore": "1.4.4" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /web/dbpatterns/comments/constants.py: -------------------------------------------------------------------------------- 1 | COMMENT_TEMPLATE = """ 2 | You have new comment for '%(document_title)s' pattern. 3 | 4 | Click to following link if you want see comments. 5 | 6 | %(document_link)s 7 | """ 8 | -------------------------------------------------------------------------------- /web/dbpatterns/comments/signals.py: -------------------------------------------------------------------------------- 1 | import django.dispatch 2 | 3 | comment_done = django.dispatch.Signal(providing_args=["comment_id"]) 4 | comment_delete = django.dispatch.Signal(providing_args=["comment_id"]) 5 | -------------------------------------------------------------------------------- /web/dbpatterns/profiles/management/signals.py: -------------------------------------------------------------------------------- 1 | from django.dispatch import Signal 2 | 3 | follow_done = Signal(providing_args=["follower", "following"]) 4 | unfollow_done = Signal(providing_args=["follower", "following"]) -------------------------------------------------------------------------------- /web/dbpatterns/profiles/forms.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.forms import UserCreationForm 2 | 3 | 4 | class RegistrationForm(UserCreationForm): 5 | class Meta(UserCreationForm.Meta): 6 | fields = ("username", "email") 7 | -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/blog/pagination.html: -------------------------------------------------------------------------------- 1 | {% if is_paginated %} 2 | {% if page_obj.has_next %} 3 | Show more 4 | {% endif %} 5 | {% endif %} -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/documents/search_form.html: -------------------------------------------------------------------------------- 1 |
2 | {{ search_form.as_p }} 3 |

4 |
-------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/blog/base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block body-id %}blog{% endblock %} 4 | 5 | {% block logo %} 6 | development blog 7 | {{ block.super }} 8 | {% endblock %} -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/auth/login_github.html: -------------------------------------------------------------------------------- 1 |

Connect via Github

2 |
3 |
4 | 5 |
6 |
-------------------------------------------------------------------------------- /web/dbpatterns/newsfeed/constants.py: -------------------------------------------------------------------------------- 1 | NEWS_TYPE_REGISTRATION = "registration" 2 | NEWS_TYPE_DOCUMENT = "document" 3 | NEWS_TYPE_FORK = "fork" 4 | NEWS_TYPE_COMMENT = "comment" 5 | NEWS_TYPE_STAR = "star" 6 | NEWS_TYPE_FOLLOWING = "following" 7 | 8 | NEWSFEED_LIMIT = 20 9 | -------------------------------------------------------------------------------- /web/dbpatterns/notifications/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url 2 | 3 | from notifications.views import NotificationListView 4 | 5 | 6 | urlpatterns = patterns('', 7 | url(r'^$', NotificationListView.as_view(), 8 | name='notifications'), 9 | ) 10 | -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/js/models/assignee.js: -------------------------------------------------------------------------------- 1 | dbpatterns.models.Assignee = Backbone.Model.extend({ 2 | defaults: { 3 | "permission": "can_edit" 4 | } 5 | }); 6 | 7 | dbpatterns.collections.Assignees = Backbone.Collection.extend({ 8 | model: dbpatterns.models.Assignee 9 | }); 10 | -------------------------------------------------------------------------------- /web/dbpatterns/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dbpatterns.settings") 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/blog/detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'blog/base.html' %} 2 | 3 | {% block title %}{{ post.title }}{% endblock %} 4 | 5 | {% block content %} 6 |
7 | {% include "blog/donation.html" %} 8 | {% include "blog/post.html" %} 9 |
10 | {% endblock %} -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/auth/complete.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | 4 | {% block content %} 5 | 6 |
7 |

Registration successful

8 | 9 | Click here for login 10 | 11 |
12 | 13 | 14 | {% endblock %} -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/blog/index.html: -------------------------------------------------------------------------------- 1 | {% extends "blog/base.html" %} 2 | 3 | {% block content %} 4 |
5 | {% for post in posts %} 6 | {% include "blog/post.html" %} 7 | {% endfor %} 8 | {% include "blog/pagination.html" %} 9 |
10 | {% endblock %} -------------------------------------------------------------------------------- /conf/requirements.pip: -------------------------------------------------------------------------------- 1 | python-memcached==1.48 2 | pymongo==2.3 3 | django==1.4.1 4 | django-compressor==1.2 5 | django-gravatar==0.1.0 6 | django-tastypie==0.9.11 7 | django-social-auth==0.7.7 8 | django-debug-toolbar==0.9.4 9 | nltk==2.0.3 10 | numpy==1.6.2 11 | south==0.7.6 12 | lettuce==0.2.12 13 | django-markitup==1.0.0 14 | markdown==2.2.0 -------------------------------------------------------------------------------- /web/dbpatterns/documents/fields.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | 4 | class SearchInput(forms.TextInput): 5 | input_type = "search" 6 | 7 | def __init__(self, attrs=None, placeholder=None): 8 | super(SearchInput, self).__init__(attrs) 9 | 10 | if placeholder is not None: 11 | self.attrs["placeholder"] = placeholder 12 | -------------------------------------------------------------------------------- /web/dbpatterns/blog/sitemaps.py: -------------------------------------------------------------------------------- 1 | from django.contrib.sitemaps import Sitemap 2 | 3 | from blog.models import Post 4 | 5 | 6 | class BlogSitemap(Sitemap): 7 | changefreq = "never" 8 | priority = 0.9 9 | 10 | def items(self): 11 | return Post.published_objects.all() 12 | 13 | def lastmod(self, obj): 14 | return obj.date_modified 15 | -------------------------------------------------------------------------------- /tests/steps/__init__.py: -------------------------------------------------------------------------------- 1 | from django.test import Client 2 | 3 | from lettuce import before, world 4 | 5 | # load all steps 6 | from .auth_steps import * 7 | from .document_steps import * 8 | from .form_steps import * 9 | from .page_steps import * 10 | 11 | @before.all 12 | def set_browser(): 13 | """ 14 | Loads django's test client. 15 | """ 16 | world.browser = Client() -------------------------------------------------------------------------------- /web/dbpatterns/documents/templatetags/document_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from documents.resources import DocumentResource 3 | 4 | register = template.Library() 5 | 6 | 7 | @register.simple_tag() 8 | def document_count_for_user(user): 9 | resource = DocumentResource() 10 | return resource.get_collection().find({ 11 | "user_id": user.pk 12 | }).count() 13 | -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/notifications/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load gravatar document_tags humanize %} 3 | 4 | {% block body-id %}notifications-view{% endblock %} 5 | 6 | {% block content %} 7 |
8 |

Notifications

9 | {% include "notifications/notifications.html" %} 10 |
11 | {% endblock %} -------------------------------------------------------------------------------- /web/dbpatterns/documents/management/commands/migrate_visibility_options.py: -------------------------------------------------------------------------------- 1 | from django.core.management import BaseCommand 2 | 3 | from documents.models import Document 4 | 5 | class Command(BaseCommand): 6 | 7 | def handle(self, *args, **options): 8 | 9 | Document.objects.collection.update( 10 | {"_id": {"$exists": True} }, 11 | {"$set": {"is_public": True}}, 12 | multi=True) -------------------------------------------------------------------------------- /web/dbpatterns/documents/exporters/__init__.py: -------------------------------------------------------------------------------- 1 | class BaseExporter(object): 2 | """ 3 | The base class of all exporters. 4 | """ 5 | 6 | def __init__(self, document): 7 | self.document = document 8 | 9 | def export(self): 10 | raise NotImplementedError 11 | 12 | def as_text(self): 13 | return "\n".join(list(self.export())) 14 | 15 | 16 | class ExporterError(Exception): 17 | pass -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/documents/fork.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |

Fork Document

6 |
7 | {% csrf_token %} 8 | {{ form.as_p }} 9 | 10 |
11 |
12 | {% endblock %} -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/blog/post.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

5 |
6 |
7 | {{ post.content }} 8 |
9 |
-------------------------------------------------------------------------------- /web/dbpatterns/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url, include 2 | 3 | from documents.resources import DocumentResource 4 | from comments.resources import CommentResource 5 | from profiles.resources import UserResource 6 | 7 | urlpatterns = patterns('', 8 | url(r'^', include(DocumentResource().urls)), 9 | url(r'^', include(CommentResource().urls)), 10 | 11 | # django views 12 | url(r'^users/$', UserResource.as_view()), 13 | ) 14 | -------------------------------------------------------------------------------- /web/dbpatterns/documents/management/commands/populate_featured_documents.py: -------------------------------------------------------------------------------- 1 | from django.core.management import BaseCommand 2 | 3 | from documents.models import Document 4 | 5 | 6 | class Command(BaseCommand): 7 | """ 8 | Populates featured documents 9 | """ 10 | def handle(self, *args, **options): 11 | 12 | Document.objects.collection.update( 13 | {"fork_count": {"$gt": 1}}, 14 | {"$set": {"is_featured": True} 15 | }, multi=True) -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/js/models/comment.js: -------------------------------------------------------------------------------- 1 | dbpatterns.models.Comment = Backbone.Model.extend({ 2 | defaults: { 3 | has_delete_permission: false 4 | } 5 | }); 6 | 7 | dbpatterns.collections.Comments = Backbone.Collection.extend({ 8 | model: dbpatterns.models.Comment, 9 | initialize: function (options) { 10 | this.document = options.document; 11 | }, 12 | url: function() { 13 | return this.document.get("comments_uri"); 14 | } 15 | }); -------------------------------------------------------------------------------- /web/dbpatterns/documents/parsers/__init__.py: -------------------------------------------------------------------------------- 1 | class BaseParser(object): 2 | parsed = None 3 | 4 | def __init__(self, text): 5 | self.text = text 6 | 7 | def is_valid(self): 8 | try: 9 | self.parsed = list(self.parse(self.text)) 10 | except ParseError as e: 11 | return False 12 | return True 13 | 14 | def parse(self, text): 15 | raise NotImplementedError 16 | 17 | 18 | class ParseError(StandardError): 19 | pass -------------------------------------------------------------------------------- /web/dbpatterns/documents/signals.py: -------------------------------------------------------------------------------- 1 | import django.dispatch 2 | 3 | assignment_done = django.dispatch.Signal(providing_args=["instance", "user_id"]) 4 | star_done = django.dispatch.Signal(providing_args=["instance", "user"]) 5 | fork_done = django.dispatch.Signal(providing_args=["instance"]) 6 | document_done = django.dispatch.Signal(providing_args=["instance"]) 7 | document_delete = django.dispatch.Signal(providing_args=["instance"]) 8 | fork_delete = django.dispatch.Signal(providing_args=["instance"]) 9 | -------------------------------------------------------------------------------- /web/dbpatterns/api/auth.py: -------------------------------------------------------------------------------- 1 | from tastypie import http 2 | from tastypie.authorization import Authorization 3 | from tastypie.exceptions import ImmediateHttpResponse 4 | 5 | 6 | class DocumentsAuthorization(Authorization): 7 | 8 | def is_authorized(self, request, object=None): 9 | 10 | if request.method == "GET": 11 | return True 12 | 13 | if request.user.is_anonymous(): 14 | raise ImmediateHttpResponse(response=http.HttpUnauthorized()) 15 | 16 | return True 17 | -------------------------------------------------------------------------------- /web/dbpatterns/notifications/context_processors.py: -------------------------------------------------------------------------------- 1 | from documents import get_collection 2 | 3 | 4 | def notifications(request): 5 | """ 6 | Returns unread notification count 7 | """ 8 | 9 | if request.user.is_anonymous(): 10 | notification_count = 0 11 | else: 12 | notification_count = get_collection("notifications").find({ 13 | "recipient": request.user.id, 14 | "is_read": False 15 | }).count() 16 | 17 | return { 18 | "notification_count": notification_count 19 | } 20 | -------------------------------------------------------------------------------- /web/dbpatterns/profiles/management/commands/create_user_profiles.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.core.management import BaseCommand 3 | 4 | from profiles.models import Profile 5 | 6 | 7 | class Command(BaseCommand): 8 | """ 9 | A migration command that creates profile instances 10 | for django's user model. 11 | """ 12 | def handle(self, *args, **options): 13 | 14 | Profile.objects.all().delete() 15 | 16 | for user in User.objects.all(): 17 | Profile.objects.get_or_create(user=user) -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/blog/latest.html: -------------------------------------------------------------------------------- 1 |
2 | 7 |
8 | 15 |
16 |
-------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/settings_local.py.ex: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | DEBUG = True 4 | TEMPLATE_DEBUG = True 5 | 6 | COMPRESS_ENABLED = False 7 | 8 | DATABASES = { 9 | 'default': { 10 | 'ENGINE': 'django.db.backends.sqlite3', 11 | 'NAME': os.path.join(os.path.dirname(__file__), '../data'), 12 | 'USER': '', 13 | 'PASSWORD': '', 14 | 'HOST': '', 15 | 'PORT': '', 16 | } 17 | } 18 | 19 | 20 | GITHUB_APP_ID = "<<< GITHUB APP ID >>>" 21 | GITHUB_API_SECRET = "<<< GITHUB SECRET KEY >>" 22 | 23 | # Make this unique, and don't share it with anybody. 24 | SECRET_KEY = '<<< SECRET KEY >>>' 25 | -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/notifications/notifications.html: -------------------------------------------------------------------------------- 1 | {% load gravatar document_tags humanize %} 2 | -------------------------------------------------------------------------------- /web/dbpatterns/notifications/constants.py: -------------------------------------------------------------------------------- 1 | NOTIFICATION_TYPE_FORK = "fork" 2 | NOTIFICATION_TYPE_COMMENT = "comment" 3 | NOTIFICATION_TYPE_STAR = "star" 4 | NOTIFICATION_TYPE_FOLLOWING = "following" 5 | NOTIFICATION_TYPE_ASSIGNMENT = "assignment" 6 | 7 | NOTIFICATION_TEMPLATES = { 8 | NOTIFICATION_TYPE_FORK: '%(username)s forked your document.', 9 | NOTIFICATION_TYPE_STAR: '%(username)s starred your document.', 10 | NOTIFICATION_TYPE_COMMENT: '%(username)s commented on your document.', 11 | NOTIFICATION_TYPE_FOLLOWING: '%(username)s is following you.', 12 | NOTIFICATION_TYPE_ASSIGNMENT: '%(username)s assigned you in a document.', 13 | } 14 | -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/auth/register.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | 4 | {% block content %} 5 | 6 |
7 | 8 |
9 |

Register

10 |
11 | {% csrf_token %} 12 | {{ form.as_p }} 13 | 14 |
15 |
16 | 17 |
18 | 19 |
20 | {% include "auth/login_github.html" %} 21 |
22 | 23 |
24 | 25 | 26 | {% endblock %} -------------------------------------------------------------------------------- /web/dbpatterns/documents/mixins.py: -------------------------------------------------------------------------------- 1 | from bson.errors import InvalidId 2 | from django.http import Http404 3 | from tastypie.exceptions import ImmediateHttpResponse 4 | 5 | from documents.resources import DocumentResource 6 | 7 | 8 | class DocumentMixin(object): 9 | """ 10 | The mixin for retrieving Document from MongoDB. 11 | """ 12 | def get_document(self): 13 | resource = DocumentResource() 14 | try: 15 | return resource.obj_get(request=self.request, 16 | pk=self.kwargs.get("slug")) 17 | except (InvalidId, ImmediateHttpResponse): 18 | raise Http404("Document is not found.") 19 | -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/auth/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | 4 | {% block content %} 5 | 6 |
7 | 8 |
9 |

Login

10 |
11 | {% csrf_token %} 12 | {{ form.as_p }} 13 | 14 |
15 |
16 | 17 |
18 | 19 |
20 | {% include "auth/login_github.html" %} 21 |
22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | {% endblock %} -------------------------------------------------------------------------------- /tests/steps/form_steps.py: -------------------------------------------------------------------------------- 1 | from lxml import html 2 | from lettuce import step, world 3 | 4 | @step('type the "(.*)" as "(.*)"') 5 | def type_the_field(step, field, value): 6 | world.data = { 7 | field: value 8 | } 9 | 10 | @step('click to save button') 11 | def click_to_save_button(step): 12 | world.page = world.browser.post(world.page_url, world.data) 13 | world.saved_url = world.page 14 | 15 | @step('the page should contains a form with "(.*)" field') 16 | def page_should_contains_form(step, field): 17 | dom = html.fromstring(world.page.content) 18 | assert len(dom.cssselect("form")) > 0 19 | assert len(dom.cssselect("input[name='%s']" % field)) > 0 -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/newsfeed/private_newsfeed.html: -------------------------------------------------------------------------------- 1 |
2 |

Your Newsfeed

3 | {% if user.is_authenticated %} 4 |
Switch to public newsfeed
5 | {% endif %} 6 |
7 |
8 | {% include "newsfeed/newsfeed.html" %} 9 | {% if next_page_url %} 10 |
11 | 12 |
13 | {% endif %} 14 |
15 |
16 |
17 |
-------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/newsfeed/public_newsfeed.html: -------------------------------------------------------------------------------- 1 |
2 |

Public Newsfeed

3 | {% if user.is_authenticated %} 4 |
Switch to your newsfeed
5 | {% endif %} 6 |
7 |
8 | {% include "newsfeed/newsfeed.html" %} 9 | {% if next_page_url %} 10 |
11 | 12 |
13 | {% endif %} 14 |
15 |
16 |
17 |
-------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/documents/stars.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load gravatar document_tags %} 3 | 4 | {% block body-id %}document-forks{% endblock %} 5 | 6 | {% block content %} 7 |
8 |

Stars of "{{ document.title }}" pattern

9 | {% for stargazer in document.get_stargazers %} 10 | 11 | gravatar of {{ stargazer.username }} 12 | {{ stargazer.username }} 13 | 14 | {% endfor %} 15 |
16 | {% endblock %} -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/js/views/notifier.js: -------------------------------------------------------------------------------- 1 | dbpatterns.views.Notification = Backbone.View.extend({ 2 | 3 | DELAY: 1000, 4 | 5 | tagName: "div", 6 | className: "notification", 7 | render: function () { 8 | this.$el.html(this.options.text); 9 | this.$el.delay(this.DELAY).slideUp("fast"); 10 | return this; 11 | } 12 | }); 13 | 14 | dbpatterns.views.Notifier = Backbone.View.extend({ 15 | el: "#notifications", 16 | initialize: function () { 17 | dbpatterns.notifications.on("flash", this.flash, this); 18 | }, 19 | flash: function (text) { 20 | this.$el.append(new dbpatterns.views.Notification({ 21 | text: text 22 | }).render().el); 23 | } 24 | }); -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/js/models/chat.js: -------------------------------------------------------------------------------- 1 | dbpatterns.models.Room = Backbone.Model.extend({ 2 | initialize: function (model, options) { 3 | this.socket = options.socket; 4 | this.clients = new dbpatterns.collections.Clients(this.get("clients")); 5 | this.messages = new dbpatterns.collections.Messages(); 6 | } 7 | }); 8 | 9 | dbpatterns.models.Client = Backbone.Model.extend({ 10 | idAttribute: "username" 11 | }); 12 | 13 | dbpatterns.models.Message = Backbone.Model.extend({ 14 | 15 | }); 16 | 17 | dbpatterns.collections.Clients = Backbone.Collection.extend({ 18 | model: dbpatterns.models.Client 19 | }); 20 | 21 | dbpatterns.collections.Messages = Backbone.Collection.extend({ 22 | model: dbpatterns.models.Message 23 | }); -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/documents/preview.html: -------------------------------------------------------------------------------- 1 | {% load gravatar_tags %} 2 |
3 |
4 | {% for entity in document.entities|slice:":4" %} 5 | 16 | {% endfor %} 17 |
18 |
-------------------------------------------------------------------------------- /tests/scenarios/documents.feature: -------------------------------------------------------------------------------- 1 | Feature: Managing documents (patterns) 2 | 3 | Background: Given I am logged in as user "tester" 4 | 5 | Scenario: Create patterns 6 | When go to the create pattern page 7 | And I type the "title" as "Friendships" 8 | When I click to save button 9 | Then the redirected page should contains "Friendships" 10 | 11 | Scenario: List my patterns 12 | When I create a pattern that named "Customers" 13 | And go to the my patterns 14 | Then the page should contains "Customers" 15 | 16 | Scenario: Remove created pattern 17 | When I create a pattern that named "" 18 | And go to the my patterns 19 | Click the first delete button 20 | And go to the my patterns 21 | Then the page should not contains "" -------------------------------------------------------------------------------- /web/dbpatterns/profiles/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import AnonymousUser, User 3 | from django.utils.encoding import smart_unicode 4 | 5 | 6 | class AnonymousProfile(AnonymousUser): 7 | username = "AnonymousUser" 8 | email = "" 9 | 10 | def get_full_name(self): 11 | return self.username 12 | 13 | 14 | class FollowedProfile(models.Model): 15 | """ 16 | A relationship model for following users 17 | """ 18 | follower = models.ForeignKey(User, related_name="following") 19 | following = models.ForeignKey(User, related_name="followers") 20 | 21 | def __unicode__(self): 22 | return smart_unicode("%s following %s" % (self.follower.username, 23 | self.following.username)) 24 | -------------------------------------------------------------------------------- /web/dbpatterns/blog/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url 2 | 3 | from blog.sitemaps import BlogSitemap 4 | from blog.views import (BlogDetailView, BlogIndexView, 5 | BlogPostsRssFeed, BlogPostsAtomFeed) 6 | 7 | 8 | urlpatterns = patterns('', 9 | # blog urls 10 | url(r'^$', BlogIndexView.as_view(), name="blog"), 11 | url(r'^(?P[-\w]+)/$', BlogDetailView.as_view(), name="blog_detail"), 12 | 13 | # rss & atom feed 14 | url(r'^feed/rss$', BlogPostsRssFeed(), name="blog_rss_feed"), 15 | url(r'^feed/atom', BlogPostsAtomFeed(), name="blog_atom_feed"), 16 | 17 | # sitemap 18 | url(r'^sitemap\.xml$', 'django.contrib.sitemaps.views.sitemap', 19 | {'sitemaps': { 20 | "blog": BlogSitemap() 21 | }}, name="blog_sitemap"), 22 | ) 23 | -------------------------------------------------------------------------------- /tests/scenarios/comments.feature: -------------------------------------------------------------------------------- 1 | Feature: Commenting on patterns 2 | Users can leave comment on patterns. 3 | 4 | Background: 5 | Given I am logged in as user "tester" 6 | And I create a pattern that named "Comment Model" 7 | 8 | Scenario: Comment pattern 9 | When go to the that pattern 10 | And I type the "body" as "Test Comment" 11 | When I submit the comment 12 | Then the comment count of that pattern should be 1 13 | 14 | Scenario: Display comment count 15 | When I leave a comment on that pattern 16 | And again go to the that pattern 17 | The page should contains "Comments (1)" 18 | 19 | Scenario: Display the comments of pattern 20 | When I leave a comment on that pattern 21 | And I look to the comments of that pattern 22 | Then the page should contains "Test Comment" -------------------------------------------------------------------------------- /web/dbpatterns/documents/management/commands/index_keywords.py: -------------------------------------------------------------------------------- 1 | from django.core.management import BaseCommand 2 | 3 | from documents import get_collection 4 | from documents.utils import extract_keywords 5 | 6 | 7 | class Command(BaseCommand): 8 | 9 | def handle(self, *args, **options): 10 | 11 | get_collection("documents").ensure_index([ 12 | ("_keywords", 1), 13 | ("title", 1), 14 | ]) 15 | 16 | for document in get_collection("documents").find(): 17 | 18 | print document.get("_id") 19 | 20 | get_collection("documents").update({ 21 | "_id": document.get("_id") 22 | }, { 23 | "$set": { 24 | "_keywords": extract_keywords(document.get("title")) 25 | } 26 | }) -------------------------------------------------------------------------------- /web/dbpatterns/blog/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.utils.translation import ugettext_lazy as _ 3 | 4 | from blog.models import Post 5 | 6 | 7 | class PostAdmin(admin.ModelAdmin): 8 | list_display = ["title", "slug", "is_published"] 9 | list_filter = ["is_published"] 10 | search_fields = ["title", "slug", "content"] 11 | actions = ["publish", "mark_as_draft"] 12 | prepopulated_fields = { 13 | "slug": ("title", ) 14 | } 15 | 16 | def publish(self, request, qs): 17 | qs.update(is_published=True) 18 | publish.short_description = _("Publish selected posts") 19 | 20 | def mark_as_draft(self, request, qs): 21 | qs.update(is_published=False) 22 | mark_as_draft.short_description = _("Mark as draft selected posts") 23 | 24 | 25 | admin.site.register(Post, PostAdmin) 26 | -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/js/models/entity.js: -------------------------------------------------------------------------------- 1 | dbpatterns.models.Entity = Backbone.Model.extend({ 2 | 3 | defaults: { 4 | "attributes": [] 5 | }, 6 | 7 | initialize: function () { 8 | this.entity_attributes = new dbpatterns.collections.Attribute(); 9 | this.entity_attributes.reset(this.get("attributes")); 10 | this.entity_attributes.on("add remove persist", this.persist, this); 11 | 12 | }, 13 | 14 | persist: function () { 15 | this.set({ 16 | "attributes": this.entity_attributes.toJSON() 17 | }); 18 | 19 | this.collection && this.collection.trigger("persist"); 20 | }, 21 | 22 | save: function () { 23 | this.trigger("persist") 24 | } 25 | 26 | }); 27 | 28 | dbpatterns.collections.Entity = Backbone.Collection.extend({ 29 | model: dbpatterns.models.Entity 30 | }); 31 | -------------------------------------------------------------------------------- /web/dbpatterns/profiles/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url 2 | from django.views.generic import TemplateView 3 | from profiles.views import (RegistrationView, LoginView, LogoutView, 4 | ProfileDetailView) 5 | 6 | 7 | urlpatterns = patterns('', 8 | url(r'^login/$', LoginView.as_view( 9 | template_name="auth/login.html"), name='auth_login'), 10 | url(r'^logout/$', LogoutView.as_view(), name='auth_logout'), 11 | url(r'^register/$', RegistrationView.as_view( 12 | template_name="auth/register.html"), name='auth_registration'), 13 | url(r'^complete/$', TemplateView.as_view( 14 | template_name="auth/complete.html"), name='auth_registration_complete'), 15 | url(r'^profile/(?P[\w\._-]+)/$', ProfileDetailView.as_view( 16 | template_name="auth/profile.html"), name='auth_profile'), 17 | ) 18 | -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | from django.contrib import admin 3 | from documents.views import HomeView 4 | 5 | 6 | admin.autodiscover() 7 | 8 | urlpatterns = patterns('', 9 | 10 | url(r'^$', HomeView.as_view(), name='home'), 11 | 12 | # notifications 13 | url(r'^notifications/', include('notifications.urls')), 14 | 15 | # documents 16 | url(r'^documents/', include('documents.urls')), 17 | 18 | # registration 19 | url(r'^accounts/', include('profiles.urls')), 20 | 21 | # api 22 | url(r'^api/', include('api.urls')), 23 | 24 | # blog 25 | url(r'^blog/', include("blog.urls")), 26 | 27 | # legacy urls 28 | url(r'^', include('documents.legacy_urls')), 29 | 30 | url(r'^admin/', include(admin.site.urls)), 31 | url(r'', include('social_auth.urls')), 32 | 33 | 34 | ) 35 | -------------------------------------------------------------------------------- /web/dbpatterns/profiles/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | from django.contrib.auth.models import User 4 | 5 | from gravatar.templatetags.gravatar import gravatar_for_user 6 | 7 | from profiles.models import FollowedProfile 8 | 9 | 10 | class FollowedProfileInline(admin.TabularInline): 11 | model = FollowedProfile 12 | fk_name = "follower" 13 | 14 | 15 | class ProfileAdmin(UserAdmin): 16 | 17 | list_display = ('gravatar', 'username', 'email', 'first_name', 18 | 'last_name', 'is_staff') 19 | ordering = ("-id", ) 20 | 21 | inlines = [FollowedProfileInline] 22 | 23 | def gravatar(self, obj): 24 | return '' % gravatar_for_user(obj) 25 | gravatar.allow_tags = True 26 | 27 | 28 | admin.site.unregister(User) 29 | admin.site.register(User, ProfileAdmin) 30 | -------------------------------------------------------------------------------- /tests/scenarios/forks.feature: -------------------------------------------------------------------------------- 1 | Feature: Forking created patterns 2 | Users can fork already created patterns. 3 | 4 | Background: 5 | Given I am logged in as user "tester" 6 | And there is a pattern that named "Products" 7 | When I am logged out 8 | And I am logged in as user "tester-2" 9 | 10 | 11 | Scenario: Show fork form 12 | When go to the that pattern 13 | And click to fork button 14 | And the page should contains a form with "title" field 15 | 16 | Scenario: Forking action 17 | When fork the that pattern as "Forked Products" 18 | Then the redirected page should contains "Forked Products" 19 | And when click to show button 20 | Then the page should contains "fork-of" 21 | 22 | Scenario: See fork count 23 | When go to the profile of "tester" 24 | Then the page should contains "Products" 25 | And also the page should contains "1 fork" -------------------------------------------------------------------------------- /tests/scenarios/stars.feature: -------------------------------------------------------------------------------- 1 | Feature: Starring patterns 2 | Users can star patterns. 3 | 4 | Background: 5 | Given I am logged in as user "tester" 6 | And there is a pattern that named "Star Wars" 7 | When i am logged out 8 | And I am logged in as user "tester-2" 9 | 10 | Scenario: Star pattern 11 | When I star that pattern 12 | Then the redirected page should contains "Unstar (1)" 13 | 14 | Scenario: Unstar pattern 15 | When I star that pattern 16 | And again click to star button 17 | Then the redirected page should contains "Star (0)" 18 | 19 | Scenario: See star count on profile detail 20 | When I star that pattern 21 | When go to the profile of "tester" 22 | The page should contains "1 star" 23 | 24 | Scenario: See stargazers 25 | When I star that pattern 26 | When I look the stargazers of that pattern 27 | The page should contains "tester-2" -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/js/libs/jquery/jquery.form2json.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Serializes form data as object. 3 | * */ 4 | !function ($) { 5 | 6 | $.fn.form2json = function(_options) { 7 | var options = $.extend({ 8 | "include": "select, input, textarea", 9 | "exclude": "input[type='button'], input[type='submit']" 10 | }, _options); 11 | 12 | var get_input_value = function (input) { 13 | if (input.is(":checkbox")) { 14 | return input.is(":checked"); 15 | } 16 | else { 17 | return input.val(); 18 | } 19 | }; 20 | 21 | var result_object = {}; 22 | this.find(options.include).not(options.exclude).each(function () { 23 | var input_name = $(this).attr("name"); 24 | result_object[input_name] = get_input_value($(this)); 25 | }); 26 | 27 | return result_object; 28 | }; 29 | 30 | }(window.jQuery); -------------------------------------------------------------------------------- /web/dbpatterns/documents/__init__.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.utils.functional import SimpleLazyObject 3 | 4 | from pymongo import Connection 5 | 6 | _connection = None 7 | 8 | 9 | def get_connection(): 10 | global _connection 11 | if not _connection: 12 | _connection = Connection( 13 | host=getattr(settings, 'MONGODB_HOST', None), 14 | port=getattr(settings, 'MONGODB_PORT', None) 15 | ) 16 | username = getattr(settings, 'MONGODB_USERNAME', None) 17 | password = getattr(settings, 'MONGODB_PASSWORD', None) 18 | db = _connection[settings.MONGODB_DATABASE] 19 | if username and password: 20 | db.authenticate(username, password) 21 | return db 22 | return _connection[settings.MONGODB_DATABASE] 23 | 24 | 25 | connection = SimpleLazyObject(get_connection) 26 | 27 | 28 | def get_collection(collection_name): 29 | return getattr(connection, collection_name) 30 | -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/documents/new.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |

New Document

6 |
7 | {% csrf_token %} 8 | {{ form.as_p }} 9 |

10 | 11 |

12 |
13 |
14 | {% endblock %} 15 | 16 | {% block scripts %} 17 | {{ block.super }} 18 | 30 | {% endblock %} -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/js/libs/jquery/jquery.class.min.js: -------------------------------------------------------------------------------- 1 | (function(c){jQuery.extend({namespace:function(d){var g=window;var f=d.split(".");for(var e=0;e 8 |

forks of "{{ document.title }}" pattern

9 |
    10 | {% for document in forks %} 11 |
  • 12 | 13 | gravatar of {{ document.user }} 14 | 15 | {{ document.title }} 16 |
    17 | {{ document.get_fork_count }} fork 18 | {{ document.star_count }} star 19 |
    20 |
  • 21 | {% empty %} 22 |
  • {{ document.title }} hasn't got any fork.
  • 23 | {% endfor %} 24 |
25 | 26 | {% endblock %} -------------------------------------------------------------------------------- /web/dbpatterns/profiles/mixins.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.contrib.auth.decorators import login_required 4 | from django.http import HttpResponse 5 | from django.utils.decorators import method_decorator 6 | 7 | 8 | class LoginRequiredMixin(object): 9 | """ 10 | Login required mixin. 11 | """ 12 | @method_decorator(login_required) 13 | def dispatch(self, *args, **kwargs): 14 | return super(LoginRequiredMixin, self).dispatch(*args, **kwargs) 15 | 16 | 17 | class JSONResponseMixin(object): 18 | """ 19 | A mixin that can be used to render a JSON response. 20 | """ 21 | response_class = HttpResponse 22 | 23 | def render_to_response(self, context, **response_kwargs): 24 | """ 25 | Returns a JSON response, transforming 'context' to make the payload. 26 | """ 27 | response_kwargs['content_type'] = 'application/json' 28 | return self.response_class( 29 | self.convert_context_to_json(context), 30 | **response_kwargs 31 | ) 32 | 33 | def convert_context_to_json(self, context): 34 | """ 35 | Convert the context dictionary into a JSON object 36 | """ 37 | return json.dumps(context) 38 | -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for dbpatterns project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | 18 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dbpatterns.settings") 19 | 20 | # This application object is used by any WSGI server configured to use this 21 | # file. This includes Django's development server, if the WSGI_APPLICATION 22 | # setting points here. 23 | from django.core.wsgi import get_wsgi_application 24 | application = get_wsgi_application() 25 | 26 | # Apply WSGI middleware here. 27 | # from helloworld.wsgi import HelloWorldApplication 28 | # application = HelloWorldApplication(application) 29 | -------------------------------------------------------------------------------- /web/dbpatterns/profiles/resources.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.db.models import Q 3 | from django.views.generic import ListView 4 | 5 | from gravatar.templatetags.gravatar import gravatar_for_user 6 | 7 | from profiles.mixins import JSONResponseMixin 8 | 9 | 10 | class UserResource(JSONResponseMixin, ListView): 11 | queryset = User.objects.all() 12 | 13 | def get_queryset(self): 14 | keyword = self.request.GET.get("term") 15 | excludes = self.request.GET.get("excludes", "").split(",") 16 | 17 | if self.request.user.is_authenticated(): 18 | excludes.append(self.request.user.id) 19 | 20 | users = User.objects.filter( 21 | Q(username__icontains=keyword) | 22 | Q(email__icontains=keyword) | 23 | Q(first_name=keyword) | Q(last_name=keyword) 24 | ).exclude(id__in=filter(bool, excludes)) 25 | 26 | return [dict(id=user.id, 27 | label=user.username, 28 | avatar=gravatar_for_user(user, size=40)) for user in users] 29 | 30 | def render_to_response(self, context, **response_kwargs): 31 | return super(UserResource, self).render_to_response( 32 | context.get("object_list"), **response_kwargs) 33 | -------------------------------------------------------------------------------- /web/dbpatterns/documents/legacy_urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url, patterns 2 | from django.views.generic import RedirectView 3 | 4 | urlpatterns = patterns( 5 | '', 6 | # Legacy URLs 7 | 8 | url(r'^my-documents/$', 9 | RedirectView.as_view(url="/documents/")), 10 | url(r'^search/$', 11 | RedirectView.as_view(url="/documents/search")), 12 | url(r'^new/$', 13 | RedirectView.as_view(url="/documents/new")), 14 | url(r'^show/(?P[-\w]+)/$', 15 | RedirectView.as_view(url="/documents/%(slug)s/")), 16 | url(r'^show/(?P[-\w]+)/forks$', 17 | RedirectView.as_view( 18 | url="/documents/%(slug)s/forks")), 19 | url(r'^show/(?P[-\w]+)/stars', 20 | RedirectView.as_view( 21 | url="/documents/%(slug)s/stars")), 22 | url(r'^edit/(?P[-\w]+)/', RedirectView.as_view( 23 | url="/documents/%(slug)s/edit")), 24 | url(r'^export/(?P[-\w]+)/(?P[-\w]+)', 25 | RedirectView.as_view( 26 | url="/documents/%(slug)s/export/%(exporter)s")), 27 | url(r'^fork/(?P[-\w]+)/$', RedirectView.as_view( 28 | url="/documents/%(slug)s/fork")), 29 | url(r'^star/(?P[-\w]+)/$', RedirectView.as_view( 30 | url="/documents/%(slug)s/star")), 31 | ) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | """ 2 | dbpatterns dot com domain is not active anymore. soon the project 3 | might be relaunched again under a different domain. 4 | """ 5 | 6 | The MIT License (MIT) 7 | 8 | Copyright (c) 2012 - 2021 Fatih Erikli 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/documents/search.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load gravatar document_tags %} 3 | 4 | {% block body-id %}document-search{% endblock %} 5 | 6 | {% block content %} 7 | 8 | 11 | 12 |
13 |
    14 | {% for document in documents %} 15 |
  • 16 | 17 | gravatar of {{ document.user }} 18 | 19 | {{ document.title }} 20 |
    21 | {{ document.get_fork_count }} fork 22 | {{ document.star_count }} star 23 |
    24 |
  • 25 | {% empty %} 26 |
  • No document found for "{{ keyword }}"
  • 27 | {% endfor %} 28 |
29 |
30 | 31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /web/dbpatterns/blog/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.encoding import smart_unicode 3 | from django.utils.translation import ugettext_lazy as _ 4 | 5 | from markitup.fields import MarkupField 6 | 7 | 8 | class PublishedManager(models.Manager): 9 | """ 10 | Returns published blog posts. 11 | """ 12 | def get_query_set(self): 13 | queryset = super(PublishedManager, self).get_query_set() 14 | return queryset.filter(is_published=True) 15 | 16 | 17 | class Post(models.Model): 18 | """ 19 | Holds blog post data. 20 | """ 21 | title = models.CharField(_("Name"), max_length=255) 22 | slug = models.SlugField(_("Slug"), max_length=255, unique=True) 23 | content = MarkupField(_("Content")) 24 | date_created = models.DateTimeField(auto_now_add=True) 25 | date_modified = models.DateTimeField(auto_now=True, auto_now_add=True) 26 | is_published = models.BooleanField(_("Published"), default=True) 27 | 28 | objects = models.Manager() 29 | published_objects = PublishedManager() 30 | 31 | class Meta: 32 | ordering = ("-date_created", ) 33 | 34 | def __unicode__(self): 35 | return smart_unicode(self.title) 36 | 37 | @models.permalink 38 | def get_absolute_url(self): 39 | return "blog_detail", [self.slug] 40 | -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/js/views/connection.js: -------------------------------------------------------------------------------- 1 | dbpatterns.views.ConnectorEndpoint = Backbone.View.extend({ 2 | initialize: function () { 3 | this.configure(); 4 | }, 5 | configure: function () { 6 | 7 | }, 8 | render: function () { 9 | jsPlumb.makeSource(this.$el, { 10 | "anchor": [ "RightMiddle", "LeftMiddle" ], 11 | "connector": ["Flowchart"], 12 | "connectorStyle": { strokeStyle:"#dedede", lineWidth:6 }, 13 | "paintStyle": { fillStyle:"#dedede" } 14 | }); 15 | 16 | jsPlumb.makeTarget(this.$el, { 17 | "anchor": [ "RightMiddle", "LeftMiddle" ], 18 | "paintStyle": { fillStyle:"#dedede" } 19 | }); 20 | } 21 | }); 22 | 23 | 24 | dbpatterns.views.Connection = Backbone.View.extend({ 25 | initialize: function () { 26 | this.model.bind("change", this.set_label, this); 27 | }, 28 | render: function () { 29 | this.connection = jsPlumb.connect({ 30 | source: this.el, 31 | target: this.options.target 32 | }); 33 | return this; 34 | }, 35 | set_label: function () { 36 | this.connection.setLabel(this.options.label(this.model)); 37 | }, 38 | destroy: function () { 39 | 40 | jsPlumb.detach(this.connection); 41 | } 42 | }); -------------------------------------------------------------------------------- /web/live/run.js: -------------------------------------------------------------------------------- 1 | var io = require('socket.io').listen(8000), 2 | _ = require('underscore'); 3 | 4 | var get_clients = function (channel) { 5 | // Returns the stored data of the clients of provided channel 6 | return _.map(io.sockets.clients(channel), function (client) { 7 | return client.store.data; 8 | }); 9 | }; 10 | 11 | io.sockets.on('connection', function (socket) { 12 | 13 | var channel = null; 14 | 15 | socket.on("connect", function (data) { 16 | channel = data.document_id; 17 | socket.join(channel); 18 | socket.set("username", data.username); 19 | socket.emit("enter", { 20 | "clients": get_clients(channel) 21 | }); 22 | socket.broadcast.to(channel).emit("join", data.username); 23 | }); 24 | 25 | socket.on("push", function (data) { 26 | socket.broadcast.to(channel).emit("pull", data); 27 | }); 28 | 29 | socket.on("message", function (data) { 30 | var message = { 31 | "body": data, 32 | "username": socket.store.data["username"] 33 | }; 34 | socket.broadcast.to(channel).emit("message", message); 35 | socket.emit("message", message); 36 | }); 37 | 38 | socket.on("disconnect", function () { 39 | socket.broadcast.to(channel).emit("leave", socket.store.data["username"]); 40 | socket.leave(channel); 41 | }); 42 | 43 | }); -------------------------------------------------------------------------------- /web/dbpatterns/notifications/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from itertools import imap 4 | from pymongo import DESCENDING 5 | 6 | from django.http import HttpResponse 7 | from django.views.generic import ListView 8 | 9 | from notifications.models import Notification 10 | 11 | 12 | class NotificationListView(ListView): 13 | template_name = "notifications/list.html" 14 | ajax_template_name = "notifications/notifications.html" 15 | context_object_name = "notifications" 16 | 17 | def get_queryset(self): 18 | notifications = self.get_notifications() 19 | return imap(Notification, notifications) 20 | 21 | def get_notifications(self): 22 | 23 | notifications = Notification.objects.filter_by_user_id( 24 | self.request.user.id) 25 | 26 | if self.request.is_ajax(): 27 | return notifications.limit(5).sort([ 28 | ("is_read", DESCENDING), 29 | ("date_created", DESCENDING)]) 30 | 31 | return notifications.sort([("date_created", DESCENDING)]) 32 | 33 | def get_template_names(self): 34 | if self.request.is_ajax(): 35 | return [self.ajax_template_name] 36 | return [self.template_name] 37 | 38 | def put(self, request, **kwargs): 39 | Notification.objects.mark_as_read(user_id=request.user.pk) 40 | return HttpResponse(json.dumps({ 41 | "success": True 42 | })) 43 | -------------------------------------------------------------------------------- /web/dbpatterns/documents/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url 2 | 3 | from documents.views import (DocumentDetailView, ExportDocumentView, 4 | DocumentForksView, DocumentStarsView, 5 | NewDocumentView, MyDocumentsView, DocumentEditView, 6 | ForkDocumentView, StarDocumentView, 7 | SearchDocumentView) 8 | 9 | urlpatterns = patterns( 10 | '', 11 | url(r'^$', MyDocumentsView.as_view(), name='my_documents'), 12 | url(r'^search$', SearchDocumentView.as_view(), name='search_document'), 13 | url(r'^new$', NewDocumentView.as_view(), name='new_document'), 14 | url(r'^(?P[-\w]+)/$', DocumentDetailView.as_view(), 15 | name='show_document'), 16 | url(r'^(?P[-\w]+)/forks$', DocumentForksView.as_view(), 17 | name='show_document_forks'), 18 | url(r'^(?P[-\w]+)/stars', DocumentStarsView.as_view(), 19 | name='show_document_stars'), 20 | url(r'^(?P[-\w]+)/export/(?P[-\w]+)$', 21 | ExportDocumentView.as_view(), name='export_document'), 22 | url(r'^(?P[-\w]+)/edit$', DocumentEditView.as_view(), 23 | name='edit_document'), 24 | url(r'^(?P[-\w]+)/fork$', ForkDocumentView.as_view(), 25 | name='fork_document'), 26 | url(r'^(?P[-\w]+)/star$', StarDocumentView.as_view(), 27 | name='star_document'), 28 | ) 29 | -------------------------------------------------------------------------------- /web/dbpatterns/blog/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.syndication.views import Feed 3 | from django.utils.feedgenerator import Atom1Feed 4 | from django.views.generic import ListView 5 | from django.views.generic.detail import DetailView 6 | 7 | from blog.models import Post 8 | 9 | 10 | class BlogIndexView(ListView): 11 | template_name = "blog/index.html" 12 | queryset = Post.published_objects.all() 13 | context_object_name = "posts" 14 | paginate_by = 30 15 | 16 | 17 | class BlogDetailView(DetailView): 18 | template_name = "blog/detail.html" 19 | model = Post 20 | context_object_name = "post" 21 | 22 | def get_queryset(self): 23 | if self.request.user.is_superuser: 24 | return self.model.objects.all() 25 | return self.model.published_objects.all() 26 | 27 | 28 | class BlogPostsRssFeed(Feed): 29 | title = settings.BLOG_FEED_TITLE 30 | link = settings.BLOG_URL 31 | description = settings.BLOG_FEED_DESCRIPTION 32 | 33 | def items(self): 34 | return Post.objects.all()[:20] 35 | 36 | def item_description(self, post): 37 | return post.content 38 | 39 | def item_pubdate(self, post): 40 | return post.date_created 41 | 42 | def item_categories(self, post): 43 | return [tag.name for tag in post.tags.all()] 44 | 45 | 46 | class BlogPostsAtomFeed(BlogPostsRssFeed): 47 | feed_type = Atom1Feed 48 | subtitle = settings.BLOG_FEED_DESCRIPTION 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Db-patterns 2 | 3 | Dbpatterns is a service that allows you to create, share, explore database models on the web. 4 | 5 | Notes: 6 | 7 | The previous domain is not active anymore. 8 | Project might be relaunched under a different domain 9 | 10 | ### Installation 11 | 12 | Install mongodb: 13 | 14 | sudo apt-get install mongodb 15 | # or on mac 16 | sudo brew install mongodb 17 | 18 | Start mongodb: 19 | 20 | mongod 21 | 22 | Create a virtual env: 23 | 24 | virtualenv dbpatterns 25 | source dbpatterns/bin/activate 26 | 27 | Clone the repository and install requirements: 28 | 29 | cd dbpatterns 30 | git clone git://github.com/fatiherikli/dbpatterns.git 31 | pip install -r dbpatterns/conf/requirements.pip 32 | 33 | Create a file that named settings_local.py 34 | Configure the database and secret key. 35 | 36 | cd dbpatterns/web/dbpatterns 37 | vim settings_local.py 38 | 39 | The example of configuration: 40 | 41 | DATABASES = { 42 | 'default': { 43 | 'ENGINE': 'django.db.backends.sqlite3', 44 | 'NAME': 'data', 45 | 'USER': '', 46 | 'PASSWORD': '', 47 | 'HOST': '', 48 | 'PORT': '', 49 | } 50 | } 51 | 52 | SECRET_KEY = '' 53 | 54 | And run the following commands: 55 | 56 | cd ../ 57 | python manage.py syncdb 58 | python manage.py runserver 59 | 60 | That's all. You can access to dbpatterns on https://localhost:8000 61 | -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/documents/edit.html: -------------------------------------------------------------------------------- 1 | {% extends "documents/app.html" %} 2 | 3 | 4 | {% block body-id %}document-edit-view{% endblock %} 5 | 6 | {% block nav %} 7 | 11 | {% endblock %} 12 | 13 | {% block content %} 14 | {{ block.super }} 15 |
16 |
17 |

Shortcuts

18 |
    19 |
  • OPTION+N: Create new entity
  • 20 |
  • OPTION+R: Rename document
  • 21 |
  • OPTION+S: Save document
  • 22 |
23 |

Auto-fill conditions

24 |
    25 |
  • Columns that named "id": integer type.
  • 26 |
  • Columns that starts with "_is": boolean type.
  • 27 |
  • Columns that starts with or ends with "date": datetime type.
  • 28 |
  • Columns that consist with table name, underscore and column name: foreign key.
  • 29 |
30 |
31 | Help 32 |
33 |
34 | 35 |
36 |
    37 |
    38 |
    39 |
    40 | 41 | Online: 2 42 | 43 | {% endblock %} -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/js/libs/backbone/backbone.shortcuts.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var Shortcuts; 3 | 4 | Shortcuts = function(options) { 5 | this.cid = _.uniqueId("backbone.shortcuts"); 6 | this.initialize.apply(this, arguments); 7 | return this.delegateShortcuts(); 8 | }; 9 | 10 | _.extend(Shortcuts.prototype, Backbone.Events, { 11 | initialize: function() {}, 12 | delegateShortcuts: function() { 13 | var callback, match, method, scope, shortcut, shortcutKey, _ref, _results; 14 | if (!this.shortcuts) return; 15 | _ref = this.shortcuts; 16 | _results = []; 17 | for (shortcut in _ref) { 18 | callback = _ref[shortcut]; 19 | if (!_.isFunction(callback)) method = this[callback]; 20 | if (!method) throw new Error("Method " + callback + " does not exist"); 21 | match = shortcut.match(/^(\S+)\s*(.*)$/); 22 | shortcutKey = match[1]; 23 | scope = match[2] === "" ? "all" : match[2]; 24 | method = _.bind(method, this); 25 | _results.push(key(shortcutKey, scope, method)); 26 | } 27 | return _results; 28 | } 29 | }); 30 | 31 | key.filter = function () { 32 | // accept all elements 33 | return true; 34 | }; 35 | 36 | Backbone.Shortcuts = Shortcuts; 37 | 38 | Backbone.Shortcuts.extend = Backbone.View.extend; 39 | 40 | }).call(this); -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/js/models/document.js: -------------------------------------------------------------------------------- 1 | dbpatterns.models.Document = Backbone.Model.extend({ 2 | 3 | urlRoot: "/api/documents", 4 | 5 | initialize: function (model, options) { 6 | this.entities = new dbpatterns.collections.Entity; 7 | this.use_websocket = options.use_websocket; 8 | if (this.use_websocket) { 9 | this.socket = io.connect(options.socket_uri); 10 | this.socket.on("pull", this.pull.bind(this)); 11 | this.on("load", this.bind_socket, this); 12 | } else { 13 | this.socket = {}; 14 | _.extend(this.socket, Backbone.Events); 15 | } 16 | }, 17 | 18 | parse: function (result) { 19 | if (!result) { return result; } 20 | this.entities.reset(result.entities); 21 | this.entities.bind("add remove persist", this.persist.bind(this)); 22 | return result; 23 | }, 24 | 25 | persist: function () { 26 | this.set({"entities": this.entities.toJSON()}); 27 | }, 28 | 29 | channel: function () { 30 | // specifies the channel of user 31 | return this.get("id") 32 | }, 33 | 34 | push: function () { 35 | // pushes the changes to the channel of user 36 | this.socket.emit("push", this.toJSON()); 37 | }, 38 | 39 | pull: function (data) { 40 | // pulls the changes from the channel 41 | this.set(data); 42 | this.entities.reset(data.entities); 43 | }, 44 | 45 | bind_socket: function () { 46 | this.on("change", this.push, this); 47 | } 48 | 49 | }); -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/js/utilities.js: -------------------------------------------------------------------------------- 1 | !(function ($) { 2 | 3 | $(function () { 4 | // auto toggle manipulation 5 | $("a[data-toggle]").click(function () { 6 | var button = $(this), 7 | target = $(button.attr("href")); 8 | button.toggleClass("active"); 9 | target.toggle().find("a").click(function (event) { 10 | target.hide(); 11 | }); 12 | return false; 13 | }); 14 | 15 | // notifications 16 | $("#user-notifications-button").click(function () { 17 | var notifications = $("#user-notifications"), 18 | notifications_section = notifications.find("section"), 19 | notifications_bubble = $(this).find("span"), 20 | notifications_url = $(this).attr("href"); 21 | notifications.toggle(); 22 | notifications_bubble.remove(); 23 | notifications_section.load(notifications_url); 24 | $.ajax({"type": "PUT", "url": notifications_url}); 25 | return false; 26 | }); 27 | 28 | // tab implementation 29 | $(".tabs nav a").click(function () { 30 | var tab_link = $(this), 31 | link_container = tab_link.parent(), 32 | focused_tab = $(tab_link.attr("href")); 33 | focused_tab.siblings("section").hide(); 34 | focused_tab.show(); 35 | link_container.siblings().removeClass("active"); 36 | link_container.addClass("active"); 37 | return false; 38 | }); 39 | 40 | }); 41 | 42 | })(window.jQuery); -------------------------------------------------------------------------------- /tests/scenarios/following.feature: -------------------------------------------------------------------------------- 1 | Feature: Following users 2 | 3 | Background: 4 | Given following users exist 5 | | username | password | 6 | | edi | 123456 | 7 | | budu | 123456 | 8 | 9 | When I am logged in as user "edi" 10 | 11 | Scenario: users can follow the others 12 | When I go to the profile of "budu" 13 | And I click to follow button 14 | And I go to the profile of "budu" again 15 | Then the page should contains "edi following budu" 16 | 17 | Scenario: see followed profiles on the profile of follower 18 | When I go to the profile of "budu" 19 | And I click to follow button 20 | And I go to my profile 21 | Then the page should contains "edi following budu" 22 | 23 | Scenario: users can't follow himself 24 | When go to my profile 25 | The page should not contains "Follow" 26 | The page should not contains "Unfollow" 27 | 28 | Scenario: users can't follow already followed users 29 | When I go to the profile of "budu" 30 | And I click to follow button 31 | And I go to the profile of "budu" again 32 | And the page should not contains "Follow" 33 | And the page should contains "Unfollow" 34 | 35 | Scenario: users can't follow already followed profiles 36 | When I go to the profile of "budu" 37 | And I click to follow button 38 | When go to the profile of "budu" again 39 | The page should not contains "Follow" 40 | The page should contains "Unfollow" 41 | 42 | Scenario: user can unfollow followed profiles 43 | When I go to the profile of "budu" 44 | And I click to follow button 45 | And go to the profile of "budu" again 46 | And when click to unfollow button 47 | And I go to the profile of "budu" again 48 | Then the page should contains "Follow" 49 | And the page should not contains "Unfollow" -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/css/autocomplete.css: -------------------------------------------------------------------------------- 1 | .ui-widget-overlay { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | } 8 | 9 | * html .ui-autocomplete { 10 | width: 1px; 11 | } 12 | 13 | /* without this, the menu expands to 100% in IE6 */ 14 | .ui-autocomplete { 15 | padding: 0; 16 | width: 200px; 17 | box-shadow: 0 0 13px #dcdcdc; 18 | position: absolute; 19 | top: 0; 20 | left: 0; 21 | cursor: default; 22 | } 23 | 24 | * html .ui-autocomplete { 25 | width: 1px; 26 | } 27 | 28 | /* without this, the menu expands to 100% in IE6 */ 29 | .ui-menu { 30 | list-style: none; 31 | padding: 0; 32 | margin: 0; 33 | display: block; 34 | outline: none; 35 | } 36 | 37 | .ui-menu .ui-menu { 38 | margin-top: -3px; 39 | position: absolute; 40 | } 41 | 42 | .ui-menu .ui-menu-item { 43 | margin: 0; 44 | padding: 0; 45 | zoom: 1; 46 | width: 100%; 47 | } 48 | 49 | .ui-menu .ui-menu-divider { 50 | margin: 5px -2px 5px -2px; 51 | height: 0; 52 | font-size: 0; 53 | line-height: 0; 54 | border-width: 1px 0 0 0; 55 | } 56 | 57 | .ui-menu .ui-menu-item a { 58 | text-decoration: none; 59 | display: block; 60 | padding: 2px .4em; 61 | line-height: 1.5; 62 | zoom: 1; 63 | font-weight: normal; 64 | } 65 | 66 | .ui-menu .ui-menu-item a.ui-state-focus, 67 | .ui-menu .ui-menu-item a.ui-state-active { 68 | background-color: #2ca3f5; 69 | color: white; 70 | } 71 | 72 | .ui-menu .ui-state-disabled { 73 | font-weight: normal; 74 | margin: .4em 0 .2em; 75 | line-height: 1.5; 76 | } 77 | 78 | .ui-menu .ui-state-disabled a { 79 | cursor: default; 80 | } 81 | 82 | .ui-menu-item { 83 | list-style-type: none; 84 | background-color: white; 85 | padding: 3px; 86 | cursor: pointer; 87 | display: block; 88 | } 89 | 90 | .ui-helper-hidden-accessible { 91 | display: none; 92 | } -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/js/libs/hipo/hipo.infinityscroll.min.js: -------------------------------------------------------------------------------- 1 | $.namespace("hipo.InfinityScroll");hipo.InfinityScroll=$.Class.extend({loader_image:"",content_selector:"",pagination_selector:".pagination",next_link_selector:".pagination a.next",on_page_load:function(){},loader:null,FOOTER_POSITION_THRESHOLD:60,MOBILE_FOOTER_POSITION_THRESHOLD:600,init:function(a){$.extend(this,a);this.hide_pagination();this.check_scroll(this.load_page.bind(this));this.prepare_loader()},load_content:function(a){var b=$(this.content_selector,a).html();$(this.content_selector).append(b)},load_page:function(){var a=this.get_next_page();if(a){this.remove_pagination();this.show_loader();var b=function(c){this.load_content(c);this.hide_pagination();this.hide_loader();this.on_page_load()};$.get(a,b.bind(this),"html")}},get_next_page:function(){if($(this.next_link_selector).length){return $(this.next_link_selector).attr("href")}else{return false}},check_scroll:function(a){$(window).scroll(function(){if($(window).scrollTop()+$(window).height()>this.get_doc_height()-this.get_footer_threshold()){a()}}.bind(this))},hide_pagination:function(){$(this.pagination_selector).hide()},remove_pagination:function(){$(this.pagination_selector).remove()},prepare_loader:function(){this.loader=$("
    ").css({display:"none","text-align":"center",padding:"10px",clear:"both"}).append($("",{src:this.loader_image}));$(this.content_selector).after(this.loader)},show_loader:function(){this.loader.show()},hide_loader:function(){this.loader.hide()},get_footer_threshold:function(){return this.is_mobile_device()?this.MOBILE_FOOTER_POSITION_THRESHOLD:this.FOOTER_POSITION_THRESHOLD},get_doc_height:function(){var a=document;return Math.max(Math.max(a.body.scrollHeight,a.documentElement.scrollHeight),Math.max(a.body.offsetHeight,a.documentElement.offsetHeight),Math.max(a.body.clientHeight,a.documentElement.clientHeight))},is_mobile_device:function(){return navigator.userAgent.toLowerCase().match(/(iphone|ipod|ipad|android)/)}}); -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/css/prettify.css: -------------------------------------------------------------------------------- 1 | pre {display: block; clear: both; margin: 0; background-color: #333; padding: 10px; font-size: 13px } 2 | pre code { 3 | font-family: 'Monaco', 'Consolas', 'Bitstream Vera Sans Mono', 'Courier-Bold', 'Courier New', monospace; 4 | -webkit-font-smoothing: subpixel-antialiased; 5 | 6 | } 7 | pre .nocode { background-color: none; color: #000 } 8 | pre .str { color: #ffa0a0 } /* string - pink */ 9 | pre .kwd { color: #f0e68c; font-weight: normal } 10 | pre .com { color: #87ceeb } /* comment - skyblue */ 11 | pre .typ { color: #98fb98 } /* type - lightgreen */ 12 | pre .lit { color: #cd5c5c } /* literal - darkred */ 13 | pre .pun { color: #fff } /* punctuation */ 14 | pre .pln { color: #fff } /* plaintext */ 15 | pre .tag { color: #f0e68c; font-weight: normal } /* html/xml tag - lightyellow */ 16 | pre .atn { color: #bdb76b; font-weight: normal } /* attribute name - khaki */ 17 | pre .atv { color: #ffa0a0 } /* attribute value - pink */ 18 | pre .dec { color: #98fb98 } /* decimal - lightgreen */ 19 | 20 | /* Specify class=linenums on a pre to get line numbering */ 21 | ol.linenums { margin-top: 0; margin-bottom: 0; color: #AEAEAE } /* IE indents via margin-left */ 22 | li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8 { list-style-type: none } 23 | /* Alternate shading for lines */ 24 | li.L1,li.L3,li.L5,li.L7,li.L9 { } 25 | 26 | @media print { 27 | pre { background-color: none } 28 | pre .str, code .str { color: #060 } 29 | pre .kwd, code .kwd { color: #006; font-weight: normal } 30 | pre .com, code .com { color: #600; font-style: italic } 31 | pre .typ, code .typ { color: #404; font-weight: normal } 32 | pre .lit, code .lit { color: #044 } 33 | pre .pun, code .pun { color: #440 } 34 | pre .pln, code .pln { color: #000 } 35 | pre .tag, code .tag { color: #006; font-weight: normal } 36 | pre .atn, code .atn { color: #404 } 37 | pre .atv, code .atv { color: #060 } 38 | } -------------------------------------------------------------------------------- /tests/scenarios/private-documents.feature: -------------------------------------------------------------------------------- 1 | Feature: Private Documents 2 | In order to create documents which can be seen by just me, 3 | As an authenticated user, 4 | I want to create a private document. 5 | 6 | Background: 7 | Given following users exist 8 | | username | password | 9 | | edi | 123456 | 10 | | budu | 123456 | 11 | 12 | When I am logged in as user "edi" 13 | 14 | Scenario: Create private patterns 15 | When go to the create pattern page 16 | And I type the "title" as "Friendships" 17 | And I choose the "is_public" option as "False" 18 | When I click to save button 19 | Then the redirected page should contains "Make Public" 20 | 21 | Scenario: The others can not see the private documents 22 | When go to the create pattern page 23 | And I type the "title" as "Hey, it's just me" 24 | And I choose the "is_public" option as "False" 25 | When I click to save button 26 | And I am logged out 27 | And I am logged in as user "budu" 28 | And I go to the created pattern 29 | Then the page should return with 404 status code. 30 | 31 | Scenario: The creator of a private pattern can make it public 32 | When go to the create pattern page 33 | And I type the "title" as "Hey, I make it public after the organizing" 34 | And I choose the "is_public" option as "False" 35 | When I click to save button 36 | And click to the make public button 37 | And I am logged out 38 | And I am logged in as user "budu" 39 | And I go to the created pattern 40 | Then the page should contains "Hey, I make it public after the organizing" 41 | 42 | Scenario: Private patterns can't be visible on the newsfeed. 43 | When go to the create pattern page 44 | And I type the "title" as "Secret work" 45 | And I choose the "is_public" option as "False" 46 | When I click to save button 47 | And I go to the newsfeed 48 | The page should not contains "Secret work" 49 | -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/documents/show.html: -------------------------------------------------------------------------------- 1 | {% extends "documents/app.html" %} 2 | {% load gravatar document_tags %} 3 | 4 | {% block body-id %}document-show-view{% endblock %} 5 | 6 | {% block tools %} 7 |
    8 |
    9 | {{ document.user.username }} 10 |
    11 |
    12 | {% document_count_for_user document.user %} patterns 13 |

    {{ document.user.username }}

    14 |
    15 |
    16 | 22 | {% endblock %} 23 | {% block nav %} 24 |
      25 |
    • Home
    • 26 | {% ifequal user document.user %} 27 |
    • Back to list
    • 28 |
    • Edit
    • 29 | {% else %} 30 |
    • Fork
    • 31 | {% endifequal %} 32 |
    • 33 | {% csrf_token %} 34 | {% if user.pk in document.stars %} 35 | 36 | {% else %} 37 | 38 | {% endif %} 39 |
    • 40 |
    41 | 42 | {% endblock %} 43 | 44 | {% block app-header %} 45 | {% if document.fork_of %} 46 |

    47 | 48 |

    49 | {% endif %} 50 | {{ block.super }} 51 | {% endblock %} -------------------------------------------------------------------------------- /web/dbpatterns/terrain.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | 5 | from django.conf import settings 6 | from django.core.management import call_command 7 | from django.test.simple import DjangoTestSuiteRunner 8 | 9 | from lettuce import * 10 | 11 | from documents.models import Document 12 | from newsfeed.models import Entry 13 | from notifications.models import Notification 14 | 15 | 16 | @before.all 17 | def switch_to_test_database(): 18 | """ 19 | Switching to the test database 20 | """ 21 | logging.info("Setting up a test database ...\n") 22 | 23 | try: 24 | from south.management.commands import patch_for_test_db_setup 25 | 26 | patch_for_test_db_setup() 27 | except ImportError: 28 | pass 29 | 30 | world.test_runner = DjangoTestSuiteRunner(interactive=False) 31 | world.test_runner.setup_test_environment() 32 | world.test_db = world.test_runner.setup_databases() 33 | call_command('syncdb', **{ 34 | 'settings': settings.SETTINGS_MODULE, 35 | 'interactive': False, 36 | 'verbosity': 0}) 37 | 38 | # Reload mongodb database 39 | settings.MONGODB_DATABASE = settings.MONGODB_TEST_DATABASE 40 | for model in [Document, Entry, Notification]: 41 | model.objects.load() 42 | model.objects.collection.remove() 43 | 44 | 45 | @after.all 46 | def after_all(total): 47 | logging.info("Destroy test database ...\n") 48 | 49 | # Destroy database. 50 | world.test_runner.teardown_databases(world.test_db) 51 | 52 | # Tear Down the test environment. 53 | world.test_runner.teardown_test_environment() 54 | 55 | 56 | @after.each_scenario 57 | def before_each_feature(scenario): 58 | logging.info("Flusing db ... \n") 59 | 60 | call_command('flush', **{ 61 | 'settings': settings.SETTINGS_MODULE, 62 | 'interactive': False}) 63 | 64 | 65 | def setup_test_directory(): 66 | sys.path.append(os.path.join(os.path.dirname(__file__), "../../tests")) 67 | __import__("steps") 68 | 69 | 70 | setup_test_directory() -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/js/libs/keymaster/keymaster.min.js: -------------------------------------------------------------------------------- 1 | // keymaster.js 2 | // (c) 2011 Thomas Fuchs 3 | // keymaster.js may be freely distributed under the MIT license. 4 | (function(a){function h(a,b){var c=a.length;while(c--)if(a[c]===b)return c;return-1}function i(a){var b,g,i,j,k;b=a.keyCode;if(b==93||b==224)b=91;if(b in d){d[b]=!0;for(i in f)f[i]==b&&(l[i]=!0);return}if(!l.filter.call(this,a))return;if(!(b in c))return;for(j=0;j0;for(i in d)if(!d[i]&&h(g.mods,+i)>-1||d[i]&&h(g.mods,+i)==-1)k=!1;(g.mods.length==0&&!d[16]&&!d[18]&&!d[17]&&!d[91]||k)&&g.method(a,g)===!1&&(a.preventDefault?a.preventDefault():a.returnValue=!1,a.stopPropagation&&a.stopPropagation(),a.cancelBubble&&(a.cancelBubble=!0))}}}function j(a){var b=a.keyCode,c;if(b==93||b==224)b=91;if(b in d){d[b]=!1;for(c in f)f[c]==b&&(l[c]=!1)}}function k(){for(b in d)d[b]=!1;for(b in f)l[b]=!1}function l(a,b,d){var e,h,i,j;d===undefined&&(d=b,b="all"),a=a.replace(/\s/g,""),e=a.split(","),e[e.length-1]==""&&(e[e.length-2]+=",");for(i=0;i1){h=a.slice(0,a.length-1);for(j=0;j 0 47 | world.page = world.browser.post(follow_link[0].get("href")) 48 | 49 | @step('click to unfollow button') 50 | def click_to_unfollow_button(step): 51 | dom = html.fromstring(world.page.content) 52 | unfollow_link = dom.cssselect(".unfollow") 53 | assert len(unfollow_link) > 0 54 | world.page = world.browser.delete(unfollow_link[0].get("href")) 55 | 56 | @step('"(.*)" following "(.*)"') 57 | def follow(step, from_username, to_username): 58 | follower = User.objects.get(username=from_username) 59 | following = User.objects.get(username=to_username) 60 | FollowedProfile.objects.create(follower=follower, following=following) -------------------------------------------------------------------------------- /web/dbpatterns/documents/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from documents.fields import SearchInput 4 | from documents.parsers import ParseError 5 | from documents.parsers.django_orm import DjangoORMParser 6 | from documents.parsers.dummy import DummyParser 7 | 8 | DOCUMENT_PARSER_BLANK = "" 9 | DOCUMENT_PARSER_DJANGO_ORM = "django-orm" 10 | 11 | DOCUMENT_PARSER_CHOICES = ( 12 | (DOCUMENT_PARSER_BLANK, "Empty Document"), 13 | (DOCUMENT_PARSER_DJANGO_ORM, "Django Models"), 14 | ) 15 | 16 | DOCUMENT_PARSERS = { 17 | DOCUMENT_PARSER_BLANK: DummyParser, 18 | DOCUMENT_PARSER_DJANGO_ORM: DjangoORMParser 19 | } 20 | 21 | EXAMPLE_DJANGO_MODEL = """ 22 | class Foo(models.Model): 23 | bar = models.CharField(max_length=255) 24 | 25 | 26 | class Bar(models.Model): 27 | foo = models.CharField(max_length=255) 28 | """ 29 | 30 | 31 | class DocumentForm(forms.Form): 32 | title = forms.CharField(label="Document title") 33 | is_public = forms.BooleanField( 34 | widget=forms.RadioSelect( 35 | choices=[(True, 'Public'), (False, 'Private')]), 36 | required=False, 37 | initial=True, 38 | label="The visibility of document") 39 | create_from = forms.CharField( 40 | label="Create from", 41 | widget=forms.Select(choices=DOCUMENT_PARSER_CHOICES), 42 | required=False) 43 | entities = forms.CharField( 44 | label="Template", 45 | widget=forms.Textarea(), 46 | initial=EXAMPLE_DJANGO_MODEL, 47 | required=False) 48 | # TODO: The initial value solution is temporary. Change it. 49 | 50 | def clean_entities(self): 51 | create_from = self.cleaned_data.get("create_from") 52 | if create_from: 53 | try: 54 | parser_class = DOCUMENT_PARSERS[create_from] 55 | except KeyError: 56 | raise forms.ValidationError("Invalid parser.") 57 | else: 58 | parser = parser_class(self.cleaned_data.get("entities")) 59 | try: 60 | parser.is_valid() 61 | except ParseError: 62 | raise forms.ValidationError("Syntax error.") 63 | else: 64 | return parser.parsed 65 | return [] 66 | 67 | 68 | class ForkDocumentForm(forms.Form): 69 | title = forms.CharField(label="Document title") 70 | 71 | 72 | class SearchForm(forms.Form): 73 | keyword = forms.CharField( 74 | widget=SearchInput(placeholder="Type a keyword")) 75 | -------------------------------------------------------------------------------- /web/dbpatterns/comments/models.py: -------------------------------------------------------------------------------- 1 | from bson import ObjectId 2 | 3 | from django.contrib.auth.models import User 4 | from django.core.urlresolvers import reverse 5 | 6 | from gravatar.templatetags.gravatar import gravatar_for_email 7 | from newsfeed.constants import NEWS_TYPE_COMMENT 8 | 9 | from profiles.models import AnonymousProfile 10 | from documents import get_collection 11 | from documents.models import Document 12 | 13 | 14 | class CommentManager(object): 15 | def get(self, **kwargs): 16 | return Comment(get_collection("comments").find_one(kwargs)) 17 | 18 | 19 | class Comment(dict): 20 | """ 21 | A model that allows create, read comments from mongodb 22 | """ 23 | __getattr__ = dict.get 24 | objects = CommentManager() 25 | 26 | @property 27 | def profile_url(self): 28 | """Return profile url of user""" 29 | return reverse("auth_profile", args=[self.user.username]) 30 | 31 | @property 32 | def avatar_url(self): 33 | """Returns the gravar address of email""" 34 | return gravatar_for_email(self.user.email, size=40) 35 | 36 | @property 37 | def username(self): 38 | """Shortcut parameter to username""" 39 | return self.user.username 40 | 41 | @property 42 | def full_name(self): 43 | """Shortcut parameter to full name""" 44 | return self.user.get_full_name() or None 45 | 46 | @property 47 | def user(self): 48 | """Returns cached user""" 49 | if not self._cached_user: 50 | self._cached_user = self.get_user() 51 | 52 | return self._cached_user 53 | 54 | def get_user(self): 55 | """Returns the user of comment""" 56 | try: 57 | return User.objects.get(id=self.user_id) 58 | except User.DoesNotExist: 59 | return AnonymousProfile() 60 | 61 | @property 62 | def document(self): 63 | """Returns the document of comment""" 64 | query = {"_id": ObjectId(self.document_id)} 65 | return Document(get_collection("documents").find_one(query)) 66 | 67 | @property 68 | def pk(self): 69 | """Shortcut method to deal with underscore restriction 70 | of django templates""" 71 | return self._id 72 | 73 | def get_news_type(self): 74 | """Returns newsfeed type.""" 75 | return NEWS_TYPE_COMMENT 76 | 77 | @property 78 | def is_public(self): 79 | """Returns the visibility of the document of comment""" 80 | return self.document.is_public 81 | -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | An error occurred :( 6 | 28 | 29 | 30 |
    31 |

    :(

    32 |

    An error occurred.

    33 |

    Please try again in a few minutes.

    34 | 35 |
    -------------------------------------------------------------------------------- /web/dbpatterns/newsfeed/management/commands/populate_newsfeed.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.core.management import BaseCommand 3 | from pymongo import DESCENDING 4 | from comments.models import Comment 5 | from documents import get_collection 6 | from documents.models import Document 7 | from newsfeed.constants import NEWS_TYPE_REGISTRATION, NEWS_TYPE_DOCUMENT, NEWS_TYPE_FORK, NEWS_TYPE_COMMENT 8 | 9 | class Command(BaseCommand): 10 | """ 11 | A management command for providing initial data for newsfeed. 12 | 13 | - User registrations 14 | - Documents 15 | - Forks 16 | - Comments 17 | 18 | """ 19 | def handle(self, *args, **options): 20 | 21 | newsfeed = get_collection("newsfeed") 22 | 23 | newsfeed.ensure_index([ 24 | ("recipients", DESCENDING), 25 | ("date_created", DESCENDING), 26 | ]) 27 | 28 | newsfeed.remove() 29 | 30 | for user in User.objects.all(): 31 | newsfeed.insert({ 32 | "object_id": user.id, 33 | "news_type": NEWS_TYPE_REGISTRATION, 34 | "date_created": user.date_joined, 35 | "recipients": [], 36 | "sender": { 37 | "username": user.username, 38 | "email": user.email # it's required for gravatar 39 | }, 40 | }) 41 | 42 | 43 | for document in map(Document, get_collection("documents").find()): 44 | news_type = NEWS_TYPE_DOCUMENT if document.fork_of else NEWS_TYPE_FORK 45 | newsfeed.insert({ 46 | "object_id": document.pk, 47 | "news_type": news_type, 48 | "date_created": document.date_created, 49 | "recipients": [], 50 | "sender": { 51 | "username": document.user.username, 52 | "email": document.user.email # it's required for gravatar 53 | }, 54 | }) 55 | 56 | for comment in map(Comment, get_collection("comments").find()): 57 | newsfeed.insert({ 58 | "object_id": comment.pk, 59 | "news_type": NEWS_TYPE_COMMENT, 60 | "date_created": comment.date_created, 61 | "recipients": [], 62 | "sender": { 63 | "username": comment.user.username, 64 | "email": comment.user.email # it's required for gravatar 65 | }, 66 | }) 67 | 68 | print get_collection("newsfeed").count() -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/js/models/attribute.js: -------------------------------------------------------------------------------- 1 | dbpatterns.models.Attribute = Backbone.Model.extend({ 2 | 3 | FOREIGN_KEY_SEPARATOR: "_", 4 | DEFAULT_TYPES: [ 5 | {'field': 'is_', 'value': 'boolean', 'function': 'startsWith'}, 6 | {'field': 'date', 'value': 'datetime', 'function': 'startsWith'}, 7 | {'field': 'date', 'value': 'datetime', 'function': 'endsWith'}, 8 | {'field': 'id', 'value': 'integer', 'function': 'endsWith'}, 9 | {'field': '_at', 'value': 'datetime', 'function': 'endsWith'} 10 | ], 11 | 12 | defaults: { 13 | "order": 0 14 | }, 15 | initialize: function () { 16 | this.detect_foreign_key(); 17 | this.fill_defaults(); 18 | this.on("fk_does_not_exist", this.invoke_foreign_key, this); 19 | this.on("change:name", this.detect_foreign_key, this); 20 | }, 21 | save: function () { 22 | this.collection.trigger("persist"); 23 | }, 24 | detect_foreign_key: function () { 25 | var tokens = this.get("name").split(this.FOREIGN_KEY_SEPARATOR); 26 | if (tokens.length > 1 && this.get("is_foreign_key") === undefined) { 27 | var entity_name = tokens.slice(0, -1).join(this.FOREIGN_KEY_SEPARATOR); 28 | var attribute_name = tokens.slice(-1)[0]; 29 | this.set({ 30 | "is_foreign_key": true, 31 | "foreign_key_entity": entity_name, 32 | "foreign_key_attribute": attribute_name 33 | }).on("render", this.trigger.bind(this, "connect")); 34 | } 35 | }, 36 | invoke_foreign_key: function () { 37 | this.set({ 38 | "is_foreign_key": false, 39 | "foreign_key_entity": "", 40 | "foreign_key_attribute": "" 41 | }); 42 | }, 43 | fill_defaults: function () { 44 | if (!this.get("type")) { 45 | this.set("type", this.get_default_type(this.get("name"))) 46 | } 47 | }, 48 | get_default_type: function (name) { 49 | var value = _(name.toLowerCase()); 50 | var detected_type = 'string'; 51 | _.forEach(this.DEFAULT_TYPES, function(field) { 52 | if (value[field.function](field.field)) { 53 | detected_type = field.value; 54 | return; 55 | } 56 | }); 57 | return detected_type; 58 | } 59 | }); 60 | 61 | dbpatterns.collections.Attribute = Backbone.Collection.extend({ 62 | model: dbpatterns.models.Attribute, 63 | comparator: function (attribute) { 64 | return attribute.get("order"); 65 | } 66 | }); 67 | -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/js/views/user.js: -------------------------------------------------------------------------------- 1 | dbpatterns.views.AssigneeItemView = Backbone.View.extend({ 2 | tagName: "li", 3 | template: $("#assignee-item-template").html(), 4 | events: { 5 | "click .remove": "destroy" 6 | }, 7 | render: function () { 8 | $(this.el).html(_.template(this.template, this.model.toJSON())); 9 | this.$el 10 | .find(".permission") 11 | .val(this.model.get("permission")) 12 | .change(function (event) { 13 | this.model.set("permission", $(event.target).val()) 14 | }.bind(this)); 15 | return this; 16 | }, 17 | destroy: function () { 18 | this.model.destroy(); 19 | } 20 | }); 21 | 22 | dbpatterns.views.AssigneesView = Backbone.View.extend({ 23 | field: "#assignees", 24 | template: $("#assignees-template").html(), 25 | 26 | initialize: function () { 27 | this.model.on("add", this.add_assignee, this); 28 | this.model.on("remove", this.render_assignees, this); 29 | }, 30 | 31 | add_assignee: function (assignee) { 32 | var users = $(this.el).find(".users"); 33 | users.append((new dbpatterns.views.AssigneeItemView({ 34 | model: assignee 35 | })).render().el) 36 | }, 37 | 38 | render_assignees: function () { 39 | $(this.el).find(".users").empty(); 40 | _.forEach(this.model.models, function (assignee) { 41 | this.add_assignee(assignee); 42 | }, this); 43 | }, 44 | 45 | render: function () { 46 | $(this.el).html(_.template(this.template, {})); 47 | var replaced_field = $(this.field).hide(), 48 | user_input = $(this.el).find("#user"); 49 | 50 | this.render_assignees(); 51 | 52 | user_input.autocomplete({ 53 | source: function(request, response) { 54 | var excludes = this.model.pluck("id").join(","); 55 | $.get("/api/users/", { 56 | term: request.term, 57 | excludes: excludes 58 | }, function(data) { 59 | response(data); 60 | }); 61 | }.bind(this), 62 | select: function (event, ui) { 63 | var assignee = new dbpatterns.models.Assignee({ 64 | "id": ui.item.id, 65 | "username": ui.item.label, 66 | "avatar": ui.item.avatar 67 | }); 68 | this.model.add(assignee); 69 | user_input.val(""); 70 | return false; 71 | }.bind(this) 72 | }); 73 | 74 | replaced_field.after(this.el) 75 | } 76 | }); 77 | -------------------------------------------------------------------------------- /web/dbpatterns/api/resources.py: -------------------------------------------------------------------------------- 1 | from bson import ObjectId 2 | 3 | from django.core.urlresolvers import reverse 4 | 5 | from tastypie.bundle import Bundle 6 | from tastypie.resources import Resource 7 | 8 | 9 | class Document(dict): 10 | # dictionary-like object for mongodb documents. 11 | __getattr__ = dict.get 12 | 13 | 14 | class MongoDBResource(Resource): 15 | """ 16 | A base resource that allows to make CRUD operations for mongodb. 17 | """ 18 | 19 | def get_object_class(self): 20 | return self._meta.object_class 21 | 22 | def get_collection(self): 23 | """ 24 | Encapsulates collection name. 25 | """ 26 | raise NotImplementedError("You should implement get_collection method.") 27 | 28 | def obj_get_list(self, request=None, **kwargs): 29 | """ 30 | Maps mongodb documents to Document class. 31 | """ 32 | return map(self.get_object_class(), self.get_collection().find()) 33 | 34 | def obj_get(self, request=None, **kwargs): 35 | """ 36 | Returns mongodb document from provided id. 37 | """ 38 | return self.get_object_class()(self.get_collection().find_one({ 39 | "_id": ObjectId(kwargs.get("pk")) 40 | })) 41 | 42 | def obj_create(self, bundle, **kwargs): 43 | """ 44 | Creates mongodb document from POST data. 45 | """ 46 | bundle.data.update(kwargs) 47 | bundle.obj = self.get_collection().insert(bundle.data) 48 | return bundle 49 | 50 | def obj_update(self, bundle, request=None, **kwargs): 51 | """ 52 | Updates mongodb document. 53 | """ 54 | self.get_collection().update( 55 | {"_id": ObjectId(kwargs.get("pk"))}, 56 | {"$set": bundle.data}) 57 | return bundle 58 | 59 | def obj_delete_list(self, request=None, **kwargs): 60 | """ 61 | Removes all documents from collection 62 | """ 63 | self.get_collection().remove() 64 | 65 | def obj_delete(self, request=None, **kwargs): 66 | """ 67 | Removes single document from collection 68 | """ 69 | parameters = {"_id": ObjectId(kwargs.get("pk"))} 70 | self.get_collection().remove(parameters) 71 | 72 | def get_resource_uri(self, item): 73 | """ 74 | Returns resource URI for bundle or object. 75 | """ 76 | if isinstance(item, Bundle): 77 | if isinstance(item.obj, ObjectId): 78 | pk = str(item.obj) 79 | else: 80 | pk = item.obj._id 81 | else: 82 | pk = item._id 83 | return reverse("api_dispatch_detail", kwargs={ 84 | "resource_name": self._meta.resource_name, 85 | "pk": pk 86 | }) 87 | -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/newsfeed/newsfeed.html: -------------------------------------------------------------------------------- 1 | {% load cache gravatar_tags humanize %} 2 | 3 | {% for entry in newsfeed %} 4 |
    5 | 10 |
    11 | {% if entry.news_type == "registration" %} 12 |

    {{ entry.sender.username }} joined to dbpatterns

    13 | 14 | {% elif entry.news_type == "document" %} 15 |

    {{ entry.sender.username }} created new pattern

    16 |

    {{ entry.related_object.title }}

    17 | 18 | {% with entry.related_object as document %} 19 | {% include "documents/preview.html" %} 20 | {% endwith %} 21 | 22 | {% elif entry.news_type == "fork" %} 23 |

    {{ entry.sender.username }} forked a pattern

    24 |

    {{ entry.related_object.title }}

    25 | 26 | {% with entry.related_object as document %} 27 | {% include "documents/preview.html" %} 28 | {% endwith %} 29 | 30 | {% elif entry.news_type == "comment" %} 31 |

    {{ entry.sender.username }} commented on a pattern

    32 |
    {{ entry.related_object.body }}
    33 | 34 | {% elif entry.news_type == "star" %} 35 |

    {{ entry.sender.username }} starred {{ entry.related_object.title }} pattern

    36 | 37 | {% elif entry.news_type == "following" %} 38 |

    {{ entry.sender.username }} following {{ entry.related_object.username }}

    39 | {% endif %} 40 | 41 | 42 |
    43 |
    44 | {% empty %} 45 |
    Seems to empty your newsfeed. You may follow somebody to see more news.
    46 | {% endfor %} -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page Not Found :( 6 | 28 | 29 | 30 |
    31 |

    Not found :(

    32 |

    Sorry, but the page you were trying to view does not exist.

    33 |

    It looks like this was the result of either:

    34 |
      35 |
    • a mistyped address
    • 36 |
    • an out-of-date link
    • 37 |
    38 | 41 | 42 |
    -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/js/libs/jquery/jquery.class.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Class 3 | * jQuery plug-in that provides several tools for building object oriented 4 | * JavaScript applications. 5 | * 6 | * Requires jQuery library (http://www.jquery.com) 7 | * 8 | * Taylan Pince (taylanpince at gmail dot com) - June 16, 2007 9 | */ 10 | 11 | (function($) { 12 | 13 | jQuery.extend({ 14 | 15 | namespace : function(spaces) { 16 | var parent_space = window; 17 | var namespaces = spaces.split("."); 18 | 19 | for (var i = 0; i < namespaces.length; i++) { 20 | if (typeof parent_space[namespaces[i]] == "undefined") { 21 | parent_space[namespaces[i]] = new Object(); 22 | } 23 | 24 | parent_space = parent_space[namespaces[i]]; 25 | } 26 | 27 | return parent_space; 28 | } 29 | 30 | }); 31 | 32 | var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\bSuper\b/ : /.*/; 33 | 34 | jQuery.Class = function() { 35 | 36 | }; 37 | 38 | jQuery.Class.extend = function(prop) { 39 | var Super = this.prototype; 40 | 41 | initializing = true; 42 | var prototype = new this(); 43 | initializing = false; 44 | 45 | for (var name in prop) { 46 | prototype[name] = typeof prop[name] == "function" && typeof Super[name] == "function" && fnTest.test(prop[name]) ? (function(name, fn) { 47 | return function() { 48 | var tmp = this.Super; 49 | 50 | this.Super = Super[name]; 51 | 52 | var ret = fn.apply(this, arguments); 53 | this.Super = tmp; 54 | 55 | return ret; 56 | }; 57 | })(name, prop[name]) : prop[name]; 58 | } 59 | 60 | function Class() { 61 | if (!initializing && this.init) { 62 | this.init.apply(this, arguments); 63 | } 64 | } 65 | 66 | Class.prototype = prototype; 67 | 68 | Class.constructor = Class; 69 | 70 | Class.extend = arguments.callee; 71 | 72 | return Class; 73 | }; 74 | 75 | if (typeof Function.bind === 'undefined') { 76 | 77 | Function.prototype.bind = function(obj) { 78 | var method = this; 79 | 80 | tmp = function() { 81 | return method.apply(obj, arguments); 82 | }; 83 | 84 | return tmp; 85 | }; 86 | 87 | } 88 | 89 | if (!Array.indexOf){ 90 | Array.prototype.indexOf = function(obj) { 91 | for (var i = 0; i < this.length; i++) { 92 | if (this[i] == obj) { 93 | return i; 94 | } 95 | } 96 | 97 | return -1; 98 | }; 99 | } 100 | 101 | })(jQuery); 102 | -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/blog/donation.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/documents/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
    5 | 11 |
    12 |
      13 | {% for document in documents %} 14 |
    • 15 | {% if not document.is_public %}PRIVATE{% endif %} 16 | {{ document.title }} 17 |
      18 | Delete 19 | {{ document.get_fork_count }} fork 20 | {{ document.star_count }} star 21 |
      22 |
    • 23 | {% empty %} 24 |
    • 25 | You have not created document yet. 26 |
      27 | Click here for new document. 28 |
    • 29 | {% endfor %} 30 |
    31 |
    32 |
    33 |
      34 | {% for document in shared %} 35 |
    • 36 | {% if not document.is_public %}PRIVATE{% endif %} 37 | {{ document.title }} 38 |
      39 | Delete 40 | {{ document.get_fork_count }} fork 41 | {{ document.star_count }} star 42 |
      43 |
    • 44 | {% empty %} 45 |
    • 46 | You have not shared document. 47 |
    • 48 | {% endfor %} 49 |
    50 |
    51 | 52 |
    53 | {% endblock %} 54 | 55 | {% block scripts %} 56 | {{ block.super }} 57 | 79 | {% endblock %} 80 | -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/js/views/comment.js: -------------------------------------------------------------------------------- 1 | dbpatterns.views.CommentForm = Backbone.View.extend({ 2 | tagName: "form", 3 | template: $("#comment-form-template").html(), 4 | events: { 5 | "submit": "submit" 6 | }, 7 | render: function () { 8 | this.$el.html(_.template(this.template, {})); 9 | return this; 10 | }, 11 | submit: function () { 12 | 13 | var data = this.$el.form2json(); 14 | 15 | if (!data.body) { 16 | return false; 17 | } 18 | 19 | var comment = new dbpatterns.models.Comment(data); 20 | this.model.create(comment, { 21 | wait: true 22 | }); 23 | this.reset(); 24 | return false; 25 | }, 26 | reset: function () { 27 | this.$el[0].reset(); 28 | } 29 | }); 30 | 31 | dbpatterns.views.CommentItem = Backbone.View.extend({ 32 | tagName: "li", 33 | template: $("#comment-item-template").html(), 34 | events: { 35 | "click .delete": "destroy" 36 | }, 37 | render: function () { 38 | this.$el.html(_.template(this.template, this.model.toJSON())); 39 | return this; 40 | }, 41 | destroy: function () { 42 | this.model.destroy(); 43 | this.$el.slideUp(300, function () { 44 | this.$el.remove(); 45 | }.bind(this)); 46 | 47 | } 48 | }); 49 | 50 | dbpatterns.views.CommentList = Backbone.View.extend({ 51 | 52 | tagName: "ul", 53 | 54 | initialize: function () { 55 | this.model.on("reset", this.render, this); 56 | this.model.on("add", this.render_comment, this); 57 | this.model.fetch(); 58 | }, 59 | 60 | render: function () { 61 | _.forEach(this.model.models, function (model) { 62 | this.render_comment(model); 63 | }, this); 64 | return this; 65 | }, 66 | 67 | render_comment: function (comment) { 68 | this.$el.append((new dbpatterns.views.CommentItem({ 69 | model: comment 70 | }).render().el)) 71 | 72 | } 73 | 74 | 75 | }); 76 | 77 | dbpatterns.views.Comments = Backbone.View.extend({ 78 | el: $("#comments"), 79 | 80 | events: { 81 | "click .close": "hide" 82 | }, 83 | 84 | initialize: function () { 85 | this.model = new dbpatterns.collections.Comments({ 86 | document: this.options.document 87 | }); 88 | }, 89 | 90 | render: function () { 91 | 92 | this.$el.append((new dbpatterns.views.CommentList({ 93 | model: this.model 94 | })).el); 95 | 96 | this.$el.append((new dbpatterns.views.CommentForm({ 97 | model: this.model 98 | }).render().el)); 99 | 100 | this.model.on("reset", function (model) { 101 | if (!model.models.length) { 102 | this.$el.find(".no-comment").delay(300).slideDown(300); 103 | } 104 | }, this); 105 | 106 | this.model.on("add", function () { 107 | this.$el.find(".no-comment").hide(); 108 | }, this); 109 | 110 | this.show(); 111 | return this; 112 | }, 113 | 114 | show: function () { 115 | this.$el.show(); 116 | }, 117 | 118 | hide: function () { 119 | this.$el.hide(); 120 | } 121 | 122 | }); -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 | 5 |
    6 |

    7 | Dbpatterns is a service that allows you to create, share, explore 8 | database models on the web. 9 | 10 |

    11 |
    12 | 13 |
    14 | 55 | 56 | {% if is_public %} 57 | {% include "newsfeed/public_newsfeed.html" %} 58 | {% else %} 59 | {% include "newsfeed/private_newsfeed.html" %} 60 | {% endif %} 61 |
    62 | 63 | 64 | {% endblock %} 65 | 66 | {% block footer %}{% endblock %} 67 | 68 | {% block scripts %} 69 | {{ block.super }} 70 | 78 | 79 | 80 | {% endblock %} 81 | 82 | -------------------------------------------------------------------------------- /web/dbpatterns/documents/exporters/sql.py: -------------------------------------------------------------------------------- 1 | from documents.constants import * 2 | from documents.exporters import BaseExporter 3 | 4 | 5 | class SQLExporter(BaseExporter): 6 | """ 7 | A base class of all sql exporters. 8 | """ 9 | TYPE_MAPPING = { 10 | TYPES_INTEGER: "int", 11 | TYPES_BOOLEAN: "bool", 12 | TYPES_TEXT: "longtext", 13 | TYPES_TIME: "time", 14 | TYPES_DATETIME: "datetime", 15 | TYPES_DATE: "date", 16 | TYPES_DOUBLE: "double precision" 17 | } 18 | DEFAULT_TYPE = "varchar" 19 | COMMA_LITERAL = "," 20 | QUOTATION_LITERAL = '"' 21 | WHITE_SPACE = "\t" 22 | DEFAULT_VARCHAR_SIZE = 255 23 | 24 | INLINE_FOREIGN_KEYS = True 25 | 26 | def quote(self, text): 27 | return self.QUOTATION_LITERAL + text + self.QUOTATION_LITERAL 28 | 29 | def export(self): 30 | 31 | for entity in self.document.get("entities") or []: 32 | 33 | yield 'CREATE TABLE %s (' % self.quote(entity.get("name")) 34 | if entity.get("attributes"): 35 | yield self.WHITE_SPACE + ('%(comma_literal)s\n%(tab)s' % { 36 | "comma_literal": self.COMMA_LITERAL, 37 | "tab": self.WHITE_SPACE 38 | }).join(list(self.build_columns(entity.get("attributes")))) 39 | 40 | yield ');' 41 | 42 | def build_columns(self, attributes): 43 | for attribute in attributes: 44 | yield " ".join(list( 45 | self.column_for_attribute(attribute))) 46 | 47 | if not self.INLINE_FOREIGN_KEYS: 48 | for attribute in attributes: 49 | if attribute.get("is_foreign_key"): 50 | yield " ".join(list( 51 | self.foreign_key_for_attribute(attribute))) 52 | 53 | def column_for_attribute(self, attribute): 54 | 55 | yield self.quote(attribute.get("name")) 56 | 57 | size = attribute.get("size") or self.DEFAULT_VARCHAR_SIZE if \ 58 | attribute.get("type") == TYPES_STRING else None 59 | column_type = self.TYPE_MAPPING.get(attribute.get("type"), 60 | self.DEFAULT_TYPE) 61 | 62 | yield "%s(%s)" % (column_type, size) if size else column_type 63 | 64 | if attribute.get("is_primary_key"): 65 | yield "PRIMARY KEY" 66 | 67 | if attribute.get("is_not_null"): 68 | yield "NOT NULL" 69 | 70 | if attribute.get("is_unique"): 71 | yield "UNIQUE" 72 | 73 | if self.INLINE_FOREIGN_KEYS and attribute.get("is_foreign_key"): 74 | for line in self.foreign_key_for_attribute(attribute): 75 | yield line 76 | 77 | def foreign_key_for_attribute(self, attribute): 78 | yield "FOREIGN KEY(%s)" % self.quote(attribute.get("name", "")) 79 | yield "REFERENCES" 80 | yield "%s (%s)" % (self.quote(attribute.get("foreign_key_entity", "")), 81 | self.quote( 82 | attribute.get("foreign_key_attribute", ""))) 83 | 84 | 85 | class SQLiteExporter(SQLExporter): 86 | pass 87 | 88 | 89 | class MysqlExporter(SQLExporter): 90 | INLINE_FOREIGN_KEYS = False 91 | QUOTATION_LITERAL = "`" 92 | 93 | 94 | class PostgresExporter(SQLExporter): 95 | INLINE_FOREIGN_KEYS = False 96 | 97 | 98 | class OracleExporter(SQLExporter): 99 | def foreign_key_for_attribute(self, attribute): 100 | yield "CONSTRAINT %s_%s" % (attribute.get("foreign_key_entity", ""), 101 | attribute.get("foreign_key_attribute", "")) 102 | yield "REFERENCES" 103 | yield "%s(%s)" % (attribute.get("foreign_key_entity", ""), 104 | attribute.get("foreign_key_attribute", "")) 105 | -------------------------------------------------------------------------------- /web/dbpatterns/documents/fixtures/test_document.json: -------------------------------------------------------------------------------- 1 | {"entities":[ 2 | { 3 | "attributes":[ 4 | { 5 | "foreign_key_attribute":"", 6 | "foreign_key_entity":"", 7 | "is_foreign_key":false, 8 | "is_primary_key":true, 9 | "name":"id", 10 | "order":0, 11 | "type":"integer" 12 | }, 13 | { 14 | "name":"name", 15 | "order":1, 16 | "type":"string" 17 | } 18 | ], 19 | "name":"permissions", 20 | "position":{ 21 | "left":784, 22 | "top":17 23 | } 24 | }, 25 | { 26 | "attributes":[ 27 | { 28 | "foreign_key_attribute":"id", 29 | "foreign_key_entity":"users", 30 | "is_foreign_key":true, 31 | "is_primary_key":false, 32 | "name":"users_id", 33 | "order":0, 34 | "type":"integer" 35 | }, 36 | { 37 | "foreign_key_attribute":"id", 38 | "foreign_key_entity":"roles", 39 | "is_foreign_key":true, 40 | "is_primary_key":false, 41 | "name":"roles_id", 42 | "order":1, 43 | "type":"integer" 44 | } 45 | ], 46 | "name":"users_roles", 47 | "position":{ 48 | "left":102, 49 | "top":328 50 | } 51 | }, 52 | { 53 | "attributes":[ 54 | { 55 | "foreign_key_attribute":"", 56 | "foreign_key_entity":"", 57 | "is_foreign_key":false, 58 | "is_primary_key":true, 59 | "name":"id", 60 | "order":0, 61 | "type":"integer" 62 | }, 63 | { 64 | "name":"name", 65 | "order":1, 66 | "type":"string" 67 | } 68 | ], 69 | "name":"roles", 70 | "position":{ 71 | "left":414, 72 | "top":17 73 | } 74 | }, 75 | { 76 | "attributes":[ 77 | { 78 | "foreign_key_attribute":"id", 79 | "foreign_key_entity":"roles", 80 | "is_foreign_key":true, 81 | "is_primary_key":false, 82 | "name":"roles_id", 83 | "order":0, 84 | "type":"integer" 85 | }, 86 | { 87 | "foreign_key_attribute":"id", 88 | "foreign_key_entity":"permissions", 89 | "is_foreign_key":true, 90 | "is_primary_key":false, 91 | "name":"permissions_id", 92 | "order":1, 93 | "type":"integer" 94 | } 95 | ], 96 | "name":"roles_permissions", 97 | "position":{ 98 | "left":480, 99 | "top":349 100 | } 101 | }, 102 | { 103 | "attributes":[ 104 | { 105 | "foreign_key_attribute":"", 106 | "foreign_key_entity":"", 107 | "is_foreign_key":false, 108 | "is_primary_key":true, 109 | "name":"id", 110 | "order":0, 111 | "type":"integer" 112 | }, 113 | { 114 | "name":"name", 115 | "order":1, 116 | "type":"string" 117 | } 118 | ], 119 | "name":"users", 120 | "position":{ 121 | "left":58, 122 | "top":15 123 | } 124 | } 125 | ], "id":"507c943589cbad13b228926a", "resource_uri":"/api/documents/507c943589cbad13b228926a/", "title":"User Roles/Permissions", "user_id":38} -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/auth/profile.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load gravatar document_tags %} 3 | 4 | {% block body-id %}profile-detail{% endblock %} 5 | 6 | {% block content %} 7 |
    8 | 52 |
    53 |

    Documents

    54 |
      55 | {% for document in documents %} 56 |
    • 57 | {{ document.title }} 58 |
      59 | {{ document.get_fork_count }} fork 60 | {{ document.star_count }} star 61 |
      62 |
    • 63 | {% empty %} 64 |
    • {{ profile }} does not have any pattern yet.
    • 65 | {% endfor %} 66 |
    67 |
    68 |
    69 | {% endblock %} 70 | 71 | {% block scripts %} 72 | {{ block.super }} 73 | 91 | {% endblock %} -------------------------------------------------------------------------------- /web/dbpatterns/documents/tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | from django.test import TestCase 5 | 6 | from documents.models import Document 7 | from documents.exporters.sql import (MysqlExporter, OracleExporter, 8 | PostgresExporter, SQLiteExporter) 9 | 10 | 11 | TEST_DOCUMENT_PATH = os.path.join(os.path.dirname(__file__), 12 | "fixtures/test_document.json") 13 | 14 | 15 | class ExporterTestCase(TestCase): 16 | def setUp(self): 17 | self.document = Document(json.load(open(TEST_DOCUMENT_PATH))) 18 | 19 | def test_mysql_exporter(self): 20 | """ 21 | Tests MySQL exporter. 22 | """ 23 | exporter = MysqlExporter(self.document) 24 | self.assertEqual(exporter.as_text(), """ 25 | CREATE TABLE `permissions` ( 26 | `id` int PRIMARY KEY, 27 | `name` varchar(255) 28 | ); 29 | CREATE TABLE `users_roles` ( 30 | `users_id` int, 31 | `roles_id` int, 32 | FOREIGN KEY(`users_id`) REFERENCES `users` (`id`), 33 | FOREIGN KEY(`roles_id`) REFERENCES `roles` (`id`) 34 | ); 35 | CREATE TABLE `roles` ( 36 | `id` int PRIMARY KEY, 37 | `name` varchar(255) 38 | ); 39 | CREATE TABLE `roles_permissions` ( 40 | `roles_id` int, 41 | `permissions_id` int, 42 | FOREIGN KEY(`roles_id`) REFERENCES `roles` (`id`), 43 | FOREIGN KEY(`permissions_id`) REFERENCES `permissions` (`id`) 44 | ); 45 | CREATE TABLE `users` ( 46 | `id` int PRIMARY KEY, 47 | `name` varchar(255) 48 | );""".strip()) 49 | 50 | 51 | def test_oracle_exporter(self): 52 | """ 53 | Tests Oracle exporter. 54 | """ 55 | exporter = OracleExporter(self.document) 56 | self.assertEqual(exporter.as_text(), """ 57 | CREATE TABLE "permissions" ( 58 | "id" int PRIMARY KEY, 59 | "name" varchar(255) 60 | ); 61 | CREATE TABLE "users_roles" ( 62 | "users_id" int CONSTRAINT users_id REFERENCES users(id), 63 | "roles_id" int CONSTRAINT roles_id REFERENCES roles(id) 64 | ); 65 | CREATE TABLE "roles" ( 66 | "id" int PRIMARY KEY, 67 | "name" varchar(255) 68 | ); 69 | CREATE TABLE "roles_permissions" ( 70 | "roles_id" int CONSTRAINT roles_id REFERENCES roles(id), 71 | "permissions_id" int CONSTRAINT permissions_id REFERENCES permissions(id) 72 | ); 73 | CREATE TABLE "users" ( 74 | "id" int PRIMARY KEY, 75 | "name" varchar(255) 76 | );""".strip()) 77 | 78 | 79 | def test_postgres_exporter(self): 80 | """ 81 | Tests Postgres exporter. 82 | """ 83 | exporter = PostgresExporter(self.document) 84 | self.assertEqual(exporter.as_text(), """ 85 | CREATE TABLE "permissions" ( 86 | "id" int PRIMARY KEY, 87 | "name" varchar(255) 88 | ); 89 | CREATE TABLE "users_roles" ( 90 | "users_id" int, 91 | "roles_id" int, 92 | FOREIGN KEY("users_id") REFERENCES "users" ("id"), 93 | FOREIGN KEY("roles_id") REFERENCES "roles" ("id") 94 | ); 95 | CREATE TABLE "roles" ( 96 | "id" int PRIMARY KEY, 97 | "name" varchar(255) 98 | ); 99 | CREATE TABLE "roles_permissions" ( 100 | "roles_id" int, 101 | "permissions_id" int, 102 | FOREIGN KEY("roles_id") REFERENCES "roles" ("id"), 103 | FOREIGN KEY("permissions_id") REFERENCES "permissions" ("id") 104 | ); 105 | CREATE TABLE "users" ( 106 | "id" int PRIMARY KEY, 107 | "name" varchar(255) 108 | );""".strip()) 109 | 110 | 111 | def test_sqlite_exporter(self): 112 | """ 113 | Tests SQLite exporter. 114 | """ 115 | exporter = SQLiteExporter(self.document) 116 | self.assertEqual(exporter.as_text(), """ 117 | CREATE TABLE "permissions" ( 118 | "id" int PRIMARY KEY, 119 | "name" varchar(255) 120 | ); 121 | CREATE TABLE "users_roles" ( 122 | "users_id" int FOREIGN KEY("users_id") REFERENCES "users" ("id"), 123 | "roles_id" int FOREIGN KEY("roles_id") REFERENCES "roles" ("id") 124 | ); 125 | CREATE TABLE "roles" ( 126 | "id" int PRIMARY KEY, 127 | "name" varchar(255) 128 | ); 129 | CREATE TABLE "roles_permissions" ( 130 | "roles_id" int FOREIGN KEY("roles_id") REFERENCES "roles" ("id"), 131 | "permissions_id" int FOREIGN KEY("permissions_id") REFERENCES "permissions" ("id") 132 | ); 133 | CREATE TABLE "users" ( 134 | "id" int PRIMARY KEY, 135 | "name" varchar(255) 136 | );""".strip()) -------------------------------------------------------------------------------- /web/dbpatterns/documents/resources.py: -------------------------------------------------------------------------------- 1 | import operator 2 | from bson import ObjectId 3 | 4 | from django.conf.urls import url 5 | from django.core.urlresolvers import reverse 6 | 7 | from tastypie import http 8 | from tastypie import fields 9 | from tastypie.exceptions import ImmediateHttpResponse 10 | from tastypie.utils import trailing_slash 11 | 12 | from api.auth import DocumentsAuthorization 13 | from api.resources import MongoDBResource 14 | from comments.resources import CommentResource 15 | from documents import get_collection 16 | from documents.models import Document 17 | from documents.signals import assignment_done 18 | 19 | 20 | class DocumentResource(MongoDBResource): 21 | id = fields.CharField(attribute="_id") 22 | title = fields.CharField(attribute="title", null=True) 23 | entities = fields.ListField(attribute="entities", null=True) 24 | user_id = fields.IntegerField(attribute="user_id", readonly=True, null=True) 25 | is_public = fields.BooleanField(attribute="is_public", null=True) 26 | assignees = fields.ListField(attribute="assignees", null=True) 27 | 28 | class Meta: 29 | resource_name = "documents" 30 | list_allowed_methods = ["get", "post"] 31 | detail_allowed_methods = ["get", "put"] 32 | authorization = DocumentsAuthorization() 33 | object_class = Document 34 | 35 | def get_collection(self): 36 | return get_collection("documents") 37 | 38 | def obj_get(self, request=None, **kwargs): 39 | """ 40 | Returns mongodb document from provided id. 41 | """ 42 | document = Document.objects.get(_id=ObjectId(kwargs.get("pk"))) 43 | 44 | if request is not None and not document.is_visible(user_id=request.user.id): 45 | raise ImmediateHttpResponse(response=http.HttpUnauthorized()) 46 | 47 | return document 48 | 49 | def obj_create(self, bundle, request=None, **kwargs): 50 | """ 51 | Populates the id of user to create document. 52 | """ 53 | return super(DocumentResource, self).obj_create( 54 | bundle, user_id=request.user.pk) 55 | 56 | def obj_update(self, bundle, request=None, **kwargs): 57 | """ 58 | - Checks the permissions of user, and updates the document 59 | - Fires assignmend_done signals for assigned users 60 | """ 61 | document = self.obj_get(request=request, pk=kwargs.get("pk")) 62 | 63 | if not document.is_editable(user_id=request.user.id): 64 | raise ImmediateHttpResponse(response=http.HttpUnauthorized()) 65 | 66 | bundle = super(DocumentResource, self).obj_update( 67 | bundle, request, **kwargs) 68 | 69 | updated_document = self.obj_get(request=request, pk=kwargs.get("pk")) 70 | if document.assignees != updated_document.assignees: 71 | original = map(operator.itemgetter("id"), document.assignees) 72 | updated = map(operator.itemgetter("id"), updated_document.assignees) 73 | for user_id in set(updated).difference(original): 74 | assignment_done.send( 75 | sender=self, 76 | user_id=user_id, 77 | instance=updated_document) 78 | 79 | return bundle 80 | 81 | def dehydrate(self, bundle): 82 | """ 83 | Inserts the comments uri to the document bundle 84 | """ 85 | bundle.data["comments_uri"] = reverse("api_get_comments", kwargs={ 86 | "resource_name": "documents", 87 | "pk": bundle.data.get("id") 88 | }) 89 | return bundle 90 | 91 | def override_urls(self): 92 | """ 93 | Adds the urls of nested resources 94 | """ 95 | return [ 96 | url(r"^(?P%s)/(?P\w[\w/-]*)/comments%s$" % 97 | (self._meta.resource_name, trailing_slash()), 98 | self.wrap_view('dispatch_comments'), name="api_get_comments"), 99 | ] 100 | 101 | def dispatch_comments(self, request, **kwargs): 102 | document = Document(self.cached_obj_get( 103 | request=request, **self.remove_api_resource_names(kwargs))) 104 | child_resource = CommentResource() 105 | return child_resource.dispatch_list(request, document_id=document.pk) 106 | -------------------------------------------------------------------------------- /tests/steps/document_steps.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from lxml import html 4 | from lettuce import * 5 | 6 | from django.core.urlresolvers import reverse 7 | 8 | from documents.models import Document 9 | from documents.signals import document_done 10 | 11 | @step('create a pattern that named "(.*)"') 12 | @step('there is a pattern that named "(.*)"') 13 | def create_pattern(step, title): 14 | world.created_document_id = Document.objects.collection.insert({ 15 | "title": title, 16 | "user_id": world.user.id 17 | }) 18 | created_document = Document.objects.get(_id=world.created_document_id) 19 | document_done.send(instance=created_document, sender=step) 20 | 21 | @step("go to the created pattern") 22 | def go_to_created_pattern(step): 23 | document_id = Document.objects.collection.find_one().get("_id") 24 | world.page = world.browser.get(reverse("show_document", args=[document_id])) 25 | 26 | @step("go to the that pattern") 27 | def go_to_pattern(step): 28 | document = Document.objects.get(_id=world.created_document_id) 29 | world.page = world.browser.get(document.get_absolute_url()) 30 | assert world.page.status_code == 200, \ 31 | "Got %s" % world.page.status_code 32 | 33 | @step("look the stargazers of that pattern") 34 | def go_to_created_pattern(step): 35 | url = reverse("show_document_stars", 36 | args=[str(world.created_document_id)]) 37 | world.page = world.browser.get(url) 38 | 39 | @step('click to show button') 40 | def click_to_show_button(step): 41 | dom = html.fromstring(world.page.content) 42 | show_link = dom.cssselect(".show") 43 | assert len(show_link) > 0 44 | world.page = world.browser.get(show_link[0].get("href")) 45 | 46 | @step('click to star button') 47 | def click_to_star_button(step): 48 | star_url = reverse("star_document", args=[str(world.created_document_id)]) 49 | world.page = world.browser.post(star_url) 50 | 51 | @step('click to fork button') 52 | def click_to_fork_button(step): 53 | fork_url = reverse("fork_document", args=[str(world.created_document_id)]) 54 | world.page = world.browser.get(fork_url, follow=False) 55 | 56 | @step('Click the first delete button') 57 | def click_the_first_delete_button(step): 58 | dom = html.fromstring(world.page.content) 59 | delete_links = dom.cssselect(".delete") 60 | assert len(delete_links) > 0 61 | assert world.browser.delete(delete_links[0].get("href")).status_code == 204 62 | 63 | @step('fork the that pattern as "(.*)"') 64 | def fork_the_created_pattern(step, title): 65 | fork_url = reverse("fork_document", args=[str(world.created_document_id)]) 66 | world.page = world.browser.post(fork_url, {"title": title}) 67 | 68 | @step('submit the comment') 69 | def submit_comment(step): 70 | comments_resource_uri = reverse("api_get_comments", 71 | args=["documents", str(world.created_document_id)]) 72 | data = world.data.copy() 73 | data["username"] = world.user.username 74 | world.page = world.browser.post(comments_resource_uri, json.dumps(data), 75 | content_type="application/json") 76 | assert world.page.status_code == 201, "Got %s" % world.page.status_code 77 | 78 | @step('the comments of that pattern') 79 | def comments_of_pattern(step): 80 | comments_resource_uri = reverse("api_get_comments", 81 | args=["documents", str(world.created_document_id)]) 82 | world.page = world.browser.get(comments_resource_uri) 83 | assert world.page.status_code == 200, "Got %s" % world.page.status_code 84 | 85 | @step("the comment count of that pattern should be (\d+)") 86 | def comment_count_of_pattern(step, comment_count): 87 | document = Document.objects.get(_id=world.created_document_id) 88 | assert document.comment_count == int(comment_count) 89 | 90 | @step('choose the "(.*)" option as "(.*)"') 91 | def choose_radio_button(step, name, value): 92 | world.data[name] = value 93 | 94 | @step('I star that pattern') 95 | def star_pattern(step): 96 | step.behave_as(""" 97 | When go to the that pattern 98 | And click to star button 99 | """) 100 | 101 | @step('I leave a comment on that pattern') 102 | def star_pattern(step): 103 | step.behave_as(""" 104 | When go to the that pattern 105 | And I type the "body" as "Test Comment" 106 | When I submit the comment 107 | """) -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/js/libs/hipo/hipo.infinityscroll.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Infinite Scroll Plugin 3 | * Requires jQuery library (http://www.jquery.com) and jQuery Class plug-in 4 | * 5 | * Fatih Erikli (fatih at hipo dot biz) - July 31, 2012 6 | */ 7 | 8 | 9 | $.namespace("hipo.InfinityScroll"); 10 | 11 | hipo.InfinityScroll = $.Class.extend({ 12 | 13 | // settings 14 | loader_image : "", 15 | content_selector : "", 16 | pagination_selector : ".pagination", 17 | next_link_selector : ".pagination a.next", 18 | on_page_load : function () {}, 19 | max_page: null, 20 | 21 | // states 22 | loader : null, 23 | active_page : 1, 24 | 25 | // constants 26 | FOOTER_POSITION_THRESHOLD : 0, 27 | MOBILE_FOOTER_POSITION_THRESHOLD : 1000, 28 | 29 | init : function (options) { 30 | 31 | $.extend(this, options); 32 | 33 | this.hide_pagination(); 34 | this.check_scroll(this.load_page.bind(this)); 35 | this.prepare_loader(); 36 | 37 | }, 38 | 39 | load_content : function (response) { 40 | 41 | var content = $(this.content_selector, response).html(); 42 | $(this.content_selector).append(content); 43 | 44 | }, 45 | 46 | load_page : function () { 47 | 48 | var next_page = this.get_next_page(); 49 | 50 | if (this.max_page) { 51 | if (this.active_page >= this.max_page) { 52 | this.show_pagination(); 53 | return; 54 | } 55 | } 56 | 57 | if (next_page) { 58 | 59 | this.remove_pagination(); 60 | this.show_loader(); 61 | 62 | var paginate = function (response) { 63 | this.load_content(response); 64 | this.hide_pagination(); 65 | this.hide_loader(); 66 | 67 | // events 68 | this.on_page_load(); 69 | }; 70 | 71 | $.get(next_page, paginate.bind(this), 'html'); 72 | this.active_page++; 73 | 74 | } 75 | 76 | }, 77 | 78 | get_next_page : function () { 79 | 80 | if ($(this.next_link_selector).length) { 81 | return $(this.next_link_selector).attr("href"); 82 | } else { 83 | return false; 84 | } 85 | 86 | }, 87 | 88 | check_scroll : function (callback) { 89 | 90 | $(window).scroll(function() { 91 | 92 | if ($(window).scrollTop() + $(window).height() > 93 | this.get_doc_height() - this.get_footer_threshold() ) { 94 | callback(); 95 | } 96 | 97 | }.bind(this)); 98 | 99 | }, 100 | 101 | hide_pagination : function () { 102 | 103 | $(this.pagination_selector).hide(); 104 | 105 | }, 106 | 107 | show_pagination : function () { 108 | 109 | $(this.pagination_selector).show(); 110 | 111 | }, 112 | 113 | remove_pagination : function () { 114 | 115 | $(this.pagination_selector).remove() 116 | 117 | }, 118 | 119 | prepare_loader : function () { 120 | 121 | this.loader = $("
    ").css({ 122 | "display": "none", 123 | "text-align": "center", 124 | "padding": "10px", 125 | "clear": "both" 126 | }).append($("", { 127 | "src": this.loader_image 128 | })); 129 | 130 | $(this.content_selector).after(this.loader); 131 | 132 | }, 133 | 134 | show_loader : function () { 135 | 136 | this.loader.show(); 137 | }, 138 | 139 | hide_loader : function () { 140 | 141 | this.loader.hide(); 142 | 143 | }, 144 | 145 | get_footer_threshold : function () { 146 | 147 | return this.is_mobile_device() ? 148 | this.MOBILE_FOOTER_POSITION_THRESHOLD : this.FOOTER_POSITION_THRESHOLD; 149 | 150 | }, 151 | 152 | get_doc_height : function () { 153 | 154 | var D = document; 155 | 156 | return Math.max( 157 | Math.max(D.body.scrollHeight, D.documentElement.scrollHeight), 158 | Math.max(D.body.offsetHeight, D.documentElement.offsetHeight), 159 | Math.max(D.body.clientHeight, D.documentElement.clientHeight) 160 | ); 161 | 162 | }, 163 | 164 | is_mobile_device : function () { 165 | 166 | return navigator.userAgent.toLowerCase().match(/(iphone|ipod|ipad|android)/); 167 | 168 | } 169 | 170 | }); -------------------------------------------------------------------------------- /web/dbpatterns/profiles/views.py: -------------------------------------------------------------------------------- 1 | from itertools import imap 2 | import json 3 | from django.contrib.auth import logout, login, authenticate 4 | from django.contrib.auth.forms import AuthenticationForm 5 | from django.contrib.auth.models import User 6 | from django.core.urlresolvers import reverse 7 | from django.http import HttpResponse 8 | from django.views.generic import ( 9 | FormView, CreateView, RedirectView, DetailView) 10 | 11 | from profiles.forms import RegistrationForm 12 | from documents.models import Document 13 | from documents.resources import DocumentResource 14 | from profiles.management.signals import follow_done, unfollow_done 15 | 16 | 17 | class RegistrationView(CreateView): 18 | form_class = RegistrationForm 19 | template_name = "auth/register.html" 20 | 21 | def form_valid(self, form): 22 | response = super(RegistrationView, self).form_valid(form) 23 | user = authenticate(username=form.cleaned_data["username"], 24 | password=form.cleaned_data["password1"]) 25 | login(self.request, user) 26 | return response 27 | 28 | def get_success_url(self): 29 | return reverse("home") 30 | 31 | 32 | class LoginView(FormView): 33 | form_class = AuthenticationForm 34 | template_name = "auth/login.html" 35 | 36 | def form_valid(self, form): 37 | login(self.request, form.get_user()) 38 | return super(LoginView, self).form_valid(form) 39 | 40 | def get_success_url(self): 41 | return self.request.GET.get("next") or reverse("home") 42 | 43 | def get_context_data(self, **kwargs): 44 | context = super(LoginView, self).get_context_data(**kwargs) 45 | context["next"] = self.request.GET.get("next", "") 46 | return context 47 | 48 | 49 | class LogoutView(RedirectView): 50 | def get(self, request, *args, **kwargs): 51 | logout(request) 52 | return super(LogoutView, self).get(request, *args, **kwargs) 53 | 54 | def get_redirect_url(self, **kwargs): 55 | return reverse("home") 56 | 57 | 58 | class ProfileDetailView(DetailView): 59 | slug_field = 'username' 60 | slug_url_kwarg = 'username' 61 | context_object_name = "profile" 62 | model = User 63 | 64 | def get_context_data(self, **kwargs): 65 | """ 66 | Adds extra context to template 67 | """ 68 | resource = DocumentResource() 69 | user = self.get_object() 70 | 71 | if self.request.user.is_anonymous(): 72 | is_followed = False 73 | can_follow = False 74 | else: 75 | is_followed = self.request.user.following.filter( 76 | following_id=user.pk).exists() 77 | can_follow = self.request.user != user 78 | 79 | documents = resource.get_collection().find({ 80 | 'user_id': user.pk, 81 | '$or': [ 82 | {'is_public': True}, 83 | {'user_id': self.request.user.id} 84 | ]}) 85 | 86 | return super(ProfileDetailView, self).get_context_data( 87 | documents=imap(Document, documents), 88 | is_followed=is_followed, 89 | can_follow=can_follow 90 | ) 91 | 92 | def delete(self, request, **kwargs): 93 | """ 94 | - Removes `FollowedProfile` object for authenticated user. 95 | - Fires unfollow_done signal 96 | """ 97 | user = self.get_object() 98 | user.followers.filter(follower=request.user).delete() 99 | 100 | unfollow_done.send(sender=self, 101 | follower=request.user, following=user) 102 | 103 | return HttpResponse(json.dumps({ 104 | "success": True 105 | })) 106 | 107 | def post(self, request, **kwargs): 108 | """ 109 | - Creates `FollowedProfile` object for authenticated user. 110 | - Fires follow_done signal 111 | """ 112 | user = self.get_object() 113 | 114 | if user.followers.filter(follower=request.user).exists(): 115 | return HttpResponse(json.dumps({ 116 | "error": "You already following this people." 117 | })) 118 | 119 | user.followers.create(follower=request.user) 120 | 121 | follow_done.send(sender=self, 122 | follower=request.user, following=user) 123 | 124 | return HttpResponse(json.dumps({ 125 | "success": True 126 | })) 127 | -------------------------------------------------------------------------------- /web/dbpatterns/profiles/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Adding model 'FollowedProfile' 12 | db.create_table('profiles_followedprofile', ( 13 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 14 | ('follower', self.gf('django.db.models.fields.related.ForeignKey')(related_name='following', to=orm['auth.User'])), 15 | ('following', self.gf('django.db.models.fields.related.ForeignKey')(related_name='followers', to=orm['auth.User'])), 16 | )) 17 | db.send_create_signal('profiles', ['FollowedProfile']) 18 | 19 | 20 | def backwards(self, orm): 21 | # Deleting model 'FollowedProfile' 22 | db.delete_table('profiles_followedprofile') 23 | 24 | 25 | models = { 26 | 'auth.group': { 27 | 'Meta': {'object_name': 'Group'}, 28 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 29 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 30 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 31 | }, 32 | 'auth.permission': { 33 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 34 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 35 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 36 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 37 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 38 | }, 39 | 'auth.user': { 40 | 'Meta': {'object_name': 'User'}, 41 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 42 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 43 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 44 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 45 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 46 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 47 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 48 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 49 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 50 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 51 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 52 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 53 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 54 | }, 55 | 'contenttypes.contenttype': { 56 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 57 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 58 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 59 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 60 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 61 | }, 62 | 'profiles.followedprofile': { 63 | 'Meta': {'object_name': 'FollowedProfile'}, 64 | 'follower': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'following'", 'to': "orm['auth.User']"}), 65 | 'following': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'followers'", 'to': "orm['auth.User']"}), 66 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) 67 | } 68 | } 69 | 70 | complete_apps = ['profiles'] -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/js/views/ui.js: -------------------------------------------------------------------------------- 1 | dbpatterns.views.EditInPlaceForm = Backbone.View.extend({ 2 | tagName: "form", 3 | input: $(""), 4 | events: { 5 | "submit": "submit" 6 | }, 7 | attributes: { 8 | "action": "#" 9 | }, 10 | shortcuts: { 11 | 'esc': 'destroy' 12 | }, 13 | initialize: function () { 14 | _.extend(this, new Backbone.Shortcuts); 15 | this.delegateShortcuts(); 16 | this.submit_callback = function () {}; 17 | this.cancel_callback = function () {}; 18 | }, 19 | render: function (initial) { 20 | this.input.val(initial || ""); 21 | this.$el.append(this.input); 22 | return this; 23 | }, 24 | focus: function () { 25 | this.input.focus(); 26 | }, 27 | submit: function (event) { 28 | this.submit_callback(this.input.val()); 29 | this.$el.remove(); 30 | event.preventDefault(); 31 | return this; 32 | }, 33 | cancel: function (callback) { 34 | this.cancel_callback = callback; 35 | }, 36 | success: function (callback) { 37 | this.submit_callback = callback; 38 | return this; 39 | }, 40 | destroy: function () { 41 | this.cancel_callback(); 42 | this.$el.remove(); 43 | } 44 | }); 45 | 46 | 47 | dbpatterns.views.Dialog = Backbone.View.extend({ 48 | tagName: "div", 49 | container: "body", 50 | className: "dialog", 51 | template: $("#dialog-template").html(), 52 | draggable: true, 53 | events: { 54 | "click .close": "destroy" 55 | }, 56 | shortcuts: { 57 | 'esc': 'destroy' 58 | }, 59 | initialize: function () { 60 | _.extend(this, new Backbone.Shortcuts); 61 | this.delegateShortcuts(); 62 | }, 63 | render: function () { 64 | this.$el.html(_.template(this.template, { 65 | title: this.options.title || "Dialog" 66 | })).appendTo(this.container); 67 | if (this.draggable) { 68 | $(this.$el).draggable(); 69 | } 70 | }, 71 | destroy: function () { 72 | this.$el.remove(); 73 | } 74 | }); 75 | 76 | dbpatterns.views.FormDialog = dbpatterns.views.Dialog.extend({ 77 | initialize: function () { 78 | dbpatterns.views.Dialog.prototype.initialize.apply(this); 79 | this.form = $(this.options.form); 80 | this.form.submit(this.submit.bind(this)); 81 | this.options.submit_callback = function () { 82 | return true; 83 | } 84 | }, 85 | events: { 86 | "click .close": "destroy" 87 | }, 88 | render: function () { 89 | dbpatterns.views.Dialog.prototype.render.apply(this); 90 | this.$el.append(this.form); 91 | this.load_data(this.model.toJSON()); 92 | this.$el.find("input").eq(0).focus(); 93 | this.trigger("render"); 94 | return this; 95 | }, 96 | load_data: function (data) { 97 | _.forEach(data, function (value, key) { 98 | var input = this.form.find("#" + key); 99 | if (input.is(":checkbox")) { 100 | input.attr("checked", value); 101 | } else { 102 | input.val(value); 103 | } 104 | }.bind(this)); 105 | }, 106 | save_data: function () { 107 | this.model.set(this.form.form2json()); 108 | this.model.save(); 109 | }, 110 | submit: function () { 111 | this.save_data(); 112 | if (this.options.submit_callback()) { 113 | this.destroy(); 114 | this.form.remove(); 115 | } 116 | return false; 117 | }, 118 | success: function (callback) { 119 | this.options.submit_callback = callback; 120 | return this; 121 | } 122 | 123 | }); 124 | 125 | 126 | dbpatterns.views.ExportDialog = dbpatterns.views.Dialog.extend({ 127 | template: $("#export-dialog-template").html(), 128 | className: "export-dialog", 129 | events: { 130 | "click .close": "destroy", 131 | "click .new-window": "new_window" 132 | }, 133 | initialize: function () { 134 | dbpatterns.views.Dialog.prototype.initialize.apply(this); 135 | }, 136 | render: function () { 137 | this.$el.html(_.template(this.template, { 138 | title: this.options.title, 139 | code: "Loading" 140 | })).appendTo(this.container); 141 | 142 | $.get(this.options.url, function (response) { 143 | this.$el.find("code").html(response); 144 | prettyPrint(); 145 | }.bind(this)) 146 | }, 147 | new_window: function () { 148 | window.open(this.options.url); 149 | } 150 | }); -------------------------------------------------------------------------------- /web/dbpatterns/comments/resources.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from bson import ObjectId 3 | 4 | from django.conf import settings 5 | from django.core.mail import send_mail 6 | from django.dispatch import receiver 7 | 8 | from tastypie import fields, http 9 | from tastypie.authorization import Authorization 10 | from tastypie.exceptions import ImmediateHttpResponse 11 | 12 | from api.resources import MongoDBResource 13 | from comments.constants import COMMENT_TEMPLATE 14 | from comments.models import Comment 15 | from comments.signals import comment_done, comment_delete 16 | from documents import get_collection 17 | 18 | 19 | class CommentResource(MongoDBResource): 20 | id = fields.CharField(attribute="_id") 21 | body = fields.CharField(attribute="body", null=True) 22 | document_id = fields.CharField( 23 | attribute="document_id", readonly=True, null=True) 24 | date_created = fields.DateTimeField( 25 | attribute="date_created", readonly=True, null=True) 26 | 27 | # profile specific fields 28 | user_id = fields.IntegerField(attribute="user_id", readonly=True, null=True) 29 | username = fields.CharField(attribute="username", readonly=True, null=True) 30 | profile_url = fields.CharField( 31 | attribute="profile_url", readonly=True, null=True) 32 | avatar_url = fields.CharField( 33 | attribute="avatar_url", readonly=True, null=True) 34 | 35 | class Meta: 36 | resource_name = "comments" 37 | list_allowed_methods = ["get", "post"] 38 | detail_allowed_methods = ["get", "delete"] 39 | authorization = Authorization() 40 | object_class = Comment 41 | always_return_data = True 42 | 43 | def dehydrate(self, bundle): 44 | """ 45 | Injects `has_delete_permission` field which indicates the user is 46 | authorized to remove comment or not. 47 | """ 48 | if bundle.request is not None and bundle.request.user.is_authenticated(): 49 | bundle.data["has_delete_permission"] = \ 50 | bundle.request.user.pk == bundle.data.get("user_id") 51 | return bundle 52 | 53 | def get_collection(self): 54 | return get_collection("comments") 55 | 56 | def obj_get_list(self, request=None, **kwargs): 57 | """ 58 | Returns comment of the document 59 | """ 60 | if "document_id" not in kwargs: 61 | return super(CommentResource, self).obj_get_list(request, **kwargs) 62 | 63 | comments = self.get_collection().find({ 64 | "document_id": kwargs.get("document_id") 65 | }).sort([['_id', 1]]) 66 | 67 | return map(self.get_object_class(), comments) 68 | 69 | def obj_delete(self, request=None, **kwargs): 70 | """ 71 | - Removes comment if user is authorized 72 | - Fires comment_delete signal 73 | """ 74 | comment = self.obj_get(**kwargs) 75 | if (request.user.is_anonymous() or 76 | not request.user.pk == comment.get("user_id")): 77 | raise ImmediateHttpResponse(response=http.HttpUnauthorized()) 78 | 79 | super(CommentResource, self).obj_delete(request, **kwargs) 80 | 81 | comment_delete.send( 82 | sender=self, 83 | comment_id=comment.id, 84 | instance=comment 85 | ) 86 | 87 | def obj_create(self, bundle, request=None, **kwargs): 88 | """ 89 | - Creates new comment 90 | - Fires comment_done signal 91 | """ 92 | bundle = super(CommentResource, self).obj_create( 93 | bundle, 94 | user_id=request.user.id, 95 | document_id=kwargs.get("document_id"), 96 | date_created=datetime.now() 97 | ) 98 | 99 | comment_id = bundle.obj 100 | comment = Comment.objects.get(_id=ObjectId(comment_id)) 101 | 102 | comment_done.send( 103 | sender=self, 104 | comment_id=comment_id, 105 | instance=comment 106 | ) 107 | 108 | bundle.obj = comment 109 | 110 | return bundle 111 | 112 | 113 | @receiver(comment_done) 114 | def comment_on(sender, comment_id, **kwargs): 115 | """ 116 | Sends an email to the author of the commented document 117 | """ 118 | comment = Comment(get_collection("comments").find_one({ 119 | "_id": ObjectId(comment_id) 120 | })) 121 | 122 | document = comment.document 123 | 124 | send_mail( 125 | subject="You have new comment(s) on your pattern", 126 | message=COMMENT_TEMPLATE % { 127 | "document_title": document.title, 128 | "document_link": settings.SITE_URL + document.get_absolute_url() 129 | }, 130 | from_email=settings.COMMENTS_FROM_EMAIL, 131 | recipient_list=['"%s" <%s>' % ( 132 | document.user.get_full_name() or document.user.username, 133 | document.user.email)], 134 | fail_silently=True 135 | ) 136 | -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load gravatar_tags %} 2 | 3 | 4 | {% block title %}dbpatterns - create, share, explore database patterns{% endblock %} 5 | {% load compress %} 6 | {% compress css %} 7 | 8 | 9 | 10 | {% endcompress %} 11 | {% block extra-head %}{% endblock %} 12 | 13 | 14 | 15 | {% block header %} 16 | 49 | {% endblock %} 50 | 51 | {% block content %} 52 | 53 | {% endblock %} 54 | 55 | {% block footer %} 56 |
    57 | dbpatterns is an open-source project. you can contribute on 58 | github. 59 |
    60 | {% endblock %} 61 | 62 | {% block scripts %} 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | {% compress js %} 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | {% endcompress %} 82 | 92 | {% endblock %} 93 | 94 | 95 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/js/views/chat.js: -------------------------------------------------------------------------------- 1 | dbpatterns.views.Room = Backbone.View.extend({ 2 | 3 | el: $("#chat"), 4 | button: $("#chat-button"), 5 | 6 | initialize: function () { 7 | this.model.socket.on("message", this.pull_message.bind(this)); 8 | this.model.socket.on("message", this.scroll.bind(this)); 9 | this.model.socket.on("join", this.join.bind(this)); 10 | this.model.socket.on("leave", this.leave.bind(this)); 11 | }, 12 | 13 | render: function () { 14 | 15 | this.$el.find("aside").html((new dbpatterns.views.Clients({ 16 | model: this.model.clients 17 | })).render().el); 18 | 19 | this.$el.find("footer").html((new dbpatterns.views.MessageForm({ 20 | model: this.model 21 | })).render().el); 22 | 23 | this.$el.find("section").html((new dbpatterns.views.Messages({ 24 | model: this.model.messages 25 | })).render().el); 26 | 27 | this.button.click(this.mark_as_read.bind(this)); 28 | 29 | this.render_client_count(); 30 | 31 | return this; 32 | }, 33 | 34 | is_active: function () { 35 | return this.button.hasClass("active"); 36 | }, 37 | 38 | render_client_count: function () { 39 | var count = this.model.clients.length; 40 | this.button.find("#online-count").html(count); 41 | if (count > 1) { 42 | this.button.show(); 43 | } 44 | }, 45 | 46 | pull_message: function (data) { 47 | data.is_read = this.is_active(); 48 | this.model.messages.add(new dbpatterns.models.Message(data)); 49 | if (!data.is_read) { 50 | this.inc_unread_count(); 51 | } 52 | }, 53 | 54 | join: function (username) { 55 | this.model.clients.add(new dbpatterns.models.Client({ 56 | username: username 57 | })); 58 | this.render_client_count(); 59 | dbpatterns.notifications.trigger("flash", username + " joined to the chat room"); 60 | }, 61 | 62 | leave: function (username) { 63 | var client = this.model.clients.get(username); 64 | this.model.clients.remove(client); 65 | dbpatterns.notifications.trigger("flash", username + " left from to the chat room"); 66 | this.render_client_count(); 67 | }, 68 | 69 | scroll: function () { 70 | // keep the scroll to bottom 71 | this.$el.find("section").stop().animate({ 72 | scrollTop: this.$el.find("section")[0].scrollHeight 73 | }, 800); 74 | }, 75 | 76 | mark_as_read: function () { 77 | this.button.find("i").remove(); 78 | }, 79 | 80 | inc_unread_count: function () { 81 | if (!this.button.find("i").length) { 82 | this.button.prepend(""); 83 | } 84 | var bubble = this.button.find("i"); 85 | bubble.html(parseInt(bubble.html() || 0) + 1); 86 | } 87 | 88 | }); 89 | 90 | dbpatterns.views.MessageForm = Backbone.View.extend({ 91 | tagName: "form", 92 | events: { 93 | "submit": "submit" 94 | }, 95 | render: function () { 96 | this.input = $("", { 97 | "type": "text", 98 | "placeholder": "Type text and press enter." 99 | }); 100 | this.$el.html(this.input); 101 | return this; 102 | }, 103 | submit: function () { 104 | this.model.socket.emit("message", this.input.val()); 105 | this.input.val(""); 106 | return false; 107 | } 108 | }); 109 | 110 | dbpatterns.views.Client = Backbone.View.extend({ 111 | tagName: "li", 112 | render: function () { 113 | this.$el.html(this.model.get("username")); 114 | return this; 115 | } 116 | }); 117 | 118 | dbpatterns.views.Clients = Backbone.View.extend({ 119 | tagName: "ul", 120 | initialize: function () { 121 | this.model.on("add remove", this.render, this); 122 | }, 123 | render: function () { 124 | this.$el.empty(); 125 | _.forEach(this.model.models, function (client) { 126 | this.$el.append(new dbpatterns.views.Client({ 127 | model: client 128 | }).render().el); 129 | }, this); 130 | return this; 131 | } 132 | }); 133 | 134 | dbpatterns.views.Message = Backbone.View.extend({ 135 | tagName: "li", 136 | template: $("#message-template").html(), 137 | render: function () { 138 | this.$el.html(_.template(this.template, this.model.toJSON())); 139 | return this; 140 | } 141 | }); 142 | 143 | dbpatterns.views.Messages = Backbone.View.extend({ 144 | tagName: "ul", 145 | initialize: function () { 146 | this.model.on("add", this.add_message, this); 147 | }, 148 | render: function () { 149 | _.forEach(this.model.models, this.add_message, this); 150 | return this; 151 | }, 152 | add_message: function (message) { 153 | this.$el.append(new dbpatterns.views.Message({ 154 | model: message 155 | }).render().el); 156 | } 157 | 158 | }); -------------------------------------------------------------------------------- /web/dbpatterns/documents/models.py: -------------------------------------------------------------------------------- 1 | from pymongo import DESCENDING 2 | 3 | from django.contrib.auth.models import User 4 | from django.core.urlresolvers import reverse 5 | 6 | from documents.constants import CAN_EDIT 7 | from newsfeed.constants import NEWS_TYPE_FORK, NEWS_TYPE_DOCUMENT 8 | from profiles.models import AnonymousProfile 9 | from documents import get_collection 10 | from documents.utils import reverse_tastypie_url 11 | 12 | 13 | class DocumentManager(object): 14 | """The manager of Document model""" 15 | 16 | def __init__(self): 17 | self.load() 18 | 19 | def load(self): 20 | """Reloads mongodb connection""" 21 | self.collection = get_collection("documents") 22 | self.collection.ensure_index([ 23 | ("date_created", DESCENDING), 24 | ]) 25 | 26 | def get(self, **kwargs): 27 | """Returns a single document""" 28 | return Document(self.collection.find_one(kwargs)) 29 | 30 | def assigned(self, user_id): 31 | """Returns assigned user list""" 32 | return self.collection.find({ 33 | "assignees.id": {"$in": [user_id]} 34 | }).sort("date_created", DESCENDING) 35 | 36 | def for_user(self, user_id): 37 | """Returns documents of the given user id""" 38 | return self.collection.find({"user_id": user_id}) \ 39 | .sort("date_created", DESCENDING) 40 | 41 | def featured(self): 42 | """Returns featured documents""" 43 | return map(Document, self.collection.find({ 44 | "is_featured": True})) 45 | 46 | def starred(self, user_id): 47 | """Returns starred documents of user""" 48 | return map(Document, self.collection.find({ 49 | "stars": {"$in": [user_id]}})) 50 | 51 | 52 | class Document(dict): 53 | """A model that allows to make CRUD for documents on mongodb""" 54 | 55 | # use dot notation to reach items of dictionary 56 | # returns none if item is not found 57 | __getattr__ = dict.get 58 | 59 | objects = DocumentManager() 60 | 61 | def __repr__(self): 62 | return self.title 63 | 64 | def get_absolute_url(self): 65 | return reverse("show_document", args=[self.pk]) 66 | 67 | def get_edit_url(self): 68 | return reverse("edit_document", args=[self.pk]) 69 | 70 | def get_resource_uri(self): 71 | return reverse_tastypie_url("documents", self.pk) 72 | 73 | def forks(self): 74 | """Returns forks of document""" 75 | forks = get_collection("documents").find({"fork_of": self.pk}) 76 | return map(Document, forks) 77 | 78 | def get_user(self): 79 | """Returns the user or anonymous user""" 80 | try: 81 | return User.objects.get(id=self.user_id) 82 | except User.DoesNotExist: 83 | return AnonymousProfile() 84 | 85 | @property 86 | def user(self): 87 | """Caches the user""" 88 | if not self._cached_user: 89 | self._cached_user = self.get_user() 90 | return self._cached_user 91 | 92 | @property 93 | def pk(self): 94 | """Shortcut method to deal with underscore restriction 95 | of django templates""" 96 | return self._id 97 | 98 | def get_fork_count(self): 99 | """Returns fork count of document""" 100 | return self.fork_count or 0 101 | 102 | def get_stars(self): 103 | """Returns starred users or empty list""" 104 | return self.stars or [] 105 | 106 | @property 107 | def assignees(self): 108 | """Returns assigned users or empty list""" 109 | return self.get('assignees') or [] 110 | 111 | @property 112 | def star_count(self): 113 | """Counts the stargazers""" 114 | return len(self.get_stars()) 115 | 116 | @property 117 | def comment_count(self): 118 | """Returns the comment count of document""" 119 | comments = get_collection("comments") 120 | return comments.find({"document_id": self.pk}).count() 121 | 122 | def get_stargazers(self): 123 | """Returns the stargazers of document""" 124 | return User.objects.filter(id__in=self.get_stars()) 125 | 126 | def get_news_type(self): 127 | """Returns the new type (fork or document) of document""" 128 | if self.fork_of is not None: 129 | return NEWS_TYPE_FORK 130 | return NEWS_TYPE_DOCUMENT 131 | 132 | def is_visible(self, user_id=None): 133 | """Indicates whether the document is visible to user""" 134 | assignees = [user.get("id") for user in self.assignees] 135 | return (self.is_public or 136 | user_id == self.user_id or 137 | user_id in assignees) 138 | 139 | def is_editable(self, user_id=None): 140 | """Indicates the user can edit this document""" 141 | if user_id == self.user_id: 142 | return True 143 | 144 | for permission in self.assignees: 145 | if (permission.get("id") == user_id and 146 | permission.get("permission") == CAN_EDIT): 147 | return True 148 | return False 149 | -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/js/views/entity.js: -------------------------------------------------------------------------------- 1 | dbpatterns.views.Entity = Backbone.View.extend({ 2 | 3 | HEADER_THRESHOLD: 10, 4 | 5 | tagName: "div", 6 | className: "entity", 7 | template: $("#entity-template").html(), 8 | 9 | events: { 10 | "click .remove-entity": "destroy", 11 | "dblclick h3": "rename" 12 | }, 13 | 14 | initialize: function () { 15 | this.model.on("change:name", this.render_name, this); 16 | this.model.on("destroy", this.detach, this); 17 | }, 18 | 19 | render: function () { 20 | this.$el.html(_.template(this.template, this.model.toJSON())); 21 | 22 | this.$el.css(this.get_entity_positions()); 23 | 24 | jsPlumb.draggable($(this.el), { 25 | "handle": "h3", 26 | "stop": this.on_drag.bind(this) 27 | }); 28 | 29 | (new dbpatterns.views.ConnectorEndpoint({ 30 | "el": this.$el 31 | })).render(); 32 | 33 | this.$el.find(".attributes").html(new dbpatterns.views.Attributes({ 34 | model: this.model.entity_attributes, 35 | app_view: this.options.app_view 36 | }).render().el); 37 | this.render_name(); 38 | 39 | return this; 40 | }, 41 | 42 | detach: function () { 43 | this.remove_reverse_relationships(); 44 | this.remove(); 45 | return this; 46 | }, 47 | 48 | remove_reverse_relationships: function () { 49 | // checks reverse relationships for jsPlump 50 | _.each(jsPlumb.getConnections(), function (connection) { 51 | var source = connection.source, 52 | target = connection.target; 53 | if (target.is(this.$el)) { 54 | jsPlumb.removeAllEndpoints(target); 55 | } 56 | }.bind(this)); 57 | }, 58 | 59 | on_drag: function (event, ui) { 60 | var top = ui.position.top, 61 | left = ui.position.left; 62 | this.model.set({ 63 | "position": { 64 | "top": top, 65 | "left": left 66 | } 67 | }); 68 | 69 | this.model.save(); 70 | }, 71 | 72 | get_entity_positions: function () { 73 | var top = this.model.get("position").top, 74 | left = this.model.get("position").left; 75 | if (top < this.HEADER_THRESHOLD) 76 | top = this.HEADER_THRESHOLD; 77 | return { "top": top, "left": left } 78 | }, 79 | 80 | destroy: function () { 81 | if (window.confirm("Are you sure?")) { 82 | this.model.entity_attributes.trigger("detach"); 83 | this.model.destroy(); 84 | } 85 | }, 86 | 87 | render_name: function () { 88 | this.$el.find("h3").html(this.model.escape("name")); 89 | this.$el.attr("data-entity", this.model.escape("name")); 90 | }, 91 | 92 | rename: function () { 93 | var name = window.prompt("Entity name", this.model.get("name")); 94 | if (name) { 95 | this.model.set({ 96 | "name": name 97 | }); 98 | this.model.save(); 99 | } 100 | }, 101 | 102 | focus: function () { 103 | this.$el.find(".new-attribute").focus(); 104 | return this; 105 | } 106 | 107 | }); 108 | 109 | 110 | dbpatterns.views.Entities = Backbone.View.extend({ 111 | 112 | POSITION_TOP_INCREASE: 50, 113 | POSITION_LEFT_INCREASE: 50, 114 | 115 | el: "article#document", 116 | 117 | events: { 118 | "click .new-entity": "new_entity" 119 | }, 120 | 121 | 122 | shortcuts: { 123 | "option+n": "new_entity" 124 | }, 125 | 126 | initialize: function () { 127 | _.extend(this, new Backbone.Shortcuts); 128 | this.delegateShortcuts(); 129 | this.model.bind("reset", this.render_entities, this); 130 | this.model.bind("add", this.add_entity, this); 131 | }, 132 | 133 | render_entities: function (entities) { 134 | this.reset_entities(); 135 | _.forEach(entities.models, this.add_entity, this); 136 | this.options.app_view.trigger("load"); 137 | }, 138 | 139 | reset_entities: function () { 140 | this.$el.find(".entity").remove(); 141 | }, 142 | 143 | add_entity: function (entity) { 144 | var entity_view = new dbpatterns.views.Entity({ 145 | model: entity, 146 | app_view: this.options.app_view 147 | }); 148 | this.$el.find("#entities").append(entity_view.render().el); 149 | entity_view.focus(); 150 | }, 151 | 152 | new_entity: function (event) { 153 | event.preventDefault(); 154 | var entity_name = window.prompt("Entity name"); 155 | 156 | if (!entity_name) { 157 | return; 158 | } 159 | 160 | var entity = new dbpatterns.models.Entity({ 161 | name: entity_name, 162 | position: this.get_entity_position() 163 | }); 164 | this.model.add(entity); 165 | }, 166 | 167 | get_entity_position: function () { 168 | var previous = dbpatterns.views.Entities.prototype._previous_position; 169 | var position = { 170 | "top": previous.top + this.POSITION_TOP_INCREASE, 171 | "left": previous.left + this.POSITION_LEFT_INCREASE 172 | }; 173 | dbpatterns.views.Entities.prototype._previous_position = position; 174 | return position; 175 | }, 176 | 177 | _previous_position: { 178 | "top": 0, 179 | "left": 0 180 | } 181 | 182 | }); 183 | -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/js/views/document.js: -------------------------------------------------------------------------------- 1 | dbpatterns.views.Document = Backbone.View.extend({ 2 | 3 | messages: { 4 | save: "The document was saved successfully", 5 | rename: "The document was renamed successfully." 6 | }, 7 | 8 | el: "article#document", 9 | 10 | events: { 11 | "click .edit-title": "rename", 12 | "click .edit-document": "show_settings_form", 13 | "click .save-document": "save_document", 14 | "click .export": "export_document", 15 | "click .show": "show_document", 16 | "click #show-comments": "render_comments" 17 | }, 18 | 19 | shortcuts: { 20 | "option+r": "rename", 21 | "option+s": "save_document", 22 | "esc": "hide_comments" 23 | }, 24 | 25 | settings_form_template: $("#document-edit-template").html(), 26 | 27 | initialize: function () { 28 | _.extend(this, new Backbone.Shortcuts); 29 | this.delegateShortcuts(); 30 | 31 | // bindings 32 | this.model.on("change:title", this.render_title, this); 33 | this.model.socket.on("enter", this.enter_room.bind(this)); 34 | 35 | // sub views 36 | this.entities_view = new dbpatterns.views.Entities({ 37 | model: this.model.entities, 38 | app_view: this 39 | }); 40 | this.comments_view = null; // it's lazy. 41 | }, 42 | 43 | render_title: function () { 44 | this.$el.find("header h1").html(this.model.escape("title")); 45 | window.document.title = this.model.get("title"); 46 | return this; 47 | }, 48 | 49 | rename: function () { 50 | 51 | var title = window.prompt("Title", this.model.get("title")); 52 | 53 | if (!title) { 54 | return false; 55 | } 56 | 57 | this.model.set("title", title); 58 | dbpatterns.notifications.trigger("flash", this.messages.rename); 59 | return this; 60 | }, 61 | 62 | save_document: function (callback) { 63 | this.model.save().success(function () { 64 | dbpatterns.notifications.trigger("flash", this.messages.save); 65 | callback && callback(); 66 | }.bind(this)); 67 | }, 68 | 69 | render_comments: function () { 70 | 71 | if (this.comments_view) { 72 | this.comments_view.show(); 73 | return; 74 | } 75 | 76 | this.comments_view = (new dbpatterns.views.Comments({ 77 | document: this.model 78 | })).render(); 79 | 80 | return false; 81 | 82 | }, 83 | 84 | hide_comments: function () { 85 | if (this.comments_view) { 86 | this.comments_view.hide(); 87 | } 88 | }, 89 | 90 | export_document: function () { 91 | var exporter_link = $(event.target); 92 | var dialog = (new dbpatterns.views.ExportDialog({ 93 | title: exporter_link.html(), 94 | url: exporter_link.attr("href") 95 | })); 96 | if (this.options.edit) { 97 | this.save_document(function () { 98 | dialog.render(); 99 | }.bind(this)); 100 | } else { 101 | dialog.render(); 102 | } 103 | return false; 104 | }, 105 | 106 | show_document: function (event) { 107 | this.save_document(_.delay(function () { 108 | window.location = $(event.target).attr("href"); 109 | }, 500)); 110 | return false; 111 | }, 112 | 113 | show_settings_form: function () { 114 | if ($('div.settings').length > 0) { 115 | return false; 116 | } 117 | (new dbpatterns.views.DocumentEditDialog({ 118 | "form": _.template(this.settings_form_template, this), 119 | "model": this.model, 120 | "title": "Document Settings" 121 | })).success(function () { 122 | this.save_document(); 123 | return true; 124 | }.bind(this)).render(); 125 | }, 126 | 127 | enter_room: function (data) { 128 | if (this.options.edit) { 129 | new dbpatterns.views.Room({ 130 | model: new dbpatterns.models.Room(data, { 131 | socket: this.model.socket 132 | }) 133 | }).render(); 134 | } 135 | } 136 | }); 137 | 138 | dbpatterns.views.DocumentEditDialog = dbpatterns.views.FormDialog.extend({ 139 | tagName: "div", 140 | className: "settings", 141 | draggable: false, 142 | 143 | initialize: function () { 144 | dbpatterns.views.FormDialog.prototype.initialize.apply(this); 145 | this.on("render", this.show_assignees) 146 | }, 147 | 148 | show_assignees: function () { 149 | 150 | this.assignees = new dbpatterns.collections.Assignees(this.model.get("assignees")); 151 | var assignees_view = new dbpatterns.views.AssigneesView({ 152 | model: this.assignees 153 | }); 154 | assignees_view.render(); 155 | }, 156 | 157 | load_data: function () { 158 | 159 | this.form.find("#title").val(this.model.get("title")); 160 | 161 | if (this.model.get("is_public")) { 162 | this.form.find("#is_public").attr("checked", "checked"); 163 | } else { 164 | this.form.find("#is_private").attr("checked", "checked"); 165 | } 166 | }, 167 | 168 | save_data: function () { 169 | this.model.set({ 170 | "title": this.form.find("#title").val(), 171 | "is_public": this.form.find("#is_public").is(":checked"), 172 | "assignees": this.assignees.toJSON() 173 | }); 174 | } 175 | }); 176 | -------------------------------------------------------------------------------- /web/dbpatterns/dbpatterns/static/js/libs/backbone/tastypie.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Backbone-tastypie.js 0.1 3 | * (c) 2011 Paul Uithol 4 | * 5 | * Backbone-tastypie may be freely distributed under the MIT license. 6 | * Add or override Backbone.js functionality, for compatibility with django-tastypie. 7 | */ 8 | (function( undefined ) { 9 | "use strict"; 10 | 11 | // Backbone.noConflict support. Save local copy of Backbone object. 12 | var Backbone = window.Backbone; 13 | 14 | Backbone.Tastypie = { 15 | doGetOnEmptyPostResponse: true, 16 | doGetOnEmptyPutResponse: false, 17 | apiKey: { 18 | username: '', 19 | key: '' 20 | } 21 | }; 22 | 23 | /** 24 | * Override Backbone's sync function, to do a GET upon receiving a HTTP CREATED. 25 | * This requires 2 requests to do a create, so you may want to use some other method in production. 26 | * Modified from http://joshbohde.com/blog/backbonejs-and-django 27 | */ 28 | Backbone.oldSync = Backbone.sync; 29 | Backbone.sync = function( method, model, options ) { 30 | var headers = ''; 31 | 32 | if ( Backbone.Tastypie.apiKey && Backbone.Tastypie.apiKey.username.length ) { 33 | headers = _.extend( { 34 | 'Authorization': 'ApiKey ' + Backbone.Tastypie.apiKey.username + ':' + Backbone.Tastypie.apiKey.key 35 | }, options.headers ); 36 | options.headers = headers; 37 | } 38 | 39 | if ( ( method === 'create' && Backbone.Tastypie.doGetOnEmptyPostResponse ) || 40 | ( method === 'update' && Backbone.Tastypie.doGetOnEmptyPutResponse ) ) { 41 | var dfd = new $.Deferred(); 42 | 43 | // Set up 'success' handling 44 | dfd.done( options.success ); 45 | options.success = function( resp, status, xhr ) { 46 | // If create is successful but doesn't return a response, fire an extra GET. 47 | // Otherwise, resolve the deferred (which triggers the original 'success' callbacks). 48 | if ( !resp && ( xhr.status === 201 || xhr.status === 202 || xhr.status === 204 ) ) { // 201 CREATED, 202 ACCEPTED or 204 NO CONTENT; response null or empty. 49 | var location = xhr.getResponseHeader( 'Location' ) || model.id; 50 | return $.ajax( { 51 | url: location, 52 | headers: headers, 53 | success: dfd.resolve, 54 | error: dfd.reject 55 | }); 56 | } 57 | else { 58 | return dfd.resolveWith( options.context || options, [ resp, status, xhr ] ); 59 | } 60 | }; 61 | 62 | // Set up 'error' handling 63 | dfd.fail( options.error ); 64 | options.error = function( xhr, status, resp ) { 65 | dfd.rejectWith( options.context || options, [ xhr, status, resp ] ); 66 | }; 67 | 68 | // Make the request, make it accessibly by assigning it to the 'request' property on the deferred 69 | dfd.request = Backbone.oldSync( method, model, options ); 70 | return dfd; 71 | } 72 | 73 | return Backbone.oldSync( method, model, options ); 74 | }; 75 | 76 | Backbone.Model.prototype.idAttribute = 'resource_uri'; 77 | 78 | Backbone.Model.prototype.url = function() { 79 | // Use the id if possible 80 | var url = this.id; 81 | 82 | // If there's no idAttribute, use the 'urlRoot'. Fallback to try to have the collection construct a url. 83 | // Explicitly add the 'id' attribute if the model has one. 84 | if ( !url ) { 85 | url = this.urlRoot; 86 | url = url || this.collection && ( _.isFunction( this.collection.url ) ? this.collection.url() : this.collection.url ); 87 | 88 | if ( url && this.has( 'id' ) ) { 89 | url = addSlash( url ) + this.get( 'id' ); 90 | } 91 | } 92 | 93 | url = url && addSlash( url ); 94 | 95 | return url || null; 96 | }; 97 | 98 | /** 99 | * Return the first entry in 'data.objects' if it exists and is an array, or else just plain 'data'. 100 | */ 101 | Backbone.Model.prototype.parse = function( data ) { 102 | return data && data.objects && ( _.isArray( data.objects ) ? data.objects[ 0 ] : data.objects ) || data; 103 | }; 104 | 105 | /** 106 | * Return 'data.objects' if it exists. 107 | * If present, the 'data.meta' object is assigned to the 'collection.meta' var. 108 | */ 109 | Backbone.Collection.prototype.parse = function( data ) { 110 | if ( data && data.meta ) { 111 | this.meta = data.meta; 112 | } 113 | 114 | return data && data.objects; 115 | }; 116 | 117 | Backbone.Collection.prototype.url = function( models ) { 118 | var url = this.urlRoot || ( models && models.length && models[0].urlRoot ); 119 | url = url && addSlash( url ); 120 | 121 | // Build a url to retrieve a set of models. This assume the last part of each model's idAttribute 122 | // (set to 'resource_uri') contains the model's id. 123 | if ( models && models.length ) { 124 | var ids = _.map( models, function( model ) { 125 | var parts = _.compact( model.id.split( '/' ) ); 126 | return parts[ parts.length - 1 ]; 127 | }); 128 | url += 'set/' + ids.join( ';' ) + '/'; 129 | } 130 | 131 | return url || null; 132 | }; 133 | 134 | var addSlash = function( str ) { 135 | return str + ( ( str.length > 0 && str.charAt( str.length - 1 ) === '/' ) ? '' : '/' ); 136 | } 137 | })(); -------------------------------------------------------------------------------- /web/dbpatterns/notifications/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from bson import ObjectId 3 | 4 | from django.core.urlresolvers import reverse 5 | from django.dispatch import receiver 6 | from pymongo import DESCENDING 7 | 8 | from comments.signals import comment_done 9 | from documents import get_collection 10 | from documents.models import Document 11 | from documents.signals import fork_done, star_done, assignment_done 12 | from profiles.management.signals import follow_done 13 | from notifications.constants import (NOTIFICATION_TYPE_COMMENT, 14 | NOTIFICATION_TYPE_FORK, 15 | NOTIFICATION_TYPE_STAR, 16 | NOTIFICATION_TYPE_FOLLOWING, 17 | NOTIFICATION_TEMPLATES, 18 | NOTIFICATION_TYPE_ASSIGNMENT) 19 | 20 | 21 | class NotificationManager(object): 22 | """ 23 | A class that allows create, edit, read notifications. 24 | """ 25 | 26 | def __init__(self): 27 | self.load() 28 | 29 | def load(self): 30 | self.collection = get_collection("notifications") 31 | self.collection.ensure_index([ 32 | ("date_created", DESCENDING), 33 | ]) 34 | 35 | def filter_by_user_id(self, user_id): 36 | """ 37 | Shows the notifications of user. 38 | """ 39 | return self.collection.find({ 40 | "recipient": user_id, 41 | }) 42 | 43 | def mark_as_read(self, user_id): 44 | """ 45 | Marks as read the notifications of user. 46 | """ 47 | self.collection.update( 48 | {"recipient": user_id, "is_read": False}, 49 | {"$set": {"is_read": True}}, multi=True) 50 | 51 | def create(self, object_id, notification_type, sender, recipient_id): 52 | """ 53 | Creates notification from provided parameters. 54 | """ 55 | # if the sender affect the own document, 56 | # ignore it. 57 | if not sender.id == recipient_id: 58 | self.collection.insert({ 59 | "is_read": False, 60 | "object_id": object_id, 61 | "type": notification_type, 62 | "date_created": datetime.today(), 63 | "sender": { 64 | "username": sender.username, 65 | "email": sender.email # it's required for gravatar 66 | }, 67 | "recipient": recipient_id 68 | }) 69 | 70 | 71 | class Notification(dict): 72 | """ 73 | A model that wraps mongodb document 74 | """ 75 | objects = NotificationManager() 76 | 77 | def get_absolute_url(self): 78 | url_resolvers = { 79 | NOTIFICATION_TYPE_COMMENT: 80 | lambda: reverse("show_document", args=[self.get("object_id")]), 81 | 82 | NOTIFICATION_TYPE_FORK: 83 | lambda: reverse("show_document", args=[self.get("object_id")]), 84 | 85 | NOTIFICATION_TYPE_STAR: 86 | lambda: reverse("show_document", args=[self.get("object_id")]), 87 | 88 | NOTIFICATION_TYPE_ASSIGNMENT: 89 | lambda: reverse("edit_document", args=[self.get("object_id")]), 90 | 91 | NOTIFICATION_TYPE_FOLLOWING: 92 | lambda: reverse("auth_profile", 93 | args=[self.get("sender").get("username")]), 94 | 95 | } 96 | return url_resolvers[self.get("type")]() 97 | 98 | def as_text(self): 99 | return NOTIFICATION_TEMPLATES[self.get("type")] % self.get("sender") 100 | 101 | 102 | @receiver(comment_done) 103 | def create_comment_notification(instance, **kwargs): 104 | """ 105 | Sends notification to the user of commented document. 106 | """ 107 | Notification.objects.create( 108 | object_id=instance.document._id, 109 | notification_type=NOTIFICATION_TYPE_COMMENT, 110 | sender=instance.user, 111 | recipient_id=instance.document.user_id 112 | ) 113 | 114 | 115 | @receiver(fork_done) 116 | def create_fork_notification(instance, **kwargs): 117 | """ 118 | Sends notification to the user of forked document. 119 | """ 120 | forked_document = Document.objects.get( 121 | _id=ObjectId(instance.fork_of)) 122 | 123 | Notification.objects.create( 124 | object_id=instance._id, 125 | notification_type=NOTIFICATION_TYPE_FORK, 126 | sender=instance.user, 127 | recipient_id=forked_document.user_id 128 | ) 129 | 130 | 131 | @receiver(star_done) 132 | def create_star_notification(instance, user, **kwargs): 133 | """ 134 | Sends notification to the user of starred document. 135 | """ 136 | Notification.objects.create( 137 | object_id=instance._id, 138 | notification_type=NOTIFICATION_TYPE_STAR, 139 | sender=user, 140 | recipient_id=instance.user_id 141 | ) 142 | 143 | 144 | @receiver(follow_done) 145 | def create_following_notification(following, follower, **kwargs): 146 | """ 147 | Sends notification to the followed user from the follower. 148 | """ 149 | Notification.objects.create( 150 | object_id=follower.id, 151 | notification_type=NOTIFICATION_TYPE_FOLLOWING, 152 | sender=follower, 153 | recipient_id=following.id 154 | ) 155 | 156 | 157 | @receiver(assignment_done) 158 | def create_assignment_notification(instance, user_id, **kwargs): 159 | """ 160 | Sends notification to the assigned users on the document 161 | """ 162 | Notification.objects.create( 163 | object_id=instance.id, 164 | notification_type=NOTIFICATION_TYPE_ASSIGNMENT, 165 | sender=instance.user, 166 | recipient_id=user_id 167 | ) 168 | --------------------------------------------------------------------------------