diff --git a/samples/development-frameworks/django/LICENSE b/samples/development-frameworks/django/LICENSE
new file mode 100644
index 00000000..fc4e74e5
--- /dev/null
+++ b/samples/development-frameworks/django/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Vitor Freitas
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/samples/development-frameworks/django/Procfile b/samples/development-frameworks/django/Procfile
new file mode 100644
index 00000000..d4f4007b
--- /dev/null
+++ b/samples/development-frameworks/django/Procfile
@@ -0,0 +1 @@
+web: gunicorn bootcamp.wsgi --log-file -
\ No newline at end of file
diff --git a/samples/development-frameworks/django/README.md b/samples/development-frameworks/django/README.md
new file mode 100644
index 00000000..1c4f3a02
--- /dev/null
+++ b/samples/development-frameworks/django/README.md
@@ -0,0 +1,87 @@
+# Bootcamp
+
+[](https://travis-ci.org/vitorfs/bootcamp)
+
+Bootcamp is an open source **enterprise social network** built with [Python][0] using the [Django Web Framework][1].
+
+The project has three basic apps:
+
+* Feed (A Twitter-like microblog)
+* Articles (A collaborative blog)
+* Question & Answers (A Stack Overflow-like platform)
+
+## Feed App
+
+The Feed app has infinite scrolling, activity notifications, live updates for likes and comments, and comment tracking.
+
+
+## Articles App
+
+The Articles app is a basic blog, with articles pagination, tag filtering and draft management.
+
+
+## Question & Answers App
+
+The Q&A app works just like Stack Overflow. You can mark a question as favorite, vote up or vote down answers, accept an answer and so on.
+
+
+## Technology Stack
+
+- Python 2.7
+- Django 1.9.4
+- Twitter Bootstrap 3
+- jQuery 2
+
+
+## Installation
+
+### 1 Install Python 2.7 and Django Framework 1.9
+
+**Python 2.7.x**
+https://www.python.org/downloads/
+
+
+
+### 2 Install dependencies
+On the project root there is a requirements.pip file. Make sure you install all the required dependencies before running Bootcamp
+
+ pip install -U -r requirements.txt
+
+**Note:** If you are having problems with Pillow installation please take a look on a detailed installation guide at: http://pillow.readthedocs.org/en/latest/installation.html
+
+
+### 3 Syncdb
+
+Edit your settings.py file with your database information
+
+ DATABASES = {
+ 'default': {
+ 'ENGINE': 'sql_server.pyodbc',
+ 'NAME': 'django',
+ 'USER': 'yourusername',
+ 'PASSWORD': 'yourpassword',
+ 'HOST': 'yourserver',
+ 'PORT': '1433',
+
+ 'OPTIONS': {
+ 'driver': 'ODBC Driver 13 for SQL Server',
+ },
+ },
+ }
+
+Then run the database migration
+
+ python manage.py migrate
+
+### 4 Run
+
+ python manage.py runserver
+
+
+## Demo
+
+Try Bootcamp now at [http://trybootcamp.vitorfs.com][2].
+
+[0]: https://www.python.org/
+[1]: https://www.djangoproject.com/
+[2]: http://trybootcamp.vitorfs.com/
diff --git a/samples/development-frameworks/django/bootcamp/__init__.py b/samples/development-frameworks/django/bootcamp/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/samples/development-frameworks/django/bootcamp/activities/__init__.py b/samples/development-frameworks/django/bootcamp/activities/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/samples/development-frameworks/django/bootcamp/activities/migrations/0001_initial.py b/samples/development-frameworks/django/bootcamp/activities/migrations/0001_initial.py
new file mode 100644
index 00000000..c8c9a7bf
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/activities/migrations/0001_initial.py
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.4 on 2016-03-18 13:32
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('feeds', '0001_initial'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('questions', '0001_initial'),
+ ('articles', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Activity',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('activity_type', models.CharField(choices=[(b'F', b'Favorite'), (b'L', b'Like'), (b'U', b'Up Vote'), (b'D', b'Down Vote')], max_length=1)),
+ ('date', models.DateTimeField(auto_now_add=True)),
+ ('feed', models.IntegerField(blank=True, null=True)),
+ ('question', models.IntegerField(blank=True, null=True)),
+ ('answer', models.IntegerField(blank=True, null=True)),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'verbose_name': 'Activity',
+ 'verbose_name_plural': 'Activities',
+ },
+ ),
+ migrations.CreateModel(
+ name='Notification',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('date', models.DateTimeField(auto_now_add=True)),
+ ('notification_type', models.CharField(choices=[(b'L', b'Liked'), (b'C', b'Commented'), (b'F', b'Favorited'), (b'A', b'Answered'), (b'W', b'Accepted Answer'), (b'E', b'Edited Article'), (b'S', b'Also Commented')], max_length=1)),
+ ('is_read', models.BooleanField(default=False)),
+ ('answer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='questions.Answer')),
+ ('article', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='articles.Article')),
+ ('feed', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='feeds.Feed')),
+ ('from_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL)),
+ ('question', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='questions.Question')),
+ ('to_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'ordering': ('-date',),
+ 'verbose_name': 'Notification',
+ 'verbose_name_plural': 'Notifications',
+ },
+ ),
+ ]
diff --git a/samples/development-frameworks/django/bootcamp/activities/migrations/__init__.py b/samples/development-frameworks/django/bootcamp/activities/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/samples/development-frameworks/django/bootcamp/activities/models.py b/samples/development-frameworks/django/bootcamp/activities/models.py
new file mode 100644
index 00000000..16c417c6
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/activities/models.py
@@ -0,0 +1,142 @@
+from django.db import models
+from django.contrib.auth.models import User
+from django.utils.html import escape
+
+
+class Activity(models.Model):
+ FAVORITE = 'F'
+ LIKE = 'L'
+ UP_VOTE = 'U'
+ DOWN_VOTE = 'D'
+ ACTIVITY_TYPES = (
+ (FAVORITE, 'Favorite'),
+ (LIKE, 'Like'),
+ (UP_VOTE, 'Up Vote'),
+ (DOWN_VOTE, 'Down Vote'),
+ )
+
+ user = models.ForeignKey(User)
+ activity_type = models.CharField(max_length=1, choices=ACTIVITY_TYPES)
+ date = models.DateTimeField(auto_now_add=True)
+ feed = models.IntegerField(null=True, blank=True)
+ question = models.IntegerField(null=True, blank=True)
+ answer = models.IntegerField(null=True, blank=True)
+
+ class Meta:
+ verbose_name = 'Activity'
+ verbose_name_plural = 'Activities'
+
+ def __unicode__(self):
+ return self.activity_type
+
+# def save(self, *args, **kwargs):
+# super(Activity, self).save(*args, **kwargs)
+# if self.activity_type == Activity.FAVORITE:
+# Question = models.get_model('questions', 'Question')
+# question = Question.objects.get(pk=self.question)
+# user = question.user
+# user.profile.reputation = user.profile.reputation + 5
+# user.save()
+
+
+class Notification(models.Model):
+ LIKED = 'L'
+ COMMENTED = 'C'
+ FAVORITED = 'F'
+ ANSWERED = 'A'
+ ACCEPTED_ANSWER = 'W'
+ EDITED_ARTICLE = 'E'
+ ALSO_COMMENTED = 'S'
+ NOTIFICATION_TYPES = (
+ (LIKED, 'Liked'),
+ (COMMENTED, 'Commented'),
+ (FAVORITED, 'Favorited'),
+ (ANSWERED, 'Answered'),
+ (ACCEPTED_ANSWER, 'Accepted Answer'),
+ (EDITED_ARTICLE, 'Edited Article'),
+ (ALSO_COMMENTED, 'Also Commented'),
+ )
+
+ _LIKED_TEMPLATE = u'{1} liked your post: {3} '
+ _COMMENTED_TEMPLATE = u'{1} commented on your post: {3} '
+ _FAVORITED_TEMPLATE = u'{1} favorited your question: {3} '
+ _ANSWERED_TEMPLATE = u'{1} answered your question: {3} '
+ _ACCEPTED_ANSWER_TEMPLATE = u'{1} accepted your answer: {3} '
+ _EDITED_ARTICLE_TEMPLATE = u'{1} edited your article: {3} '
+ _ALSO_COMMENTED_TEMPLATE = u'{1} also commentend on the post: {3} '
+
+ from_user = models.ForeignKey(User, related_name='+')
+ to_user = models.ForeignKey(User, related_name='+')
+ date = models.DateTimeField(auto_now_add=True)
+ feed = models.ForeignKey('feeds.Feed', null=True, blank=True)
+ question = models.ForeignKey('questions.Question', null=True, blank=True)
+ answer = models.ForeignKey('questions.Answer', null=True, blank=True)
+ article = models.ForeignKey('articles.Article', null=True, blank=True)
+ notification_type = models.CharField(max_length=1, choices=NOTIFICATION_TYPES)
+ is_read = models.BooleanField(default=False)
+
+ class Meta:
+ verbose_name = 'Notification'
+ verbose_name_plural = 'Notifications'
+ ordering = ('-date',)
+
+ def __unicode__(self):
+ if self.notification_type == self.LIKED:
+ return self._LIKED_TEMPLATE.format(
+ escape(self.from_user.username),
+ escape(self.from_user.profile.get_screen_name()),
+ self.feed.pk,
+ escape(self.get_summary(self.feed.post))
+ )
+ elif self.notification_type == self.COMMENTED:
+ return self._COMMENTED_TEMPLATE.format(
+ escape(self.from_user.username),
+ escape(self.from_user.profile.get_screen_name()),
+ self.feed.pk,
+ escape(self.get_summary(self.feed.post))
+ )
+ elif self.notification_type == self.FAVORITED:
+ return self._FAVORITED_TEMPLATE.format(
+ escape(self.from_user.username),
+ escape(self.from_user.profile.get_screen_name()),
+ self.question.pk,
+ escape(self.get_summary(self.question.title))
+ )
+ elif self.notification_type == self.ANSWERED:
+ return self._ANSWERED_TEMPLATE.format(
+ escape(self.from_user.username),
+ escape(self.from_user.profile.get_screen_name()),
+ self.question.pk,
+ escape(self.get_summary(self.question.title))
+ )
+ elif self.notification_type == self.ACCEPTED_ANSWER:
+ return self._ACCEPTED_ANSWER_TEMPLATE.format(
+ escape(self.from_user.username),
+ escape(self.from_user.profile.get_screen_name()),
+ self.answer.question.pk,
+ escape(self.get_summary(self.answer.description))
+ )
+ elif self.notification_type == self.EDITED_ARTICLE:
+ return self._EDITED_ARTICLE_TEMPLATE.format(
+ escape(self.from_user.username),
+ escape(self.from_user.profile.get_screen_name()),
+ self.article.slug,
+ escape(self.get_summary(self.article.title))
+ )
+ elif self.notification_type == self.ALSO_COMMENTED:
+ return self._ALSO_COMMENTED_TEMPLATE.format(
+ escape(self.from_user.username),
+ escape(self.from_user.profile.get_screen_name()),
+ self.feed.pk,
+ escape(self.get_summary(self.feed.post))
+ )
+ else:
+ return 'Ooops! Something went wrong.'
+
+ def get_summary(self, value):
+ summary_size = 50
+ if len(value) > summary_size:
+ return u'{0}...'.format(value[:summary_size])
+
+ else:
+ return value
diff --git a/samples/development-frameworks/django/bootcamp/activities/static/css/notifications.css b/samples/development-frameworks/django/bootcamp/activities/static/css/notifications.css
new file mode 100644
index 00000000..160df29d
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/activities/static/css/notifications.css
@@ -0,0 +1,33 @@
+ul.all-notifications {
+ padding: 0;
+ margin-top: 1em;
+}
+
+ul.all-notifications li {
+ list-style: none;
+ border-bottom: 1px solid #eeeeee;
+ padding: .8em 0;
+}
+
+ul.all-notifications li:last-child {
+ border-bottom: none;
+}
+
+ul.all-notifications li small {
+ color: #cccccc;
+ font-size: .8em;
+}
+
+ul.all-notifications li p {
+ margin: 0;
+}
+
+ul.all-notifications li div {
+ margin-left: 40px;
+ padding-left: 1em;
+}
+
+ul.all-notifications .user-picture {
+ width: 40px;
+ float: left;
+}
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/activities/static/js/notifications.js b/samples/development-frameworks/django/bootcamp/activities/static/js/notifications.js
new file mode 100644
index 00000000..ff839b40
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/activities/static/js/notifications.js
@@ -0,0 +1,42 @@
+$(function () {
+ $('#notifications').popover({html: true, content: 'Loading...', trigger: 'manual'});
+
+ $("#notifications").click(function () {
+ if ($(".popover").is(":visible")) {
+ $("#notifications").popover('hide');
+ }
+ else {
+ $("#notifications").popover('show');
+ $.ajax({
+ url: '/notifications/last/',
+ beforeSend: function () {
+ $(".popover-content").html("
");
+ $("#notifications").removeClass("new-notifications");
+ },
+ success: function (data) {
+ $(".popover-content").html(data);
+ }
+ });
+ }
+ return false;
+ });
+
+ function check_notifications() {
+ $.ajax({
+ url: '/notifications/check/',
+ cache: false,
+ success: function (data) {
+ if (data != "0") {
+ $("#notifications").addClass("new-notifications");
+ }
+ else {
+ $("#notifications").removeClass("new-notifications");
+ }
+ },
+ complete: function () {
+ window.setTimeout(check_notifications, 30000);
+ }
+ });
+ };
+ check_notifications();
+});
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/activities/templates/activities/last_notifications.html b/samples/development-frameworks/django/bootcamp/activities/templates/activities/last_notifications.html
new file mode 100644
index 00000000..fd7f6e0e
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/activities/templates/activities/last_notifications.html
@@ -0,0 +1,15 @@
+{% load i18n %}
+{% load humanize %}
+
+
diff --git a/samples/development-frameworks/django/bootcamp/activities/templates/activities/notifications.html b/samples/development-frameworks/django/bootcamp/activities/templates/activities/notifications.html
new file mode 100644
index 00000000..9b7eb15d
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/activities/templates/activities/notifications.html
@@ -0,0 +1,30 @@
+{% extends 'base.html' %}
+{% load staticfiles %}
+{% load i18n %}
+
+{% load humanize %}
+
+{% block title %} Notifications {% endblock %}
+
+{% block head %}
+
+{% endblock head %}
+
+{% block main %}
+
+
+ {% for notification in notifications %}
+
+
+
+
{{ notification.date|naturaltime }}
+
{{ notification|safe }}
+
+
+ {% empty %}
+ {% trans 'You have no notification' %}
+ {% endfor %}
+
+{% endblock main %}
diff --git a/samples/development-frameworks/django/bootcamp/activities/tests.py b/samples/development-frameworks/django/bootcamp/activities/tests.py
new file mode 100644
index 00000000..7ce503c2
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/activities/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/samples/development-frameworks/django/bootcamp/activities/views.py b/samples/development-frameworks/django/bootcamp/activities/views.py
new file mode 100644
index 00000000..6b4312f7
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/activities/views.py
@@ -0,0 +1,42 @@
+from django.shortcuts import render
+from bootcamp.activities.models import Notification
+from django.http import HttpResponse
+from django.contrib.auth.decorators import login_required
+from bootcamp.decorators import ajax_required
+
+
+@login_required
+def notifications(request):
+ user = request.user
+ notifications = Notification.objects.filter(to_user=user)
+ unread = Notification.objects.filter(to_user=user, is_read=False)
+ for notification in unread:
+ notification.is_read = True
+ notification.save()
+
+ return render(request, 'activities/notifications.html',
+ {'notifications': notifications})
+
+
+@login_required
+@ajax_required
+def last_notifications(request):
+ user = request.user
+ notifications = Notification.objects.filter(to_user=user,
+ is_read=False)[:5]
+ for notification in notifications:
+ notification.is_read = True
+ notification.save()
+
+ return render(request,
+ 'activities/last_notifications.html',
+ {'notifications': notifications})
+
+
+@login_required
+@ajax_required
+def check_notifications(request):
+ user = request.user
+ notifications = Notification.objects.filter(to_user=user,
+ is_read=False)[:5]
+ return HttpResponse(len(notifications))
diff --git a/samples/development-frameworks/django/bootcamp/articles/__init__.py b/samples/development-frameworks/django/bootcamp/articles/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/samples/development-frameworks/django/bootcamp/articles/forms.py b/samples/development-frameworks/django/bootcamp/articles/forms.py
new file mode 100644
index 00000000..7ee9346b
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/articles/forms.py
@@ -0,0 +1,20 @@
+from django import forms
+from bootcamp.articles.models import Article
+
+
+class ArticleForm(forms.ModelForm):
+ status = forms.CharField(widget=forms.HiddenInput())
+ title = forms.CharField(
+ widget=forms.TextInput(attrs={'class': 'form-control'}),
+ max_length=255)
+ content = forms.CharField(
+ widget=forms.Textarea(attrs={'class': 'form-control'}),
+ max_length=4000)
+ tags = forms.CharField(
+ widget=forms.TextInput(attrs={'class': 'form-control'}),
+ max_length=255, required=False,
+ help_text='Use spaces to separate the tags, such as "java jsf primefaces"')
+
+ class Meta:
+ model = Article
+ fields = ['title', 'content', 'tags', 'status']
diff --git a/samples/development-frameworks/django/bootcamp/articles/migrations/0001_initial.py b/samples/development-frameworks/django/bootcamp/articles/migrations/0001_initial.py
new file mode 100644
index 00000000..3edb75d9
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/articles/migrations/0001_initial.py
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.4 on 2016-03-18 13:32
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Article',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=255)),
+ ('slug', models.SlugField(blank=True, max_length=255, null=True)),
+ ('content', models.TextField(max_length=4000)),
+ ('status', models.CharField(choices=[(b'D', b'Draft'), (b'P', b'Published')], default=b'D', max_length=1)),
+ ('create_date', models.DateTimeField(auto_now_add=True)),
+ ('update_date', models.DateTimeField(blank=True, null=True)),
+ ('create_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ('update_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'ordering': ('-create_date',),
+ 'verbose_name': 'Article',
+ 'verbose_name_plural': 'Articles',
+ },
+ ),
+ migrations.CreateModel(
+ name='ArticleComment',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('comment', models.CharField(max_length=500)),
+ ('date', models.DateTimeField(auto_now_add=True)),
+ ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='articles.Article')),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'ordering': ('date',),
+ 'verbose_name': 'Article Comment',
+ 'verbose_name_plural': 'Article Comments',
+ },
+ ),
+ migrations.CreateModel(
+ name='Tag',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('tag', models.CharField(max_length=50)),
+ ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='articles.Article')),
+ ],
+ options={
+ 'verbose_name': 'Tag',
+ 'verbose_name_plural': 'Tags',
+ },
+ ),
+ migrations.AlterUniqueTogether(
+ name='tag',
+ unique_together=set([('tag', 'article')]),
+ ),
+ migrations.AlterIndexTogether(
+ name='tag',
+ index_together=set([('tag', 'article')]),
+ ),
+ ]
diff --git a/samples/development-frameworks/django/bootcamp/articles/migrations/__init__.py b/samples/development-frameworks/django/bootcamp/articles/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/samples/development-frameworks/django/bootcamp/articles/models.py b/samples/development-frameworks/django/bootcamp/articles/models.py
new file mode 100644
index 00000000..51234f14
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/articles/models.py
@@ -0,0 +1,116 @@
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+from django.contrib.auth.models import User
+from datetime import datetime
+from django.template.defaultfilters import slugify
+import markdown
+
+
+class Article(models.Model):
+ DRAFT = 'D'
+ PUBLISHED = 'P'
+ STATUS = (
+ (DRAFT, 'Draft'),
+ (PUBLISHED, 'Published'),
+ )
+
+ title = models.CharField(max_length=255)
+ slug = models.SlugField(max_length=255, null=True, blank=True)
+ content = models.TextField(max_length=4000)
+ status = models.CharField(max_length=1, choices=STATUS, default=DRAFT)
+ create_user = models.ForeignKey(User)
+ create_date = models.DateTimeField(auto_now_add=True)
+ update_date = models.DateTimeField(blank=True, null=True)
+ update_user = models.ForeignKey(User, null=True, blank=True,
+ related_name="+")
+
+ class Meta:
+ verbose_name = _("Article")
+ verbose_name_plural = _("Articles")
+ ordering = ("-create_date",)
+
+ def __unicode__(self):
+ return self.title
+
+ def save(self, *args, **kwargs):
+ if not self.pk:
+ super(Article, self).save(*args, **kwargs)
+ else:
+ self.update_date = datetime.now()
+ if not self.slug:
+ slug_str = "%s %s" % (self.pk, self.title.lower())
+ self.slug = slugify(slug_str)
+ super(Article, self).save(*args, **kwargs)
+
+ def get_content_as_markdown(self):
+ return markdown.markdown(self.content, safe_mode='escape')
+
+ @staticmethod
+ def get_published():
+ articles = Article.objects.filter(status=Article.PUBLISHED)
+ return articles
+
+ def create_tags(self, tags):
+ tags = tags.strip()
+ tag_list = tags.split(' ')
+ for tag in tag_list:
+ if tag:
+ t, created = Tag.objects.get_or_create(tag=tag.lower(),
+ article=self)
+
+ def get_tags(self):
+ return Tag.objects.filter(article=self)
+
+ def get_summary(self):
+ if len(self.content) > 255:
+ return u'{0}...'.format(self.content[:255])
+ else:
+ return self.content
+
+ def get_summary_as_markdown(self):
+ return markdown.markdown(self.get_summary(), safe_mode='escape')
+
+ def get_comments(self):
+ return ArticleComment.objects.filter(article=self)
+
+
+class Tag(models.Model):
+ tag = models.CharField(max_length=50)
+ article = models.ForeignKey(Article)
+
+ class Meta:
+ verbose_name = _('Tag')
+ verbose_name_plural = _('Tags')
+ unique_together = (('tag', 'article'),)
+ index_together = [['tag', 'article'], ]
+
+ def __unicode__(self):
+ return self.tag
+
+ @staticmethod
+ def get_popular_tags():
+ tags = Tag.objects.all()
+ count = {}
+ for tag in tags:
+ if tag.article.status == Article.PUBLISHED:
+ if tag.tag in count:
+ count[tag.tag] = count[tag.tag] + 1
+ else:
+ count[tag.tag] = 1
+ sorted_count = sorted(count.items(), key=lambda t: t[1], reverse=True)
+ return sorted_count[:20]
+
+
+class ArticleComment(models.Model):
+ article = models.ForeignKey(Article)
+ comment = models.CharField(max_length=500)
+ date = models.DateTimeField(auto_now_add=True)
+ user = models.ForeignKey(User)
+
+ class Meta:
+ verbose_name = _("Article Comment")
+ verbose_name_plural = _("Article Comments")
+ ordering = ("date",)
+
+ def __unicode__(self):
+ return u'{0} - {1}'.format(self.user.username, self.article.title)
diff --git a/samples/development-frameworks/django/bootcamp/articles/static/css/articles.css b/samples/development-frameworks/django/bootcamp/articles/static/css/articles.css
new file mode 100644
index 00000000..ebdec821
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/articles/static/css/articles.css
@@ -0,0 +1,103 @@
+.articles article {
+ padding-bottom: 1.4em;
+ border-bottom: 1px solid #eeeeee;
+ margin-top: 2.4em;
+}
+
+.articles article p {
+ font-size: 1.2em;
+}
+
+.articles article:last-child {
+ border-bottom: none;
+}
+
+.info {
+ margin-bottom: .5em;
+ color: #a0a0a0;
+}
+
+.info a {
+ color: #a0a0a0;
+}
+
+.info > span {
+ margin-right: 1em;
+}
+
+.info .user img {
+ width: 20px;
+ border-radius: 3px;
+}
+
+article .tags {
+ font-size: 1em;
+}
+
+.popular-tags h4 {
+ margin-top: 2em;
+}
+
+.popular-tags .label {
+ margin-top: .4em;
+ font-size: .8em;
+ line-height: 2;
+}
+
+.popular-tags a:hover, .tags a:hover {
+ text-decoration: none;
+}
+
+.user-portrait img {
+ border-radius: 4px;
+ width: 34px;
+}
+
+.user-portrait {
+ width: 40px;
+ float: left;
+}
+
+.comment-input {
+ float: left;
+ width: -moz-calc(100% - 40px);
+ width: -webkit-calc(100% - 40px);
+ width: calc(100% - 40px);
+}
+
+.post-comment {
+ margin-bottom: 1em;
+}
+
+.comment-portrait {
+ width: 40px;
+ border-radius: 5px;
+ float: left;
+}
+
+.comment-text {
+ margin-left: 50px;
+}
+
+.comment-text h5 {
+ padding-top: .1em;
+ margin: .2em 0;
+}
+
+.comment-text h5 small {
+ margin-left: .6em;
+}
+
+.comment-text p {
+ margin: 0;
+ font-size: .9em;
+}
+
+.comment {
+ border-bottom: 1px solid #e3e3e3;
+ padding: .8em 0;
+}
+
+.comment:last-child {
+ border-bottom: none;
+}
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/articles/static/js/articles.js b/samples/development-frameworks/django/bootcamp/articles/static/js/articles.js
new file mode 100644
index 00000000..954f3d8a
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/articles/static/js/articles.js
@@ -0,0 +1,56 @@
+$(function () {
+ $(".publish").click(function () {
+ $("input[name='status']").val("P");
+ $("form").submit();
+ });
+
+ $(".draft").click(function () {
+ $("input[name='status']").val("D");
+ $("form").submit();
+ });
+
+ $(".preview").click(function () {
+ $.ajax({
+ url: '/articles/preview/',
+ data: $("form").serialize(),
+ cache: false,
+ type: 'post',
+ beforeSend: function () {
+ $("#preview .modal-body").html("");
+ },
+ success: function (data) {
+ $("#preview .modal-body").html(data);
+ }
+ });
+ });
+
+ $("#comment").focus(function () {
+ $(this).attr("rows", "3");
+ $("#comment-helper").fadeIn();
+ });
+
+ $("#comment").blur(function () {
+ $(this).attr("rows", "1");
+ $("#comment-helper").fadeOut();
+ });
+
+ $("#comment").keydown(function (evt) {
+ var keyCode = evt.which?evt.which:evt.keyCode;
+ if (evt.ctrlKey && (keyCode == 10 || keyCode == 13)) {
+ $.ajax({
+ url: '/articles/comment/',
+ data: $("#comment-form").serialize(),
+ cache: false,
+ type: 'post',
+ success: function (data) {
+ $("#comment-list").html(data);
+ var comment_count = $("#comment-list .comment").length;
+ $(".comment-count").text(comment_count);
+ $("#comment").val("");
+ $("#comment").blur();
+ }
+ });
+ }
+ });
+
+});
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/articles/templates/articles/article.html b/samples/development-frameworks/django/bootcamp/articles/templates/articles/article.html
new file mode 100644
index 00000000..d7427196
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/articles/templates/articles/article.html
@@ -0,0 +1,19 @@
+{% extends 'base.html' %}
+{% load staticfiles %}
+{% load i18n %}
+
+{% block title %}{{ article.title }}{% endblock %}
+
+{% block head %}
+
+
+{% endblock head %}
+
+{% block main %}
+
+ {% trans 'Articles' %}
+ {% trans 'Article' %}
+
+ {% include 'articles/partial_article.html' with article=article %}
+ {% include 'articles/partial_article_comments.html' with article=article %}
+{% endblock main %}
diff --git a/samples/development-frameworks/django/bootcamp/articles/templates/articles/articles.html b/samples/development-frameworks/django/bootcamp/articles/templates/articles/articles.html
new file mode 100644
index 00000000..83f31170
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/articles/templates/articles/articles.html
@@ -0,0 +1,41 @@
+{% extends 'base.html' %}
+{% load staticfiles %}
+{% load i18n %}
+
+{% block title %} {% trans 'Articles' %} {% endblock %}
+
+{% block head %}
+
+{% endblock head %}
+
+{% block main %}
+
+
+
+
+ {% for article in articles %}
+ {% include 'articles/partial_article.html' with article=article %}
+ {% empty %}
+
+ {% endfor %}
+
+
+
+
+
+
+ {% include 'paginator.html' with paginator=articles %}
+
+
+{% endblock main %}
diff --git a/samples/development-frameworks/django/bootcamp/articles/templates/articles/drafts.html b/samples/development-frameworks/django/bootcamp/articles/templates/articles/drafts.html
new file mode 100644
index 00000000..b54b0fc5
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/articles/templates/articles/drafts.html
@@ -0,0 +1,42 @@
+{% extends 'base.html' %}
+{% load staticfiles %}
+{% load i18n %}
+
+{% block head %}
+
+{% endblock head %}
+
+{% block main %}
+
+ {% trans 'Articles' %}
+ {% trans 'Drafts' %}
+
+
+
+
+ {% trans 'Title' %}
+ {% trans 'Content' %}
+ {% trans 'Tags' %}
+
+
+
+ {% for article in drafts %}
+
+ {{ article.title }}
+ {{ article.get_summary_as_markdown|safe }}
+
+ {% for tag in article.get_tags %}
+ {{ tag }}
+ {% endfor %}
+
+
+ {% empty %}
+
+
+ {% trans 'No draft to display' %}
+
+
+ {% endfor %}
+
+
+{% endblock main %}
diff --git a/samples/development-frameworks/django/bootcamp/articles/templates/articles/edit.html b/samples/development-frameworks/django/bootcamp/articles/templates/articles/edit.html
new file mode 100644
index 00000000..1d992c05
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/articles/templates/articles/edit.html
@@ -0,0 +1,39 @@
+{% extends 'base.html' %}
+{% load staticfiles %}
+{% load i18n %}
+
+{% block head %}
+
+{% endblock head %}
+
+{% block main %}
+
+ {% trans 'Articles' %}
+ {% trans 'Drafts' %}
+ {% trans 'Edit' %}
+
+
+{% endblock main %}
diff --git a/samples/development-frameworks/django/bootcamp/articles/templates/articles/partial_article.html b/samples/development-frameworks/django/bootcamp/articles/templates/articles/partial_article.html
new file mode 100644
index 00000000..4c7c2af6
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/articles/templates/articles/partial_article.html
@@ -0,0 +1,27 @@
+
+
+
+
+ {{ article.get_content_as_markdown|safe }}
+
+ {% if article.get_tags %}
+
+ {% endif %}
+
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/articles/templates/articles/partial_article_comment.html b/samples/development-frameworks/django/bootcamp/articles/templates/articles/partial_article_comment.html
new file mode 100644
index 00000000..fd512296
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/articles/templates/articles/partial_article_comment.html
@@ -0,0 +1,12 @@
+{% load humanize %}
+
+
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/articles/templates/articles/partial_article_comments.html b/samples/development-frameworks/django/bootcamp/articles/templates/articles/partial_article_comments.html
new file mode 100644
index 00000000..fcf77c25
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/articles/templates/articles/partial_article_comments.html
@@ -0,0 +1,23 @@
+{% load i18n %}
+
+
+ {% trans 'Comments' %}
+
+
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/articles/templates/articles/write.html b/samples/development-frameworks/django/bootcamp/articles/templates/articles/write.html
new file mode 100644
index 00000000..fc80219f
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/articles/templates/articles/write.html
@@ -0,0 +1,55 @@
+{% extends 'base.html' %}
+{% load staticfiles %}
+{% load i18n %}
+
+{% block head %}
+
+{% endblock head %}
+
+{% block main %}
+
+ {% trans 'Articles' %}
+ {% trans 'Write Article' %}
+
+
+
+
+
+{% endblock main %}
diff --git a/samples/development-frameworks/django/bootcamp/articles/tests/__init__.py b/samples/development-frameworks/django/bootcamp/articles/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/samples/development-frameworks/django/bootcamp/articles/tests/test_articles.py b/samples/development-frameworks/django/bootcamp/articles/tests/test_articles.py
new file mode 100644
index 00000000..de52e152
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/articles/tests/test_articles.py
@@ -0,0 +1,39 @@
+from django.test import TestCase, Client, RequestFactory
+from django.core.urlresolvers import reverse
+from bootcamp.articles.models import Article
+from django.contrib.auth.models import User
+
+
+class ArticleTest(TestCase):
+ def setUp(self):
+ self.client = Client()
+ self.factory = RequestFactory()
+
+ def test_validate_article_edition(self):
+ c = Client()
+ user1 = User.objects.create_user(username="teste1234",
+ email="reallynice@gmail.com",
+ password="supersecret123")
+ user2 = User.objects.create_user(username="teste12345",
+ email="reallynice2@gmail.com",
+ password="supersecret123")
+
+ article = Article()
+ article.title = "nicetitle"
+ article.content = "nicecontent"
+ article.create_user = user2
+ article.create_user.id = user2.id
+ article.save()
+ self.client.login(username="teste1234", password="supersecret123")
+ response = self.client.get(reverse('edit_article', kwargs={'id': '1'}))
+ self.assertEqual(response.status_code, 302)
+ response = self.client.post(reverse('edit_article',
+ kwargs={'id': '1'}))
+ self.assertEqual(response.status_code, 302)
+ self.client.login(username="teste12345", password="supersecret123")
+ response = self.client.get(reverse('edit_article', kwargs={'id': '1'}),
+ user=user2)
+ self.assertEqual(response.status_code, 200)
+ response = self.client.post(reverse('edit_article',
+ kwargs={'id': '1'}))
+ self.assertEqual(response.status_code, 200)
diff --git a/samples/development-frameworks/django/bootcamp/articles/urls.py b/samples/development-frameworks/django/bootcamp/articles/urls.py
new file mode 100644
index 00000000..8b22d5d8
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/articles/urls.py
@@ -0,0 +1,16 @@
+# coding: utf-8
+
+from django.conf.urls import patterns, include, url
+
+from bootcamp.articles import views
+
+urlpatterns = [
+ url(r'^$', views.articles, name='articles'),
+ url(r'^write/$', views.write, name='write'),
+ url(r'^preview/$', views.preview, name='preview'),
+ url(r'^drafts/$', views.drafts, name='drafts'),
+ url(r'^comment/$', views.comment, name='comment'),
+ url(r'^tag/(?P.+)/$', views.tag, name='tag'),
+ url(r'^edit/(?P\d+)/$', views.edit, name='edit_article'),
+ url(r'^(?P[-\w]+)/$', views.article, name='article'),
+]
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/articles/views.py b/samples/development-frameworks/django/bootcamp/articles/views.py
new file mode 100644
index 00000000..2e6f2850
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/articles/views.py
@@ -0,0 +1,144 @@
+from django.shortcuts import render, redirect, get_object_or_404
+from django.http import HttpResponseBadRequest, HttpResponse
+from bootcamp.articles.models import Article, Tag, ArticleComment
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+from bootcamp.articles.forms import ArticleForm
+from django.contrib.auth.decorators import login_required
+from bootcamp.decorators import ajax_required
+import markdown
+from django.template.loader import render_to_string
+
+
+def _articles(request, articles):
+ paginator = Paginator(articles, 10)
+ page = request.GET.get('page')
+ try:
+ articles = paginator.page(page)
+ except PageNotAnInteger:
+ articles = paginator.page(1)
+ except EmptyPage:
+ articles = paginator.page(paginator.num_pages)
+ popular_tags = Tag.get_popular_tags()
+ return render(request, 'articles/articles.html', {
+ 'articles': articles,
+ 'popular_tags': popular_tags
+ })
+
+
+@login_required
+def articles(request):
+ all_articles = Article.get_published()
+ return _articles(request, all_articles)
+
+
+@login_required
+def article(request, slug):
+ article = get_object_or_404(Article, slug=slug, status=Article.PUBLISHED)
+ return render(request, 'articles/article.html', {'article': article})
+
+
+@login_required
+def tag(request, tag_name):
+ tags = Tag.objects.filter(tag=tag_name)
+ articles = []
+ for tag in tags:
+ if tag.article.status == Article.PUBLISHED:
+ articles.append(tag.article)
+ return _articles(request, articles)
+
+
+@login_required
+def write(request):
+ if request.method == 'POST':
+ form = ArticleForm(request.POST)
+ if form.is_valid():
+ article = Article()
+ article.create_user = request.user
+ article.title = form.cleaned_data.get('title')
+ article.content = form.cleaned_data.get('content')
+ status = form.cleaned_data.get('status')
+ if status in [Article.PUBLISHED, Article.DRAFT]:
+ article.status = form.cleaned_data.get('status')
+ article.save()
+ tags = form.cleaned_data.get('tags')
+ article.create_tags(tags)
+ return redirect('/articles/')
+ else:
+ form = ArticleForm()
+ return render(request, 'articles/write.html', {'form': form})
+
+
+@login_required
+def drafts(request):
+ drafts = Article.objects.filter(create_user=request.user,
+ status=Article.DRAFT)
+ return render(request, 'articles/drafts.html', {'drafts': drafts})
+
+
+@login_required
+def edit(request, id):
+ tags = ''
+ if id:
+ article = get_object_or_404(Article, pk=id)
+ for tag in article.get_tags():
+ tags = u'{0} {1}'.format(tags, tag.tag)
+ tags = tags.strip()
+ else:
+ article = Article(create_user=request.user)
+
+ if article.create_user.id != request.user.id:
+ return redirect('home')
+
+ if request.POST:
+ form = ArticleForm(request.POST, instance=article)
+ if form.is_valid():
+ form.save()
+ return redirect('/articles/')
+ else:
+ form = ArticleForm(instance=article, initial={'tags': tags})
+ return render(request, 'articles/edit.html', {'form': form})
+
+
+@login_required
+@ajax_required
+def preview(request):
+ try:
+ if request.method == 'POST':
+ content = request.POST.get('content')
+ html = 'Nothing to display :('
+ if len(content.strip()) > 0:
+ html = markdown.markdown(content, safe_mode='escape')
+ return HttpResponse(html)
+ else:
+ return HttpResponseBadRequest()
+
+ except Exception, e:
+ return HttpResponseBadRequest()
+
+
+@login_required
+@ajax_required
+def comment(request):
+ try:
+ if request.method == 'POST':
+ article_id = request.POST.get('article')
+ article = Article.objects.get(pk=article_id)
+ comment = request.POST.get('comment')
+ comment = comment.strip()
+ if len(comment) > 0:
+ article_comment = ArticleComment(user=request.user,
+ article=article,
+ comment=comment)
+ article_comment.save()
+ html = u''
+ for comment in article.get_comments():
+ html = u'{0}{1}'.format(html, render_to_string('articles/partial_article_comment.html',
+ {'comment': comment}))
+
+ return HttpResponse(html)
+
+ else:
+ return HttpResponseBadRequest()
+
+ except Exception, e:
+ return HttpResponseBadRequest()
diff --git a/samples/development-frameworks/django/bootcamp/authentication/__init__.py b/samples/development-frameworks/django/bootcamp/authentication/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/samples/development-frameworks/django/bootcamp/authentication/forms.py b/samples/development-frameworks/django/bootcamp/authentication/forms.py
new file mode 100644
index 00000000..ecce4a79
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/authentication/forms.py
@@ -0,0 +1,92 @@
+from django import forms
+from django.core.exceptions import ValidationError
+from django.contrib.auth.models import User
+from bootcamp.settings import ALLOWED_SIGNUP_DOMAINS
+
+
+def SignupDomainValidator(value):
+ if '*' not in ALLOWED_SIGNUP_DOMAINS:
+ try:
+ domain = value[value.index("@"):]
+ if domain not in ALLOWED_SIGNUP_DOMAINS:
+ raise ValidationError(u'Invalid domain. Allowed domains on this network: {0}'.format(','.join(ALLOWED_SIGNUP_DOMAINS)))
+
+ except Exception, e:
+ raise ValidationError(u'Invalid domain. Allowed domains on this network: {0}'.format(','.join(ALLOWED_SIGNUP_DOMAINS)))
+
+
+def ForbiddenUsernamesValidator(value):
+ forbidden_usernames = ['admin', 'settings', 'news', 'about', 'help',
+ 'signin', 'signup', 'signout', 'terms', 'privacy',
+ 'cookie', 'new', 'login', 'logout', 'administrator',
+ 'join', 'account', 'username', 'root', 'blog',
+ 'user', 'users', 'billing', 'subscribe', 'reviews',
+ 'review', 'blog', 'blogs', 'edit', 'mail', 'email',
+ 'home', 'job', 'jobs', 'contribute', 'newsletter',
+ 'shop', 'profile', 'register', 'auth',
+ 'authentication', 'campaign', 'config', 'delete',
+ 'remove', 'forum', 'forums', 'download',
+ 'downloads', 'contact', 'blogs', 'feed', 'feeds',
+ 'faq', 'intranet', 'log', 'registration', 'search',
+ 'explore', 'rss', 'support', 'status', 'static',
+ 'media', 'setting', 'css', 'js', 'follow',
+ 'activity', 'questions', 'articles', 'network', ]
+
+ if value.lower() in forbidden_usernames:
+ raise ValidationError('This is a reserved word.')
+
+
+def InvalidUsernameValidator(value):
+ if '@' in value or '+' in value or '-' in value:
+ raise ValidationError('Enter a valid username.')
+
+
+def UniqueEmailValidator(value):
+ if User.objects.filter(email__iexact=value).exists():
+ raise ValidationError('User with this Email already exists.')
+
+
+def UniqueUsernameIgnoreCaseValidator(value):
+ if User.objects.filter(username__iexact=value).exists():
+ raise ValidationError('User with this Username already exists.')
+
+
+class SignUpForm(forms.ModelForm):
+ username = forms.CharField(
+ widget=forms.TextInput(attrs={'class': 'form-control'}),
+ max_length=30,
+ required=True,
+ help_text='Usernames may contain alphanumeric , _ and . characters')
+ password = forms.CharField(
+ widget=forms.PasswordInput(attrs={'class': 'form-control'}))
+ confirm_password = forms.CharField(
+ widget=forms.PasswordInput(attrs={'class': 'form-control'}),
+ label="Confirm your password",
+ required=True)
+ email = forms.CharField(
+ widget=forms.EmailInput(attrs={'class': 'form-control'}),
+ required=True,
+ max_length=75)
+
+ class Meta:
+ model = User
+ exclude = ['last_login', 'date_joined']
+ fields = ['username', 'email', 'password', 'confirm_password', ]
+
+ def __init__(self, *args, **kwargs):
+ super(SignUpForm, self).__init__(*args, **kwargs)
+ self.fields['username'].validators.append(ForbiddenUsernamesValidator)
+ self.fields['username'].validators.append(InvalidUsernameValidator)
+ self.fields['username'].validators.append(
+ UniqueUsernameIgnoreCaseValidator)
+ self.fields['email'].validators.append(UniqueEmailValidator)
+ self.fields['email'].validators.append(SignupDomainValidator)
+
+ def clean(self):
+ super(SignUpForm, self).clean()
+ password = self.cleaned_data.get('password')
+ confirm_password = self.cleaned_data.get('confirm_password')
+ if password and password != confirm_password:
+ self._errors['password'] = self.error_class(
+ ['Passwords don\'t match'])
+ return self.cleaned_data
diff --git a/samples/development-frameworks/django/bootcamp/authentication/migrations/0001_initial.py b/samples/development-frameworks/django/bootcamp/authentication/migrations/0001_initial.py
new file mode 100644
index 00000000..24c287e1
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/authentication/migrations/0001_initial.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.4 on 2016-03-18 13:32
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Profile',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('location', models.CharField(blank=True, max_length=50, null=True)),
+ ('url', models.CharField(blank=True, max_length=50, null=True)),
+ ('job_title', models.CharField(blank=True, max_length=50, null=True)),
+ ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'db_table': 'auth_profile',
+ },
+ ),
+ ]
diff --git a/samples/development-frameworks/django/bootcamp/authentication/migrations/__init__.py b/samples/development-frameworks/django/bootcamp/authentication/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/samples/development-frameworks/django/bootcamp/authentication/models.py b/samples/development-frameworks/django/bootcamp/authentication/models.py
new file mode 100644
index 00000000..fc85b9a8
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/authentication/models.py
@@ -0,0 +1,132 @@
+import urllib
+import hashlib
+import os.path
+from django.contrib.auth.models import User
+from django.db.models.signals import post_save
+from django.db import models
+from django.conf import settings
+from bootcamp.activities.models import Notification
+
+
+class Profile(models.Model):
+ user = models.OneToOneField(User)
+ location = models.CharField(max_length=50, null=True, blank=True)
+ url = models.CharField(max_length=50, null=True, blank=True)
+ job_title = models.CharField(max_length=50, null=True, blank=True)
+ # reputation = models.IntegerField(default=0)
+ # language = models.CharField(max_length=5, default='en')
+
+ class Meta:
+ db_table = 'auth_profile'
+
+ def get_url(self):
+ url = self.url
+ if "http://" not in self.url and "https://" not in self.url and len(self.url) > 0:
+ url = "http://" + str(self.url)
+
+ return url
+
+ def get_picture(self):
+ no_picture = 'http://trybootcamp.vitorfs.com/static/img/user.png'
+ try:
+ filename = settings.MEDIA_ROOT + '/profile_pictures/' + self.user.username + '.jpg'
+ picture_url = settings.MEDIA_URL + 'profile_pictures/' + self.user.username + '.jpg'
+ if os.path.isfile(filename):
+ return picture_url
+ else:
+ gravatar_url = u'http://www.gravatar.com/avatar/{0}?{1}'.format(
+ hashlib.md5(self.user.email.lower()).hexdigest(),
+ urllib.urlencode({'d': no_picture, 's': '256'})
+ )
+ return gravatar_url
+
+ except Exception, e:
+ return no_picture
+
+ def get_screen_name(self):
+ try:
+ if self.user.get_full_name():
+ return self.user.get_full_name()
+ else:
+ return self.user.username
+ except:
+ return self.user.username
+
+ def notify_liked(self, feed):
+ if self.user != feed.user:
+ Notification(notification_type=Notification.LIKED,
+ from_user=self.user, to_user=feed.user,
+ feed=feed).save()
+
+ def unotify_liked(self, feed):
+ if self.user != feed.user:
+ Notification.objects.filter(notification_type=Notification.LIKED,
+ from_user=self.user, to_user=feed.user,
+ feed=feed).delete()
+
+ def notify_commented(self, feed):
+ if self.user != feed.user:
+ Notification(notification_type=Notification.COMMENTED,
+ from_user=self.user, to_user=feed.user,
+ feed=feed).save()
+
+ def notify_also_commented(self, feed):
+ comments = feed.get_comments()
+ users = []
+ for comment in comments:
+ if comment.user != self.user and comment.user != feed.user:
+ users.append(comment.user.pk)
+
+ users = list(set(users))
+ for user in users:
+ Notification(notification_type=Notification.ALSO_COMMENTED,
+ from_user=self.user,
+ to_user=User(id=user), feed=feed).save()
+
+ def notify_favorited(self, question):
+ if self.user != question.user:
+ Notification(notification_type=Notification.FAVORITED,
+ from_user=self.user, to_user=question.user,
+ question=question).save()
+
+ def unotify_favorited(self, question):
+ if self.user != question.user:
+ Notification.objects.filter(
+ notification_type=Notification.FAVORITED,
+ from_user=self.user,
+ to_user=question.user,
+ question=question).delete()
+
+ def notify_answered(self, question):
+ if self.user != question.user:
+ Notification(notification_type=Notification.ANSWERED,
+ from_user=self.user,
+ to_user=question.user,
+ question=question).save()
+
+ def notify_accepted(self, answer):
+ if self.user != answer.user:
+ Notification(notification_type=Notification.ACCEPTED_ANSWER,
+ from_user=self.user,
+ to_user=answer.user,
+ answer=answer).save()
+
+ def unotify_accepted(self, answer):
+ if self.user != answer.user:
+ Notification.objects.filter(
+ notification_type=Notification.ACCEPTED_ANSWER,
+ from_user=self.user,
+ to_user=answer.user,
+ answer=answer).delete()
+
+
+def create_user_profile(sender, instance, created, **kwargs):
+ if created:
+ Profile.objects.create(user=instance)
+
+
+def save_user_profile(sender, instance, **kwargs):
+ instance.profile.save()
+
+post_save.connect(create_user_profile, sender=User)
+post_save.connect(save_user_profile, sender=User)
diff --git a/samples/development-frameworks/django/bootcamp/authentication/static/css/signup.css b/samples/development-frameworks/django/bootcamp/authentication/static/css/signup.css
new file mode 100644
index 00000000..bf96699a
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/authentication/static/css/signup.css
@@ -0,0 +1,41 @@
+body {
+ background-color: #006389;
+}
+
+.logo {
+ margin: 0;
+ font-size: 3em;
+ font-weight: 200;
+ font-family: "Audiowide", cursive;
+ text-align: center;
+ text-shadow: 1px 1px 0 #333333;
+ line-height: 70px;
+}
+
+.logo a {
+ color: #92c35e;
+}
+
+.logo a:hover, .logo a:focus {
+ color: #92c35e;
+ text-decoration: none;
+}
+
+.signup {
+ background-color: #f6f6f6;
+ width: 70%;
+ border-radius: 7px;
+ margin: 10px auto 0;
+}
+
+.signup {
+ padding: 30px 20px;
+}
+
+.signup form {
+ margin-top: 20px;
+}
+
+.signup h2 {
+ margin: 0;
+}
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/authentication/templates/authentication/signup.html b/samples/development-frameworks/django/bootcamp/authentication/templates/authentication/signup.html
new file mode 100644
index 00000000..3ba3355c
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/authentication/templates/authentication/signup.html
@@ -0,0 +1,30 @@
+{% extends 'base.html' %}
+
+{% load staticfiles i18n %}
+
+{% block head %}
+
+{% endblock head %}
+
+{% block body %}
+
+
+
{% trans 'Sign up for Bootcamp' %}
+
+
+{% endblock body %}
diff --git a/samples/development-frameworks/django/bootcamp/authentication/tests.py b/samples/development-frameworks/django/bootcamp/authentication/tests.py
new file mode 100644
index 00000000..7ce503c2
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/authentication/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/samples/development-frameworks/django/bootcamp/authentication/views.py b/samples/development-frameworks/django/bootcamp/authentication/views.py
new file mode 100644
index 00000000..b0b7464f
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/authentication/views.py
@@ -0,0 +1,31 @@
+from django.shortcuts import render, redirect
+from django.contrib.auth import authenticate, login
+from bootcamp.authentication.forms import SignUpForm
+from django.contrib.auth.models import User
+from bootcamp.feeds.models import Feed
+
+
+def signup(request):
+ if request.method == 'POST':
+ form = SignUpForm(request.POST)
+ if not form.is_valid():
+ return render(request, 'authentication/signup.html',
+ {'form': form})
+
+ else:
+ username = form.cleaned_data.get('username')
+ email = form.cleaned_data.get('email')
+ password = form.cleaned_data.get('password')
+ User.objects.create_user(username=username, password=password,
+ email=email)
+ user = authenticate(username=username, password=password)
+ login(request, user)
+ welcome_post = u'{0} has joined the network.'.format(user.username,
+ user.username)
+ feed = Feed(user=user, post=welcome_post)
+ feed.save()
+ return redirect('/')
+
+ else:
+ return render(request, 'authentication/signup.html',
+ {'form': SignUpForm()})
diff --git a/samples/development-frameworks/django/bootcamp/core/__init__.py b/samples/development-frameworks/django/bootcamp/core/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/samples/development-frameworks/django/bootcamp/core/forms.py b/samples/development-frameworks/django/bootcamp/core/forms.py
new file mode 100644
index 00000000..8a441c17
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/core/forms.py
@@ -0,0 +1,69 @@
+from django import forms
+from django.contrib.auth.models import User
+
+
+class ProfileForm(forms.ModelForm):
+
+ first_name = forms.CharField(
+ widget=forms.TextInput(attrs={'class': 'form-control'}),
+ max_length=30,
+ required=False)
+ last_name = forms.CharField(
+ widget=forms.TextInput(attrs={'class': 'form-control'}),
+ max_length=30,
+ required=False)
+ job_title = forms.CharField(
+ widget=forms.TextInput(attrs={'class': 'form-control'}),
+ max_length=50,
+ required=False)
+ email = forms.CharField(
+ widget=forms.TextInput(attrs={'class': 'form-control'}),
+ max_length=75,
+ required=False)
+ url = forms.CharField(
+ widget=forms.TextInput(attrs={'class': 'form-control'}),
+ max_length=50,
+ required=False)
+ location = forms.CharField(
+ widget=forms.TextInput(attrs={'class': 'form-control'}),
+ max_length=50,
+ required=False)
+
+ class Meta:
+ model = User
+ fields = ['first_name', 'last_name', 'job_title',
+ 'email', 'url', 'location', ]
+
+
+class ChangePasswordForm(forms.ModelForm):
+ id = forms.CharField(widget=forms.HiddenInput())
+ old_password = forms.CharField(
+ widget=forms.PasswordInput(attrs={'class': 'form-control'}),
+ label="Old password",
+ required=True)
+
+ new_password = forms.CharField(
+ widget=forms.PasswordInput(attrs={'class': 'form-control'}),
+ label="New password",
+ required=True)
+ confirm_password = forms.CharField(
+ widget=forms.PasswordInput(attrs={'class': 'form-control'}),
+ label="Confirm new password",
+ required=True)
+
+ class Meta:
+ model = User
+ fields = ['id', 'old_password', 'new_password', 'confirm_password']
+
+ def clean(self):
+ super(ChangePasswordForm, self).clean()
+ old_password = self.cleaned_data.get('old_password')
+ new_password = self.cleaned_data.get('new_password')
+ confirm_password = self.cleaned_data.get('confirm_password')
+ id = self.cleaned_data.get('id')
+ user = User.objects.get(pk=id)
+ if not user.check_password(old_password):
+ self._errors['old_password'] = self.error_class(['Old password don\'t match'])
+ if new_password and new_password != confirm_password:
+ self._errors['new_password'] = self.error_class(['Passwords don\'t match'])
+ return self.cleaned_data
diff --git a/samples/development-frameworks/django/bootcamp/core/migrations/__init__.py b/samples/development-frameworks/django/bootcamp/core/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/samples/development-frameworks/django/bootcamp/core/models.py b/samples/development-frameworks/django/bootcamp/core/models.py
new file mode 100644
index 00000000..71a83623
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/core/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/samples/development-frameworks/django/bootcamp/core/static/css/cover.css b/samples/development-frameworks/django/bootcamp/core/static/css/cover.css
new file mode 100644
index 00000000..567298ab
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/core/static/css/cover.css
@@ -0,0 +1,39 @@
+body {
+ background-color: #006389;
+}
+
+.cover {
+ width: 400px;
+ height: 400px;
+ position: fixed;
+ margin-left: -200px;
+ margin-top: -200px;
+ top: 50%;
+ left: 50%;
+}
+
+.logo {
+ margin: 0;
+ font-size: 3em;
+ font-weight: 200;
+ font-family: "Audiowide", cursive;
+ color: #92c35e;
+ text-align: center;
+ text-shadow: 1px 1px 0 #333333;
+ line-height: 70px;
+}
+
+.login {
+ background-color: #f6f6f6;
+ width: 100%;
+ height: 330px;
+ border-radius: 7px;
+}
+
+.login {
+ padding: 10px 20px;
+}
+
+.login form {
+ margin-top: 20px;
+}
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/core/static/css/network.css b/samples/development-frameworks/django/bootcamp/core/static/css/network.css
new file mode 100644
index 00000000..f275c5c6
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/core/static/css/network.css
@@ -0,0 +1,7 @@
+.users {
+ margin-top: 2em;
+}
+
+.users .panel-heading img {
+ margin-right: .6em;
+}
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/core/static/css/profile.css b/samples/development-frameworks/django/bootcamp/core/static/css/profile.css
new file mode 100644
index 00000000..65885e13
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/core/static/css/profile.css
@@ -0,0 +1,17 @@
+.profile {
+ margin-top: 1em;
+}
+
+.user-profile .picture {
+ width: 200px;
+ border-radius: 5px;
+}
+
+.user-profile ul {
+ padding: 0;
+ margin-top: .6em;
+}
+
+.user-profile ul li {
+ list-style: none;
+}
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/core/static/js/picture.js b/samples/development-frameworks/django/bootcamp/core/static/js/picture.js
new file mode 100644
index 00000000..86e8b492
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/core/static/js/picture.js
@@ -0,0 +1,35 @@
+$(function () {
+
+ var jcrop_api,
+ boundx,
+ boundy,
+ xsize = 200,
+ ysize = 200;
+
+ $("#crop-picture").Jcrop({
+ aspectRatio: xsize / ysize,
+ onSelect: updateCoords,
+ setSelect: [0, 0, 200, 200]
+ },function(){
+ var bounds = this.getBounds();
+ boundx = bounds[0];
+ boundy = bounds[1];
+ jcrop_api = this;
+ });
+
+ function updateCoords(c) {
+ $("#x").val(c.x);
+ $("#y").val(c.y);
+ $("#w").val(c.w);
+ $("#h").val(c.h);
+ };
+
+ $("#btn-upload-picture").click(function () {
+ $("#picture-upload-form input[name='picture']").click();
+ });
+
+ $("#picture-upload-form input[name='picture']").change(function () {
+ $("#picture-upload-form").submit();
+ });
+
+});
diff --git a/samples/development-frameworks/django/bootcamp/core/templates/core/cover.html b/samples/development-frameworks/django/bootcamp/core/templates/core/cover.html
new file mode 100644
index 00000000..307f840f
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/core/templates/core/cover.html
@@ -0,0 +1,46 @@
+{% extends 'base.html' %}
+{% load staticfiles %}
+{% load i18n %}
+
+{% block head %}
+
+{% endblock head %}
+
+{% block body %}
+
+
+
Bootcamp
+ {% if form.non_field_errors %}
+ {% for error in form.non_field_errors %}
+
+ ×
+ {{ error }}
+
+ {% endfor %}
+ {% endif %}
+
+
{% trans 'Log in' %}
+
+
+
+{% endblock body %}
diff --git a/samples/development-frameworks/django/bootcamp/core/templates/core/network.html b/samples/development-frameworks/django/bootcamp/core/templates/core/network.html
new file mode 100644
index 00000000..6c5a5909
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/core/templates/core/network.html
@@ -0,0 +1,50 @@
+{% extends 'base.html' %}
+{% load staticfiles %}
+{% load i18n %}
+
+{% block title %}{% trans 'Network' %}{% endblock %}
+
+{% block head %}
+
+{% endblock head %}
+
+{% block main %}
+
+
+
+
+ {% for user in users %}
+
+
+
+
+ {% if user.profile.job_title %}
+
{% trans 'Job Title' %}: {{ user.profile.job_title }}
+ {% endif %}
+
{% trans 'Username' %}: {{ user.username }}
+ {% if user.profile.location %}
+
{% trans 'Location' %}: {{ user.profile.location }}
+ {% endif %}
+ {% if user.profile.url %}
+
{% trans 'Url' %}: {{ user.profile.get_url }}
+ {% endif %}
+
+
+
+ {% if forloop.counter|divisibleby:3 %}
{% endif %}
+ {% endfor %}
+
+
+
+
+ {% include 'paginator.html' with paginator=users %}
+
+
+
+
+{% endblock main %}
diff --git a/samples/development-frameworks/django/bootcamp/core/templates/core/partial_settings_menu.html b/samples/development-frameworks/django/bootcamp/core/templates/core/partial_settings_menu.html
new file mode 100644
index 00000000..c15956e5
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/core/templates/core/partial_settings_menu.html
@@ -0,0 +1,6 @@
+{% load i18n %}
+
diff --git a/samples/development-frameworks/django/bootcamp/core/templates/core/password.html b/samples/development-frameworks/django/bootcamp/core/templates/core/password.html
new file mode 100644
index 00000000..0783e6b0
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/core/templates/core/password.html
@@ -0,0 +1,49 @@
+{% extends 'base.html' %}
+{% load i18n %}
+
+{% block title %}{% trans 'Account Settings' %}{% endblock %}
+
+{% block main %}
+
+
+
+ {% include 'core/partial_settings_menu.html' with active='password' %}
+
+
+ {% if messages %}
+ {% for message in messages %}
+
+ ×
+ {{ message }}
+
+ {% endfor %}
+ {% endif %}
+
{% trans 'Change Password' %}
+
+
+
+{% endblock main %}
diff --git a/samples/development-frameworks/django/bootcamp/core/templates/core/picture.html b/samples/development-frameworks/django/bootcamp/core/templates/core/picture.html
new file mode 100644
index 00000000..74b60fbc
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/core/templates/core/picture.html
@@ -0,0 +1,75 @@
+{% extends 'base.html' %}
+
+{% load i18n static %}
+
+{% block title %}{% trans 'Account Settings' %}{% endblock %}
+
+{% block head %}
+
+
+
+{% endblock head %}
+
+{% block main %}
+
+
+
+ {% include 'core/partial_settings_menu.html' with active='picture' %}
+
+
+ {% if messages %}
+ {% for message in messages %}
+
+ ×
+ {{ message }}
+
+ {% endfor %}
+ {% endif %}
+
{% trans 'Change Picture' %}
+
+
+
+ {% if uploaded_picture %}
+
+ {% endif %}
+
+
+{% endblock main %}
diff --git a/samples/development-frameworks/django/bootcamp/core/templates/core/profile.html b/samples/development-frameworks/django/bootcamp/core/templates/core/profile.html
new file mode 100644
index 00000000..40d434da
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/core/templates/core/profile.html
@@ -0,0 +1,55 @@
+{% extends 'base.html' %}
+{% load staticfiles %}
+{% load i18n %}
+
+{% block title %}{{ page_user.profile.get_screen_name }}{% endblock %}
+
+{% block head %}
+
+
+
+
+{% endblock head %}
+
+{% block main %}
+
+
+
+
+
+
+ {% if page_user.profile.job_title %}
+ {{ page_user.profile.job_title }}
+ {% endif %}
+ {% if page_user.profile.location %}
+ {{ page_user.profile.location }}
+ {% endif %}
+ {% if page_user.profile.url %}
+ {{ page_user.profile.get_url }}
+ {% endif %}
+
+
+
+
{% trans 'Last Feeds by' %} {{ page_user.profile.get_screen_name }}
+
+
+ {% for feed in feeds %}
+ {% include 'feeds/partial_feed.html' with feed=feed %}
+ {% endfor %}
+
+
+
+
+
+
+
+
+{% endblock main %}
diff --git a/samples/development-frameworks/django/bootcamp/core/templates/core/settings.html b/samples/development-frameworks/django/bootcamp/core/templates/core/settings.html
new file mode 100644
index 00000000..306295ae
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/core/templates/core/settings.html
@@ -0,0 +1,62 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% get_available_languages as LANGUAGES %}
+{% get_current_language as CURRENT_LANGUAGE %}
+
+{% block title %}Account Settings{% endblock %}
+
+{% block main %}
+
+
+
+ {% include 'core/partial_settings_menu.html' with active='profile' %}
+
+
+ {% if messages %}
+ {% for message in messages %}
+
+ ×
+ {{ message }}
+
+ {% endfor %}
+ {% endif %}
+
{% trans 'Edit Profile' %}
+
+
+
+{% endblock main %}
diff --git a/samples/development-frameworks/django/bootcamp/core/tests.py b/samples/development-frameworks/django/bootcamp/core/tests.py
new file mode 100644
index 00000000..7ce503c2
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/core/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/samples/development-frameworks/django/bootcamp/core/views.py b/samples/development-frameworks/django/bootcamp/core/views.py
new file mode 100644
index 00000000..6fdb5451
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/core/views.py
@@ -0,0 +1,158 @@
+import os
+from PIL import Image
+
+from django.contrib.auth.models import User
+from django.contrib.auth.decorators import login_required
+from django.contrib import messages
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+from django.conf import settings as django_settings
+from django.shortcuts import render, redirect, get_object_or_404
+
+from bootcamp.core.forms import ProfileForm, ChangePasswordForm
+from bootcamp.feeds.models import Feed
+from bootcamp.feeds.views import FEEDS_NUM_PAGES
+from bootcamp.feeds.views import feeds
+
+
+def home(request):
+ if request.user.is_authenticated():
+ return feeds(request)
+ else:
+ return render(request, 'core/cover.html')
+
+
+@login_required
+def network(request):
+ users_list = User.objects.filter(is_active=True).order_by('username')
+ paginator = Paginator(users_list, 100)
+ page = request.GET.get('page')
+ try:
+ users = paginator.page(page)
+ except PageNotAnInteger:
+ users = paginator.page(1)
+ except EmptyPage:
+ users = paginator.page(paginator.num_pages)
+ return render(request, 'core/network.html', {'users': users})
+
+
+@login_required
+def profile(request, username):
+ page_user = get_object_or_404(User, username=username)
+ all_feeds = Feed.get_feeds().filter(user=page_user)
+ paginator = Paginator(all_feeds, FEEDS_NUM_PAGES)
+ feeds = paginator.page(1)
+ from_feed = -1
+ if feeds:
+ from_feed = feeds[0].id
+ return render(request, 'core/profile.html', {
+ 'page_user': page_user,
+ 'feeds': feeds,
+ 'from_feed': from_feed,
+ 'page': 1
+ })
+
+
+@login_required
+def settings(request):
+ user = request.user
+ if request.method == 'POST':
+ form = ProfileForm(request.POST)
+ if form.is_valid():
+ user.first_name = form.cleaned_data.get('first_name')
+ user.last_name = form.cleaned_data.get('last_name')
+ user.profile.job_title = form.cleaned_data.get('job_title')
+ user.email = form.cleaned_data.get('email')
+ user.profile.url = form.cleaned_data.get('url')
+ user.profile.location = form.cleaned_data.get('location')
+ user.save()
+ messages.add_message(request,
+ messages.SUCCESS,
+ 'Your profile was successfully edited.')
+
+ else:
+ form = ProfileForm(instance=user, initial={
+ 'job_title': user.profile.job_title,
+ 'url': user.profile.url,
+ 'location': user.profile.location
+ })
+ return render(request, 'core/settings.html', {'form': form})
+
+
+@login_required
+def picture(request):
+ uploaded_picture = False
+ try:
+ if request.GET.get('upload_picture') == 'uploaded':
+ uploaded_picture = True
+
+ except Exception, e:
+ pass
+
+ return render(request, 'core/picture.html',
+ {'uploaded_picture': uploaded_picture})
+
+
+@login_required
+def password(request):
+ user = request.user
+ if request.method == 'POST':
+ form = ChangePasswordForm(request.POST)
+ if form.is_valid():
+ new_password = form.cleaned_data.get('new_password')
+ user.set_password(new_password)
+ user.save()
+ messages.add_message(request, messages.SUCCESS,
+ 'Your password was successfully changed.')
+
+ else:
+ form = ChangePasswordForm(instance=user)
+
+ return render(request, 'core/password.html', {'form': form})
+
+
+@login_required
+def upload_picture(request):
+ try:
+ profile_pictures = django_settings.MEDIA_ROOT + '/profile_pictures/'
+ if not os.path.exists(profile_pictures):
+ os.makedirs(profile_pictures)
+ f = request.FILES['picture']
+ filename = profile_pictures + request.user.username + '_tmp.jpg'
+ with open(filename, 'wb+') as destination:
+ for chunk in f.chunks():
+ destination.write(chunk)
+ im = Image.open(filename)
+ width, height = im.size
+ if width > 350:
+ new_width = 350
+ new_height = (height * 350) / width
+ new_size = new_width, new_height
+ im.thumbnail(new_size, Image.ANTIALIAS)
+ im.save(filename)
+
+ return redirect('/settings/picture/?upload_picture=uploaded')
+
+ except Exception, e:
+ print e
+ return redirect('/settings/picture/')
+
+
+@login_required
+def save_uploaded_picture(request):
+ try:
+ x = int(request.POST.get('x'))
+ y = int(request.POST.get('y'))
+ w = int(request.POST.get('w'))
+ h = int(request.POST.get('h'))
+ tmp_filename = django_settings.MEDIA_ROOT + '/profile_pictures/' + request.user.username + '_tmp.jpg'
+ filename = django_settings.MEDIA_ROOT + '/profile_pictures/' + request.user.username + '.jpg'
+ im = Image.open(tmp_filename)
+ cropped_im = im.crop((x, y, w+x, h+y))
+ cropped_im.thumbnail((200, 200), Image.ANTIALIAS)
+ cropped_im.save(filename)
+ os.remove(tmp_filename)
+
+ except Exception, e:
+ pass
+
+ return redirect('/settings/picture/')
diff --git a/samples/development-frameworks/django/bootcamp/decorators.py b/samples/development-frameworks/django/bootcamp/decorators.py
new file mode 100644
index 00000000..99392382
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/decorators.py
@@ -0,0 +1,13 @@
+from django.http import HttpResponseBadRequest
+
+
+def ajax_required(f):
+ def wrap(request, *args, **kwargs):
+ if not request.is_ajax():
+ return HttpResponseBadRequest()
+
+ return f(request, *args, **kwargs)
+
+ wrap.__doc__ = f.__doc__
+ wrap.__name__ = f.__name__
+ return wrap
diff --git a/samples/development-frameworks/django/bootcamp/feeds/__init__.py b/samples/development-frameworks/django/bootcamp/feeds/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/samples/development-frameworks/django/bootcamp/feeds/migrations/0001_initial.py b/samples/development-frameworks/django/bootcamp/feeds/migrations/0001_initial.py
new file mode 100644
index 00000000..e82c5f22
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/feeds/migrations/0001_initial.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.4 on 2016-03-18 13:32
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Feed',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('date', models.DateTimeField(auto_now_add=True)),
+ ('post', models.TextField(max_length=255)),
+ ('likes', models.IntegerField(default=0)),
+ ('comments', models.IntegerField(default=0)),
+ ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='feeds.Feed')),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'ordering': ('-date',),
+ 'verbose_name': 'Feed',
+ 'verbose_name_plural': 'Feeds',
+ },
+ ),
+ ]
diff --git a/samples/development-frameworks/django/bootcamp/feeds/migrations/__init__.py b/samples/development-frameworks/django/bootcamp/feeds/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/samples/development-frameworks/django/bootcamp/feeds/models.py b/samples/development-frameworks/django/bootcamp/feeds/models.py
new file mode 100644
index 00000000..b9e60084
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/feeds/models.py
@@ -0,0 +1,73 @@
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+from django.contrib.auth.models import User
+from bootcamp.activities.models import Activity
+from django.utils.html import escape
+import bleach
+
+
+class Feed(models.Model):
+ user = models.ForeignKey(User)
+ date = models.DateTimeField(auto_now_add=True)
+ post = models.TextField(max_length=255)
+ parent = models.ForeignKey('Feed', null=True, blank=True)
+ likes = models.IntegerField(default=0)
+ comments = models.IntegerField(default=0)
+
+ class Meta:
+ verbose_name = _('Feed')
+ verbose_name_plural = _('Feeds')
+ ordering = ('-date',)
+
+ def __unicode__(self):
+ return self.post
+
+ @staticmethod
+ def get_feeds(from_feed=None):
+ if from_feed is not None:
+ feeds = Feed.objects.filter(parent=None, id__lte=from_feed)
+ else:
+ feeds = Feed.objects.filter(parent=None)
+ return feeds
+
+ @staticmethod
+ def get_feeds_after(feed):
+ feeds = Feed.objects.filter(parent=None, id__gt=feed)
+ return feeds
+
+ def get_comments(self):
+ return Feed.objects.filter(parent=self).order_by('date')
+
+ def calculate_likes(self):
+ likes = Activity.objects.filter(activity_type=Activity.LIKE,
+ feed=self.pk).count()
+ self.likes = likes
+ self.save()
+ return self.likes
+
+ def get_likes(self):
+ likes = Activity.objects.filter(activity_type=Activity.LIKE,
+ feed=self.pk)
+ return likes
+
+ def get_likers(self):
+ likes = self.get_likes()
+ likers = []
+ for like in likes:
+ likers.append(like.user)
+ return likers
+
+ def calculate_comments(self):
+ self.comments = Feed.objects.filter(parent=self).count()
+ self.save()
+ return self.comments
+
+ def comment(self, user, post):
+ feed_comment = Feed(user=user, post=post, parent=self)
+ feed_comment.save()
+ self.comments = Feed.objects.filter(parent=self).count()
+ self.save()
+ return feed_comment
+
+ def linkfy_post(self):
+ return bleach.linkify(escape(self.post))
diff --git a/samples/development-frameworks/django/bootcamp/feeds/static/css/feeds.css b/samples/development-frameworks/django/bootcamp/feeds/static/css/feeds.css
new file mode 100644
index 00000000..07906cff
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/feeds/static/css/feeds.css
@@ -0,0 +1,167 @@
+ul.stream {
+ margin: 0;
+ padding: 0;
+}
+
+ul.stream li {
+ list-style: none;
+ border-bottom: 1px solid #eeeeee;
+ padding: 1em 0;
+}
+
+ul.stream li:last-child {
+ border-bottom: none;
+}
+
+ul.stream li a img.user {
+ width: 60px;
+ border-radius: 100px;
+ float: left;
+}
+
+ul.stream li div.post {
+ margin-left: 60px;
+ padding: 0 0 0 1.2em;
+ overflow-x: auto;
+}
+
+ul.stream li div.post h3 {
+ font-size: 1em;
+ margin: 0;
+ margin-bottom: .2em;
+}
+
+ul.stream li div.post h3 small {
+ margin-left: .3em;
+ font-size: .8em;
+}
+
+ul.stream li div.post p {
+ margin: 0;
+}
+
+ul.stream li div.post div.interaction {
+ padding-top: .2em;
+}
+
+ul.stream li div.post div.interaction a {
+ margin-right: .6em;
+ font-size: .8em;
+}
+
+.stream-update {
+ text-align: center;
+ border-bottom: 1px solid #eeeeee;
+ display: none;
+}
+
+.stream-update a {
+ display: block;
+ padding: .6em 0;
+ background-color: #f5f8fa;
+}
+
+.stream-update a:hover {
+ text-decoration: none;
+ background-color: #e1e8ed;
+}
+
+.compose {
+ display: none;
+ border-bottom: 1px solid #eee;
+ padding-left: 15px;
+ padding-right: 15px;
+}
+
+.compose h2 {
+ font-size: 1.4em;
+}
+
+.comments {
+ margin-top: .6em;
+ display: none;
+}
+
+.comments ol {
+ margin: .8em 0 0;
+ padding: .2em 0;
+ background-color: #f4f4f4;
+ border-radius: 3px;
+ overflow-x: auto;
+}
+
+.comments ol li {
+ list-style: none;
+ padding: 0;
+}
+
+.comments ol li img.user-comment {
+ width: 35px;
+ border-radius: 4px;
+ float: left;
+ margin-left: 10px;
+}
+
+.comments ol li div {
+ margin-left: 45px;
+ padding: 0 .8em;
+ font-size: .9em;
+}
+
+.comments ol li {
+ padding: .6em .6em .6em 0;
+ border-bottom: none;
+}
+
+.comments ol li h4 {
+ margin: 0;
+ margin-left: 45px;
+ padding: 0 0 .2em .8em;
+ font-size: .9em;
+}
+
+.comments ol li h4 small {
+ margin-left: .3em;
+}
+
+.empty {
+ margin: 0 .8em;
+ font-size: .9em;
+}
+
+.load {
+ text-align: center;
+ padding-top: 1em;
+ border-top: 1px solid #eeeeee;
+ display: none;
+ padding: 15px 0;
+}
+
+.loadcomment {
+ text-align: center;
+}
+
+.remove-feed {
+ color: #bbbbbb;
+ font-size: .8em;
+ padding-top: .2em;
+ float: right;
+ cursor: pointer;
+}
+
+.remove-feed:hover {
+ color: #333333;
+}
+
+.panel-feed {
+ margin-top: 20px;
+}
+
+.panel-feed .panel-body {
+ padding: 0;
+}
+
+.feed-container {
+ padding-left: 15px;
+ padding-right: 15px;
+}
diff --git a/samples/development-frameworks/django/bootcamp/feeds/static/js/feeds.js b/samples/development-frameworks/django/bootcamp/feeds/static/js/feeds.js
new file mode 100644
index 00000000..4ace6539
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/feeds/static/js/feeds.js
@@ -0,0 +1,296 @@
+$(function () {
+ var page_title = $(document).attr("title");
+
+ function hide_stream_update() {
+ $(".stream-update").hide();
+ $(".stream-update .new-posts").text("");
+ $(document).attr("title", page_title);
+ };
+
+ $("body").keydown(function (evt) {
+ var keyCode = evt.which?evt.which:evt.keyCode;
+ if (evt.ctrlKey && keyCode == 80) {
+ $(".btn-compose").click();
+ return false;
+ }
+ });
+
+ $("#compose-form textarea[name='post']").keydown(function (evt) {
+ var keyCode = evt.which?evt.which:evt.keyCode;
+ if (evt.ctrlKey && (keyCode == 10 || keyCode == 13)) {
+ $(".btn-post").click();
+ }
+ });
+
+ $(".btn-compose").click(function () {
+ if ($(".compose").hasClass("composing")) {
+ $(".compose").removeClass("composing");
+ $(".compose").slideUp();
+ }
+ else {
+ $(".compose").addClass("composing");
+ $(".compose textarea").val("");
+ $(".compose").slideDown(400, function () {
+ $(".compose textarea").focus();
+ });
+ }
+ });
+
+ $(".btn-cancel-compose").click(function () {
+ $(".compose").slideUp();
+ });
+
+ $(".btn-post").click(function () {
+ var last_feed = $(".stream li:first-child").attr("feed-id");
+ if (last_feed == undefined) {
+ last_feed = "0";
+ }
+ $("#compose-form input[name='last_feed']").val(last_feed);
+ $.ajax({
+ url: '/feeds/post/',
+ data: $("#compose-form").serialize(),
+ type: 'post',
+ cache: false,
+ success: function (data) {
+ $("ul.stream").prepend(data);
+ $(".compose").slideUp();
+ $(".compose").removeClass("composing");
+ hide_stream_update();
+ }
+ });
+ });
+
+ $("ul.stream").on("click", ".like", function () {
+ var li = $(this).closest("li");
+ var feed = $(li).attr("feed-id");
+ var csrf = $(li).attr("csrf");
+ $.ajax({
+ url: '/feeds/like/',
+ data: {
+ 'feed': feed,
+ 'csrfmiddlewaretoken': csrf
+ },
+ type: 'post',
+ cache: false,
+ success: function (data) {
+ if ($(".like", li).hasClass("unlike")) {
+ $(".like", li).removeClass("unlike");
+ $(".like .text", li).text("Like");
+ }
+ else {
+ $(".like", li).addClass("unlike");
+ $(".like .text", li).text("Unlike");
+ }
+ $(".like .like-count", li).text(data);
+ }
+ });
+ return false;
+ });
+
+ $("ul.stream").on("click", ".comment", function () {
+ var post = $(this).closest(".post");
+ if ($(".comments", post).hasClass("tracking")) {
+ $(".comments", post).slideUp();
+ $(".comments", post).removeClass("tracking");
+ }
+ else {
+ $(".comments", post).show();
+ $(".comments", post).addClass("tracking");
+ $(".comments input[name='post']", post).focus();
+ var feed = $(post).closest("li").attr("feed-id");
+ $.ajax({
+ url: '/feeds/comment/',
+ data: { 'feed': feed },
+ cache: false,
+ beforeSend: function () {
+ $("ol", post).html("");
+ },
+ success: function (data) {
+ $("ol", post).html(data);
+ $(".comment-count", post).text($("ol li", post).not(".empty").length);
+ }
+ });
+ }
+ return false;
+ });
+
+ $("ul.stream").on("keydown", ".comments input[name='post']", function (evt) {
+ var keyCode = evt.which?evt.which:evt.keyCode;
+ if (keyCode == 13) {
+ var form = $(this).closest("form");
+ var container = $(this).closest(".comments");
+ var input = $(this);
+ $.ajax({
+ url: '/feeds/comment/',
+ data: $(form).serialize(),
+ type: 'post',
+ cache: false,
+ beforeSend: function () {
+ $(input).val("");
+ },
+ success: function (data) {
+ $("ol", container).html(data);
+ var post_container = $(container).closest(".post");
+ $(".comment-count", post_container).text($("ol li", container).length);
+ }
+ });
+ return false;
+ }
+ });
+
+ var load_feeds = function () {
+ if (!$("#load_feed").hasClass("no-more-feeds")) {
+ var page = $("#load_feed input[name='page']").val();
+ var next_page = parseInt($("#load_feed input[name='page']").val()) + 1;
+ $("#load_feed input[name='page']").val(next_page);
+ $.ajax({
+ url: '/feeds/load/',
+ data: $("#load_feed").serialize(),
+ cache: false,
+ beforeSend: function () {
+ $(".load").show();
+ },
+ success: function (data) {
+ if (data.length > 0) {
+ $("ul.stream").append(data)
+ }
+ else {
+ $("#load_feed").addClass("no-more-feeds");
+ }
+ },
+ complete: function () {
+ $(".load").hide();
+ }
+ });
+ }
+ };
+
+ $("#load_feed").bind("enterviewport", load_feeds).bullseye();
+
+ function check_new_feeds () {
+ var last_feed = $(".stream li:first-child").attr("feed-id");
+ var feed_source = $("#feed_source").val();
+ if (last_feed != undefined) {
+ $.ajax({
+ url: '/feeds/check/',
+ data: {
+ 'last_feed': last_feed,
+ 'feed_source': feed_source
+ },
+ cache: false,
+ success: function (data) {
+ if (parseInt(data) > 0) {
+ $(".stream-update .new-posts").text(data);
+ $(".stream-update").show();
+ $(document).attr("title", "(" + data + ") " + page_title);
+ }
+ },
+ complete: function() {
+ window.setTimeout(check_new_feeds, 30000);
+ }
+ });
+ }
+ else {
+ window.setTimeout(check_new_feeds, 30000);
+ }
+ };
+ check_new_feeds();
+
+ $(".stream-update a").click(function () {
+ var last_feed = $(".stream li:first-child").attr("feed-id");
+ var feed_source = $("#feed_source").val();
+ $.ajax({
+ url: '/feeds/load_new/',
+ data: {
+ 'last_feed': last_feed,
+ 'feed_source': feed_source
+ },
+ cache: false,
+ success: function (data) {
+ $("ul.stream").prepend(data);
+ },
+ complete: function () {
+ hide_stream_update();
+ }
+ });
+ return false;
+ });
+
+ $("input,textarea").attr("autocomplete", "off");
+
+ function update_feeds () {
+ var first_feed = $(".stream li:first-child").attr("feed-id");
+ var last_feed = $(".stream li:last-child").attr("feed-id");
+ var feed_source = $("#feed_source").val();
+
+ if (first_feed != undefined && last_feed != undefined) {
+ $.ajax({
+ url: '/feeds/update/',
+ data: {
+ 'first_feed': first_feed,
+ 'last_feed': last_feed,
+ 'feed_source': feed_source
+ },
+ cache: false,
+ success: function (data) {
+ $.each(data, function(id, feed) {
+ var li = $("li[feed-id='" + id + "']");
+ $(".like-count", li).text(feed.likes);
+ $(".comment-count", li).text(feed.comments);
+ });
+ },
+ complete: function () {
+ window.setTimeout(update_feeds, 30000);
+ }
+ });
+ }
+ else {
+ window.setTimeout(update_feeds, 30000);
+ }
+ };
+ update_feeds();
+
+ function track_comments () {
+ $(".tracking").each(function () {
+ var container = $(this);
+ var feed = $(this).closest("li").attr("feed-id");
+ $.ajax({
+ url: '/feeds/track_comments/',
+ data: {'feed': feed},
+ cache: false,
+ success: function (data) {
+ $("ol", container).html(data);
+ var post_container = $(container).closest(".post");
+ $(".comment-count", post_container).text($("ol li", container).length);
+ }
+ });
+ });
+ window.setTimeout(track_comments, 30000);
+ };
+ track_comments();
+
+ $("ul.stream").on("click", ".remove-feed", function () {
+ var li = $(this).closest("li");
+ var feed = $(li).attr("feed-id");
+ var csrf = $(li).attr("csrf");
+ $.ajax({
+ url: '/feeds/remove/',
+ data: {
+ 'feed': feed,
+ 'csrfmiddlewaretoken': csrf
+ },
+ type: 'post',
+ cache: false,
+ success: function (data) {
+ $(li).fadeOut(400, function () {
+ $(li).remove();
+ });
+ }
+ });
+ });
+
+ $("#compose-form textarea[name='post']").keyup(function () {
+ $(this).count(255);
+ });
+
+});
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/feeds/templates/feeds/feed.html b/samples/development-frameworks/django/bootcamp/feeds/templates/feeds/feed.html
new file mode 100644
index 00000000..9688eec9
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/feeds/templates/feeds/feed.html
@@ -0,0 +1,22 @@
+{% extends 'base.html' %}
+{% load staticfiles %}
+{% load i18n %}
+
+{% load humanize %}
+
+{% block title %} {% trans 'Feed' %} {% endblock %}
+
+{% block head %}
+
+
+
+{% endblock head %}
+
+{% block main %}
+
+
+ {% include 'feeds/partial_feed.html' with feed=feed %}
+
+{% endblock main %}
diff --git a/samples/development-frameworks/django/bootcamp/feeds/templates/feeds/feeds.html b/samples/development-frameworks/django/bootcamp/feeds/templates/feeds/feeds.html
new file mode 100644
index 00000000..29a225b0
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/feeds/templates/feeds/feeds.html
@@ -0,0 +1,70 @@
+{% extends 'base.html' %}
+{% load staticfiles %}
+{% load i18n %}
+
+{% block head %}
+
+
+
+{% endblock head %}
+
+{% block main %}
+
+
+
+
+
+
+
+
+
{% trans 'Latest posts' %}
+
+
+
+
+
{% trans "Compose a new post" %}
+
+
+
+
+ {% for feed in feeds %}
+ {% include 'feeds/partial_feed.html' with feed=feed %}
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{% endblock main %}
diff --git a/samples/development-frameworks/django/bootcamp/feeds/templates/feeds/partial_feed.html b/samples/development-frameworks/django/bootcamp/feeds/templates/feeds/partial_feed.html
new file mode 100644
index 00000000..b19b9a6b
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/feeds/templates/feeds/partial_feed.html
@@ -0,0 +1,44 @@
+{% load i18n %}
+{% load humanize %}
+
+
+
+
+
+ {% if feed.user == user %}
+
+ {% endif %}
+
+
{{ feed.linkfy_post|safe }}
+
+
+
+
+
diff --git a/samples/development-frameworks/django/bootcamp/feeds/templates/feeds/partial_feed_comments.html b/samples/development-frameworks/django/bootcamp/feeds/templates/feeds/partial_feed_comments.html
new file mode 100644
index 00000000..1f775aa4
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/feeds/templates/feeds/partial_feed_comments.html
@@ -0,0 +1,22 @@
+{% load humanize %}
+{% load i18n %}
+
+{% for comment in feed.get_comments %}
+
+ {% if comment.user == user %}
+
+ {% endif %}
+
+
+
+
+ {{ comment.linkfy_post|safe }}
+
+{% empty %}
+ {% trans 'Be the first one to comment' %}
+{% endfor %}
diff --git a/samples/development-frameworks/django/bootcamp/feeds/tests.py b/samples/development-frameworks/django/bootcamp/feeds/tests.py
new file mode 100644
index 00000000..7ce503c2
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/feeds/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/samples/development-frameworks/django/bootcamp/feeds/urls.py b/samples/development-frameworks/django/bootcamp/feeds/urls.py
new file mode 100644
index 00000000..04e9b2bb
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/feeds/urls.py
@@ -0,0 +1,19 @@
+# coding: utf-8
+
+from django.conf.urls import url
+
+from bootcamp.feeds import views
+
+urlpatterns = [
+ url(r'^$', views.feeds, name='feeds'),
+ url(r'^post/$', views.post, name='post'),
+ url(r'^like/$', views.like, name='like'),
+ url(r'^comment/$', views.comment, name='comment'),
+ url(r'^load/$', views.load, name='load'),
+ url(r'^check/$', views.check, name='check'),
+ url(r'^load_new/$', views.load_new, name='load_new'),
+ url(r'^update/$', views.update, name='update'),
+ url(r'^track_comments/$', views.track_comments, name='track_comments'),
+ url(r'^remove/$', views.remove, name='remove_feed'),
+ url(r'^(\d+)/$', views.feed, name='feed'),
+]
diff --git a/samples/development-frameworks/django/bootcamp/feeds/views.py b/samples/development-frameworks/django/bootcamp/feeds/views.py
new file mode 100644
index 00000000..564add97
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/feeds/views.py
@@ -0,0 +1,208 @@
+from django.shortcuts import render, get_object_or_404
+from django.http import HttpResponse, HttpResponseBadRequest,\
+ HttpResponseForbidden
+from bootcamp.feeds.models import Feed
+from bootcamp.activities.models import Activity
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+from django.template.loader import render_to_string
+from django.template.context_processors import csrf
+import json
+from django.contrib.auth.decorators import login_required
+from bootcamp.decorators import ajax_required
+
+FEEDS_NUM_PAGES = 10
+
+
+@login_required
+def feeds(request):
+ all_feeds = Feed.get_feeds()
+ paginator = Paginator(all_feeds, FEEDS_NUM_PAGES)
+ feeds = paginator.page(1)
+ from_feed = -1
+ if feeds:
+ from_feed = feeds[0].id
+ return render(request, 'feeds/feeds.html', {
+ 'feeds': feeds,
+ 'from_feed': from_feed,
+ 'page': 1,
+ })
+
+
+def feed(request, pk):
+ feed = get_object_or_404(Feed, pk=pk)
+ return render(request, 'feeds/feed.html', {'feed': feed})
+
+
+@login_required
+@ajax_required
+def load(request):
+ from_feed = request.GET.get('from_feed')
+ page = request.GET.get('page')
+ feed_source = request.GET.get('feed_source')
+ all_feeds = Feed.get_feeds(from_feed)
+ if feed_source != 'all':
+ all_feeds = all_feeds.filter(user__id=feed_source)
+ paginator = Paginator(all_feeds, FEEDS_NUM_PAGES)
+ try:
+ feeds = paginator.page(page)
+ except PageNotAnInteger:
+ return HttpResponseBadRequest()
+ except EmptyPage:
+ feeds = []
+ html = u''
+ csrf_token = unicode(csrf(request)['csrf_token'])
+ for feed in feeds:
+ html = u'{0}{1}'.format(html,
+ render_to_string('feeds/partial_feed.html',
+ {
+ 'feed': feed,
+ 'user': request.user,
+ 'csrf_token': csrf_token
+ }))
+
+ return HttpResponse(html)
+
+
+def _html_feeds(last_feed, user, csrf_token, feed_source='all'):
+ feeds = Feed.get_feeds_after(last_feed)
+ if feed_source != 'all':
+ feeds = feeds.filter(user__id=feed_source)
+ html = u''
+ for feed in feeds:
+ html = u'{0}{1}'.format(html,
+ render_to_string('feeds/partial_feed.html',
+ {
+ 'feed': feed,
+ 'user': user,
+ 'csrf_token': csrf_token
+ }))
+
+ return html
+
+
+@login_required
+@ajax_required
+def load_new(request):
+ last_feed = request.GET.get('last_feed')
+ user = request.user
+ csrf_token = unicode(csrf(request)['csrf_token'])
+ html = _html_feeds(last_feed, user, csrf_token)
+ return HttpResponse(html)
+
+
+@login_required
+@ajax_required
+def check(request):
+ last_feed = request.GET.get('last_feed')
+ feed_source = request.GET.get('feed_source')
+ feeds = Feed.get_feeds_after(last_feed)
+ if feed_source != 'all':
+ feeds = feeds.filter(user__id=feed_source)
+ count = feeds.count()
+ return HttpResponse(count)
+
+
+@login_required
+@ajax_required
+def post(request):
+ last_feed = request.POST.get('last_feed')
+ user = request.user
+ csrf_token = unicode(csrf(request)['csrf_token'])
+ feed = Feed()
+ feed.user = user
+ post = request.POST['post']
+ post = post.strip()
+ if len(post) > 0:
+ feed.post = post[:255]
+ feed.save()
+ html = _html_feeds(last_feed, user, csrf_token)
+ return HttpResponse(html)
+
+
+@login_required
+@ajax_required
+def like(request):
+ feed_id = request.POST['feed']
+ feed = Feed.objects.get(pk=feed_id)
+ user = request.user
+ like = Activity.objects.filter(activity_type=Activity.LIKE, feed=feed_id,
+ user=user)
+ if like:
+ user.profile.unotify_liked(feed)
+ like.delete()
+
+ else:
+ like = Activity(activity_type=Activity.LIKE, feed=feed_id, user=user)
+ like.save()
+ user.profile.notify_liked(feed)
+
+ return HttpResponse(feed.calculate_likes())
+
+
+@login_required
+@ajax_required
+def comment(request):
+ if request.method == 'POST':
+ feed_id = request.POST['feed']
+ feed = Feed.objects.get(pk=feed_id)
+ post = request.POST['post']
+ post = post.strip()
+ if len(post) > 0:
+ post = post[:255]
+ user = request.user
+ feed.comment(user=user, post=post)
+ user.profile.notify_commented(feed)
+ user.profile.notify_also_commented(feed)
+ return render(request, 'feeds/partial_feed_comments.html',
+ {'feed': feed})
+
+ else:
+ feed_id = request.GET.get('feed')
+ feed = Feed.objects.get(pk=feed_id)
+ return render(request, 'feeds/partial_feed_comments.html',
+ {'feed': feed})
+
+
+@login_required
+@ajax_required
+def update(request):
+ first_feed = request.GET.get('first_feed')
+ last_feed = request.GET.get('last_feed')
+ feed_source = request.GET.get('feed_source')
+ feeds = Feed.get_feeds().filter(id__range=(last_feed, first_feed))
+ if feed_source != 'all':
+ feeds = feeds.filter(user__id=feed_source)
+ dump = {}
+ for feed in feeds:
+ dump[feed.pk] = {'likes': feed.likes, 'comments': feed.comments}
+ data = json.dumps(dump)
+ return HttpResponse(data, content_type='application/json')
+
+
+@login_required
+@ajax_required
+def track_comments(request):
+ feed_id = request.GET.get('feed')
+ feed = Feed.objects.get(pk=feed_id)
+ return render(request, 'feeds/partial_feed_comments.html', {'feed': feed})
+
+
+@login_required
+@ajax_required
+def remove(request):
+ try:
+ feed_id = request.POST.get('feed')
+ feed = Feed.objects.get(pk=feed_id)
+ if feed.user == request.user:
+ likes = feed.get_likes()
+ parent = feed.parent
+ for like in likes:
+ like.delete()
+ feed.delete()
+ if parent:
+ parent.calculate_comments()
+ return HttpResponse()
+ else:
+ return HttpResponseForbidden()
+ except Exception, e:
+ return HttpResponseBadRequest()
diff --git a/samples/development-frameworks/django/bootcamp/locale/es/LC_MESSAGES/django.po b/samples/development-frameworks/django/bootcamp/locale/es/LC_MESSAGES/django.po
new file mode 100644
index 00000000..04bf63d5
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/locale/es/LC_MESSAGES/django.po
@@ -0,0 +1,331 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-06-05 22:50-0300\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: activities/templates/activities/last_notifications.html:12
+msgid "You have no unread notification"
+msgstr "Usted no tiene ninguna notificación sin leer"
+
+#: activities/templates/activities/last_notifications.html:14
+msgid "See all"
+msgstr "Ver todo"
+
+#: activities/templates/activities/notifications.html:26
+msgid "You have no notification"
+msgstr "Usted no tiene ninguna notificación"
+
+#: articles/models.py:25 articles/templates/articles/article.html:13
+msgid "Article"
+msgstr "Artículo"
+
+#: articles/models.py:26 articles/templates/articles/article.html:12
+#: articles/templates/articles/articles.html:4
+#: articles/templates/articles/articles.html:16
+#: articles/templates/articles/drafts.html:10
+#: articles/templates/articles/edit.html:10
+#: articles/templates/articles/write.html:10 templates/base.html:39
+msgid "Articles"
+msgstr "Artículos"
+
+#: articles/models.py:68
+msgid "Tag"
+msgstr ""
+
+#: articles/models.py:69 articles/templates/articles/drafts.html:18
+msgid "Tags"
+msgstr ""
+
+#: articles/templates/articles/articles.html:13
+#: articles/templates/articles/write.html:11
+#, fuzzy
+msgid "Write Article"
+msgstr "Escribe el artículo"
+
+#: articles/templates/articles/articles.html:15
+#: articles/templates/articles/drafts.html:11
+#: articles/templates/articles/edit.html:11
+msgid "Drafts"
+msgstr "Proyecto"
+
+#: articles/templates/articles/articles.html:24
+msgid "There is no published article yet"
+msgstr "No hay ningún artículo publicado aún"
+
+#: articles/templates/articles/articles.html:24
+msgid "Be the first one to publish"
+msgstr "¡Sé el primero en publicar"
+
+#: articles/templates/articles/articles.html:29
+msgid "Popular Tags"
+msgstr "Etiquetas populares"
+
+#: articles/templates/articles/drafts.html:16
+msgid "Title"
+msgstr "Título"
+
+#: articles/templates/articles/drafts.html:17
+msgid "Content"
+msgstr "Contenido"
+
+#: articles/templates/articles/drafts.html:35
+msgid "No draft to display"
+msgstr "Ningún proyecto para mostrar"
+
+#: articles/templates/articles/edit.html:12
+msgid "Edit"
+msgstr "Editar"
+
+#: articles/templates/articles/edit.html:30
+#: articles/templates/articles/write.html:29
+msgid "Publish"
+msgstr "Publicar"
+
+#: articles/templates/articles/edit.html:31
+#: articles/templates/articles/write.html:30
+msgid "Save Draft"
+msgstr "Guardar proyecto"
+
+#: articles/templates/articles/edit.html:32
+#: articles/templates/articles/write.html:31
+#: feeds/templates/feeds/feeds.html:29
+#: questions/templates/questions/ask.html:28
+msgid "Cancel"
+msgstr "Cancelar"
+
+#: auth/templates/auth/signup.html:11 core/templates/core/cover.html:39
+msgid "Sign up for Bootcamp"
+msgstr "Inscríbete Bootcamp"
+
+#: auth/templates/auth/signup.html:26
+msgid "Create an account"
+msgstr "Crear una cuenta"
+
+#: core/templates/core/cover.html:20 core/templates/core/cover.html.py:38
+msgid "Log in"
+msgstr "Iniciar la sesión"
+
+#: core/templates/core/cover.html:24 core/templates/core/network.html:27
+msgid "Username"
+msgstr "Nombre de usuario"
+
+#: core/templates/core/cover.html:31
+#: core/templates/core/partial_settings_menu.html:5
+msgid "Password"
+msgstr "Contraseña"
+
+#: core/templates/core/network.html:4 templates/base.html:41
+msgid "Network"
+msgstr "Red"
+
+#: core/templates/core/network.html:25
+msgid "Job Title"
+msgstr "Título del trabajo"
+
+#: core/templates/core/network.html:28
+msgid "Email"
+msgstr ""
+
+#: core/templates/core/network.html:30
+msgid "Location"
+msgstr "Ubicación"
+
+#: core/templates/core/network.html:33
+msgid "Url"
+msgstr ""
+
+#: core/templates/core/partial_settings_menu.html:3
+msgid "Profile"
+msgstr "Perfil"
+
+#: core/templates/core/partial_settings_menu.html:4
+msgid "Picture"
+msgstr "Foto"
+
+#: core/templates/core/password.html:4 core/templates/core/password.html:8
+#: core/templates/core/picture.html:4 core/templates/core/picture.html:14
+#: core/templates/core/settings.html:8 templates/base.html:50
+msgid "Account Settings"
+msgstr "Configuración de la cuenta"
+
+#: core/templates/core/password.html:23
+msgid "Change Password"
+msgstr "Cambiar contraseña"
+
+#: core/templates/core/password.html:43 core/templates/core/settings.html:42
+msgid "Save"
+msgstr "Guardar"
+
+#: core/templates/core/picture.html:29
+msgid "Change Picture"
+msgstr "Cambiar Foto"
+
+#: core/templates/core/picture.html:34
+msgid "Upload new picture"
+msgstr "Subir nueva foto"
+
+#: core/templates/core/picture.html:45
+msgid "Crop Picture"
+msgstr "Recortar foto"
+
+#: core/templates/core/picture.html:49
+msgid "Crop the profile picture and then click on the"
+msgstr "Recortar la foto de perfil y luego haga clic en el"
+
+#: core/templates/core/picture.html:49
+msgid "Save Picture"
+msgstr "Guardar Foto"
+
+#: core/templates/core/picture.html:49
+msgid "button"
+msgstr "botón"
+
+#: core/templates/core/picture.html:64
+msgid "Close"
+msgstr "Cerrar"
+
+#: core/templates/core/picture.html:65
+msgid "Save changes"
+msgstr "Guardar cambios"
+
+#: core/templates/core/profile.html:37
+msgid "Last Feeds by"
+msgstr "Últimos Feeds de"
+
+#: core/templates/core/settings.html:23
+msgid "Edit Profile"
+msgstr "Editar Perfil"
+
+#: feeds/models.py:16 feeds/templates/feeds/feed.html:6 templates/base.html:38
+msgid "Feed"
+msgstr ""
+
+#: feeds/models.py:17
+msgid "Feeds"
+msgstr ""
+
+#: feeds/templates/feeds/feeds.html:12
+msgid "Press Ctrl + P to compose"
+msgstr "Presione Ctrl + P para redactar"
+
+#: feeds/templates/feeds/feeds.html:13
+msgid "Compose"
+msgstr "Redactar"
+
+#: feeds/templates/feeds/feeds.html:18
+msgid "Compose a new post"
+msgstr "Redactar un nuevo mensaje"
+
+#: feeds/templates/feeds/feeds.html:27
+msgid "Post"
+msgstr "Mensaje"
+
+#: feeds/templates/feeds/feeds.html:34
+msgid "new posts"
+msgstr "mensajes nuevos"
+
+#: feeds/templates/feeds/partial_feed.html:8
+msgid "Click to remove this feed"
+msgstr "Haga clic para eliminar esta feed"
+
+#: feeds/templates/feeds/partial_feed.html:16
+msgid "Unlike"
+msgstr ""
+
+#: feeds/templates/feeds/partial_feed.html:22
+msgid "Like"
+msgstr "Gustar"
+
+#: feeds/templates/feeds/partial_feed.html:27
+msgid "Comment"
+msgstr "Comentario"
+
+#: feeds/templates/feeds/partial_feed.html:35
+msgid "Write a comment..."
+msgstr "Escribe un comentario..."
+
+#: feeds/templates/feeds/partial_feed_comments.html:11
+msgid "Be the first one to comment"
+msgstr "¡Sé el primero en comentar"
+
+#: questions/templates/questions/ask.html:6
+#: questions/templates/questions/question.html:13
+#: questions/templates/questions/questions.html:16
+msgid "Questions"
+msgstr "Preguntas"
+
+#: questions/templates/questions/ask.html:7
+#: questions/templates/questions/questions.html:14
+msgid "Ask Question"
+msgstr "Pregunte a la pregunta"
+
+#: questions/templates/questions/ask.html:27
+msgid "Post Your Question"
+msgstr "Publicar tu pregunta"
+
+#: questions/templates/questions/partial_answer.html:7
+msgid "Click to up vote; click again to toggle"
+msgstr "Haga clic para up vote; haga clic de nuevo para alternar"
+
+#: questions/templates/questions/partial_answer.html:9
+msgid "Click to down vote; click again to toggle"
+msgstr "Haga clic para vote down; haga clic de nuevo para alternar"
+
+#: questions/templates/questions/partial_answer.html:11
+msgid "Click to unaccept the answer"
+msgstr "Haga clic para inaceptable la respuesta"
+
+#: questions/templates/questions/partial_answer.html:15
+msgid "Click to accept the answer"
+msgstr "Haga clic para aceptar la respuesta"
+
+#: questions/templates/questions/partial_answer.html:22
+msgid "answered"
+msgstr "contestado"
+
+#: questions/templates/questions/question.html:14
+msgid "Question"
+msgstr "Pregunta"
+
+#: questions/templates/questions/question.html:56
+msgid "Post Your Answer"
+msgstr "Publicar tu respuesta"
+
+#: questions/templates/questions/questions.html:20
+msgid "All Questions"
+msgstr "Todas las preguntas"
+
+#: questions/templates/questions/questions.html:21
+msgid "Unanswered"
+msgstr "sin respuesta"
+
+#: questions/templates/questions/questions.html:22
+msgid "Answered"
+msgstr "Contestada"
+
+#: questions/templates/questions/questions.html:27
+msgid "No question to display"
+msgstr "No hay preguntas para mostrar"
+
+#: templates/base.html:40
+msgid "Q&A"
+msgstr ""
+
+#: templates/base.html:51
+msgid "Log out"
+msgstr "Finalizar la sesión"
diff --git a/samples/development-frameworks/django/bootcamp/locale/jp/LC_MESSAGES/django.po b/samples/development-frameworks/django/bootcamp/locale/jp/LC_MESSAGES/django.po
new file mode 100644
index 00000000..00ac2ef8
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/locale/jp/LC_MESSAGES/django.po
@@ -0,0 +1,333 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-06-07 11:09-0300\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: activities/templates/activities/last_notifications.html:12
+msgid "You have no unread notification"
+msgstr ""
+
+#: activities/templates/activities/last_notifications.html:14
+msgid "See all"
+msgstr ""
+
+#: activities/templates/activities/notifications.html:26
+msgid "You have no notification"
+msgstr ""
+
+#: articles/models.py:25 articles/templates/articles/article.html:13
+msgid "Article"
+msgstr ""
+
+#: articles/models.py:26 articles/templates/articles/article.html:12
+#: articles/templates/articles/articles.html:4
+#: articles/templates/articles/articles.html:16
+#: articles/templates/articles/drafts.html:10
+#: articles/templates/articles/edit.html:10
+#: articles/templates/articles/write.html:10 templates/base.html:39
+msgid "Articles"
+msgstr ""
+
+#: articles/models.py:68
+msgid "Tag"
+msgstr ""
+
+#: articles/models.py:69 articles/templates/articles/drafts.html:18
+msgid "Tags"
+msgstr ""
+
+#: articles/templates/articles/articles.html:13
+#: articles/templates/articles/write.html:11
+msgid "Write Article"
+msgstr ""
+
+#: articles/templates/articles/articles.html:15
+#: articles/templates/articles/drafts.html:11
+#: articles/templates/articles/edit.html:11
+msgid "Drafts"
+msgstr ""
+
+#: articles/templates/articles/articles.html:24
+msgid "There is no published article yet"
+msgstr ""
+
+#: articles/templates/articles/articles.html:24
+msgid "Be the first one to publish"
+msgstr ""
+
+#: articles/templates/articles/articles.html:29
+msgid "Popular Tags"
+msgstr ""
+
+#: articles/templates/articles/drafts.html:16
+msgid "Title"
+msgstr ""
+
+#: articles/templates/articles/drafts.html:17
+msgid "Content"
+msgstr ""
+
+#: articles/templates/articles/drafts.html:35
+msgid "No draft to display"
+msgstr ""
+
+#: articles/templates/articles/edit.html:12
+msgid "Edit"
+msgstr ""
+
+#: articles/templates/articles/edit.html:30
+#: articles/templates/articles/write.html:29
+msgid "Publish"
+msgstr ""
+
+#: articles/templates/articles/edit.html:31
+#: articles/templates/articles/write.html:30
+msgid "Save Draft"
+msgstr ""
+
+#: articles/templates/articles/edit.html:32
+#: articles/templates/articles/write.html:31
+#: feeds/templates/feeds/feeds.html:29
+#: questions/templates/questions/ask.html:28
+msgid "Cancel"
+msgstr ""
+
+#: auth/templates/auth/signup.html:11 core/templates/core/cover.html:39
+msgid "Sign up for Bootcamp"
+msgstr ""
+
+#: auth/templates/auth/signup.html:26
+msgid "Create an account"
+msgstr ""
+
+#: core/templates/core/cover.html:20 core/templates/core/cover.html.py:38
+msgid "Log in"
+msgstr ""
+
+#: core/templates/core/cover.html:24 core/templates/core/network.html:27
+msgid "Username"
+msgstr ""
+
+#: core/templates/core/cover.html:31
+#: core/templates/core/partial_settings_menu.html:5
+msgid "Password"
+msgstr ""
+
+#: core/templates/core/network.html:4 templates/base.html:41
+msgid "Network"
+msgstr ""
+
+#: core/templates/core/network.html:25
+msgid "Job Title"
+msgstr ""
+
+#: core/templates/core/network.html:28
+msgid "Email"
+msgstr ""
+
+#: core/templates/core/network.html:30
+msgid "Location"
+msgstr ""
+
+#: core/templates/core/network.html:33
+msgid "Url"
+msgstr ""
+
+#: core/templates/core/partial_settings_menu.html:3
+msgid "Profile"
+msgstr ""
+
+#: core/templates/core/partial_settings_menu.html:4
+msgid "Picture"
+msgstr ""
+
+#: core/templates/core/password.html:4 core/templates/core/password.html:8
+#: core/templates/core/picture.html:4 core/templates/core/picture.html:14
+#: core/templates/core/settings.html:10 templates/base.html:51
+msgid "Account Settings"
+msgstr ""
+
+#: core/templates/core/password.html:23
+msgid "Change Password"
+msgstr ""
+
+#: core/templates/core/password.html:43 core/templates/core/settings.html:54
+msgid "Save"
+msgstr ""
+
+#: core/templates/core/picture.html:29
+msgid "Change Picture"
+msgstr ""
+
+#: core/templates/core/picture.html:34
+msgid "Upload new picture"
+msgstr ""
+
+#: core/templates/core/picture.html:45
+msgid "Crop Picture"
+msgstr ""
+
+#: core/templates/core/picture.html:49
+msgid "Crop the profile picture and then click on the"
+msgstr ""
+
+#: core/templates/core/picture.html:49
+msgid "Save Picture"
+msgstr ""
+
+#: core/templates/core/picture.html:49
+msgid "button"
+msgstr ""
+
+#: core/templates/core/picture.html:64
+msgid "Close"
+msgstr ""
+
+#: core/templates/core/picture.html:65
+msgid "Save changes"
+msgstr ""
+
+#: core/templates/core/profile.html:37
+msgid "Last Feeds by"
+msgstr ""
+
+#: core/templates/core/settings.html:25
+msgid "Edit Profile"
+msgstr ""
+
+#: feeds/models.py:16 feeds/templates/feeds/feed.html:6 templates/base.html:38
+msgid "Feed"
+msgstr ""
+
+#: feeds/models.py:17
+msgid "Feeds"
+msgstr ""
+
+#: feeds/templates/feeds/feeds.html:12
+msgid "Press Ctrl + P to compose"
+msgstr ""
+
+#: feeds/templates/feeds/feeds.html:13
+msgid "Compose"
+msgstr ""
+
+#: feeds/templates/feeds/feeds.html:18
+msgid "Compose a new post"
+msgstr ""
+
+#: feeds/templates/feeds/feeds.html:27
+msgid "Post"
+msgstr ""
+
+#: feeds/templates/feeds/feeds.html:34
+msgid "new posts"
+msgstr ""
+
+#: feeds/templates/feeds/partial_feed.html:8
+msgid "Click to remove this feed"
+msgstr ""
+
+#: feeds/templates/feeds/partial_feed.html:16
+msgid "Unlike"
+msgstr ""
+
+#: feeds/templates/feeds/partial_feed.html:22
+msgid "Like"
+msgstr ""
+
+#: feeds/templates/feeds/partial_feed.html:27
+msgid "Comment"
+msgstr ""
+
+#: feeds/templates/feeds/partial_feed.html:35
+msgid "Write a comment..."
+msgstr ""
+
+#: feeds/templates/feeds/partial_feed_comments.html:11
+msgid "Be the first one to comment"
+msgstr ""
+
+#: questions/templates/questions/ask.html:6
+#: questions/templates/questions/question.html:13
+#: questions/templates/questions/questions.html:16
+msgid "Questions"
+msgstr ""
+
+#: questions/templates/questions/ask.html:7
+#: questions/templates/questions/questions.html:14
+msgid "Ask Question"
+msgstr ""
+
+#: questions/templates/questions/ask.html:27
+msgid "Post Your Question"
+msgstr ""
+
+#: questions/templates/questions/partial_answer.html:7
+msgid "Click to up vote; click again to toggle"
+msgstr ""
+
+#: questions/templates/questions/partial_answer.html:9
+msgid "Click to down vote; click again to toggle"
+msgstr ""
+
+#: questions/templates/questions/partial_answer.html:11
+msgid "Click to unaccept the answer"
+msgstr ""
+
+#: questions/templates/questions/partial_answer.html:15
+msgid "Click to accept the answer"
+msgstr ""
+
+#: questions/templates/questions/partial_answer.html:22
+msgid "answered"
+msgstr ""
+
+#: questions/templates/questions/question.html:14
+msgid "Question"
+msgstr ""
+
+#: questions/templates/questions/question.html:56
+msgid "Post Your Answer"
+msgstr ""
+
+#: questions/templates/questions/questions.html:20
+msgid "All Questions"
+msgstr ""
+
+#: questions/templates/questions/questions.html:21
+msgid "Unanswered"
+msgstr ""
+
+#: questions/templates/questions/questions.html:22
+msgid "Answered"
+msgstr ""
+
+#: questions/templates/questions/questions.html:27
+msgid "No question to display"
+msgstr ""
+
+#: templates/base.html:40
+msgid "Q&A"
+msgstr ""
+
+#: templates/base.html:42
+msgid "Search"
+msgstr ""
+
+#: templates/base.html:52
+msgid "Log out"
+msgstr ""
diff --git a/samples/development-frameworks/django/bootcamp/locale/pt_BR/LC_MESSAGES/django.po b/samples/development-frameworks/django/bootcamp/locale/pt_BR/LC_MESSAGES/django.po
new file mode 100644
index 00000000..19d4b0a7
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/locale/pt_BR/LC_MESSAGES/django.po
@@ -0,0 +1,334 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-06-05 22:50-0300\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: activities/templates/activities/last_notifications.html:12
+msgid "You have no unread notification"
+msgstr "Você não possui nenhuma notificação não lida"
+
+#: activities/templates/activities/last_notifications.html:14
+msgid "See all"
+msgstr "Ver tudo"
+
+#: activities/templates/activities/notifications.html:26
+msgid "You have no notification"
+msgstr "Você não possui notificação"
+
+#: articles/models.py:25 articles/templates/articles/article.html:13
+#, fuzzy
+msgid "Article"
+msgstr "Artigo"
+
+#: articles/models.py:26 articles/templates/articles/article.html:12
+#: articles/templates/articles/articles.html:4
+#: articles/templates/articles/articles.html:16
+#: articles/templates/articles/drafts.html:10
+#: articles/templates/articles/edit.html:10
+#: articles/templates/articles/write.html:10 templates/base.html:39
+msgid "Articles"
+msgstr "Artigos"
+
+#: articles/models.py:68
+msgid "Tag"
+msgstr "Tag"
+
+#: articles/models.py:69 articles/templates/articles/drafts.html:18
+msgid "Tags"
+msgstr "Tags"
+
+#: articles/templates/articles/articles.html:13
+#: articles/templates/articles/write.html:11
+#, fuzzy
+msgid "Write Article"
+msgstr "Escrever Artigo"
+
+#: articles/templates/articles/articles.html:15
+#: articles/templates/articles/drafts.html:11
+#: articles/templates/articles/edit.html:11
+msgid "Drafts"
+msgstr "Rascunhos"
+
+#: articles/templates/articles/articles.html:24
+msgid "There is no published article yet"
+msgstr "Não há artigo publicado ainda"
+
+#: articles/templates/articles/articles.html:24
+msgid "Be the first one to publish"
+msgstr "Seja o primeiro a publicar"
+
+#: articles/templates/articles/articles.html:29
+msgid "Popular Tags"
+msgstr "Tags populares"
+
+#: articles/templates/articles/drafts.html:16
+#, fuzzy
+msgid "Title"
+msgstr "Meu titulo"
+
+#: articles/templates/articles/drafts.html:17
+msgid "Content"
+msgstr "Conteúdo"
+
+#: articles/templates/articles/drafts.html:35
+msgid "No draft to display"
+msgstr "Nenhum rascunho para exibir"
+
+#: articles/templates/articles/edit.html:12
+msgid "Edit"
+msgstr "Editar"
+
+#: articles/templates/articles/edit.html:30
+#: articles/templates/articles/write.html:29
+msgid "Publish"
+msgstr "Publicar"
+
+#: articles/templates/articles/edit.html:31
+#: articles/templates/articles/write.html:30
+msgid "Save Draft"
+msgstr "Salvar rascunho"
+
+#: articles/templates/articles/edit.html:32
+#: articles/templates/articles/write.html:31
+#: feeds/templates/feeds/feeds.html:29
+#: questions/templates/questions/ask.html:28
+msgid "Cancel"
+msgstr "Cancelar"
+
+#: auth/templates/auth/signup.html:11 core/templates/core/cover.html:39
+msgid "Sign up for Bootcamp"
+msgstr ""
+
+#: auth/templates/auth/signup.html:26
+msgid "Create an account"
+msgstr "Criar uma conta"
+
+#: core/templates/core/cover.html:20 core/templates/core/cover.html.py:38
+#, fuzzy
+msgid "Log in"
+msgstr "Entrar"
+
+#: core/templates/core/cover.html:24 core/templates/core/network.html:27
+msgid "Username"
+msgstr "Nome de usuário"
+
+#: core/templates/core/cover.html:31
+#: core/templates/core/partial_settings_menu.html:5
+msgid "Password"
+msgstr "Senha"
+
+#: core/templates/core/network.html:4 templates/base.html:41
+msgid "Network"
+msgstr "Rede"
+
+#: core/templates/core/network.html:25
+#, fuzzy
+msgid "Job Title"
+msgstr "Cargo"
+
+#: core/templates/core/network.html:28
+msgid "Email"
+msgstr ""
+
+#: core/templates/core/network.html:30
+msgid "Location"
+msgstr "Localização"
+
+#: core/templates/core/network.html:33
+msgid "Url"
+msgstr ""
+
+#: core/templates/core/partial_settings_menu.html:3
+msgid "Profile"
+msgstr "Perfil"
+
+#: core/templates/core/partial_settings_menu.html:4
+msgid "Picture"
+msgstr "Foto"
+
+#: core/templates/core/password.html:4 core/templates/core/password.html:8
+#: core/templates/core/picture.html:4 core/templates/core/picture.html:14
+#: core/templates/core/settings.html:8 templates/base.html:50
+msgid "Account Settings"
+msgstr "Configuração da conta"
+
+#: core/templates/core/password.html:23
+msgid "Change Password"
+msgstr "Trocar senha"
+
+#: core/templates/core/password.html:43 core/templates/core/settings.html:42
+msgid "Save"
+msgstr "Salvar"
+
+#: core/templates/core/picture.html:29
+msgid "Change Picture"
+msgstr "Trocar foto"
+
+#: core/templates/core/picture.html:34
+msgid "Upload new picture"
+msgstr "Carregar nova foto"
+
+#: core/templates/core/picture.html:45
+msgid "Crop Picture"
+msgstr "Recortar foto"
+
+#: core/templates/core/picture.html:49
+msgid "Crop the profile picture and then click on the"
+msgstr "Recorte a foto do seu perfil, e em seguida, clique no"
+
+#: core/templates/core/picture.html:49
+msgid "Save Picture"
+msgstr "Salvar foto"
+
+#: core/templates/core/picture.html:49
+msgid "button"
+msgstr "botão"
+
+#: core/templates/core/picture.html:64
+msgid "Close"
+msgstr "Fechar"
+
+#: core/templates/core/picture.html:65
+msgid "Save changes"
+msgstr "Salvar alterações"
+
+#: core/templates/core/profile.html:37
+msgid "Last Feeds by"
+msgstr "Últimos Feeds por"
+
+#: core/templates/core/settings.html:23
+msgid "Edit Profile"
+msgstr "Editar Perfil"
+
+#: feeds/models.py:16 feeds/templates/feeds/feed.html:6 templates/base.html:38
+msgid "Feed"
+msgstr ""
+
+#: feeds/models.py:17
+msgid "Feeds"
+msgstr ""
+
+#: feeds/templates/feeds/feeds.html:12
+msgid "Press Ctrl + P to compose"
+msgstr "Pressione Ctrl + P para escrever"
+
+#: feeds/templates/feeds/feeds.html:13
+msgid "Compose"
+msgstr "Escrever"
+
+#: feeds/templates/feeds/feeds.html:18
+msgid "Compose a new post"
+msgstr "Escrever uma nova postagem"
+
+#: feeds/templates/feeds/feeds.html:27
+msgid "Post"
+msgstr "Postagem"
+
+#: feeds/templates/feeds/feeds.html:34
+msgid "new posts"
+msgstr "novas postagens"
+
+#: feeds/templates/feeds/partial_feed.html:8
+msgid "Click to remove this feed"
+msgstr "Clique para remover o feed"
+
+#: feeds/templates/feeds/partial_feed.html:16
+msgid "Unlike"
+msgstr "Não gostei"
+
+#: feeds/templates/feeds/partial_feed.html:22
+msgid "Like"
+msgstr "Gostei"
+
+#: feeds/templates/feeds/partial_feed.html:27
+msgid "Comment"
+msgstr "Comentário"
+
+#: feeds/templates/feeds/partial_feed.html:35
+msgid "Write a comment..."
+msgstr "Escrever um comentário"
+
+#: feeds/templates/feeds/partial_feed_comments.html:11
+msgid "Be the first one to comment"
+msgstr "Seja o primeiro a comentar"
+
+#: questions/templates/questions/ask.html:6
+#: questions/templates/questions/question.html:13
+#: questions/templates/questions/questions.html:16
+msgid "Questions"
+msgstr "Perguntas"
+
+#: questions/templates/questions/ask.html:7
+#: questions/templates/questions/questions.html:14
+msgid "Ask Question"
+msgstr "Pergunte"
+
+#: questions/templates/questions/ask.html:27
+msgid "Post Your Question"
+msgstr "Poste sua pergunta"
+
+#: questions/templates/questions/partial_answer.html:7
+msgid "Click to up vote; click again to toggle"
+msgstr "Clique para votar; clique novamente para alterar"
+
+#: questions/templates/questions/partial_answer.html:9
+msgid "Click to down vote; click again to toggle"
+msgstr "Clique para votar; clique novamente para alterar"
+
+#: questions/templates/questions/partial_answer.html:11
+msgid "Click to unaccept the answer"
+msgstr ""
+
+#: questions/templates/questions/partial_answer.html:15
+msgid "Click to accept the answer"
+msgstr "Clique caso não queira aceitar essa resposta"
+
+#: questions/templates/questions/partial_answer.html:22
+msgid "answered"
+msgstr "respondidas"
+
+#: questions/templates/questions/question.html:14
+msgid "Question"
+msgstr "Pergunta"
+
+#: questions/templates/questions/question.html:56
+msgid "Post Your Answer"
+msgstr "Postar sua resposta"
+
+#: questions/templates/questions/questions.html:20
+msgid "All Questions"
+msgstr "Todas as perguntas"
+
+#: questions/templates/questions/questions.html:21
+msgid "Unanswered"
+msgstr "Sem resposta"
+
+#: questions/templates/questions/questions.html:22
+msgid "Answered"
+msgstr "Respondida"
+
+#: questions/templates/questions/questions.html:27
+msgid "No question to display"
+msgstr "Nenhuma pergunta para exibir"
+
+#: templates/base.html:40
+msgid "Q&A"
+msgstr ""
+
+#: templates/base.html:51
+msgid "Log out"
+msgstr "Sair"
diff --git a/samples/development-frameworks/django/bootcamp/locale/zh_CN/LC_MESSAGES/django.po b/samples/development-frameworks/django/bootcamp/locale/zh_CN/LC_MESSAGES/django.po
new file mode 100644
index 00000000..b873306f
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/locale/zh_CN/LC_MESSAGES/django.po
@@ -0,0 +1,331 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-06-05 22:50-0300\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: activities/templates/activities/last_notifications.html:12
+msgid "You have no unread notification"
+msgstr "您有未阅读的信息"
+
+#: activities/templates/activities/last_notifications.html:14
+msgid "See all"
+msgstr "查看所有"
+
+#: activities/templates/activities/notifications.html:26
+msgid "You have no notification"
+msgstr "您没有消息"
+
+#: articles/models.py:25 articles/templates/articles/article.html:13
+msgid "Article"
+msgstr "文章"
+
+#: articles/models.py:26 articles/templates/articles/article.html:12
+#: articles/templates/articles/articles.html:4
+#: articles/templates/articles/articles.html:16
+#: articles/templates/articles/drafts.html:10
+#: articles/templates/articles/edit.html:10
+#: articles/templates/articles/write.html:10 templates/base.html:39
+msgid "Articles"
+msgstr "文章"
+
+#: articles/models.py:68
+msgid "Tag"
+msgstr "标签"
+
+#: articles/models.py:69 articles/templates/articles/drafts.html:18
+msgid "Tags"
+msgstr "标签"
+
+#: articles/templates/articles/articles.html:13
+#: articles/templates/articles/write.html:11
+#, fuzzy
+msgid "Write Article"
+msgstr "新建文章"
+
+#: articles/templates/articles/articles.html:15
+#: articles/templates/articles/drafts.html:11
+#: articles/templates/articles/edit.html:11
+msgid "Drafts"
+msgstr "草稿"
+
+#: articles/templates/articles/articles.html:24
+msgid "There is no published article yet"
+msgstr "目前还没有发布任何文章"
+
+#: articles/templates/articles/articles.html:24
+msgid "Be the first one to publish"
+msgstr "来做一个第一人吧~"
+
+#: articles/templates/articles/articles.html:29
+msgid "Popular Tags"
+msgstr "热门标签"
+
+#: articles/templates/articles/drafts.html:16
+msgid "Title"
+msgstr "标题"
+
+#: articles/templates/articles/drafts.html:17
+msgid "Content"
+msgstr "内容"
+
+#: articles/templates/articles/drafts.html:35
+msgid "No draft to display"
+msgstr "无草稿显示"
+
+#: articles/templates/articles/edit.html:12
+msgid "Edit"
+msgstr "编辑"
+
+#: articles/templates/articles/edit.html:30
+#: articles/templates/articles/write.html:29
+msgid "Publish"
+msgstr "发布"
+
+#: articles/templates/articles/edit.html:31
+#: articles/templates/articles/write.html:30
+msgid "Save Draft"
+msgstr "保存草稿"
+
+#: articles/templates/articles/edit.html:32
+#: articles/templates/articles/write.html:31
+#: feeds/templates/feeds/feeds.html:29
+#: questions/templates/questions/ask.html:28
+msgid "Cancel"
+msgstr "取消"
+
+#: auth/templates/auth/signup.html:11 core/templates/core/cover.html:39
+msgid "Sign up for Bootcamp"
+msgstr "注册Bootcamp"
+
+#: auth/templates/auth/signup.html:26
+msgid "Create an account"
+msgstr "创建一个账户"
+
+#: core/templates/core/cover.html:20 core/templates/core/cover.html.py:38
+msgid "Log in"
+msgstr "登录"
+
+#: core/templates/core/cover.html:24 core/templates/core/network.html:27
+msgid "Username"
+msgstr "用户名"
+
+#: core/templates/core/cover.html:31
+#: core/templates/core/partial_settings_menu.html:5
+msgid "Password"
+msgstr "密码"
+
+#: core/templates/core/network.html:4 templates/base.html:41
+msgid "Network"
+msgstr "网络"
+
+#: core/templates/core/network.html:25
+msgid "Job Title"
+msgstr "职位"
+
+#: core/templates/core/network.html:28
+msgid "Email"
+msgstr "邮箱"
+
+#: core/templates/core/network.html:30
+msgid "Location"
+msgstr "位置"
+
+#: core/templates/core/network.html:33
+msgid "Url"
+msgstr "地址"
+
+#: core/templates/core/partial_settings_menu.html:3
+msgid "Profile"
+msgstr "资料"
+
+#: core/templates/core/partial_settings_menu.html:4
+msgid "Picture"
+msgstr "图片"
+
+#: core/templates/core/password.html:4 core/templates/core/password.html:8
+#: core/templates/core/picture.html:4 core/templates/core/picture.html:14
+#: core/templates/core/settings.html:8 templates/base.html:50
+msgid "Account Settings"
+msgstr "账户设置"
+
+#: core/templates/core/password.html:23
+msgid "Change Password"
+msgstr "修改密码"
+
+#: core/templates/core/password.html:43 core/templates/core/settings.html:42
+msgid "Save"
+msgstr "保存"
+
+#: core/templates/core/picture.html:29
+msgid "Change Picture"
+msgstr "修改图片"
+
+#: core/templates/core/picture.html:34
+msgid "Upload new picture"
+msgstr "上传新头像"
+
+#: core/templates/core/picture.html:45
+msgid "Crop Picture"
+msgstr "修剪头像"
+
+#: core/templates/core/picture.html:49
+msgid "Crop the profile picture and then click on the"
+msgstr "修剪头像图片然后点击"
+
+#: core/templates/core/picture.html:49
+msgid "Save Picture"
+msgstr "保存头像"
+
+#: core/templates/core/picture.html:49
+msgid "button"
+msgstr "按钮"
+
+#: core/templates/core/picture.html:64
+msgid "Close"
+msgstr "关闭"
+
+#: core/templates/core/picture.html:65
+msgid "Save changes"
+msgstr "保存修改"
+
+#: core/templates/core/profile.html:37
+msgid "Last Feeds by"
+msgstr "最后一条动弹"
+
+#: core/templates/core/settings.html:23
+msgid "Edit Profile"
+msgstr "编辑资料"
+
+#: feeds/models.py:16 feeds/templates/feeds/feed.html:6 templates/base.html:38
+msgid "Feed"
+msgstr "动弹"
+
+#: feeds/models.py:17
+msgid "Feeds"
+msgstr "动弹"
+
+#: feeds/templates/feeds/feeds.html:12
+msgid "Press Ctrl + P to compose"
+msgstr "按下 Ctrl+P 来发布"
+
+#: feeds/templates/feeds/feeds.html:13
+msgid "Compose"
+msgstr "创建"
+
+#: feeds/templates/feeds/feeds.html:18
+msgid "Compose a new post"
+msgstr "创建一篇新文章"
+
+#: feeds/templates/feeds/feeds.html:27
+msgid "Post"
+msgstr "发布"
+
+#: feeds/templates/feeds/feeds.html:34
+msgid "new posts"
+msgstr "新文章"
+
+#: feeds/templates/feeds/partial_feed.html:8
+msgid "Click to remove this feed"
+msgstr "点击删除这条动弹"
+
+#: feeds/templates/feeds/partial_feed.html:16
+msgid "Unlike"
+msgstr "不喜欢"
+
+#: feeds/templates/feeds/partial_feed.html:22
+msgid "Like"
+msgstr "喜欢"
+
+#: feeds/templates/feeds/partial_feed.html:27
+msgid "Comment"
+msgstr "评论"
+
+#: feeds/templates/feeds/partial_feed.html:35
+msgid "Write a comment..."
+msgstr "写一条评论"
+
+#: feeds/templates/feeds/partial_feed_comments.html:11
+msgid "Be the first one to comment"
+msgstr "写第一条评论"
+
+#: questions/templates/questions/ask.html:6
+#: questions/templates/questions/question.html:13
+#: questions/templates/questions/questions.html:16
+msgid "Questions"
+msgstr "问答"
+
+#: questions/templates/questions/ask.html:7
+#: questions/templates/questions/questions.html:14
+msgid "Ask Question"
+msgstr "提问"
+
+#: questions/templates/questions/ask.html:27
+msgid "Post Your Question"
+msgstr "提交问题"
+
+#: questions/templates/questions/partial_answer.html:7
+msgid "Click to up vote; click again to toggle"
+msgstr "点击推荐,再次点击取消"
+
+#: questions/templates/questions/partial_answer.html:9
+msgid "Click to down vote; click again to toggle"
+msgstr "点击不推荐,再次点击取消"
+
+#: questions/templates/questions/partial_answer.html:11
+msgid "Click to unaccept the answer"
+msgstr "点击不接受答案"
+
+#: questions/templates/questions/partial_answer.html:15
+msgid "Click to accept the answer"
+msgstr "点击同意该答案"
+
+#: questions/templates/questions/partial_answer.html:22
+msgid "answered"
+msgstr "已回答"
+
+#: questions/templates/questions/question.html:14
+msgid "Question"
+msgstr "问题"
+
+#: questions/templates/questions/question.html:56
+msgid "Post Your Answer"
+msgstr "提交您的问题"
+
+#: questions/templates/questions/questions.html:20
+msgid "All Questions"
+msgstr "所有问题"
+
+#: questions/templates/questions/questions.html:21
+msgid "Unanswered"
+msgstr "寻求答案"
+
+#: questions/templates/questions/questions.html:22
+msgid "Answered"
+msgstr "已解决"
+
+#: questions/templates/questions/questions.html:27
+msgid "No question to display"
+msgstr "没有问题显示"
+
+#: templates/base.html:40
+msgid "Q&A"
+msgstr "问答"
+
+#: templates/base.html:51
+msgid "Log out"
+msgstr "登出"
diff --git a/samples/development-frameworks/django/bootcamp/messenger/__init__.py b/samples/development-frameworks/django/bootcamp/messenger/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/samples/development-frameworks/django/bootcamp/messenger/migrations/0001_initial.py b/samples/development-frameworks/django/bootcamp/messenger/migrations/0001_initial.py
new file mode 100644
index 00000000..1b1d125c
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/messenger/migrations/0001_initial.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.4 on 2016-03-18 13:32
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Message',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('message', models.TextField(blank=True, max_length=1000)),
+ ('date', models.DateTimeField(auto_now_add=True)),
+ ('is_read', models.BooleanField(default=False)),
+ ('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL)),
+ ('from_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL)),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'ordering': ('date',),
+ 'db_table': 'messages_message',
+ 'verbose_name': 'Message',
+ 'verbose_name_plural': 'Messages',
+ },
+ ),
+ ]
diff --git a/samples/development-frameworks/django/bootcamp/messenger/migrations/__init__.py b/samples/development-frameworks/django/bootcamp/messenger/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/samples/development-frameworks/django/bootcamp/messenger/models.py b/samples/development-frameworks/django/bootcamp/messenger/models.py
new file mode 100644
index 00000000..69c83bbb
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/messenger/models.py
@@ -0,0 +1,56 @@
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+from django.contrib.auth.models import User
+from django.db.models import Max
+
+
+class Message(models.Model):
+ user = models.ForeignKey(User, related_name='+')
+ message = models.TextField(max_length=1000, blank=True)
+ date = models.DateTimeField(auto_now_add=True)
+ conversation = models.ForeignKey(User, related_name='+')
+ from_user = models.ForeignKey(User, related_name='+')
+ is_read = models.BooleanField(default=False)
+
+ class Meta:
+ verbose_name = _('Message')
+ verbose_name_plural = _('Messages')
+ ordering = ('date',)
+ db_table = 'messages_message'
+
+ def __unicode__(self):
+ return self.message
+
+ @staticmethod
+ def send_message(from_user, to_user, message):
+ message = message[:1000]
+ current_user_message = Message(from_user=from_user,
+ message=message,
+ user=from_user,
+ conversation=to_user,
+ is_read=True)
+ current_user_message.save()
+ Message(from_user=from_user,
+ conversation=from_user,
+ message=message,
+ user=to_user).save()
+
+ return current_user_message
+
+ @staticmethod
+ def get_conversations(user):
+ conversations = Message.objects.filter(
+ user=user).values('conversation').annotate(
+ last=Max('date')).order_by('-last')
+ users = []
+ for conversation in conversations:
+ users.append({
+ 'user': User.objects.get(pk=conversation['conversation']),
+ 'last': conversation['last'],
+ 'unread': Message.objects.filter(user=user,
+ conversation__pk=conversation[
+ 'conversation'],
+ is_read=False).count(),
+ })
+
+ return users
diff --git a/samples/development-frameworks/django/bootcamp/messenger/static/css/messages.css b/samples/development-frameworks/django/bootcamp/messenger/static/css/messages.css
new file mode 100644
index 00000000..76571bee
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/messenger/static/css/messages.css
@@ -0,0 +1,74 @@
+.conversation {
+ margin: 0;
+ padding: 0;
+}
+
+.conversation li {
+ list-style: none;
+ padding: .6em 0;
+}
+
+.conversation li h5 {
+ margin: 0;
+ margin-bottom: .2em;
+}
+
+.conversation li div {
+ margin-left: 50px;
+}
+
+.conversation .picture {
+ width: 40px;
+ border-radius: 5px;
+ float: left;
+}
+
+.conversation-portrait {
+ width: 20px;
+ border-radius: 3px;
+ margin-right: 5px;
+}
+
+.typeahead, .tt-query, .tt-hint {
+ border: 1px solid #CCCCCC;
+ border-radius: 5px;
+ font-size: 1.2em;
+ height: 35px;
+ line-height: 35px;
+ outline: medium none;
+ padding: 4px 12px;
+ width: 300px;
+}
+.typeahead {
+ background-color: #FFFFFF;
+}
+.typeahead:focus {
+ border: 2px solid #0097CF;
+}
+.tt-query {
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset;
+}
+.tt-hint {
+ color: #999999;
+}
+.tt-dropdown-menu {
+ background-color: #FFFFFF;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ border-radius: 8px;
+ box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+ margin-top: 8px;
+ padding: 8px 0;
+ width: 300px;
+}
+.tt-suggestion {
+ font-size: 1.2em;
+ line-height: 24px;
+ padding: 3px 20px;
+}
+.tt-suggestion.tt-cursor {
+ background-color: #0097CF;
+ color: #FFFFFF;
+}
+.tt-suggestion p {
+ margin: 0;
+}
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/messenger/static/js/check_messages.js b/samples/development-frameworks/django/bootcamp/messenger/static/js/check_messages.js
new file mode 100644
index 00000000..70863e18
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/messenger/static/js/check_messages.js
@@ -0,0 +1,15 @@
+$(function () {
+ function check_messages() {
+ $.ajax({
+ url: '/messages/check/',
+ cache: false,
+ success: function (data) {
+ $("#unread-count").text(data);
+ },
+ complete: function () {
+ window.setTimeout(check_messages, 60000);
+ }
+ });
+ };
+ check_messages();
+});
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/messenger/static/js/messages.js b/samples/development-frameworks/django/bootcamp/messenger/static/js/messages.js
new file mode 100644
index 00000000..05fc9fb8
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/messenger/static/js/messages.js
@@ -0,0 +1,16 @@
+$(function () {
+ $("#send").submit(function () {
+ $.ajax({
+ url: '/messages/send/',
+ data: $("#send").serialize(),
+ cache: false,
+ type: 'post',
+ success: function (data) {
+ $(".send-message").before(data);
+ $("input[name='message']").val('');
+ $("input[name='message']").focus();
+ }
+ });
+ return false;
+ });
+});
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/messenger/static/js/messages.typehead.js b/samples/development-frameworks/django/bootcamp/messenger/static/js/messages.typehead.js
new file mode 100644
index 00000000..89d6869f
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/messenger/static/js/messages.typehead.js
@@ -0,0 +1,32 @@
+$(function () {
+ var substringMatcher = function(strs) {
+ return function findMatches(q, cb) {
+ var matches, substringRegex;
+ matches = [];
+ substrRegex = new RegExp(q, 'i');
+ $.each(strs, function(i, str) {
+ if (substrRegex.test(str)) {
+ matches.push({ value: str });
+ }
+ });
+ cb(matches);
+ };
+ };
+
+ $.ajax({
+ url: '/messages/users/',
+ cache: false,
+ success: function (data) {
+ $('#to').typeahead({
+ hint: true,
+ highlight: true,
+ minLength: 1
+ },
+ {
+ name: 'data',
+ displayKey: 'value',
+ source: substringMatcher(data)
+ });
+ }
+ });
+});
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/messenger/templates/messenger/base_messages.html b/samples/development-frameworks/django/bootcamp/messenger/templates/messenger/base_messages.html
new file mode 100644
index 00000000..3ec1e078
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/messenger/templates/messenger/base_messages.html
@@ -0,0 +1,24 @@
+{% extends 'base.html' %}
+{% load staticfiles %}
+
+{% block head %}
+
+
+
+{% endblock head %}
+
+{% block main %}
+
+
+
+ {% include 'messenger/includes/partial_conversations.html' with conversations=conversations active=active %}
+
+
+ {% block container %}
+ {% endblock container %}
+
+
+{% endblock main %}
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/messenger/templates/messenger/inbox.html b/samples/development-frameworks/django/bootcamp/messenger/templates/messenger/inbox.html
new file mode 100644
index 00000000..f38282ca
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/messenger/templates/messenger/inbox.html
@@ -0,0 +1,28 @@
+{% extends 'messenger/base_messages.html' %}
+{% load i18n %}
+
+{% block title %}{% trans 'Inbox' %}{% endblock %}
+
+{% block page_header %}{% trans 'Inbox' %}{% endblock %}
+
+{% block container %}
+ {% if messages %}
+
+ {% else %}
+ Your inbox is empty!
+ {% endif %}
+{% endblock container %}
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/messenger/templates/messenger/includes/partial_conversations.html b/samples/development-frameworks/django/bootcamp/messenger/templates/messenger/includes/partial_conversations.html
new file mode 100644
index 00000000..e46cc507
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/messenger/templates/messenger/includes/partial_conversations.html
@@ -0,0 +1,23 @@
+{% load i18n %}
+
+
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/messenger/templates/messenger/includes/partial_message.html b/samples/development-frameworks/django/bootcamp/messenger/templates/messenger/includes/partial_message.html
new file mode 100644
index 00000000..c3d472c9
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/messenger/templates/messenger/includes/partial_message.html
@@ -0,0 +1,14 @@
+
+
+
+
+ {{ message.message }}
+
+
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/messenger/templates/messenger/new.html b/samples/development-frameworks/django/bootcamp/messenger/templates/messenger/new.html
new file mode 100644
index 00000000..196e690c
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/messenger/templates/messenger/new.html
@@ -0,0 +1,31 @@
+{% extends 'messenger/base_messages.html' %}
+{% load staticfiles %}
+{% load i18n %}
+
+{% block title %}{% trans 'New message' %}{% endblock %}
+
+{% block page_header %}{% trans 'New message' %}{% endblock %}
+
+{% block container %}
+
+ {% csrf_token %}
+
+
+
+
+
+{% endblock container %}
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/messenger/tests.py b/samples/development-frameworks/django/bootcamp/messenger/tests.py
new file mode 100644
index 00000000..7ce503c2
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/messenger/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/samples/development-frameworks/django/bootcamp/messenger/urls.py b/samples/development-frameworks/django/bootcamp/messenger/urls.py
new file mode 100644
index 00000000..0bf6ce8a
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/messenger/urls.py
@@ -0,0 +1,15 @@
+# coding: utf-8
+
+from django.conf.urls import url
+from bootcamp.messenger import views
+
+
+urlpatterns = [
+ url(r'^$', views.inbox, name='inbox'),
+ url(r'^new/$', views.new, name='new_message'),
+ url(r'^send/$', views.send, name='send_message'),
+ url(r'^delete/$', views.delete, name='delete_message'),
+ url(r'^users/$', views.users, name='users_message'),
+ url(r'^check/$', views.check, name='check_message'),
+ url(r'^(?P[^/]+)/$', views.messages, name='messages'),
+]
diff --git a/samples/development-frameworks/django/bootcamp/messenger/views.py b/samples/development-frameworks/django/bootcamp/messenger/views.py
new file mode 100644
index 00000000..9a3644ba
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/messenger/views.py
@@ -0,0 +1,127 @@
+from django.shortcuts import render, redirect
+from django.http import HttpResponse, HttpResponseBadRequest
+from django.contrib.auth.decorators import login_required
+from bootcamp.decorators import ajax_required
+from django.contrib.auth.models import User
+import json
+from bootcamp.messenger.models import Message
+
+
+@login_required
+def inbox(request):
+ conversations = Message.get_conversations(user=request.user)
+ active_conversation = None
+ messages = None
+ if conversations:
+ conversation = conversations[0]
+ active_conversation = conversation['user'].username
+ messages = Message.objects.filter(user=request.user,
+ conversation=conversation['user'])
+ messages.update(is_read=True)
+ for conversation in conversations:
+ if conversation['user'].username == active_conversation:
+ conversation['unread'] = 0
+
+ return render(request, 'messenger/inbox.html', {
+ 'messages': messages,
+ 'conversations': conversations,
+ 'active': active_conversation
+ })
+
+
+@login_required
+def messages(request, username):
+ conversations = Message.get_conversations(user=request.user)
+ active_conversation = username
+ messages = Message.objects.filter(user=request.user,
+ conversation__username=username)
+ messages.update(is_read=True)
+ for conversation in conversations:
+ if conversation['user'].username == username:
+ conversation['unread'] = 0
+
+ return render(request, 'messenger/inbox.html', {
+ 'messages': messages,
+ 'conversations': conversations,
+ 'active': active_conversation
+ })
+
+
+@login_required
+def new(request):
+ if request.method == 'POST':
+ from_user = request.user
+ to_user_username = request.POST.get('to')
+ try:
+ to_user = User.objects.get(username=to_user_username)
+
+ except Exception, e:
+ try:
+ to_user_username = to_user_username[
+ to_user_username.rfind('(')+1:len(to_user_username)-1]
+ to_user = User.objects.get(username=to_user_username)
+
+ except Exception, e:
+ return redirect('/messages/new/')
+
+ message = request.POST.get('message')
+ if len(message.strip()) == 0:
+ return redirect('/messages/new/')
+
+ if from_user != to_user:
+ Message.send_message(from_user, to_user, message)
+
+ return redirect(u'/messages/{0}/'.format(to_user_username))
+
+ else:
+ conversations = Message.get_conversations(user=request.user)
+ return render(request, 'messenger/new.html',
+ {'conversations': conversations})
+
+
+@login_required
+@ajax_required
+def delete(request):
+ return HttpResponse()
+
+
+@login_required
+@ajax_required
+def send(request):
+ if request.method == 'POST':
+ from_user = request.user
+ to_user_username = request.POST.get('to')
+ to_user = User.objects.get(username=to_user_username)
+ message = request.POST.get('message')
+ if len(message.strip()) == 0:
+ return HttpResponse()
+ if from_user != to_user:
+ msg = Message.send_message(from_user, to_user, message)
+ return render(request, 'messenger/includes/partial_message.html',
+ {'message': msg})
+
+ return HttpResponse()
+ else:
+ return HttpResponseBadRequest()
+
+
+@login_required
+@ajax_required
+def users(request):
+ users = User.objects.filter(is_active=True)
+ dump = []
+ template = u'{0} ({1})'
+ for user in users:
+ if user.profile.get_screen_name() != user.username:
+ dump.append(template.format(user.profile.get_screen_name(), user.username))
+ else:
+ dump.append(user.username)
+ data = json.dumps(dump)
+ return HttpResponse(data, content_type='application/json')
+
+
+@login_required
+@ajax_required
+def check(request):
+ count = Message.objects.filter(user=request.user, is_read=False).count()
+ return HttpResponse(count)
diff --git a/samples/development-frameworks/django/bootcamp/questions/__init__.py b/samples/development-frameworks/django/bootcamp/questions/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/samples/development-frameworks/django/bootcamp/questions/forms.py b/samples/development-frameworks/django/bootcamp/questions/forms.py
new file mode 100644
index 00000000..51adf462
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/questions/forms.py
@@ -0,0 +1,32 @@
+from django import forms
+from bootcamp.questions.models import Question, Answer
+
+
+class QuestionForm(forms.ModelForm):
+ title = forms.CharField(
+ widget=forms.TextInput(attrs={'class': 'form-control'}),
+ max_length=255)
+ description = forms.CharField(
+ widget=forms.Textarea(attrs={'class': 'form-control'}),
+ max_length=2000)
+ tags = forms.CharField(
+ widget=forms.TextInput(attrs={'class': 'form-control'}),
+ max_length=255,
+ required=False,
+ help_text='Use spaces to separate the tags, such as "asp.net mvc5 javascript"')
+
+ class Meta:
+ model = Question
+ fields = ['title', 'description', 'tags']
+
+
+class AnswerForm(forms.ModelForm):
+ question = forms.ModelChoiceField(widget=forms.HiddenInput(),
+ queryset=Question.objects.all())
+ description = forms.CharField(
+ widget=forms.Textarea(attrs={'class': 'form-control', 'rows': '4'}),
+ max_length=2000)
+
+ class Meta:
+ model = Answer
+ fields = ['question', 'description']
diff --git a/samples/development-frameworks/django/bootcamp/questions/migrations/0001_initial.py b/samples/development-frameworks/django/bootcamp/questions/migrations/0001_initial.py
new file mode 100644
index 00000000..7ab5b0e1
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/questions/migrations/0001_initial.py
@@ -0,0 +1,83 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.4 on 2016-03-18 13:32
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Answer',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('description', models.TextField(max_length=2000)),
+ ('create_date', models.DateTimeField(auto_now_add=True)),
+ ('update_date', models.DateTimeField(blank=True, null=True)),
+ ('votes', models.IntegerField(default=0)),
+ ('is_accepted', models.BooleanField(default=False)),
+ ],
+ options={
+ 'ordering': ('-is_accepted', '-votes', 'create_date'),
+ 'verbose_name': 'Answer',
+ 'verbose_name_plural': 'Answers',
+ },
+ ),
+ migrations.CreateModel(
+ name='Question',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=255)),
+ ('description', models.TextField(max_length=2000)),
+ ('create_date', models.DateTimeField(auto_now_add=True)),
+ ('update_date', models.DateTimeField(auto_now_add=True)),
+ ('favorites', models.IntegerField(default=0)),
+ ('has_accepted_answer', models.BooleanField(default=False)),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'ordering': ('-update_date',),
+ 'verbose_name': 'Question',
+ 'verbose_name_plural': 'Questions',
+ },
+ ),
+ migrations.CreateModel(
+ name='Tag',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('tag', models.CharField(max_length=50)),
+ ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='questions.Question')),
+ ],
+ options={
+ 'verbose_name': 'Tag',
+ 'verbose_name_plural': 'Tags',
+ },
+ ),
+ migrations.AddField(
+ model_name='answer',
+ name='question',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='questions.Question'),
+ ),
+ migrations.AddField(
+ model_name='answer',
+ name='user',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
+ ),
+ migrations.AlterUniqueTogether(
+ name='tag',
+ unique_together=set([('tag', 'question')]),
+ ),
+ migrations.AlterIndexTogether(
+ name='tag',
+ index_together=set([('tag', 'question')]),
+ ),
+ ]
diff --git a/samples/development-frameworks/django/bootcamp/questions/migrations/__init__.py b/samples/development-frameworks/django/bootcamp/questions/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/samples/development-frameworks/django/bootcamp/questions/models.py b/samples/development-frameworks/django/bootcamp/questions/models.py
new file mode 100644
index 00000000..3936c7aa
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/questions/models.py
@@ -0,0 +1,147 @@
+from django.db import models
+from django.contrib.auth.models import User
+from bootcamp.activities.models import Activity
+import markdown
+
+
+class Question(models.Model):
+ user = models.ForeignKey(User)
+ title = models.CharField(max_length=255)
+ description = models.TextField(max_length=2000)
+ create_date = models.DateTimeField(auto_now_add=True)
+ update_date = models.DateTimeField(auto_now_add=True)
+ favorites = models.IntegerField(default=0)
+ has_accepted_answer = models.BooleanField(default=False)
+
+ class Meta:
+ verbose_name = 'Question'
+ verbose_name_plural = 'Questions'
+ ordering = ('-update_date',)
+
+ def __unicode__(self):
+ return self.title
+
+ @staticmethod
+ def get_unanswered():
+ return Question.objects.filter(has_accepted_answer=False)
+
+ @staticmethod
+ def get_answered():
+ return Question.objects.filter(has_accepted_answer=True)
+
+ def get_answers(self):
+ return Answer.objects.filter(question=self)
+
+ def get_answers_count(self):
+ return Answer.objects.filter(question=self).count()
+
+ def get_accepted_answer(self):
+ return Answer.objects.get(question=self, is_accepted=True)
+
+ def get_description_as_markdown(self):
+ return markdown.markdown(self.description, safe_mode='escape')
+
+ def get_description_preview(self):
+ if len(self.description) > 255:
+ return u'{0}...'.format(self.description[:255])
+ else:
+ return self.description
+
+ def get_description_preview_as_markdown(self):
+ return markdown.markdown(self.get_description_preview(),
+ safe_mode='escape')
+
+ def calculate_favorites(self):
+ favorites = Activity.objects.filter(activity_type=Activity.FAVORITE,
+ question=self.pk).count()
+ self.favorites = favorites
+ self.save()
+ return self.favorites
+
+ def get_favoriters(self):
+ favorites = Activity.objects.filter(activity_type=Activity.FAVORITE,
+ question=self.pk)
+ favoriters = []
+ for favorite in favorites:
+ favoriters.append(favorite.user)
+ return favoriters
+
+ def create_tags(self, tags):
+ tags = tags.strip()
+ tag_list = tags.split(' ')
+ for tag in tag_list:
+ t, created = Tag.objects.get_or_create(tag=tag.lower(),
+ question=self)
+
+ def get_tags(self):
+ return Tag.objects.filter(question=self)
+
+
+class Answer(models.Model):
+ user = models.ForeignKey(User)
+ question = models.ForeignKey(Question)
+ description = models.TextField(max_length=2000)
+ create_date = models.DateTimeField(auto_now_add=True)
+ update_date = models.DateTimeField(null=True, blank=True)
+ votes = models.IntegerField(default=0)
+ is_accepted = models.BooleanField(default=False)
+
+ class Meta:
+ verbose_name = 'Answer'
+ verbose_name_plural = 'Answers'
+ ordering = ('-is_accepted', '-votes', 'create_date',)
+
+ def __unicode__(self):
+ return self.description
+
+ def accept(self):
+ answers = Answer.objects.filter(question=self.question)
+ for answer in answers:
+ answer.is_accepted = False
+ answer.save()
+ self.is_accepted = True
+ self.save()
+ self.question.has_accepted_answer = True
+ self.question.save()
+
+ def calculate_votes(self):
+ up_votes = Activity.objects.filter(activity_type=Activity.UP_VOTE,
+ answer=self.pk).count()
+ down_votes = Activity.objects.filter(activity_type=Activity.DOWN_VOTE,
+ answer=self.pk).count()
+ self.votes = up_votes - down_votes
+ self.save()
+ return self.votes
+
+ def get_up_voters(self):
+ votes = Activity.objects.filter(activity_type=Activity.UP_VOTE,
+ answer=self.pk)
+ voters = []
+ for vote in votes:
+ voters.append(vote.user)
+ return voters
+
+ def get_down_voters(self):
+ votes = Activity.objects.filter(activity_type=Activity.DOWN_VOTE,
+ answer=self.pk)
+ voters = []
+ for vote in votes:
+ voters.append(vote.user)
+ return voters
+
+ def get_description_as_markdown(self):
+ return markdown.markdown(self.description, safe_mode='escape')
+
+
+class Tag(models.Model):
+ tag = models.CharField(max_length=50)
+ question = models.ForeignKey(Question)
+
+ class Meta:
+ verbose_name = 'Tag'
+ verbose_name_plural = 'Tags'
+ unique_together = (('tag', 'question'),)
+ index_together = [['tag', 'question'], ]
+
+ def __unicode__(self):
+ return self.tag
diff --git a/samples/development-frameworks/django/bootcamp/questions/static/css/questions.css b/samples/development-frameworks/django/bootcamp/questions/static/css/questions.css
new file mode 100644
index 00000000..bce657cc
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/questions/static/css/questions.css
@@ -0,0 +1,125 @@
+.questions {
+ margin-top: 1em;
+}
+
+.questions .pagination {
+ margin: 0;
+}
+
+.questions .user {
+ width: 25px;
+ border-radius: 3px;
+}
+
+.questions .asked {
+ color: #aaaaaa;
+ font-size: .8em;
+}
+
+.questions .username {
+ margin-left: .3em;
+ font-weight: 500;
+ font-size: 0.8em;
+}
+
+.questions .panel-body:hover {
+ background-color: #f5f8fa;
+ cursor: pointer;
+}
+
+.questions .question-user {
+ margin-bottom: .6em;
+}
+
+.options {
+ text-align: center;
+
+}
+
+.options span {
+ display: block;
+ font-size: 1.2em;
+}
+
+.options span.favorite {
+ font-size: 3em;
+ margin-top: .4em;
+}
+
+.options span.vote {
+ font-size: 2em;
+ color: #ddd;
+}
+
+.options span.vote:hover {
+ cursor: pointer;
+ color: #428bca;
+}
+
+.options span.voted {
+ color: #333;
+}
+
+.options span.accept {
+ font-size: 2.6em;
+ color: #dddddd;
+ margin-top: .2em;
+}
+
+.options span.accept:hover {
+ cursor: pointer;
+ color: #5cb85c;
+}
+
+.options span.accepted {
+ color: #5cb85c;
+}
+
+.answer {
+ margin-top: 1em;
+}
+
+.answer .answer-user, .question .question-user {
+ margin-bottom: .6em;
+}
+
+.answer .answer-user .user, .question .question-user .user {
+ width: 40px;
+ border-radius: 5px;
+}
+
+.answer .answer-user .username, .question .question-user .username {
+ margin-left: .4em;
+}
+
+.answer .answered, .question .asked {
+ color: #aaaaaa;
+ margin-left: .6em;
+}
+
+.favorite {
+ cursor: pointer;
+}
+
+.favorite:hover {
+ color: #f0ad4e;
+}
+
+.favorited {
+ color: #f0ad4e;
+}
+
+.question-info {
+ text-align: center;
+ float: right;
+ padding: 0 1em;
+}
+
+.question-info h5 {
+ margin: 0;
+ margin-bottom: .2em;
+}
+
+.question-info .info:first-child {
+ margin-bottom: 1.2em;
+}
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/questions/static/js/questions.js b/samples/development-frameworks/django/bootcamp/questions/static/js/questions.js
new file mode 100644
index 00000000..7c08deb0
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/questions/static/js/questions.js
@@ -0,0 +1,93 @@
+$(function () {
+ $(".question .panel-body").click(function () {
+ var question_id = $(this).closest(".question").attr("question-id");
+ location.href = "/questions/" + question_id;
+ });
+
+ $(".accept").click(function () {
+ var span = $(this);
+ var question = $(".question").attr("question-id");
+ var answer = $(this).closest(".answer").attr("answer-id");
+ var csrf = $("input[name='csrfmiddlewaretoken']", $(this).closest(".answer")).val();
+ $.ajax({
+ url: '/questions/answer/accept/',
+ data: {
+ 'question': question,
+ 'answer': answer,
+ 'csrfmiddlewaretoken': csrf
+ },
+ type: 'post',
+ cache: false,
+ success: function (data) {
+ $(".accept").removeClass("accepted");
+ $(".accept").prop("title", "Click to accept the answer");
+ $(span).addClass("accepted");
+ $(span).prop("title", "Click to unaccept the answer");
+ }
+ });
+ });
+
+ $(".vote").click(function () {
+ var span = $(this);
+ var answer = $(this).closest(".answer").attr("answer-id");
+ var csrf = $("input[name='csrfmiddlewaretoken']", $(this).closest(".answer")).val();
+ var vote = "";
+ if ($(this).hasClass("voted")) {
+ var vote = "R";
+ }
+ else if ($(this).hasClass("up-vote")) {
+ vote = "U";
+ }
+ else if ($(this).hasClass("down-vote")) {
+ vote = "D";
+ }
+ $.ajax({
+ url: '/questions/answer/vote/',
+ data: {
+ 'answer': answer,
+ 'vote': vote,
+ 'csrfmiddlewaretoken': csrf
+ },
+ type: 'post',
+ cache: false,
+ success: function (data) {
+ var options = $(span).closest('.options');
+ $('.vote', options).removeClass('voted');
+ if (vote == 'U' || vote == 'D') {
+ $(span).addClass('voted');
+ }
+ $('.votes', options).text(data);
+ }
+ });
+ });
+
+ $(".favorite").click(function () {
+ var span = $(this);
+ var question = $(this).closest(".question").attr("question-id");
+ var csrf = $("input[name='csrfmiddlewaretoken']", $(this).closest(".question")).val();
+
+ $.ajax({
+ url: '/questions/favorite/',
+ data: {
+ 'question': question,
+ 'csrfmiddlewaretoken': csrf
+ },
+ type: 'post',
+ cache: false,
+ success: function (data) {
+ if ($(span).hasClass("favorited")) {
+ $(span).removeClass("glyphicon-star")
+ .removeClass("favorited")
+ .addClass("glyphicon-star-empty");
+ }
+ else {
+ $(span).removeClass("glyphicon-star-empty")
+ .addClass("glyphicon-star")
+ .addClass("favorited");
+ }
+ $(".favorite-count").text(data);
+ }
+ });
+
+ });
+});
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/questions/templates/questions/ask.html b/samples/development-frameworks/django/bootcamp/questions/templates/questions/ask.html
new file mode 100644
index 00000000..99ac48e7
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/questions/templates/questions/ask.html
@@ -0,0 +1,35 @@
+{% extends 'base.html' %}
+{% load i18n %}
+
+{% block main %}
+
+ {% trans 'Questions' %}
+ {% trans 'Ask Question' %}
+
+
+ {% csrf_token %}
+ {% for field in form.visible_fields %}
+
+ {% endfor %}
+
+
+{% endblock main %}
diff --git a/samples/development-frameworks/django/bootcamp/questions/templates/questions/partial_answer.html b/samples/development-frameworks/django/bootcamp/questions/templates/questions/partial_answer.html
new file mode 100644
index 00000000..4727fa8b
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/questions/templates/questions/partial_answer.html
@@ -0,0 +1,29 @@
+{% load i18n %}
+{% load humanize %}
+
+
+ {% csrf_token %}
+
+
+ {{ answer.votes }}
+
+ {% if answer.is_accepted and user == question.user %}
+
+ {% elif answer.is_accepted %}
+
+ {% elif user == question.user %}
+
+ {% endif %}
+
+
+
+
+ {{ answer.get_description_as_markdown|safe }}
+
+
+
+
diff --git a/samples/development-frameworks/django/bootcamp/questions/templates/questions/partial_question.html b/samples/development-frameworks/django/bootcamp/questions/templates/questions/partial_question.html
new file mode 100644
index 00000000..8bf514c5
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/questions/templates/questions/partial_question.html
@@ -0,0 +1,39 @@
+{% load humanize %}
+
+
+
+
+
+
+
Answers
+ {{ question.get_answers_count }}
+
+
+
Favorites
+ {{ question.favorites }}
+
+
+
+
+ {{ question.get_description_preview_as_markdown|safe }}
+
+ {% if question.get_tags %}
+
+ {% for tag in question.get_tags %}
+ {{ tag }}
+ {% endfor %}
+
+ {% endif %}
+
+
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/questions/templates/questions/question.html b/samples/development-frameworks/django/bootcamp/questions/templates/questions/question.html
new file mode 100644
index 00000000..262184d9
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/questions/templates/questions/question.html
@@ -0,0 +1,64 @@
+{% extends 'base.html' %}
+{% load staticfiles %}
+{% load i18n %}
+
+{% load humanize %}
+
+{% block head %}
+
+
+{% endblock head %}
+
+{% block main %}
+
+ {% trans "Questions" %}
+ {% trans "Question" %}
+
+
+ {% csrf_token %}
+
+ {% if user in question.get_favoriters %}
+
+ {% else %}
+
+ {% endif %}
+ {{ question.favorites }}
+
+
+
{{ question.title }}
+
+
+ {{ question.get_description_as_markdown|safe }}
+
+ {% if question.get_tag_list %}
+
+ {% for tag in question.get_tag_list %}
+ {{ tag }}
+ {% endfor %}
+
+ {% endif %}
+
+
+
+
+ {% for answer in question.get_answers %}
+ {% include 'questions/partial_answer.html' with question=question answer=answer %}
+ {% endfor %}
+
Your Answer
+
+ {% csrf_token %}
+ {{ form.question }}
+
+ {% include 'markdown_editor.html' with textarea='id_description' %}
+ {{ form.description }}
+
+
+ {% trans "Post Your Answer"%}
+
+
+
+{% endblock main %}
diff --git a/samples/development-frameworks/django/bootcamp/questions/templates/questions/questions.html b/samples/development-frameworks/django/bootcamp/questions/templates/questions/questions.html
new file mode 100644
index 00000000..e41c9a42
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/questions/templates/questions/questions.html
@@ -0,0 +1,32 @@
+{% extends 'base.html' %}
+{% load staticfiles %}
+{% load i18n %}
+
+{% block title %}Questions{% endblock %}
+
+{% block head %}
+
+
+{% endblock head %}
+
+{% block main %}
+
+
+
+ {% for question in questions %}
+ {% include 'questions/partial_question.html' with question=question %}
+ {% empty %}
+
{% trans "No question to display" %}
+ {% endfor %}
+ {% include 'paginator.html' with paginator=questions %}
+
+{% endblock main %}
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/questions/tests.py b/samples/development-frameworks/django/bootcamp/questions/tests.py
new file mode 100644
index 00000000..7ce503c2
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/questions/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/samples/development-frameworks/django/bootcamp/questions/urls.py b/samples/development-frameworks/django/bootcamp/questions/urls.py
new file mode 100644
index 00000000..38632d52
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/questions/urls.py
@@ -0,0 +1,18 @@
+# coding: utf-8
+
+from django.conf.urls import url
+
+from bootcamp.questions import views
+
+urlpatterns = [
+ url(r'^$', views.questions, name='questions'),
+ url(r'^answered/$', views.answered, name='answered'),
+ url(r'^unanswered/$', views.unanswered, name='unanswered'),
+ url(r'^all/$', views.all, name='all'),
+ url(r'^ask/$', views.ask, name='ask'),
+ url(r'^favorite/$', views.favorite, name='favorite'),
+ url(r'^answer/$', views.answer, name='answer'),
+ url(r'^answer/accept/$', views.accept, name='accept'),
+ url(r'^answer/vote/$', views.vote, name='vote'),
+ url(r'^(\d+)/$', views.question, name='question'),
+]
diff --git a/samples/development-frameworks/django/bootcamp/questions/views.py b/samples/development-frameworks/django/bootcamp/questions/views.py
new file mode 100644
index 00000000..95b18e21
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/questions/views.py
@@ -0,0 +1,164 @@
+from django.shortcuts import render, redirect, get_object_or_404
+from django.http import HttpResponse, HttpResponseForbidden
+from bootcamp.questions.models import Question, Answer
+from bootcamp.questions.forms import QuestionForm, AnswerForm
+from bootcamp.activities.models import Activity
+from django.db.models import Q
+from django.contrib.auth.decorators import login_required
+from bootcamp.decorators import ajax_required
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+
+
+@login_required
+def _questions(request, questions, active):
+ paginator = Paginator(questions, 10)
+ page = request.GET.get('page')
+ try:
+ questions = paginator.page(page)
+ except PageNotAnInteger:
+ questions = paginator.page(1)
+ except EmptyPage:
+ questions = paginator.page(paginator.num_pages)
+ return render(request, 'questions/questions.html', {
+ 'questions': questions,
+ 'active': active
+ })
+
+
+@login_required
+def questions(request):
+ return unanswered(request)
+
+
+@login_required
+def answered(request):
+ questions = Question.get_answered()
+ return _questions(request, questions, 'answered')
+
+
+@login_required
+def unanswered(request):
+ questions = Question.get_unanswered()
+ return _questions(request, questions, 'unanswered')
+
+
+@login_required
+def all(request):
+ questions = Question.objects.all()
+ return _questions(request, questions, 'all')
+
+
+@login_required
+def ask(request):
+ if request.method == 'POST':
+ form = QuestionForm(request.POST)
+ if form.is_valid():
+ question = Question()
+ question.user = request.user
+ question.title = form.cleaned_data.get('title')
+ question.description = form.cleaned_data.get('description')
+ question.save()
+ tags = form.cleaned_data.get('tags')
+ question.create_tags(tags)
+ return redirect('/questions/')
+
+ else:
+ return render(request, 'questions/ask.html', {'form': form})
+
+ else:
+ form = QuestionForm()
+
+ return render(request, 'questions/ask.html', {'form': form})
+
+
+@login_required
+def question(request, pk):
+ question = get_object_or_404(Question, pk=pk)
+ form = AnswerForm(initial={'question': question})
+ return render(request, 'questions/question.html', {
+ 'question': question,
+ 'form': form
+ })
+
+
+@login_required
+def answer(request):
+ if request.method == 'POST':
+ form = AnswerForm(request.POST)
+ if form.is_valid():
+ user = request.user
+ answer = Answer()
+ answer.user = request.user
+ answer.question = form.cleaned_data.get('question')
+ answer.description = form.cleaned_data.get('description')
+ answer.save()
+ user.profile.notify_answered(answer.question)
+ return redirect(u'/questions/{0}/'.format(answer.question.pk))
+ else:
+ question = form.cleaned_data.get('question')
+ return render(request, 'questions/question.html', {
+ 'question': question,
+ 'form': form
+ })
+ else:
+ return redirect('/questions/')
+
+
+@login_required
+@ajax_required
+def accept(request):
+ answer_id = request.POST['answer']
+ answer = Answer.objects.get(pk=answer_id)
+ user = request.user
+ try:
+ # answer.accept cleans previous accepted answer
+ user.profile.unotify_accepted(answer.question.get_accepted_answer())
+
+ except Exception, e:
+ pass
+
+ if answer.question.user == user:
+ answer.accept()
+ user.profile.notify_accepted(answer)
+ return HttpResponse()
+
+ else:
+ return HttpResponseForbidden()
+
+
+@login_required
+@ajax_required
+def vote(request):
+ answer_id = request.POST['answer']
+ answer = Answer.objects.get(pk=answer_id)
+ vote = request.POST['vote']
+ user = request.user
+ activity = Activity.objects.filter(
+ Q(activity_type=Activity.UP_VOTE) | Q(activity_type=Activity.DOWN_VOTE),
+ user=user, answer=answer_id)
+ if activity:
+ activity.delete()
+ if vote in [Activity.UP_VOTE, Activity.DOWN_VOTE]:
+ activity = Activity(activity_type=vote, user=user, answer=answer_id)
+ activity.save()
+ return HttpResponse(answer.calculate_votes())
+
+
+@login_required
+@ajax_required
+def favorite(request):
+ question_id = request.POST['question']
+ question = Question.objects.get(pk=question_id)
+ user = request.user
+ activity = Activity.objects.filter(activity_type=Activity.FAVORITE,
+ user=user, question=question_id)
+ if activity:
+ activity.delete()
+ user.profile.unotify_favorited(question)
+ else:
+ activity = Activity(activity_type=Activity.FAVORITE, user=user,
+ question=question_id)
+ activity.save()
+ user.profile.notify_favorited(question)
+
+ return HttpResponse(question.calculate_favorites())
diff --git a/samples/development-frameworks/django/bootcamp/search/__init__.py b/samples/development-frameworks/django/bootcamp/search/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/samples/development-frameworks/django/bootcamp/search/migrations/__init__.py b/samples/development-frameworks/django/bootcamp/search/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/samples/development-frameworks/django/bootcamp/search/models.py b/samples/development-frameworks/django/bootcamp/search/models.py
new file mode 100644
index 00000000..0f022274
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/search/models.py
@@ -0,0 +1 @@
+from django.db import models
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/search/static/css/search.css b/samples/development-frameworks/django/bootcamp/search/static/css/search.css
new file mode 100644
index 00000000..8800ee25
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/search/static/css/search.css
@@ -0,0 +1,91 @@
+.results ul {
+ padding: 0;
+}
+
+.results ul li {
+ list-style: none;
+ padding: .8em .4em;
+ border-bottom: 1px solid #eeeeee;
+}
+
+.results ul li:last-child {
+ border-bottom: none;
+}
+
+.results ul li img {
+ width: 45px;
+ border-radius: 5px;
+}
+
+.feed-results h3 {
+ margin: 0;
+ font-size: 1.2em;
+}
+
+.feed-results .post {
+ margin-left: 55px;
+ padding-top: .2em;
+}
+
+.feed-results .post p {
+ margin: 0;
+ margin-top: .2em;
+}
+
+[class$='-results'] li:hover {
+ cursor: pointer;
+ background-color: #f5f8fa;
+}
+
+.info {
+ margin-bottom: .5em;
+ color: #a0a0a0;
+}
+
+.info a {
+ color: #a0a0a0;
+}
+
+.info > span {
+ margin-right: 1em;
+}
+
+.info .user img {
+ width: 20px;
+ border-radius: 3px;
+}
+
+.no-result {
+ margin-top: 2em;
+}
+
+.result-user {
+ width: 20px;
+ border-radius: 4px;
+ margin-right: .2em;
+}
+
+.results {
+ margin-top: 1em;
+}
+
+.article-content ul,
+.article-content ol,
+.question-description ul,
+.question-description ol {
+ padding-left: 20px;
+}
+
+.article-content ul li,
+.question-description ul li {
+ border-bottom: none;
+ list-style: disc;
+ padding: 0;
+}
+
+.article-content ol li,
+.question-description ol li {
+ border-bottom: none;
+ list-style: decimal;
+ padding: 0;
+}
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/search/static/js/search.js b/samples/development-frameworks/django/bootcamp/search/static/js/search.js
new file mode 100644
index 00000000..8f7a5ab4
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/search/static/js/search.js
@@ -0,0 +1,17 @@
+$(function () {
+ $(".feed-results li").click(function () {
+ var feed = $(this).attr("feed-id");
+ location.href = "/feeds/" + feed + "/";
+ });
+
+ $(".articles-results li").click(function () {
+ var article = $(this).attr("article-slug");
+ location.href = "/articles/" + article + "/";
+ });
+
+ $(".questions-results li").click(function () {
+ var question = $(this).attr("question-id");
+ location.href = "/questions/" + question + "/";
+ });
+
+});
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/search/templates/search/partial_articles_results.html b/samples/development-frameworks/django/bootcamp/search/templates/search/partial_articles_results.html
new file mode 100644
index 00000000..5ee50b14
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/search/templates/search/partial_articles_results.html
@@ -0,0 +1,25 @@
+{% load i18n %}
+
+{% trans 'Articles' %}
+{% if results %}
+
+ {% for article in results %}
+
+
+
+ {{ article.get_summary_as_markdown|safe }}
+
+ {% endfor %}
+
+{% else %}
+ {% trans 'No article found' %} :(
+{% endif %}
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/search/templates/search/partial_feed_results.html b/samples/development-frameworks/django/bootcamp/search/templates/search/partial_feed_results.html
new file mode 100644
index 00000000..c29c7bad
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/search/templates/search/partial_feed_results.html
@@ -0,0 +1,24 @@
+{% load i18n %}
+{% load humanize %}
+
+{% trans 'Feed' %}
+{% if results %}
+
+ {% for feed in results %}
+
+
+
+
+
+
+
{{ feed.linkfy_post|safe }}
+
+
+ {% endfor %}
+
+{% else %}
+ {% trans 'No feed found' %} :(
+{% endif %}
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/search/templates/search/partial_questions_results.html b/samples/development-frameworks/django/bootcamp/search/templates/search/partial_questions_results.html
new file mode 100644
index 00000000..72619618
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/search/templates/search/partial_questions_results.html
@@ -0,0 +1,23 @@
+{% load i18n %}
+{% load humanize %}
+
+{% trans 'Questions' %}
+{% if results %}
+
+{% else %}
+ {% trans 'No question found' %} :(
+{% endif %}
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/search/templates/search/partial_results_menu.html b/samples/development-frameworks/django/bootcamp/search/templates/search/partial_results_menu.html
new file mode 100644
index 00000000..5bcb56a7
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/search/templates/search/partial_results_menu.html
@@ -0,0 +1,20 @@
+{% load i18n %}
+
+
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/search/templates/search/partial_users_results.html b/samples/development-frameworks/django/bootcamp/search/templates/search/partial_users_results.html
new file mode 100644
index 00000000..4a732b88
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/search/templates/search/partial_users_results.html
@@ -0,0 +1,20 @@
+{% load i18n %}
+
+{% trans 'Users' %}
+{% if results %}
+
+ {% for user_result in results %}
+
+
+
+ {% endfor %}
+
+{% else %}
+ {% trans 'No user found' %} :(
+{% endif %}
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/search/templates/search/results.html b/samples/development-frameworks/django/bootcamp/search/templates/search/results.html
new file mode 100644
index 00000000..d0daa53d
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/search/templates/search/results.html
@@ -0,0 +1,48 @@
+{% extends 'base.html' %}
+{% load staticfiles %}
+{% load i18n %}
+{% load humanize %}
+
+{% block title %} Search {% endblock %}
+
+{% block head %}
+
+
+{% endblock head %}
+
+{% block main %}
+
+
+
+ {% include 'search/partial_results_menu.html' with active=active count=count querystring=querystring %}
+
+
+ {% if active == 'feed' %}
+ {% include 'search/partial_feed_results.html' with results=results %}
+ {% elif active == 'articles' %}
+ {% include 'search/partial_articles_results.html' with results=results %}
+ {% elif active == 'questions' %}
+ {% include 'search/partial_questions_results.html' with results=results %}
+ {% elif active == 'users' %}
+ {% include 'search/partial_users_results.html' with results=results %}
+ {% endif %}
+
+
+{% endblock main %}
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/search/templates/search/search.html b/samples/development-frameworks/django/bootcamp/search/templates/search/search.html
new file mode 100644
index 00000000..9d5415a8
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/search/templates/search/search.html
@@ -0,0 +1,30 @@
+{% extends 'base.html' %}
+{% load staticfiles %}
+{% load i18n %}
+{% load humanize %}
+
+{% block title %} Search {% endblock %}
+
+{% block head %}
+
+{% endblock head %}
+
+{% block main %}
+
+
+
+
+
{% trans "Search Feed, Articles, Questions and Users" %}
+
+
+
+
+
+
+
+
+
+
+{% endblock main %}
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/search/tests.py b/samples/development-frameworks/django/bootcamp/search/tests.py
new file mode 100644
index 00000000..7ce503c2
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/search/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/samples/development-frameworks/django/bootcamp/search/views.py b/samples/development-frameworks/django/bootcamp/search/views.py
new file mode 100644
index 00000000..aa10e615
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/search/views.py
@@ -0,0 +1,52 @@
+from django.shortcuts import render, redirect
+from django.db.models import Q
+from django.contrib.auth.models import User
+from bootcamp.feeds.models import Feed
+from bootcamp.articles.models import Article
+from bootcamp.questions.models import Question
+from django.contrib.auth.decorators import login_required
+
+
+@login_required
+def search(request):
+ if 'q' in request.GET:
+ querystring = request.GET.get('q').strip()
+ if len(querystring) == 0:
+ return redirect('/search/')
+
+ try:
+ search_type = request.GET.get('type')
+ if search_type not in ['feed', 'articles', 'questions', 'users']:
+ search_type = 'feed'
+
+ except Exception, e:
+ search_type = 'feed'
+
+ count = {}
+ results = {}
+ results['feed'] = Feed.objects.filter(post__icontains=querystring,
+ parent=None)
+ results['articles'] = Article.objects.filter(
+ Q(title__icontains=querystring) | Q(content__icontains=querystring)
+ )
+ results['questions'] = Question.objects.filter(
+ Q(title__icontains=querystring) | Q(
+ description__icontains=querystring))
+ results['users'] = User.objects.filter(
+ Q(username__icontains=querystring) | Q(
+ first_name__icontains=querystring) | Q(
+ last_name__icontains=querystring))
+ count['feed'] = results['feed'].count()
+ count['articles'] = results['articles'].count()
+ count['questions'] = results['questions'].count()
+ count['users'] = results['users'].count()
+
+ return render(request, 'search/results.html', {
+ 'hide_search': True,
+ 'querystring': querystring,
+ 'active': search_type,
+ 'count': count,
+ 'results': results[search_type],
+ })
+ else:
+ return render(request, 'search/search.html', {'hide_search': True})
diff --git a/samples/development-frameworks/django/bootcamp/settings.py b/samples/development-frameworks/django/bootcamp/settings.py
new file mode 100644
index 00000000..f412c7a0
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/settings.py
@@ -0,0 +1,104 @@
+from unipath import Path
+from decouple import config
+import dj_database_url
+
+
+PROJECT_DIR = Path(__file__).parent
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/
+
+SECRET_KEY = config('SECRET_KEY')
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = config('DEBUG', default=False, cast=bool)
+TEMPLATE_DEBUG = DEBUG
+
+DATABASES = {
+ 'default': dj_database_url.config(default=config('DATABASE_URL'))
+}
+
+ALLOWED_HOSTS = ['*']
+
+# Application definition
+
+INSTALLED_APPS = (
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ 'django.contrib.humanize',
+
+ 'bootcamp.activities',
+ 'bootcamp.articles',
+ 'bootcamp.authentication',
+ 'bootcamp.core',
+ 'bootcamp.feeds',
+ 'bootcamp.messenger',
+ 'bootcamp.questions',
+ 'bootcamp.search',
+)
+
+MIDDLEWARE_CLASSES = (
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.middleware.locale.LocaleMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+)
+
+ROOT_URLCONF = 'bootcamp.urls'
+
+WSGI_APPLICATION = 'bootcamp.wsgi.application'
+
+# Internationalization
+# https://docs.djangoproject.com/en/1.6/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+LANGUAGES = (
+ ('en', 'English'),
+ ('pt-br', 'Portuguese'),
+ ('es', 'Spanish')
+)
+
+LOCALE_PATHS = (PROJECT_DIR.child('locale'), )
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/1.6/howto/static-files/
+
+STATIC_ROOT = PROJECT_DIR.parent.child('staticfiles')
+STATIC_URL = '/static/'
+
+STATICFILES_DIRS = (
+ PROJECT_DIR.child('static'),
+)
+
+STATICFILES_STORAGE = 'whitenoise.django.GzipManifestStaticFilesStorage'
+
+
+MEDIA_ROOT = PROJECT_DIR.parent.child('media')
+MEDIA_URL = '/media/'
+
+TEMPLATE_DIRS = (
+ PROJECT_DIR.child('templates'),
+)
+
+LOGIN_URL = '/'
+LOGIN_REDIRECT_URL = '/feeds/'
+
+ALLOWED_SIGNUP_DOMAINS = ['*']
+
+FILE_UPLOAD_TEMP_DIR = '/tmp/'
+FILE_UPLOAD_PERMISSIONS = 0o644
diff --git a/samples/development-frameworks/django/bootcamp/static/css/bootcamp.css b/samples/development-frameworks/django/bootcamp/static/css/bootcamp.css
new file mode 100644
index 00000000..eda6b450
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/static/css/bootcamp.css
@@ -0,0 +1,76 @@
+@import url(https://fonts.googleapis.com/css?family=Audiowide);
+
+body {
+ padding-top: 70px;
+}
+
+header .navbar-brand {
+ font-size: 1.4em;
+ font-weight: 200;
+ font-family: "Audiowide", cursive;
+}
+
+main .container {
+
+}
+
+.page-header {
+ margin: 0;
+}
+
+.page-header h1 {
+ margin: 0;
+ font-weight: 100;
+ font-size: 2em;
+}
+
+.no-data {
+ text-align: center;
+ padding: 1em 0;
+}
+
+#notifications {
+ font-size: 1.5em;
+ padding: 13px;
+ color: #dddddd;
+}
+
+#notifications.new-notifications {
+ color: #428bca;
+}
+
+.popover {
+ max-width: 350px;
+ width: 350px;
+}
+
+.popover ul {
+ padding: 0;
+ margin: 0;
+}
+
+.popover ul li {
+ list-style: none;
+ border-bottom: 1px solid #eeeeee;
+ padding: .4em 0;
+}
+
+.popover ul li:last-child {
+ border-bottom: none;
+}
+
+.popover ul li .user-picture {
+ width: 45px;
+ float: left;
+}
+
+.popover ul li p {
+ font-size: .9em;
+ padding: 0 0 0 .6em;
+ margin-left: 45px;
+ margin-bottom: 0;
+}
+
+.markdown {
+ margin-bottom: .8em;
+}
diff --git a/samples/development-frameworks/django/bootcamp/static/css/jquery.Jcrop.min.css b/samples/development-frameworks/django/bootcamp/static/css/jquery.Jcrop.min.css
new file mode 100644
index 00000000..ec9c5d66
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/static/css/jquery.Jcrop.min.css
@@ -0,0 +1,30 @@
+/* jquery.Jcrop.min.css v0.9.12 (build:20130126) */
+.jcrop-holder{direction:ltr;text-align:left;}
+.jcrop-vline,.jcrop-hline{background:#FFF url(../img/Jcrop.gif);font-size:0;position:absolute;}
+.jcrop-vline{height:100%;width:1px!important;}
+.jcrop-vline.right{right:0;}
+.jcrop-hline{height:1px!important;width:100%;}
+.jcrop-hline.bottom{bottom:0;}
+.jcrop-tracker{-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;-webkit-user-select:none;height:100%;width:100%;}
+.jcrop-handle{background-color:#333;border:1px #EEE solid;font-size:1px;height:7px;width:7px;}
+.jcrop-handle.ord-n{left:50%;margin-left:-4px;margin-top:-4px;top:0;}
+.jcrop-handle.ord-s{bottom:0;left:50%;margin-bottom:-4px;margin-left:-4px;}
+.jcrop-handle.ord-e{margin-right:-4px;margin-top:-4px;right:0;top:50%;}
+.jcrop-handle.ord-w{left:0;margin-left:-4px;margin-top:-4px;top:50%;}
+.jcrop-handle.ord-nw{left:0;margin-left:-4px;margin-top:-4px;top:0;}
+.jcrop-handle.ord-ne{margin-right:-4px;margin-top:-4px;right:0;top:0;}
+.jcrop-handle.ord-se{bottom:0;margin-bottom:-4px;margin-right:-4px;right:0;}
+.jcrop-handle.ord-sw{bottom:0;left:0;margin-bottom:-4px;margin-left:-4px;}
+.jcrop-dragbar.ord-n,.jcrop-dragbar.ord-s{height:7px;width:100%;}
+.jcrop-dragbar.ord-e,.jcrop-dragbar.ord-w{height:100%;width:7px;}
+.jcrop-dragbar.ord-n{margin-top:-4px;}
+.jcrop-dragbar.ord-s{bottom:0;margin-bottom:-4px;}
+.jcrop-dragbar.ord-e{margin-right:-4px;right:0;}
+.jcrop-dragbar.ord-w{margin-left:-4px;}
+.jcrop-light .jcrop-vline,.jcrop-light .jcrop-hline{background:#FFF;filter:alpha(opacity=70)!important;opacity:.70!important;}
+.jcrop-light .jcrop-handle{-moz-border-radius:3px;-webkit-border-radius:3px;background-color:#000;border-color:#FFF;border-radius:3px;}
+.jcrop-dark .jcrop-vline,.jcrop-dark .jcrop-hline{background:#000;filter:alpha(opacity=70)!important;opacity:.7!important;}
+.jcrop-dark .jcrop-handle{-moz-border-radius:3px;-webkit-border-radius:3px;background-color:#FFF;border-color:#000;border-radius:3px;}
+.solid-line .jcrop-vline,.solid-line .jcrop-hline{background:#FFF;}
+.jcrop-holder img,img.jcrop-preview{max-width:none;}
+.jcrop-keymgr{display:none!important;}
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/static/img/Jcrop.gif b/samples/development-frameworks/django/bootcamp/static/img/Jcrop.gif
new file mode 100644
index 00000000..72ea7ccb
Binary files /dev/null and b/samples/development-frameworks/django/bootcamp/static/img/Jcrop.gif differ
diff --git a/samples/development-frameworks/django/bootcamp/static/img/favicon.png b/samples/development-frameworks/django/bootcamp/static/img/favicon.png
new file mode 100644
index 00000000..1537675f
Binary files /dev/null and b/samples/development-frameworks/django/bootcamp/static/img/favicon.png differ
diff --git a/samples/development-frameworks/django/bootcamp/static/img/loading.gif b/samples/development-frameworks/django/bootcamp/static/img/loading.gif
new file mode 100644
index 00000000..ec59bde7
Binary files /dev/null and b/samples/development-frameworks/django/bootcamp/static/img/loading.gif differ
diff --git a/samples/development-frameworks/django/bootcamp/static/img/user.png b/samples/development-frameworks/django/bootcamp/static/img/user.png
new file mode 100644
index 00000000..c7d631ec
Binary files /dev/null and b/samples/development-frameworks/django/bootcamp/static/img/user.png differ
diff --git a/samples/development-frameworks/django/bootcamp/static/js/bootcamp.js b/samples/development-frameworks/django/bootcamp/static/js/bootcamp.js
new file mode 100644
index 00000000..5a1431e5
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/static/js/bootcamp.js
@@ -0,0 +1,13 @@
+$(function () {
+ $.fn.count = function (limit) {
+ var length = limit - $(this).val().length;
+ var form = $(this).closest("form");
+ if (length <= 0) {
+ $(".form-group", form).addClass("has-error");
+ }
+ else {
+ $(".form-group", form).removeClass("has-error");
+ }
+ $(".help-count", form).text(length);
+ };
+});
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/static/js/bootcamp.markdown.js b/samples/development-frameworks/django/bootcamp/static/js/bootcamp.markdown.js
new file mode 100644
index 00000000..5b01b6f3
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/static/js/bootcamp.markdown.js
@@ -0,0 +1,87 @@
+$(function () {
+
+ $.fn.markdown = function () {
+ var _textarea = $(this);
+
+ $(".markdown .btn-group button").click(function (e) {
+ e.preventDefault();
+ var action = $(this).attr("ref");
+ var selection = $(_textarea).selection();
+
+ switch (action) {
+ case "header":
+ $(_textarea).selection("replace", {text: "# " + selection});
+ break;
+ case "bold":
+ $(_textarea).selection("replace", {text: "**" + selection + "**"});
+ break;
+ case "italic":
+ $(_textarea).selection("replace", {text: "_" + selection + "_"});
+ break;
+ case "list":
+ var selection_list = selection.split("\n");
+ var selection_list_result = "";
+ for (var i = 0 ; i < selection_list.length ; i++) {
+ selection_list_result += "* " + selection_list[i] + "\n";
+ };
+ if (selection_list_result.length > 0) {
+ selection_list_result = selection_list_result.substring(0, selection_list_result.length - 1);
+ }
+ $(_textarea).selection("replace", {text: selection_list_result});
+ break;
+ case "link":
+ $("#markdown_link_text").val("");
+ $("#markdown_url").val("");
+ $("#markdown_insert_link").modal("show");
+ break;
+ case "picture":
+ $("#markdown_picture_url").val("");
+ $("#markdown_alt_text").val("");
+ $("#markdown_insert_picture").modal("show");
+ break;
+ case "indent-left":
+ var selection_list = selection.split("\n");
+ var selection_list_result = "";
+ for (var i = 0; i < selection_list.length; i++) {
+ selection_list_result += " " + selection_list[i] + "\n";
+ };
+ if (selection_list_result.length > 0) {
+ selection_list_result = selection_list_result.substring(0, selection_list_result.length - 1);
+ }
+ $(_textarea).selection("replace", {text: selection_list_result});
+ break;
+ case "indent-right":
+ var selection_list = selection.split("\n");
+ var selection_list_result = "";
+ for (var i = 0; i < selection_list.length ; i++) {
+ selection_list_result += selection_list[i].trim() + "\n";
+ };
+ if (selection_list_result.length > 0) {
+ selection_list_result = selection_list_result.substring(0, selection_list_result.length - 1);
+ }
+ $(_textarea).selection("replace", {text: selection_list_result});
+ break;
+ };
+
+ });
+
+ $(".add_link").click(function () {
+ var selection = $(_textarea).selection();
+ var text = $("#markdown_link_text").val();
+ var url = $("#markdown_url").val();
+ var link = "[" + text + "](" + url + ")";
+ $(_textarea).selection("replace", {text: link});
+ $("#markdown_insert_link").modal("hide")
+ });
+
+ $(".add_picture").click(function () {
+ var selection = $(_textarea).selection();
+ var url = $("#markdown_picture_url").val();
+ var alt = $("#markdown_alt_text").val();
+ var picture = "";
+ $(_textarea).selection("replace", {text: picture});
+ $("#markdown_insert_picture").modal("hide")
+ });
+
+ };
+});
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/static/js/ga.js b/samples/development-frameworks/django/bootcamp/static/js/ga.js
new file mode 100644
index 00000000..e2064f4c
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/static/js/ga.js
@@ -0,0 +1,7 @@
+ (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
+ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
+ m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
+ })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
+
+ ga('create', 'UA-42049022-5', 'vitorfs.com');
+ ga('send', 'pageview');
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/static/js/jquery.Jcrop.min.js b/samples/development-frameworks/django/bootcamp/static/js/jquery.Jcrop.min.js
new file mode 100644
index 00000000..4c9c7adb
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/static/js/jquery.Jcrop.min.js
@@ -0,0 +1,22 @@
+/**
+ * jquery.Jcrop.min.js v0.9.12 (build:20130202)
+ * jQuery Image Cropping Plugin - released under MIT License
+ * Copyright (c) 2008-2013 Tapmodo Interactive LLC
+ * https://github.com/tapmodo/Jcrop
+ */
+(function(a){a.Jcrop=function(b,c){function i(a){return Math.round(a)+"px"}function j(a){return d.baseClass+"-"+a}function k(){return a.fx.step.hasOwnProperty("backgroundColor")}function l(b){var c=a(b).offset();return[c.left,c.top]}function m(a){return[a.pageX-e[0],a.pageY-e[1]]}function n(b){typeof b!="object"&&(b={}),d=a.extend(d,b),a.each(["onChange","onSelect","onRelease","onDblClick"],function(a,b){typeof d[b]!="function"&&(d[b]=function(){})})}function o(a,b,c){e=l(D),bc.setCursor(a==="move"?a:a+"-resize");if(a==="move")return bc.activateHandlers(q(b),v,c);var d=_.getFixed(),f=r(a),g=_.getCorner(r(f));_.setPressed(_.getCorner(f)),_.setCurrent(g),bc.activateHandlers(p(a,d),v,c)}function p(a,b){return function(c){if(!d.aspectRatio)switch(a){case"e":c[1]=b.y2;break;case"w":c[1]=b.y2;break;case"n":c[0]=b.x2;break;case"s":c[0]=b.x2}else switch(a){case"e":c[1]=b.y+1;break;case"w":c[1]=b.y+1;break;case"n":c[0]=b.x+1;break;case"s":c[0]=b.x+1}_.setCurrent(c),bb.update()}}function q(a){var b=a;return bd.watchKeys
+(),function(a){_.moveOffset([a[0]-b[0],a[1]-b[1]]),b=a,bb.update()}}function r(a){switch(a){case"n":return"sw";case"s":return"nw";case"e":return"nw";case"w":return"ne";case"ne":return"sw";case"nw":return"se";case"se":return"nw";case"sw":return"ne"}}function s(a){return function(b){return d.disabled?!1:a==="move"&&!d.allowMove?!1:(e=l(D),W=!0,o(a,m(b)),b.stopPropagation(),b.preventDefault(),!1)}}function t(a,b,c){var d=a.width(),e=a.height();d>b&&b>0&&(d=b,e=b/a.width()*a.height()),e>c&&c>0&&(e=c,d=c/a.height()*a.width()),T=a.width()/d,U=a.height()/e,a.width(d).height(e)}function u(a){return{x:a.x*T,y:a.y*U,x2:a.x2*T,y2:a.y2*U,w:a.w*T,h:a.h*U}}function v(a){var b=_.getFixed();b.w>d.minSelect[0]&&b.h>d.minSelect[1]?(bb.enableHandles(),bb.done()):bb.release(),bc.setCursor(d.allowSelect?"crosshair":"default")}function w(a){if(d.disabled)return!1;if(!d.allowSelect)return!1;W=!0,e=l(D),bb.disableHandles(),bc.setCursor("crosshair");var b=m(a);return _.setPressed(b),bb.update(),bc.activateHandlers(x,v,a.type.substring
+(0,5)==="touch"),bd.watchKeys(),a.stopPropagation(),a.preventDefault(),!1}function x(a){_.setCurrent(a),bb.update()}function y(){var b=a("
").addClass(j("tracker"));return g&&b.css({opacity:0,backgroundColor:"white"}),b}function be(a){G.removeClass().addClass(j("holder")).addClass(a)}function bf(a,b){function t(){window.setTimeout(u,l)}var c=a[0]/T,e=a[1]/U,f=a[2]/T,g=a[3]/U;if(X)return;var h=_.flipCoords(c,e,f,g),i=_.getFixed(),j=[i.x,i.y,i.x2,i.y2],k=j,l=d.animationDelay,m=h[0]-j[0],n=h[1]-j[1],o=h[2]-j[2],p=h[3]-j[3],q=0,r=d.swingSpeed;c=k[0],e=k[1],f=k[2],g=k[3],bb.animMode(!0);var s,u=function(){return function(){q+=(100-q)/r,k[0]=Math.round(c+q/100*m),k[1]=Math.round(e+q/100*n),k[2]=Math.round(f+q/100*o),k[3]=Math.round(g+q/100*p),q>=99.8&&(q=100),q<100?(bh(k),t()):(bb.done(),bb.animMode(!1),typeof b=="function"&&b.call(bs))}}();t()}function bg(a){bh([a[0]/T,a[1]/U,a[2]/T,a[3]/U]),d.onSelect.call(bs,u(_.getFixed())),bb.enableHandles()}function bh(a){_.setPressed([a[0],a[1]]),_.setCurrent([a[2],
+a[3]]),bb.update()}function bi(){return u(_.getFixed())}function bj(){return _.getFixed()}function bk(a){n(a),br()}function bl(){d.disabled=!0,bb.disableHandles(),bb.setCursor("default"),bc.setCursor("default")}function bm(){d.disabled=!1,br()}function bn(){bb.done(),bc.activateHandlers(null,null)}function bo(){G.remove(),A.show(),A.css("visibility","visible"),a(b).removeData("Jcrop")}function bp(a,b){bb.release(),bl();var c=new Image;c.onload=function(){var e=c.width,f=c.height,g=d.boxWidth,h=d.boxHeight;D.width(e).height(f),D.attr("src",a),H.attr("src",a),t(D,g,h),E=D.width(),F=D.height(),H.width(E).height(F),M.width(E+L*2).height(F+L*2),G.width(E).height(F),ba.resize(E,F),bm(),typeof b=="function"&&b.call(bs)},c.src=a}function bq(a,b,c){var e=b||d.bgColor;d.bgFade&&k()&&d.fadeTime&&!c?a.animate({backgroundColor:e},{queue:!1,duration:d.fadeTime}):a.css("backgroundColor",e)}function br(a){d.allowResize?a?bb.enableOnly():bb.enableHandles():bb.disableHandles(),bc.setCursor(d.allowSelect?"crosshair":"default"),bb
+.setCursor(d.allowMove?"move":"default"),d.hasOwnProperty("trueSize")&&(T=d.trueSize[0]/E,U=d.trueSize[1]/F),d.hasOwnProperty("setSelect")&&(bg(d.setSelect),bb.done(),delete d.setSelect),ba.refresh(),d.bgColor!=N&&(bq(d.shade?ba.getShades():G,d.shade?d.shadeColor||d.bgColor:d.bgColor),N=d.bgColor),O!=d.bgOpacity&&(O=d.bgOpacity,d.shade?ba.refresh():bb.setBgOpacity(O)),P=d.maxSize[0]||0,Q=d.maxSize[1]||0,R=d.minSize[0]||0,S=d.minSize[1]||0,d.hasOwnProperty("outerImage")&&(D.attr("src",d.outerImage),delete d.outerImage),bb.refresh()}var d=a.extend({},a.Jcrop.defaults),e,f=navigator.userAgent.toLowerCase(),g=/msie/.test(f),h=/msie [1-6]\./.test(f);typeof b!="object"&&(b=a(b)[0]),typeof c!="object"&&(c={}),n(c);var z={border:"none",visibility:"visible",margin:0,padding:0,position:"absolute",top:0,left:0},A=a(b),B=!0;if(b.tagName=="IMG"){if(A[0].width!=0&&A[0].height!=0)A.width(A[0].width),A.height(A[0].height);else{var C=new Image;C.src=A[0].src,A.width(C.width),A.height(C.height)}var D=A.clone().removeAttr("id").
+css(z).show();D.width(A.width()),D.height(A.height()),A.after(D).hide()}else D=A.css(z).show(),B=!1,d.shade===null&&(d.shade=!0);t(D,d.boxWidth,d.boxHeight);var E=D.width(),F=D.height(),G=a("
").width(E).height(F).addClass(j("holder")).css({position:"relative",backgroundColor:d.bgColor}).insertAfter(A).append(D);d.addClass&&G.addClass(d.addClass);var H=a("
"),I=a("
").width("100%").height("100%").css({zIndex:310,position:"absolute",overflow:"hidden"}),J=a("
").width("100%").height("100%").css("zIndex",320),K=a("
").css({position:"absolute",zIndex:600}).dblclick(function(){var a=_.getFixed();d.onDblClick.call(bs,a)}).insertBefore(D).append(I,J);B&&(H=a(" ").attr("src",D.attr("src")).css(z).width(E).height(F),I.append(H)),h&&K.css({overflowY:"hidden"});var L=d.boundary,M=y().width(E+L*2).height(F+L*2).css({position:"absolute",top:i(-L),left:i(-L),zIndex:290}).mousedown(w),N=d.bgColor,O=d.bgOpacity,P,Q,R,S,T,U,V=!0,W,X,Y;e=l(D);var Z=function(){function a(){var a={},b=["touchstart"
+,"touchmove","touchend"],c=document.createElement("div"),d;try{for(d=0;da+f&&(f-=f+a),0>b+g&&(g-=g+b),FE&&(r=E,u=Math.abs((r-a)/f),s=k<0?b-u:u+b)):(r=c,u=l/f,s=k<0?b-u:b+u,s<0?(s=0,t=Math.abs((s-b)*f),r=j<0?a-t:t+a):s>F&&(s=F,t=Math.abs(s-b)*f,r=j<0?a-t:t+a)),r>a?(r-ah&&(r=a+h),s>b?s=b+(r-a)/f:s=b-(r-a)/f):rh&&(r=a-h),s>b?s=b+(a-r)/f:s=b-(a-r)/f),r<0?(a-=r,r=0):r>E&&(a-=r-E,r=E),s<0?(b-=s,s=0):s>F&&(b-=s-F,s=F),q(o(a,b,r,s))}function n(a){return a[0]<0&&(a[0]=0),a[1]<0&&(a[1]=0),a[0]>E&&(a[0]=E),a[1]>F&&(a[1]=F),[Math.round(a[0]),Math.round(a[1])]}function o(a,b,c,d){var e=a,f=c,g=b,h=d;return c P&&(c=d>0?a+P:a-P),Q&&Math.abs
+(f)>Q&&(e=f>0?b+Q:b-Q),S/U&&Math.abs(f)0?b+S/U:b-S/U),R/T&&Math.abs(d)0?a+R/T:a-R/T),a<0&&(c-=a,a-=a),b<0&&(e-=b,b-=b),c<0&&(a-=c,c-=c),e<0&&(b-=e,e-=e),c>E&&(g=c-E,a-=g,c-=g),e>F&&(g=e-F,b-=g,e-=g),a>E&&(g=a-F,e-=g,b-=g),b>F&&(g=b-F,e-=g,b-=g),q(o(a,b,c,e))}function q(a){return{x:a[0],y:a[1],x2:a[2],y2:a[3],w:a[2]-a[0],h:a[3]-a[1]}}var a=0,b=0,c=0,e=0,f,g;return{flipCoords:o,setPressed:h,setCurrent:i,getOffset:j,moveOffset:k,getCorner:l,getFixed:m}}(),ba=function(){function f(a,b){e.left.css({height:i(b)}),e.right.css({height:i(b)})}function g(){return h(_.getFixed())}function h(a){e.top.css({left:i(a.x),width:i(a.w),height:i(a.y)}),e.bottom.css({top:i(a.y2),left:i(a.x),width:i(a.w),height:i(F-a.y2)}),e.right.css({left:i(a.x2),width:i(E-a.x2)}),e.left.css({width:i(a.x)})}function j(){return a("
").css({position:"absolute",backgroundColor:d.shadeColor||d.bgColor}).appendTo(c)}function k(){b||(b=!0,c.insertBefore(D),g(),bb.setBgOpacity(1,0,1),H.hide(),l(d.shadeColor||d.bgColor,1),bb.
+isAwake()?n(d.bgOpacity,1):n(1,1))}function l(a,b){bq(p(),a,b)}function m(){b&&(c.remove(),H.show(),b=!1,bb.isAwake()?bb.setBgOpacity(d.bgOpacity,1,1):(bb.setBgOpacity(1,1,1),bb.disableHandles()),bq(G,0,1))}function n(a,e){b&&(d.bgFade&&!e?c.animate({opacity:1-a},{queue:!1,duration:d.fadeTime}):c.css({opacity:1-a}))}function o(){d.shade?k():m(),bb.isAwake()&&n(d.bgOpacity)}function p(){return c.children()}var b=!1,c=a("
").css({position:"absolute",zIndex:240,opacity:0}),e={top:j(),left:j().height(F),right:j().height(F),bottom:j()};return{update:g,updateRaw:h,getShades:p,setBgColor:l,enable:k,disable:m,resize:f,refresh:o,opacity:n}}(),bb=function(){function k(b){var c=a("
").css({position:"absolute",opacity:d.borderOpacity}).addClass(j(b));return I.append(c),c}function l(b,c){var d=a("
").mousedown(s(b)).css({cursor:b+"-resize",position:"absolute",zIndex:c}).addClass("ord-"+b);return Z.support&&d.bind("touchstart.jcrop",Z.createDragger(b)),J.append(d),d}function m(a){var b=d.handleSize,e=l(a,c++
+).css({opacity:d.handleOpacity}).addClass(j("handle"));return b&&e.width(b).height(b),e}function n(a){return l(a,c++).addClass("jcrop-dragbar")}function o(a){var b;for(b=0;b ').css({position:"fixed",left:"-120px",width:"12px"}).addClass("jcrop-keymgr"),c=a("
").css({position:"absolute",overflow:"hidden"}).append(b);return d.keySupport&&(b.keydown(i).blur(f),h||!d.fixedSupport?(b.css({position:"absolute",left:"-20px"}),c.append(b).insertBefore(D)):b.insertBefore(D)),{watchKeys:e}}();Z.support&&M.bind("touchstart.jcrop",Z.newSelection),J.hide(),br(!0);var bs={setImage:bp,animateTo:bf,setSelect:bg,setOptions:bk,tellSelect:bi,tellScaled:bj,setClass:be,disable:bl,enable:bm,cancel:bn,release:bb.release,destroy:bo,focus:bd.watchKeys,getBounds:function(){return[E*T,F*U]},getWidgetSize:function(){return[E,F]},getScaleFactor:function(){return[T,U]},getOptions:function(){return d},ui:{holder:G,selection:K}};return g&&G.bind("selectstart",function(){return!1}),A.data("Jcrop",bs),bs},a.fn.Jcrop=function(b,c){var d;return this.each(function(){if(a(this).data("Jcrop")){if(
+b==="api")return a(this).data("Jcrop");a(this).data("Jcrop").setOptions(b)}else this.tagName=="IMG"?a.Jcrop.Loader(this,function(){a(this).css({display:"block",visibility:"hidden"}),d=a.Jcrop(this,b),a.isFunction(c)&&c.call(d)}):(a(this).css({display:"block",visibility:"hidden"}),d=a.Jcrop(this,b),a.isFunction(c)&&c.call(d))}),this},a.Jcrop.Loader=function(b,c,d){function g(){f.complete?(e.unbind(".jcloader"),a.isFunction(c)&&c.call(f)):window.setTimeout(g,50)}var e=a(b),f=e[0];e.bind("load.jcloader",g).bind("error.jcloader",function(b){e.unbind(".jcloader"),a.isFunction(d)&&d.call(f)}),f.complete&&a.isFunction(c)&&(e.unbind(".jcloader"),c.call(f))},a.Jcrop.defaults={allowSelect:!0,allowMove:!0,allowResize:!0,trackDocument:!0,baseClass:"jcrop",addClass:null,bgColor:"black",bgOpacity:.6,bgFade:!1,borderOpacity:.4,handleOpacity:.5,handleSize:null,aspectRatio:0,keySupport:!0,createHandles:["n","s","e","w","nw","ne","se","sw"],createDragbars:["n","s","e","w"],createBorders:["n","s","e","w"],drawBorders:!0,dragEdges
+:!0,fixedSupport:!0,touchSupport:null,shade:null,boxWidth:0,boxHeight:0,boundary:2,fadeTime:400,animationDelay:20,swingSpeed:3,minSelect:[0,0],maxSize:[0,0],minSize:[0,0],onChange:function(){},onSelect:function(){},onDblClick:function(){},onRelease:function(){}}})(jQuery);
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/static/js/jquery.bullseye-1.0-min.js b/samples/development-frameworks/django/bootcamp/static/js/jquery.bullseye-1.0-min.js
new file mode 100644
index 00000000..22f7e2c8
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/static/js/jquery.bullseye-1.0-min.js
@@ -0,0 +1,10 @@
+/*!
+* jQuery Bullseye v1.0
+* http://pixeltango.com
+*
+* Copyright 2010, Mickel Andersson
+* Dual licensed under the MIT or GPL Version 2 licenses.
+*
+* Date: Fri Aug 31 19:09:11 2010 +0100
+*/
+jQuery.fn.bullseye = function (b, h) { b = jQuery.extend({ offsetTop: 0, offsetHeight: 0, extendDown: false }, b); return this.each(function () { var a = $(this), c = $(h == null ? window : h), g = function () { var d = a.outerWidth(), e = a.outerHeight() + b.offsetHeight; c.width(); var f = c.height(), i = c.scrollTop(), j = c.scrollLeft() + d; f = i + f; var k = a.offset().left; d = k + d; var l = a.offset().top + b.offsetTop; e = l + e; if (f < l || (b.extendDown ? false : i > e) || j < k || j > d) { if (a.data("is-focused")) { a.data("is-focused", false); a.trigger("leaveviewport") } } else if (!a.data("is-focused")) { a.data("is-focused", true); a.trigger("enterviewport") } }; c.scroll(g).resize(g); g() }) };
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/static/js/jquery.selection.js b/samples/development-frameworks/django/bootcamp/static/js/jquery.selection.js
new file mode 100644
index 00000000..5886074e
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/static/js/jquery.selection.js
@@ -0,0 +1,354 @@
+/*!
+ * jQuery.selection - jQuery Plugin
+ *
+ * Copyright (c) 2010-2014 IWASAKI Koji (@madapaja).
+ * http://blog.madapaja.net/
+ * Under The MIT License
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+(function($, win, doc) {
+ /**
+ * get caret status of the selection of the element
+ *
+ * @param {Element} element target DOM element
+ * @return {Object} return
+ * @return {String} return.text selected text
+ * @return {Number} return.start start position of the selection
+ * @return {Number} return.end end position of the selection
+ */
+ var _getCaretInfo = function(element){
+ var res = {
+ text: '',
+ start: 0,
+ end: 0
+ };
+
+ if (!element.value) {
+ /* no value or empty string */
+ return res;
+ }
+
+ try {
+ if (win.getSelection) {
+ /* except IE */
+ res.start = element.selectionStart;
+ res.end = element.selectionEnd;
+ res.text = element.value.slice(res.start, res.end);
+ } else if (doc.selection) {
+ /* for IE */
+ element.focus();
+
+ var range = doc.selection.createRange(),
+ range2 = doc.body.createTextRange();
+
+ res.text = range.text;
+
+ try {
+ range2.moveToElementText(element);
+ range2.setEndPoint('StartToStart', range);
+ } catch (e) {
+ range2 = element.createTextRange();
+ range2.setEndPoint('StartToStart', range);
+ }
+
+ res.start = element.value.length - range2.text.length;
+ res.end = res.start + range.text.length;
+ }
+ } catch (e) {
+ /* give up */
+ }
+
+ return res;
+ };
+
+ /**
+ * caret operation for the element
+ * @type {Object}
+ */
+ var _CaretOperation = {
+ /**
+ * get caret position
+ *
+ * @param {Element} element target element
+ * @return {Object} return
+ * @return {Number} return.start start position for the selection
+ * @return {Number} return.end end position for the selection
+ */
+ getPos: function(element) {
+ var tmp = _getCaretInfo(element);
+ return {start: tmp.start, end: tmp.end};
+ },
+
+ /**
+ * set caret position
+ *
+ * @param {Element} element target element
+ * @param {Object} toRange caret position
+ * @param {Number} toRange.start start position for the selection
+ * @param {Number} toRange.end end position for the selection
+ * @param {String} caret caret mode: any of the following: "keep" | "start" | "end"
+ */
+ setPos: function(element, toRange, caret) {
+ caret = this._caretMode(caret);
+
+ if (caret === 'start') {
+ toRange.end = toRange.start;
+ } else if (caret === 'end') {
+ toRange.start = toRange.end;
+ }
+
+ element.focus();
+ try {
+ if (element.createTextRange) {
+ var range = element.createTextRange();
+
+ if (win.navigator.userAgent.toLowerCase().indexOf("msie") >= 0) {
+ toRange.start = element.value.substr(0, toRange.start).replace(/\r/g, '').length;
+ toRange.end = element.value.substr(0, toRange.end).replace(/\r/g, '').length;
+ }
+
+ range.collapse(true);
+ range.moveStart('character', toRange.start);
+ range.moveEnd('character', toRange.end - toRange.start);
+
+ range.select();
+ } else if (element.setSelectionRange) {
+ element.setSelectionRange(toRange.start, toRange.end);
+ }
+ } catch (e) {
+ /* give up */
+ }
+ },
+
+ /**
+ * get selected text
+ *
+ * @param {Element} element target element
+ * @return {String} return selected text
+ */
+ getText: function(element) {
+ return _getCaretInfo(element).text;
+ },
+
+ /**
+ * get caret mode
+ *
+ * @param {String} caret caret mode
+ * @return {String} return any of the following: "keep" | "start" | "end"
+ */
+ _caretMode: function(caret) {
+ caret = caret || "keep";
+ if (caret === false) {
+ caret = 'end';
+ }
+
+ switch (caret) {
+ case 'keep':
+ case 'start':
+ case 'end':
+ break;
+
+ default:
+ caret = 'keep';
+ }
+
+ return caret;
+ },
+
+ /**
+ * replace selected text
+ *
+ * @param {Element} element target element
+ * @param {String} text replacement text
+ * @param {String} caret caret mode: any of the following: "keep" | "start" | "end"
+ */
+ replace: function(element, text, caret) {
+ var tmp = _getCaretInfo(element),
+ orig = element.value,
+ pos = $(element).scrollTop(),
+ range = {start: tmp.start, end: tmp.start + text.length};
+
+ element.value = orig.substr(0, tmp.start) + text + orig.substr(tmp.end);
+
+ $(element).scrollTop(pos);
+ this.setPos(element, range, caret);
+ },
+
+ /**
+ * insert before the selected text
+ *
+ * @param {Element} element target element
+ * @param {String} text insertion text
+ * @param {String} caret caret mode: any of the following: "keep" | "start" | "end"
+ */
+ insertBefore: function(element, text, caret) {
+ var tmp = _getCaretInfo(element),
+ orig = element.value,
+ pos = $(element).scrollTop(),
+ range = {start: tmp.start + text.length, end: tmp.end + text.length};
+
+ element.value = orig.substr(0, tmp.start) + text + orig.substr(tmp.start);
+
+ $(element).scrollTop(pos);
+ this.setPos(element, range, caret);
+ },
+
+ /**
+ * insert after the selected text
+ *
+ * @param {Element} element target element
+ * @param {String} text insertion text
+ * @param {String} caret caret mode: any of the following: "keep" | "start" | "end"
+ */
+ insertAfter: function(element, text, caret) {
+ var tmp = _getCaretInfo(element),
+ orig = element.value,
+ pos = $(element).scrollTop(),
+ range = {start: tmp.start, end: tmp.end};
+
+ element.value = orig.substr(0, tmp.end) + text + orig.substr(tmp.end);
+
+ $(element).scrollTop(pos);
+ this.setPos(element, range, caret);
+ }
+ };
+
+ /* add jQuery.selection */
+ $.extend({
+ /**
+ * get selected text on the window
+ *
+ * @param {String} mode selection mode: any of the following: "text" | "html"
+ * @return {String} return
+ */
+ selection: function(mode) {
+ var getText = ((mode || 'text').toLowerCase() === 'text');
+
+ try {
+ if (win.getSelection) {
+ if (getText) {
+ // get text
+ return win.getSelection().toString();
+ } else {
+ // get html
+ var sel = win.getSelection(), range;
+
+ if (sel.getRangeAt) {
+ range = sel.getRangeAt(0);
+ } else {
+ range = doc.createRange();
+ range.setStart(sel.anchorNode, sel.anchorOffset);
+ range.setEnd(sel.focusNode, sel.focusOffset);
+ }
+
+ return $('
').append(range.cloneContents()).html();
+ }
+ } else if (doc.selection) {
+ if (getText) {
+ // get text
+ return doc.selection.createRange().text;
+ } else {
+ // get html
+ return doc.selection.createRange().htmlText;
+ }
+ }
+ } catch (e) {
+ /* give up */
+ }
+
+ return '';
+ }
+ });
+
+ /* add selection */
+ $.fn.extend({
+ selection: function(mode, opts) {
+ opts = opts || {};
+
+ switch (mode) {
+ /**
+ * selection('getPos')
+ * get caret position
+ *
+ * @return {Object} return
+ * @return {Number} return.start start position for the selection
+ * @return {Number} return.end end position for the selection
+ */
+ case 'getPos':
+ return _CaretOperation.getPos(this[0]);
+
+ /**
+ * selection('setPos', opts)
+ * set caret position
+ *
+ * @param {Number} opts.start start position for the selection
+ * @param {Number} opts.end end position for the selection
+ */
+ case 'setPos':
+ return this.each(function() {
+ _CaretOperation.setPos(this, opts);
+ });
+
+ /**
+ * selection('replace', opts)
+ * replace the selected text
+ *
+ * @param {String} opts.text replacement text
+ * @param {String} opts.caret caret mode: any of the following: "keep" | "start" | "end"
+ */
+ case 'replace':
+ return this.each(function() {
+ _CaretOperation.replace(this, opts.text, opts.caret);
+ });
+
+ /**
+ * selection('insert', opts)
+ * insert before/after the selected text
+ *
+ * @param {String} opts.text insertion text
+ * @param {String} opts.caret caret mode: any of the following: "keep" | "start" | "end"
+ * @param {String} opts.mode insertion mode: any of the following: "before" | "after"
+ */
+ case 'insert':
+ return this.each(function() {
+ if (opts.mode === 'before') {
+ _CaretOperation.insertBefore(this, opts.text, opts.caret);
+ } else {
+ _CaretOperation.insertAfter(this, opts.text, opts.caret);
+ }
+ });
+
+ /**
+ * selection('get')
+ * get selected text
+ *
+ * @return {String} return
+ */
+ case 'get':
+ /* falls through */
+ default:
+ return _CaretOperation.getText(this[0]);
+ }
+
+ return this;
+ }
+ });
+})(jQuery, window, window.document);
diff --git a/samples/development-frameworks/django/bootcamp/static/js/jquery.typeahead.bundle.js b/samples/development-frameworks/django/bootcamp/static/js/jquery.typeahead.bundle.js
new file mode 100644
index 00000000..19631501
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/static/js/jquery.typeahead.bundle.js
@@ -0,0 +1,1716 @@
+/*!
+ * typeahead.js 0.10.2
+ * https://github.com/twitter/typeahead.js
+ * Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
+ */
+
+(function($) {
+ var _ = {
+ isMsie: function() {
+ return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false;
+ },
+ isBlankString: function(str) {
+ return !str || /^\s*$/.test(str);
+ },
+ escapeRegExChars: function(str) {
+ return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
+ },
+ isString: function(obj) {
+ return typeof obj === "string";
+ },
+ isNumber: function(obj) {
+ return typeof obj === "number";
+ },
+ isArray: $.isArray,
+ isFunction: $.isFunction,
+ isObject: $.isPlainObject,
+ isUndefined: function(obj) {
+ return typeof obj === "undefined";
+ },
+ bind: $.proxy,
+ each: function(collection, cb) {
+ $.each(collection, reverseArgs);
+ function reverseArgs(index, value) {
+ return cb(value, index);
+ }
+ },
+ map: $.map,
+ filter: $.grep,
+ every: function(obj, test) {
+ var result = true;
+ if (!obj) {
+ return result;
+ }
+ $.each(obj, function(key, val) {
+ if (!(result = test.call(null, val, key, obj))) {
+ return false;
+ }
+ });
+ return !!result;
+ },
+ some: function(obj, test) {
+ var result = false;
+ if (!obj) {
+ return result;
+ }
+ $.each(obj, function(key, val) {
+ if (result = test.call(null, val, key, obj)) {
+ return false;
+ }
+ });
+ return !!result;
+ },
+ mixin: $.extend,
+ getUniqueId: function() {
+ var counter = 0;
+ return function() {
+ return counter++;
+ };
+ }(),
+ templatify: function templatify(obj) {
+ return $.isFunction(obj) ? obj : template;
+ function template() {
+ return String(obj);
+ }
+ },
+ defer: function(fn) {
+ setTimeout(fn, 0);
+ },
+ debounce: function(func, wait, immediate) {
+ var timeout, result;
+ return function() {
+ var context = this, args = arguments, later, callNow;
+ later = function() {
+ timeout = null;
+ if (!immediate) {
+ result = func.apply(context, args);
+ }
+ };
+ callNow = immediate && !timeout;
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ if (callNow) {
+ result = func.apply(context, args);
+ }
+ return result;
+ };
+ },
+ throttle: function(func, wait) {
+ var context, args, timeout, result, previous, later;
+ previous = 0;
+ later = function() {
+ previous = new Date();
+ timeout = null;
+ result = func.apply(context, args);
+ };
+ return function() {
+ var now = new Date(), remaining = wait - (now - previous);
+ context = this;
+ args = arguments;
+ if (remaining <= 0) {
+ clearTimeout(timeout);
+ timeout = null;
+ previous = now;
+ result = func.apply(context, args);
+ } else if (!timeout) {
+ timeout = setTimeout(later, remaining);
+ }
+ return result;
+ };
+ },
+ noop: function() {}
+ };
+ var VERSION = "0.10.2";
+ var tokenizers = function(root) {
+ return {
+ nonword: nonword,
+ whitespace: whitespace,
+ obj: {
+ nonword: getObjTokenizer(nonword),
+ whitespace: getObjTokenizer(whitespace)
+ }
+ };
+ function whitespace(s) {
+ return s.split(/\s+/);
+ }
+ function nonword(s) {
+ return s.split(/\W+/);
+ }
+ function getObjTokenizer(tokenizer) {
+ return function setKey(key) {
+ return function tokenize(o) {
+ return tokenizer(o[key]);
+ };
+ };
+ }
+ }();
+ var LruCache = function() {
+ function LruCache(maxSize) {
+ this.maxSize = maxSize || 100;
+ this.size = 0;
+ this.hash = {};
+ this.list = new List();
+ }
+ _.mixin(LruCache.prototype, {
+ set: function set(key, val) {
+ var tailItem = this.list.tail, node;
+ if (this.size >= this.maxSize) {
+ this.list.remove(tailItem);
+ delete this.hash[tailItem.key];
+ }
+ if (node = this.hash[key]) {
+ node.val = val;
+ this.list.moveToFront(node);
+ } else {
+ node = new Node(key, val);
+ this.list.add(node);
+ this.hash[key] = node;
+ this.size++;
+ }
+ },
+ get: function get(key) {
+ var node = this.hash[key];
+ if (node) {
+ this.list.moveToFront(node);
+ return node.val;
+ }
+ }
+ });
+ function List() {
+ this.head = this.tail = null;
+ }
+ _.mixin(List.prototype, {
+ add: function add(node) {
+ if (this.head) {
+ node.next = this.head;
+ this.head.prev = node;
+ }
+ this.head = node;
+ this.tail = this.tail || node;
+ },
+ remove: function remove(node) {
+ node.prev ? node.prev.next = node.next : this.head = node.next;
+ node.next ? node.next.prev = node.prev : this.tail = node.prev;
+ },
+ moveToFront: function(node) {
+ this.remove(node);
+ this.add(node);
+ }
+ });
+ function Node(key, val) {
+ this.key = key;
+ this.val = val;
+ this.prev = this.next = null;
+ }
+ return LruCache;
+ }();
+ var PersistentStorage = function() {
+ var ls, methods;
+ try {
+ ls = window.localStorage;
+ ls.setItem("~~~", "!");
+ ls.removeItem("~~~");
+ } catch (err) {
+ ls = null;
+ }
+ function PersistentStorage(namespace) {
+ this.prefix = [ "__", namespace, "__" ].join("");
+ this.ttlKey = "__ttl__";
+ this.keyMatcher = new RegExp("^" + this.prefix);
+ }
+ if (ls && window.JSON) {
+ methods = {
+ _prefix: function(key) {
+ return this.prefix + key;
+ },
+ _ttlKey: function(key) {
+ return this._prefix(key) + this.ttlKey;
+ },
+ get: function(key) {
+ if (this.isExpired(key)) {
+ this.remove(key);
+ }
+ return decode(ls.getItem(this._prefix(key)));
+ },
+ set: function(key, val, ttl) {
+ if (_.isNumber(ttl)) {
+ ls.setItem(this._ttlKey(key), encode(now() + ttl));
+ } else {
+ ls.removeItem(this._ttlKey(key));
+ }
+ return ls.setItem(this._prefix(key), encode(val));
+ },
+ remove: function(key) {
+ ls.removeItem(this._ttlKey(key));
+ ls.removeItem(this._prefix(key));
+ return this;
+ },
+ clear: function() {
+ var i, key, keys = [], len = ls.length;
+ for (i = 0; i < len; i++) {
+ if ((key = ls.key(i)).match(this.keyMatcher)) {
+ keys.push(key.replace(this.keyMatcher, ""));
+ }
+ }
+ for (i = keys.length; i--; ) {
+ this.remove(keys[i]);
+ }
+ return this;
+ },
+ isExpired: function(key) {
+ var ttl = decode(ls.getItem(this._ttlKey(key)));
+ return _.isNumber(ttl) && now() > ttl ? true : false;
+ }
+ };
+ } else {
+ methods = {
+ get: _.noop,
+ set: _.noop,
+ remove: _.noop,
+ clear: _.noop,
+ isExpired: _.noop
+ };
+ }
+ _.mixin(PersistentStorage.prototype, methods);
+ return PersistentStorage;
+ function now() {
+ return new Date().getTime();
+ }
+ function encode(val) {
+ return JSON.stringify(_.isUndefined(val) ? null : val);
+ }
+ function decode(val) {
+ return JSON.parse(val);
+ }
+ }();
+ var Transport = function() {
+ var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests = 6, requestCache = new LruCache(10);
+ function Transport(o) {
+ o = o || {};
+ this._send = o.transport ? callbackToDeferred(o.transport) : $.ajax;
+ this._get = o.rateLimiter ? o.rateLimiter(this._get) : this._get;
+ }
+ Transport.setMaxPendingRequests = function setMaxPendingRequests(num) {
+ maxPendingRequests = num;
+ };
+ Transport.resetCache = function clearCache() {
+ requestCache = new LruCache(10);
+ };
+ _.mixin(Transport.prototype, {
+ _get: function(url, o, cb) {
+ var that = this, jqXhr;
+ if (jqXhr = pendingRequests[url]) {
+ jqXhr.done(done).fail(fail);
+ } else if (pendingRequestsCount < maxPendingRequests) {
+ pendingRequestsCount++;
+ pendingRequests[url] = this._send(url, o).done(done).fail(fail).always(always);
+ } else {
+ this.onDeckRequestArgs = [].slice.call(arguments, 0);
+ }
+ function done(resp) {
+ cb && cb(null, resp);
+ requestCache.set(url, resp);
+ }
+ function fail() {
+ cb && cb(true);
+ }
+ function always() {
+ pendingRequestsCount--;
+ delete pendingRequests[url];
+ if (that.onDeckRequestArgs) {
+ that._get.apply(that, that.onDeckRequestArgs);
+ that.onDeckRequestArgs = null;
+ }
+ }
+ },
+ get: function(url, o, cb) {
+ var resp;
+ if (_.isFunction(o)) {
+ cb = o;
+ o = {};
+ }
+ if (resp = requestCache.get(url)) {
+ _.defer(function() {
+ cb && cb(null, resp);
+ });
+ } else {
+ this._get(url, o, cb);
+ }
+ return !!resp;
+ }
+ });
+ return Transport;
+ function callbackToDeferred(fn) {
+ return function customSendWrapper(url, o) {
+ var deferred = $.Deferred();
+ fn(url, o, onSuccess, onError);
+ return deferred;
+ function onSuccess(resp) {
+ _.defer(function() {
+ deferred.resolve(resp);
+ });
+ }
+ function onError(err) {
+ _.defer(function() {
+ deferred.reject(err);
+ });
+ }
+ };
+ }
+ }();
+ var SearchIndex = function() {
+ function SearchIndex(o) {
+ o = o || {};
+ if (!o.datumTokenizer || !o.queryTokenizer) {
+ $.error("datumTokenizer and queryTokenizer are both required");
+ }
+ this.datumTokenizer = o.datumTokenizer;
+ this.queryTokenizer = o.queryTokenizer;
+ this.reset();
+ }
+ _.mixin(SearchIndex.prototype, {
+ bootstrap: function bootstrap(o) {
+ this.datums = o.datums;
+ this.trie = o.trie;
+ },
+ add: function(data) {
+ var that = this;
+ data = _.isArray(data) ? data : [ data ];
+ _.each(data, function(datum) {
+ var id, tokens;
+ id = that.datums.push(datum) - 1;
+ tokens = normalizeTokens(that.datumTokenizer(datum));
+ _.each(tokens, function(token) {
+ var node, chars, ch;
+ node = that.trie;
+ chars = token.split("");
+ while (ch = chars.shift()) {
+ node = node.children[ch] || (node.children[ch] = newNode());
+ node.ids.push(id);
+ }
+ });
+ });
+ },
+ get: function get(query) {
+ var that = this, tokens, matches;
+ tokens = normalizeTokens(this.queryTokenizer(query));
+ _.each(tokens, function(token) {
+ var node, chars, ch, ids;
+ if (matches && matches.length === 0) {
+ return false;
+ }
+ node = that.trie;
+ chars = token.split("");
+ while (node && (ch = chars.shift())) {
+ node = node.children[ch];
+ }
+ if (node && chars.length === 0) {
+ ids = node.ids.slice(0);
+ matches = matches ? getIntersection(matches, ids) : ids;
+ } else {
+ matches = [];
+ return false;
+ }
+ });
+ return matches ? _.map(unique(matches), function(id) {
+ return that.datums[id];
+ }) : [];
+ },
+ reset: function reset() {
+ this.datums = [];
+ this.trie = newNode();
+ },
+ serialize: function serialize() {
+ return {
+ datums: this.datums,
+ trie: this.trie
+ };
+ }
+ });
+ return SearchIndex;
+ function normalizeTokens(tokens) {
+ tokens = _.filter(tokens, function(token) {
+ return !!token;
+ });
+ tokens = _.map(tokens, function(token) {
+ return token.toLowerCase();
+ });
+ return tokens;
+ }
+ function newNode() {
+ return {
+ ids: [],
+ children: {}
+ };
+ }
+ function unique(array) {
+ var seen = {}, uniques = [];
+ for (var i = 0; i < array.length; i++) {
+ if (!seen[array[i]]) {
+ seen[array[i]] = true;
+ uniques.push(array[i]);
+ }
+ }
+ return uniques;
+ }
+ function getIntersection(arrayA, arrayB) {
+ var ai = 0, bi = 0, intersection = [];
+ arrayA = arrayA.sort(compare);
+ arrayB = arrayB.sort(compare);
+ while (ai < arrayA.length && bi < arrayB.length) {
+ if (arrayA[ai] < arrayB[bi]) {
+ ai++;
+ } else if (arrayA[ai] > arrayB[bi]) {
+ bi++;
+ } else {
+ intersection.push(arrayA[ai]);
+ ai++;
+ bi++;
+ }
+ }
+ return intersection;
+ function compare(a, b) {
+ return a - b;
+ }
+ }
+ }();
+ var oParser = function() {
+ return {
+ local: getLocal,
+ prefetch: getPrefetch,
+ remote: getRemote
+ };
+ function getLocal(o) {
+ return o.local || null;
+ }
+ function getPrefetch(o) {
+ var prefetch, defaults;
+ defaults = {
+ url: null,
+ thumbprint: "",
+ ttl: 24 * 60 * 60 * 1e3,
+ filter: null,
+ ajax: {}
+ };
+ if (prefetch = o.prefetch || null) {
+ prefetch = _.isString(prefetch) ? {
+ url: prefetch
+ } : prefetch;
+ prefetch = _.mixin(defaults, prefetch);
+ prefetch.thumbprint = VERSION + prefetch.thumbprint;
+ prefetch.ajax.type = prefetch.ajax.type || "GET";
+ prefetch.ajax.dataType = prefetch.ajax.dataType || "json";
+ !prefetch.url && $.error("prefetch requires url to be set");
+ }
+ return prefetch;
+ }
+ function getRemote(o) {
+ var remote, defaults;
+ defaults = {
+ url: null,
+ wildcard: "%QUERY",
+ replace: null,
+ rateLimitBy: "debounce",
+ rateLimitWait: 300,
+ send: null,
+ filter: null,
+ ajax: {}
+ };
+ if (remote = o.remote || null) {
+ remote = _.isString(remote) ? {
+ url: remote
+ } : remote;
+ remote = _.mixin(defaults, remote);
+ remote.rateLimiter = /^throttle$/i.test(remote.rateLimitBy) ? byThrottle(remote.rateLimitWait) : byDebounce(remote.rateLimitWait);
+ remote.ajax.type = remote.ajax.type || "GET";
+ remote.ajax.dataType = remote.ajax.dataType || "json";
+ delete remote.rateLimitBy;
+ delete remote.rateLimitWait;
+ !remote.url && $.error("remote requires url to be set");
+ }
+ return remote;
+ function byDebounce(wait) {
+ return function(fn) {
+ return _.debounce(fn, wait);
+ };
+ }
+ function byThrottle(wait) {
+ return function(fn) {
+ return _.throttle(fn, wait);
+ };
+ }
+ }
+ }();
+ (function(root) {
+ var old, keys;
+ old = root.Bloodhound;
+ keys = {
+ data: "data",
+ protocol: "protocol",
+ thumbprint: "thumbprint"
+ };
+ root.Bloodhound = Bloodhound;
+ function Bloodhound(o) {
+ if (!o || !o.local && !o.prefetch && !o.remote) {
+ $.error("one of local, prefetch, or remote is required");
+ }
+ this.limit = o.limit || 5;
+ this.sorter = getSorter(o.sorter);
+ this.dupDetector = o.dupDetector || ignoreDuplicates;
+ this.local = oParser.local(o);
+ this.prefetch = oParser.prefetch(o);
+ this.remote = oParser.remote(o);
+ this.cacheKey = this.prefetch ? this.prefetch.cacheKey || this.prefetch.url : null;
+ this.index = new SearchIndex({
+ datumTokenizer: o.datumTokenizer,
+ queryTokenizer: o.queryTokenizer
+ });
+ this.storage = this.cacheKey ? new PersistentStorage(this.cacheKey) : null;
+ }
+ Bloodhound.noConflict = function noConflict() {
+ root.Bloodhound = old;
+ return Bloodhound;
+ };
+ Bloodhound.tokenizers = tokenizers;
+ _.mixin(Bloodhound.prototype, {
+ _loadPrefetch: function loadPrefetch(o) {
+ var that = this, serialized, deferred;
+ if (serialized = this._readFromStorage(o.thumbprint)) {
+ this.index.bootstrap(serialized);
+ deferred = $.Deferred().resolve();
+ } else {
+ deferred = $.ajax(o.url, o.ajax).done(handlePrefetchResponse);
+ }
+ return deferred;
+ function handlePrefetchResponse(resp) {
+ that.clear();
+ that.add(o.filter ? o.filter(resp) : resp);
+ that._saveToStorage(that.index.serialize(), o.thumbprint, o.ttl);
+ }
+ },
+ _getFromRemote: function getFromRemote(query, cb) {
+ var that = this, url, uriEncodedQuery;
+ query = query || "";
+ uriEncodedQuery = encodeURIComponent(query);
+ url = this.remote.replace ? this.remote.replace(this.remote.url, query) : this.remote.url.replace(this.remote.wildcard, uriEncodedQuery);
+ return this.transport.get(url, this.remote.ajax, handleRemoteResponse);
+ function handleRemoteResponse(err, resp) {
+ err ? cb([]) : cb(that.remote.filter ? that.remote.filter(resp) : resp);
+ }
+ },
+ _saveToStorage: function saveToStorage(data, thumbprint, ttl) {
+ if (this.storage) {
+ this.storage.set(keys.data, data, ttl);
+ this.storage.set(keys.protocol, location.protocol, ttl);
+ this.storage.set(keys.thumbprint, thumbprint, ttl);
+ }
+ },
+ _readFromStorage: function readFromStorage(thumbprint) {
+ var stored = {}, isExpired;
+ if (this.storage) {
+ stored.data = this.storage.get(keys.data);
+ stored.protocol = this.storage.get(keys.protocol);
+ stored.thumbprint = this.storage.get(keys.thumbprint);
+ }
+ isExpired = stored.thumbprint !== thumbprint || stored.protocol !== location.protocol;
+ return stored.data && !isExpired ? stored.data : null;
+ },
+ _initialize: function initialize() {
+ var that = this, local = this.local, deferred;
+ deferred = this.prefetch ? this._loadPrefetch(this.prefetch) : $.Deferred().resolve();
+ local && deferred.done(addLocalToIndex);
+ this.transport = this.remote ? new Transport(this.remote) : null;
+ return this.initPromise = deferred.promise();
+ function addLocalToIndex() {
+ that.add(_.isFunction(local) ? local() : local);
+ }
+ },
+ initialize: function initialize(force) {
+ return !this.initPromise || force ? this._initialize() : this.initPromise;
+ },
+ add: function add(data) {
+ this.index.add(data);
+ },
+ get: function get(query, cb) {
+ var that = this, matches = [], cacheHit = false;
+ matches = this.index.get(query);
+ matches = this.sorter(matches).slice(0, this.limit);
+ if (matches.length < this.limit && this.transport) {
+ cacheHit = this._getFromRemote(query, returnRemoteMatches);
+ }
+ if (!cacheHit) {
+ (matches.length > 0 || !this.transport) && cb && cb(matches);
+ }
+ function returnRemoteMatches(remoteMatches) {
+ var matchesWithBackfill = matches.slice(0);
+ _.each(remoteMatches, function(remoteMatch) {
+ var isDuplicate;
+ isDuplicate = _.some(matchesWithBackfill, function(match) {
+ return that.dupDetector(remoteMatch, match);
+ });
+ !isDuplicate && matchesWithBackfill.push(remoteMatch);
+ return matchesWithBackfill.length < that.limit;
+ });
+ cb && cb(that.sorter(matchesWithBackfill));
+ }
+ },
+ clear: function clear() {
+ this.index.reset();
+ },
+ clearPrefetchCache: function clearPrefetchCache() {
+ this.storage && this.storage.clear();
+ },
+ clearRemoteCache: function clearRemoteCache() {
+ this.transport && Transport.resetCache();
+ },
+ ttAdapter: function ttAdapter() {
+ return _.bind(this.get, this);
+ }
+ });
+ return Bloodhound;
+ function getSorter(sortFn) {
+ return _.isFunction(sortFn) ? sort : noSort;
+ function sort(array) {
+ return array.sort(sortFn);
+ }
+ function noSort(array) {
+ return array;
+ }
+ }
+ function ignoreDuplicates() {
+ return false;
+ }
+ })(this);
+ var html = {
+ wrapper: '',
+ dropdown: '',
+ dataset: '
',
+ suggestions: ' ',
+ suggestion: '
'
+ };
+ var css = {
+ wrapper: {
+ position: "relative",
+ display: "inline-block"
+ },
+ hint: {
+ position: "absolute",
+ top: "0",
+ left: "0",
+ borderColor: "transparent",
+ boxShadow: "none"
+ },
+ input: {
+ position: "relative",
+ verticalAlign: "top",
+ backgroundColor: "transparent"
+ },
+ inputWithNoHint: {
+ position: "relative",
+ verticalAlign: "top"
+ },
+ dropdown: {
+ position: "absolute",
+ top: "100%",
+ left: "0",
+ zIndex: "100",
+ display: "none"
+ },
+ suggestions: {
+ display: "block"
+ },
+ suggestion: {
+ whiteSpace: "nowrap",
+ cursor: "pointer"
+ },
+ suggestionChild: {
+ whiteSpace: "normal"
+ },
+ ltr: {
+ left: "0",
+ right: "auto"
+ },
+ rtl: {
+ left: "auto",
+ right: " 0"
+ }
+ };
+ if (_.isMsie()) {
+ _.mixin(css.input, {
+ backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)"
+ });
+ }
+ if (_.isMsie() && _.isMsie() <= 7) {
+ _.mixin(css.input, {
+ marginTop: "-1px"
+ });
+ }
+ var EventBus = function() {
+ var namespace = "typeahead:";
+ function EventBus(o) {
+ if (!o || !o.el) {
+ $.error("EventBus initialized without el");
+ }
+ this.$el = $(o.el);
+ }
+ _.mixin(EventBus.prototype, {
+ trigger: function(type) {
+ var args = [].slice.call(arguments, 1);
+ this.$el.trigger(namespace + type, args);
+ }
+ });
+ return EventBus;
+ }();
+ var EventEmitter = function() {
+ var splitter = /\s+/, nextTick = getNextTick();
+ return {
+ onSync: onSync,
+ onAsync: onAsync,
+ off: off,
+ trigger: trigger
+ };
+ function on(method, types, cb, context) {
+ var type;
+ if (!cb) {
+ return this;
+ }
+ types = types.split(splitter);
+ cb = context ? bindContext(cb, context) : cb;
+ this._callbacks = this._callbacks || {};
+ while (type = types.shift()) {
+ this._callbacks[type] = this._callbacks[type] || {
+ sync: [],
+ async: []
+ };
+ this._callbacks[type][method].push(cb);
+ }
+ return this;
+ }
+ function onAsync(types, cb, context) {
+ return on.call(this, "async", types, cb, context);
+ }
+ function onSync(types, cb, context) {
+ return on.call(this, "sync", types, cb, context);
+ }
+ function off(types) {
+ var type;
+ if (!this._callbacks) {
+ return this;
+ }
+ types = types.split(splitter);
+ while (type = types.shift()) {
+ delete this._callbacks[type];
+ }
+ return this;
+ }
+ function trigger(types) {
+ var type, callbacks, args, syncFlush, asyncFlush;
+ if (!this._callbacks) {
+ return this;
+ }
+ types = types.split(splitter);
+ args = [].slice.call(arguments, 1);
+ while ((type = types.shift()) && (callbacks = this._callbacks[type])) {
+ syncFlush = getFlush(callbacks.sync, this, [ type ].concat(args));
+ asyncFlush = getFlush(callbacks.async, this, [ type ].concat(args));
+ syncFlush() && nextTick(asyncFlush);
+ }
+ return this;
+ }
+ function getFlush(callbacks, context, args) {
+ return flush;
+ function flush() {
+ var cancelled;
+ for (var i = 0; !cancelled && i < callbacks.length; i += 1) {
+ cancelled = callbacks[i].apply(context, args) === false;
+ }
+ return !cancelled;
+ }
+ }
+ function getNextTick() {
+ var nextTickFn;
+ if (window.setImmediate) {
+ nextTickFn = function nextTickSetImmediate(fn) {
+ setImmediate(function() {
+ fn();
+ });
+ };
+ } else {
+ nextTickFn = function nextTickSetTimeout(fn) {
+ setTimeout(function() {
+ fn();
+ }, 0);
+ };
+ }
+ return nextTickFn;
+ }
+ function bindContext(fn, context) {
+ return fn.bind ? fn.bind(context) : function() {
+ fn.apply(context, [].slice.call(arguments, 0));
+ };
+ }
+ }();
+ var highlight = function(doc) {
+ var defaults = {
+ node: null,
+ pattern: null,
+ tagName: "strong",
+ className: null,
+ wordsOnly: false,
+ caseSensitive: false
+ };
+ return function hightlight(o) {
+ var regex;
+ o = _.mixin({}, defaults, o);
+ if (!o.node || !o.pattern) {
+ return;
+ }
+ o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ];
+ regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly);
+ traverse(o.node, hightlightTextNode);
+ function hightlightTextNode(textNode) {
+ var match, patternNode;
+ if (match = regex.exec(textNode.data)) {
+ wrapperNode = doc.createElement(o.tagName);
+ o.className && (wrapperNode.className = o.className);
+ patternNode = textNode.splitText(match.index);
+ patternNode.splitText(match[0].length);
+ wrapperNode.appendChild(patternNode.cloneNode(true));
+ textNode.parentNode.replaceChild(wrapperNode, patternNode);
+ }
+ return !!match;
+ }
+ function traverse(el, hightlightTextNode) {
+ var childNode, TEXT_NODE_TYPE = 3;
+ for (var i = 0; i < el.childNodes.length; i++) {
+ childNode = el.childNodes[i];
+ if (childNode.nodeType === TEXT_NODE_TYPE) {
+ i += hightlightTextNode(childNode) ? 1 : 0;
+ } else {
+ traverse(childNode, hightlightTextNode);
+ }
+ }
+ }
+ };
+ function getRegex(patterns, caseSensitive, wordsOnly) {
+ var escapedPatterns = [], regexStr;
+ for (var i = 0; i < patterns.length; i++) {
+ escapedPatterns.push(_.escapeRegExChars(patterns[i]));
+ }
+ regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")";
+ return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i");
+ }
+ }(window.document);
+ var Input = function() {
+ var specialKeyCodeMap;
+ specialKeyCodeMap = {
+ 9: "tab",
+ 27: "esc",
+ 37: "left",
+ 39: "right",
+ 13: "enter",
+ 38: "up",
+ 40: "down"
+ };
+ function Input(o) {
+ var that = this, onBlur, onFocus, onKeydown, onInput;
+ o = o || {};
+ if (!o.input) {
+ $.error("input is missing");
+ }
+ onBlur = _.bind(this._onBlur, this);
+ onFocus = _.bind(this._onFocus, this);
+ onKeydown = _.bind(this._onKeydown, this);
+ onInput = _.bind(this._onInput, this);
+ this.$hint = $(o.hint);
+ this.$input = $(o.input).on("blur.tt", onBlur).on("focus.tt", onFocus).on("keydown.tt", onKeydown);
+ if (this.$hint.length === 0) {
+ this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop;
+ }
+ if (!_.isMsie()) {
+ this.$input.on("input.tt", onInput);
+ } else {
+ this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) {
+ if (specialKeyCodeMap[$e.which || $e.keyCode]) {
+ return;
+ }
+ _.defer(_.bind(that._onInput, that, $e));
+ });
+ }
+ this.query = this.$input.val();
+ this.$overflowHelper = buildOverflowHelper(this.$input);
+ }
+ Input.normalizeQuery = function(str) {
+ return (str || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " ");
+ };
+ _.mixin(Input.prototype, EventEmitter, {
+ _onBlur: function onBlur() {
+ this.resetInputValue();
+ this.trigger("blurred");
+ },
+ _onFocus: function onFocus() {
+ this.trigger("focused");
+ },
+ _onKeydown: function onKeydown($e) {
+ var keyName = specialKeyCodeMap[$e.which || $e.keyCode];
+ this._managePreventDefault(keyName, $e);
+ if (keyName && this._shouldTrigger(keyName, $e)) {
+ this.trigger(keyName + "Keyed", $e);
+ }
+ },
+ _onInput: function onInput() {
+ this._checkInputValue();
+ },
+ _managePreventDefault: function managePreventDefault(keyName, $e) {
+ var preventDefault, hintValue, inputValue;
+ switch (keyName) {
+ case "tab":
+ hintValue = this.getHint();
+ inputValue = this.getInputValue();
+ preventDefault = hintValue && hintValue !== inputValue && !withModifier($e);
+ break;
+
+ case "up":
+ case "down":
+ preventDefault = !withModifier($e);
+ break;
+
+ default:
+ preventDefault = false;
+ }
+ preventDefault && $e.preventDefault();
+ },
+ _shouldTrigger: function shouldTrigger(keyName, $e) {
+ var trigger;
+ switch (keyName) {
+ case "tab":
+ trigger = !withModifier($e);
+ break;
+
+ default:
+ trigger = true;
+ }
+ return trigger;
+ },
+ _checkInputValue: function checkInputValue() {
+ var inputValue, areEquivalent, hasDifferentWhitespace;
+ inputValue = this.getInputValue();
+ areEquivalent = areQueriesEquivalent(inputValue, this.query);
+ hasDifferentWhitespace = areEquivalent ? this.query.length !== inputValue.length : false;
+ if (!areEquivalent) {
+ this.trigger("queryChanged", this.query = inputValue);
+ } else if (hasDifferentWhitespace) {
+ this.trigger("whitespaceChanged", this.query);
+ }
+ },
+ focus: function focus() {
+ this.$input.focus();
+ },
+ blur: function blur() {
+ this.$input.blur();
+ },
+ getQuery: function getQuery() {
+ return this.query;
+ },
+ setQuery: function setQuery(query) {
+ this.query = query;
+ },
+ getInputValue: function getInputValue() {
+ return this.$input.val();
+ },
+ setInputValue: function setInputValue(value, silent) {
+ this.$input.val(value);
+ silent ? this.clearHint() : this._checkInputValue();
+ },
+ resetInputValue: function resetInputValue() {
+ this.setInputValue(this.query, true);
+ },
+ getHint: function getHint() {
+ return this.$hint.val();
+ },
+ setHint: function setHint(value) {
+ this.$hint.val(value);
+ },
+ clearHint: function clearHint() {
+ this.setHint("");
+ },
+ clearHintIfInvalid: function clearHintIfInvalid() {
+ var val, hint, valIsPrefixOfHint, isValid;
+ val = this.getInputValue();
+ hint = this.getHint();
+ valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0;
+ isValid = val !== "" && valIsPrefixOfHint && !this.hasOverflow();
+ !isValid && this.clearHint();
+ },
+ getLanguageDirection: function getLanguageDirection() {
+ return (this.$input.css("direction") || "ltr").toLowerCase();
+ },
+ hasOverflow: function hasOverflow() {
+ var constraint = this.$input.width() - 2;
+ this.$overflowHelper.text(this.getInputValue());
+ return this.$overflowHelper.width() >= constraint;
+ },
+ isCursorAtEnd: function() {
+ var valueLength, selectionStart, range;
+ valueLength = this.$input.val().length;
+ selectionStart = this.$input[0].selectionStart;
+ if (_.isNumber(selectionStart)) {
+ return selectionStart === valueLength;
+ } else if (document.selection) {
+ range = document.selection.createRange();
+ range.moveStart("character", -valueLength);
+ return valueLength === range.text.length;
+ }
+ return true;
+ },
+ destroy: function destroy() {
+ this.$hint.off(".tt");
+ this.$input.off(".tt");
+ this.$hint = this.$input = this.$overflowHelper = null;
+ }
+ });
+ return Input;
+ function buildOverflowHelper($input) {
+ return $(' ').css({
+ position: "absolute",
+ visibility: "hidden",
+ whiteSpace: "pre",
+ fontFamily: $input.css("font-family"),
+ fontSize: $input.css("font-size"),
+ fontStyle: $input.css("font-style"),
+ fontVariant: $input.css("font-variant"),
+ fontWeight: $input.css("font-weight"),
+ wordSpacing: $input.css("word-spacing"),
+ letterSpacing: $input.css("letter-spacing"),
+ textIndent: $input.css("text-indent"),
+ textRendering: $input.css("text-rendering"),
+ textTransform: $input.css("text-transform")
+ }).insertAfter($input);
+ }
+ function areQueriesEquivalent(a, b) {
+ return Input.normalizeQuery(a) === Input.normalizeQuery(b);
+ }
+ function withModifier($e) {
+ return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey;
+ }
+ }();
+ var Dataset = function() {
+ var datasetKey = "ttDataset", valueKey = "ttValue", datumKey = "ttDatum";
+ function Dataset(o) {
+ o = o || {};
+ o.templates = o.templates || {};
+ if (!o.source) {
+ $.error("missing source");
+ }
+ if (o.name && !isValidName(o.name)) {
+ $.error("invalid dataset name: " + o.name);
+ }
+ this.query = null;
+ this.highlight = !!o.highlight;
+ this.name = o.name || _.getUniqueId();
+ this.source = o.source;
+ this.displayFn = getDisplayFn(o.display || o.displayKey);
+ this.templates = getTemplates(o.templates, this.displayFn);
+ this.$el = $(html.dataset.replace("%CLASS%", this.name));
+ }
+ Dataset.extractDatasetName = function extractDatasetName(el) {
+ return $(el).data(datasetKey);
+ };
+ Dataset.extractValue = function extractDatum(el) {
+ return $(el).data(valueKey);
+ };
+ Dataset.extractDatum = function extractDatum(el) {
+ return $(el).data(datumKey);
+ };
+ _.mixin(Dataset.prototype, EventEmitter, {
+ _render: function render(query, suggestions) {
+ if (!this.$el) {
+ return;
+ }
+ var that = this, hasSuggestions;
+ this.$el.empty();
+ hasSuggestions = suggestions && suggestions.length;
+ if (!hasSuggestions && this.templates.empty) {
+ this.$el.html(getEmptyHtml()).prepend(that.templates.header ? getHeaderHtml() : null).append(that.templates.footer ? getFooterHtml() : null);
+ } else if (hasSuggestions) {
+ this.$el.html(getSuggestionsHtml()).prepend(that.templates.header ? getHeaderHtml() : null).append(that.templates.footer ? getFooterHtml() : null);
+ }
+ this.trigger("rendered");
+ function getEmptyHtml() {
+ return that.templates.empty({
+ query: query,
+ isEmpty: true
+ });
+ }
+ function getSuggestionsHtml() {
+ var $suggestions, nodes;
+ $suggestions = $(html.suggestions).css(css.suggestions);
+ nodes = _.map(suggestions, getSuggestionNode);
+ $suggestions.append.apply($suggestions, nodes);
+ that.highlight && highlight({
+ node: $suggestions[0],
+ pattern: query
+ });
+ return $suggestions;
+ function getSuggestionNode(suggestion) {
+ var $el;
+ $el = $(html.suggestion).append(that.templates.suggestion(suggestion)).data(datasetKey, that.name).data(valueKey, that.displayFn(suggestion)).data(datumKey, suggestion);
+ $el.children().each(function() {
+ $(this).css(css.suggestionChild);
+ });
+ return $el;
+ }
+ }
+ function getHeaderHtml() {
+ return that.templates.header({
+ query: query,
+ isEmpty: !hasSuggestions
+ });
+ }
+ function getFooterHtml() {
+ return that.templates.footer({
+ query: query,
+ isEmpty: !hasSuggestions
+ });
+ }
+ },
+ getRoot: function getRoot() {
+ return this.$el;
+ },
+ update: function update(query) {
+ var that = this;
+ this.query = query;
+ this.canceled = false;
+ this.source(query, render);
+ function render(suggestions) {
+ if (!that.canceled && query === that.query) {
+ that._render(query, suggestions);
+ }
+ }
+ },
+ cancel: function cancel() {
+ this.canceled = true;
+ },
+ clear: function clear() {
+ this.cancel();
+ this.$el.empty();
+ this.trigger("rendered");
+ },
+ isEmpty: function isEmpty() {
+ return this.$el.is(":empty");
+ },
+ destroy: function destroy() {
+ this.$el = null;
+ }
+ });
+ return Dataset;
+ function getDisplayFn(display) {
+ display = display || "value";
+ return _.isFunction(display) ? display : displayFn;
+ function displayFn(obj) {
+ return obj[display];
+ }
+ }
+ function getTemplates(templates, displayFn) {
+ return {
+ empty: templates.empty && _.templatify(templates.empty),
+ header: templates.header && _.templatify(templates.header),
+ footer: templates.footer && _.templatify(templates.footer),
+ suggestion: templates.suggestion || suggestionTemplate
+ };
+ function suggestionTemplate(context) {
+ return "" + displayFn(context) + "
";
+ }
+ }
+ function isValidName(str) {
+ return /^[_a-zA-Z0-9-]+$/.test(str);
+ }
+ }();
+ var Dropdown = function() {
+ function Dropdown(o) {
+ var that = this, onSuggestionClick, onSuggestionMouseEnter, onSuggestionMouseLeave;
+ o = o || {};
+ if (!o.menu) {
+ $.error("menu is required");
+ }
+ this.isOpen = false;
+ this.isEmpty = true;
+ this.datasets = _.map(o.datasets, initializeDataset);
+ onSuggestionClick = _.bind(this._onSuggestionClick, this);
+ onSuggestionMouseEnter = _.bind(this._onSuggestionMouseEnter, this);
+ onSuggestionMouseLeave = _.bind(this._onSuggestionMouseLeave, this);
+ this.$menu = $(o.menu).on("click.tt", ".tt-suggestion", onSuggestionClick).on("mouseenter.tt", ".tt-suggestion", onSuggestionMouseEnter).on("mouseleave.tt", ".tt-suggestion", onSuggestionMouseLeave);
+ _.each(this.datasets, function(dataset) {
+ that.$menu.append(dataset.getRoot());
+ dataset.onSync("rendered", that._onRendered, that);
+ });
+ }
+ _.mixin(Dropdown.prototype, EventEmitter, {
+ _onSuggestionClick: function onSuggestionClick($e) {
+ this.trigger("suggestionClicked", $($e.currentTarget));
+ },
+ _onSuggestionMouseEnter: function onSuggestionMouseEnter($e) {
+ this._removeCursor();
+ this._setCursor($($e.currentTarget), true);
+ },
+ _onSuggestionMouseLeave: function onSuggestionMouseLeave() {
+ this._removeCursor();
+ },
+ _onRendered: function onRendered() {
+ this.isEmpty = _.every(this.datasets, isDatasetEmpty);
+ this.isEmpty ? this._hide() : this.isOpen && this._show();
+ this.trigger("datasetRendered");
+ function isDatasetEmpty(dataset) {
+ return dataset.isEmpty();
+ }
+ },
+ _hide: function() {
+ this.$menu.hide();
+ },
+ _show: function() {
+ this.$menu.css("display", "block");
+ },
+ _getSuggestions: function getSuggestions() {
+ return this.$menu.find(".tt-suggestion");
+ },
+ _getCursor: function getCursor() {
+ return this.$menu.find(".tt-cursor").first();
+ },
+ _setCursor: function setCursor($el, silent) {
+ $el.first().addClass("tt-cursor");
+ !silent && this.trigger("cursorMoved");
+ },
+ _removeCursor: function removeCursor() {
+ this._getCursor().removeClass("tt-cursor");
+ },
+ _moveCursor: function moveCursor(increment) {
+ var $suggestions, $oldCursor, newCursorIndex, $newCursor;
+ if (!this.isOpen) {
+ return;
+ }
+ $oldCursor = this._getCursor();
+ $suggestions = this._getSuggestions();
+ this._removeCursor();
+ newCursorIndex = $suggestions.index($oldCursor) + increment;
+ newCursorIndex = (newCursorIndex + 1) % ($suggestions.length + 1) - 1;
+ if (newCursorIndex === -1) {
+ this.trigger("cursorRemoved");
+ return;
+ } else if (newCursorIndex < -1) {
+ newCursorIndex = $suggestions.length - 1;
+ }
+ this._setCursor($newCursor = $suggestions.eq(newCursorIndex));
+ this._ensureVisible($newCursor);
+ },
+ _ensureVisible: function ensureVisible($el) {
+ var elTop, elBottom, menuScrollTop, menuHeight;
+ elTop = $el.position().top;
+ elBottom = elTop + $el.outerHeight(true);
+ menuScrollTop = this.$menu.scrollTop();
+ menuHeight = this.$menu.height() + parseInt(this.$menu.css("paddingTop"), 10) + parseInt(this.$menu.css("paddingBottom"), 10);
+ if (elTop < 0) {
+ this.$menu.scrollTop(menuScrollTop + elTop);
+ } else if (menuHeight < elBottom) {
+ this.$menu.scrollTop(menuScrollTop + (elBottom - menuHeight));
+ }
+ },
+ close: function close() {
+ if (this.isOpen) {
+ this.isOpen = false;
+ this._removeCursor();
+ this._hide();
+ this.trigger("closed");
+ }
+ },
+ open: function open() {
+ if (!this.isOpen) {
+ this.isOpen = true;
+ !this.isEmpty && this._show();
+ this.trigger("opened");
+ }
+ },
+ setLanguageDirection: function setLanguageDirection(dir) {
+ this.$menu.css(dir === "ltr" ? css.ltr : css.rtl);
+ },
+ moveCursorUp: function moveCursorUp() {
+ this._moveCursor(-1);
+ },
+ moveCursorDown: function moveCursorDown() {
+ this._moveCursor(+1);
+ },
+ getDatumForSuggestion: function getDatumForSuggestion($el) {
+ var datum = null;
+ if ($el.length) {
+ datum = {
+ raw: Dataset.extractDatum($el),
+ value: Dataset.extractValue($el),
+ datasetName: Dataset.extractDatasetName($el)
+ };
+ }
+ return datum;
+ },
+ getDatumForCursor: function getDatumForCursor() {
+ return this.getDatumForSuggestion(this._getCursor().first());
+ },
+ getDatumForTopSuggestion: function getDatumForTopSuggestion() {
+ return this.getDatumForSuggestion(this._getSuggestions().first());
+ },
+ update: function update(query) {
+ _.each(this.datasets, updateDataset);
+ function updateDataset(dataset) {
+ dataset.update(query);
+ }
+ },
+ empty: function empty() {
+ _.each(this.datasets, clearDataset);
+ this.isEmpty = true;
+ function clearDataset(dataset) {
+ dataset.clear();
+ }
+ },
+ isVisible: function isVisible() {
+ return this.isOpen && !this.isEmpty;
+ },
+ destroy: function destroy() {
+ this.$menu.off(".tt");
+ this.$menu = null;
+ _.each(this.datasets, destroyDataset);
+ function destroyDataset(dataset) {
+ dataset.destroy();
+ }
+ }
+ });
+ return Dropdown;
+ function initializeDataset(oDataset) {
+ return new Dataset(oDataset);
+ }
+ }();
+ var Typeahead = function() {
+ var attrsKey = "ttAttrs";
+ function Typeahead(o) {
+ var $menu, $input, $hint;
+ o = o || {};
+ if (!o.input) {
+ $.error("missing input");
+ }
+ this.isActivated = false;
+ this.autoselect = !!o.autoselect;
+ this.minLength = _.isNumber(o.minLength) ? o.minLength : 1;
+ this.$node = buildDomStructure(o.input, o.withHint);
+ $menu = this.$node.find(".tt-dropdown-menu");
+ $input = this.$node.find(".tt-input");
+ $hint = this.$node.find(".tt-hint");
+ $input.on("blur.tt", function($e) {
+ var active, isActive, hasActive;
+ active = document.activeElement;
+ isActive = $menu.is(active);
+ hasActive = $menu.has(active).length > 0;
+ if (_.isMsie() && (isActive || hasActive)) {
+ $e.preventDefault();
+ $e.stopImmediatePropagation();
+ _.defer(function() {
+ $input.focus();
+ });
+ }
+ });
+ $menu.on("mousedown.tt", function($e) {
+ $e.preventDefault();
+ });
+ this.eventBus = o.eventBus || new EventBus({
+ el: $input
+ });
+ this.dropdown = new Dropdown({
+ menu: $menu,
+ datasets: o.datasets
+ }).onSync("suggestionClicked", this._onSuggestionClicked, this).onSync("cursorMoved", this._onCursorMoved, this).onSync("cursorRemoved", this._onCursorRemoved, this).onSync("opened", this._onOpened, this).onSync("closed", this._onClosed, this).onAsync("datasetRendered", this._onDatasetRendered, this);
+ this.input = new Input({
+ input: $input,
+ hint: $hint
+ }).onSync("focused", this._onFocused, this).onSync("blurred", this._onBlurred, this).onSync("enterKeyed", this._onEnterKeyed, this).onSync("tabKeyed", this._onTabKeyed, this).onSync("escKeyed", this._onEscKeyed, this).onSync("upKeyed", this._onUpKeyed, this).onSync("downKeyed", this._onDownKeyed, this).onSync("leftKeyed", this._onLeftKeyed, this).onSync("rightKeyed", this._onRightKeyed, this).onSync("queryChanged", this._onQueryChanged, this).onSync("whitespaceChanged", this._onWhitespaceChanged, this);
+ this._setLanguageDirection();
+ }
+ _.mixin(Typeahead.prototype, {
+ _onSuggestionClicked: function onSuggestionClicked(type, $el) {
+ var datum;
+ if (datum = this.dropdown.getDatumForSuggestion($el)) {
+ this._select(datum);
+ }
+ },
+ _onCursorMoved: function onCursorMoved() {
+ var datum = this.dropdown.getDatumForCursor();
+ this.input.setInputValue(datum.value, true);
+ this.eventBus.trigger("cursorchanged", datum.raw, datum.datasetName);
+ },
+ _onCursorRemoved: function onCursorRemoved() {
+ this.input.resetInputValue();
+ this._updateHint();
+ },
+ _onDatasetRendered: function onDatasetRendered() {
+ this._updateHint();
+ },
+ _onOpened: function onOpened() {
+ this._updateHint();
+ this.eventBus.trigger("opened");
+ },
+ _onClosed: function onClosed() {
+ this.input.clearHint();
+ this.eventBus.trigger("closed");
+ },
+ _onFocused: function onFocused() {
+ this.isActivated = true;
+ this.dropdown.open();
+ },
+ _onBlurred: function onBlurred() {
+ this.isActivated = false;
+ this.dropdown.empty();
+ this.dropdown.close();
+ },
+ _onEnterKeyed: function onEnterKeyed(type, $e) {
+ var cursorDatum, topSuggestionDatum;
+ cursorDatum = this.dropdown.getDatumForCursor();
+ topSuggestionDatum = this.dropdown.getDatumForTopSuggestion();
+ if (cursorDatum) {
+ this._select(cursorDatum);
+ $e.preventDefault();
+ } else if (this.autoselect && topSuggestionDatum) {
+ this._select(topSuggestionDatum);
+ $e.preventDefault();
+ }
+ },
+ _onTabKeyed: function onTabKeyed(type, $e) {
+ var datum;
+ if (datum = this.dropdown.getDatumForCursor()) {
+ this._select(datum);
+ $e.preventDefault();
+ } else {
+ this._autocomplete(true);
+ }
+ },
+ _onEscKeyed: function onEscKeyed() {
+ this.dropdown.close();
+ this.input.resetInputValue();
+ },
+ _onUpKeyed: function onUpKeyed() {
+ var query = this.input.getQuery();
+ this.dropdown.isEmpty && query.length >= this.minLength ? this.dropdown.update(query) : this.dropdown.moveCursorUp();
+ this.dropdown.open();
+ },
+ _onDownKeyed: function onDownKeyed() {
+ var query = this.input.getQuery();
+ this.dropdown.isEmpty && query.length >= this.minLength ? this.dropdown.update(query) : this.dropdown.moveCursorDown();
+ this.dropdown.open();
+ },
+ _onLeftKeyed: function onLeftKeyed() {
+ this.dir === "rtl" && this._autocomplete();
+ },
+ _onRightKeyed: function onRightKeyed() {
+ this.dir === "ltr" && this._autocomplete();
+ },
+ _onQueryChanged: function onQueryChanged(e, query) {
+ this.input.clearHintIfInvalid();
+ query.length >= this.minLength ? this.dropdown.update(query) : this.dropdown.empty();
+ this.dropdown.open();
+ this._setLanguageDirection();
+ },
+ _onWhitespaceChanged: function onWhitespaceChanged() {
+ this._updateHint();
+ this.dropdown.open();
+ },
+ _setLanguageDirection: function setLanguageDirection() {
+ var dir;
+ if (this.dir !== (dir = this.input.getLanguageDirection())) {
+ this.dir = dir;
+ this.$node.css("direction", dir);
+ this.dropdown.setLanguageDirection(dir);
+ }
+ },
+ _updateHint: function updateHint() {
+ var datum, val, query, escapedQuery, frontMatchRegEx, match;
+ datum = this.dropdown.getDatumForTopSuggestion();
+ if (datum && this.dropdown.isVisible() && !this.input.hasOverflow()) {
+ val = this.input.getInputValue();
+ query = Input.normalizeQuery(val);
+ escapedQuery = _.escapeRegExChars(query);
+ frontMatchRegEx = new RegExp("^(?:" + escapedQuery + ")(.+$)", "i");
+ match = frontMatchRegEx.exec(datum.value);
+ match ? this.input.setHint(val + match[1]) : this.input.clearHint();
+ } else {
+ this.input.clearHint();
+ }
+ },
+ _autocomplete: function autocomplete(laxCursor) {
+ var hint, query, isCursorAtEnd, datum;
+ hint = this.input.getHint();
+ query = this.input.getQuery();
+ isCursorAtEnd = laxCursor || this.input.isCursorAtEnd();
+ if (hint && query !== hint && isCursorAtEnd) {
+ datum = this.dropdown.getDatumForTopSuggestion();
+ datum && this.input.setInputValue(datum.value);
+ this.eventBus.trigger("autocompleted", datum.raw, datum.datasetName);
+ }
+ },
+ _select: function select(datum) {
+ this.input.setQuery(datum.value);
+ this.input.setInputValue(datum.value, true);
+ this._setLanguageDirection();
+ this.eventBus.trigger("selected", datum.raw, datum.datasetName);
+ this.dropdown.close();
+ _.defer(_.bind(this.dropdown.empty, this.dropdown));
+ },
+ open: function open() {
+ this.dropdown.open();
+ },
+ close: function close() {
+ this.dropdown.close();
+ },
+ setVal: function setVal(val) {
+ if (this.isActivated) {
+ this.input.setInputValue(val);
+ } else {
+ this.input.setQuery(val);
+ this.input.setInputValue(val, true);
+ }
+ this._setLanguageDirection();
+ },
+ getVal: function getVal() {
+ return this.input.getQuery();
+ },
+ destroy: function destroy() {
+ this.input.destroy();
+ this.dropdown.destroy();
+ destroyDomStructure(this.$node);
+ this.$node = null;
+ }
+ });
+ return Typeahead;
+ function buildDomStructure(input, withHint) {
+ var $input, $wrapper, $dropdown, $hint;
+ $input = $(input);
+ $wrapper = $(html.wrapper).css(css.wrapper);
+ $dropdown = $(html.dropdown).css(css.dropdown);
+ $hint = $input.clone().css(css.hint).css(getBackgroundStyles($input));
+ $hint.val("").removeData().addClass("tt-hint").removeAttr("id name placeholder").prop("disabled", true).attr({
+ autocomplete: "off",
+ spellcheck: "false"
+ });
+ $input.data(attrsKey, {
+ dir: $input.attr("dir"),
+ autocomplete: $input.attr("autocomplete"),
+ spellcheck: $input.attr("spellcheck"),
+ style: $input.attr("style")
+ });
+ $input.addClass("tt-input").attr({
+ autocomplete: "off",
+ spellcheck: false
+ }).css(withHint ? css.input : css.inputWithNoHint);
+ try {
+ !$input.attr("dir") && $input.attr("dir", "auto");
+ } catch (e) {}
+ return $input.wrap($wrapper).parent().prepend(withHint ? $hint : null).append($dropdown);
+ }
+ function getBackgroundStyles($el) {
+ return {
+ backgroundAttachment: $el.css("background-attachment"),
+ backgroundClip: $el.css("background-clip"),
+ backgroundColor: $el.css("background-color"),
+ backgroundImage: $el.css("background-image"),
+ backgroundOrigin: $el.css("background-origin"),
+ backgroundPosition: $el.css("background-position"),
+ backgroundRepeat: $el.css("background-repeat"),
+ backgroundSize: $el.css("background-size")
+ };
+ }
+ function destroyDomStructure($node) {
+ var $input = $node.find(".tt-input");
+ _.each($input.data(attrsKey), function(val, key) {
+ _.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val);
+ });
+ $input.detach().removeData(attrsKey).removeClass("tt-input").insertAfter($node);
+ $node.remove();
+ }
+ }();
+ (function() {
+ var old, typeaheadKey, methods;
+ old = $.fn.typeahead;
+ typeaheadKey = "ttTypeahead";
+ methods = {
+ initialize: function initialize(o, datasets) {
+ datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 1);
+ o = o || {};
+ return this.each(attach);
+ function attach() {
+ var $input = $(this), eventBus, typeahead;
+ _.each(datasets, function(d) {
+ d.highlight = !!o.highlight;
+ });
+ typeahead = new Typeahead({
+ input: $input,
+ eventBus: eventBus = new EventBus({
+ el: $input
+ }),
+ withHint: _.isUndefined(o.hint) ? true : !!o.hint,
+ minLength: o.minLength,
+ autoselect: o.autoselect,
+ datasets: datasets
+ });
+ $input.data(typeaheadKey, typeahead);
+ }
+ },
+ open: function open() {
+ return this.each(openTypeahead);
+ function openTypeahead() {
+ var $input = $(this), typeahead;
+ if (typeahead = $input.data(typeaheadKey)) {
+ typeahead.open();
+ }
+ }
+ },
+ close: function close() {
+ return this.each(closeTypeahead);
+ function closeTypeahead() {
+ var $input = $(this), typeahead;
+ if (typeahead = $input.data(typeaheadKey)) {
+ typeahead.close();
+ }
+ }
+ },
+ val: function val(newVal) {
+ return !arguments.length ? getVal(this.first()) : this.each(setVal);
+ function setVal() {
+ var $input = $(this), typeahead;
+ if (typeahead = $input.data(typeaheadKey)) {
+ typeahead.setVal(newVal);
+ }
+ }
+ function getVal($input) {
+ var typeahead, query;
+ if (typeahead = $input.data(typeaheadKey)) {
+ query = typeahead.getVal();
+ }
+ return query;
+ }
+ },
+ destroy: function destroy() {
+ return this.each(unattach);
+ function unattach() {
+ var $input = $(this), typeahead;
+ if (typeahead = $input.data(typeaheadKey)) {
+ typeahead.destroy();
+ $input.removeData(typeaheadKey);
+ }
+ }
+ }
+ };
+ $.fn.typeahead = function(method) {
+ if (methods[method]) {
+ return methods[method].apply(this, [].slice.call(arguments, 1));
+ } else {
+ return methods.initialize.apply(this, arguments);
+ }
+ };
+ $.fn.typeahead.noConflict = function noConflict() {
+ $.fn.typeahead = old;
+ return this;
+ };
+ })();
+})(window.jQuery);
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/templates/403.html b/samples/development-frameworks/django/bootcamp/templates/403.html
new file mode 100644
index 00000000..42a4ecef
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/templates/403.html
@@ -0,0 +1,8 @@
+{% extends 'base.html' %}
+
+{% block main %}
+
+ Hey you!
+{% endblock main %}
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/templates/404.html b/samples/development-frameworks/django/bootcamp/templates/404.html
new file mode 100644
index 00000000..59162dd2
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/templates/404.html
@@ -0,0 +1,8 @@
+{% extends 'base.html' %}
+
+{% block main %}
+
+ We could not find the page you are looking for :(
+{% endblock main %}
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/templates/500.html b/samples/development-frameworks/django/bootcamp/templates/500.html
new file mode 100644
index 00000000..6aabf4e6
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/templates/500.html
@@ -0,0 +1,8 @@
+{% extends 'base.html' %}
+
+{% block main %}
+
+ Ooops! Something went wrong. That's all we know.
+{% endblock main %}
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/templates/base.html b/samples/development-frameworks/django/bootcamp/templates/base.html
new file mode 100644
index 00000000..f5ccc86a
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/templates/base.html
@@ -0,0 +1,85 @@
+{% load staticfiles i18n %}
+
+
+
+
+
+ {% block title %}Bootcamp{% endblock %}
+
+
+
+
+
+
+
+ {% block head %}{% endblock %}
+
+
+ {% block body %}
+
+
+
+ {% block main %}
+ {% endblock main %}
+
+
+
+
+ {% endblock body %}
+
+
+
diff --git a/samples/development-frameworks/django/bootcamp/templates/markdown_editor.html b/samples/development-frameworks/django/bootcamp/templates/markdown_editor.html
new file mode 100644
index 00000000..2a12969f
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/templates/markdown_editor.html
@@ -0,0 +1,82 @@
+{% load staticfiles %}
+
+
+
+
+
+
+
+
+
+
+
+
You can learn more about markdown syntax here .
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Help
+
+
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/templates/paginator.html b/samples/development-frameworks/django/bootcamp/templates/paginator.html
new file mode 100644
index 00000000..2d8f29f1
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/templates/paginator.html
@@ -0,0 +1,19 @@
+
\ No newline at end of file
diff --git a/samples/development-frameworks/django/bootcamp/urls.py b/samples/development-frameworks/django/bootcamp/urls.py
new file mode 100644
index 00000000..30cd75e4
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/urls.py
@@ -0,0 +1,45 @@
+# coding: utf-8
+
+from django.conf.urls import include, url
+from django.conf import settings
+from django.conf.urls.static import static
+from django.contrib.auth import views as auth_views
+
+from bootcamp.core import views as core_views
+from bootcamp.authentication import views as bootcamp_auth_views
+from bootcamp.activities import views as activities_views
+from bootcamp.search import views as search_views
+
+
+urlpatterns = [
+ url(r'^$', core_views.home, name='home'),
+ url(r'^login', auth_views.login, {'template_name': 'core/cover.html'},
+ name='login'),
+ url(r'^logout', auth_views.logout, {'next_page': '/'}, name='logout'),
+ url(r'^signup/$', bootcamp_auth_views.signup, name='signup'),
+ url(r'^settings/$', core_views.settings, name='settings'),
+ url(r'^settings/picture/$', core_views.picture, name='picture'),
+ url(r'^settings/upload_picture/$', core_views.upload_picture,
+ name='upload_picture'),
+ url(r'^settings/save_uploaded_picture/$', core_views.save_uploaded_picture,
+ name='save_uploaded_picture'),
+ url(r'^settings/password/$', core_views.password, name='password'),
+ url(r'^network/$', core_views.network, name='network'),
+ url(r'^feeds/', include('bootcamp.feeds.urls')),
+ url(r'^questions/', include('bootcamp.questions.urls')),
+ url(r'^articles/', include('bootcamp.articles.urls')),
+ url(r'^messages/', include('bootcamp.messenger.urls')),
+ url(r'^notifications/$', activities_views.notifications,
+ name='notifications'),
+ url(r'^notifications/last/$', activities_views.last_notifications,
+ name='last_notifications'),
+ url(r'^notifications/check/$', activities_views.check_notifications,
+ name='check_notifications'),
+ url(r'^search/$', search_views.search, name='search'),
+ url(r'^(?P[^/]+)/$', core_views.profile, name='profile'),
+ url(r'^i18n/', include('django.conf.urls.i18n', namespace='i18n')),
+]
+
+if settings.DEBUG:
+ urlpatterns += static(settings.MEDIA_URL,
+ document_root=settings.MEDIA_ROOT)
diff --git a/samples/development-frameworks/django/bootcamp/wsgi.py b/samples/development-frameworks/django/bootcamp/wsgi.py
new file mode 100644
index 00000000..0331415d
--- /dev/null
+++ b/samples/development-frameworks/django/bootcamp/wsgi.py
@@ -0,0 +1,9 @@
+import os
+from django.core.wsgi import get_wsgi_application
+from whitenoise.django import DjangoWhiteNoise
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bootcamp.settings")
+
+application = get_wsgi_application()
+application = DjangoWhiteNoise(application)
+
diff --git a/samples/development-frameworks/django/manage.py b/samples/development-frameworks/django/manage.py
new file mode 100644
index 00000000..978f21db
--- /dev/null
+++ b/samples/development-frameworks/django/manage.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+import os
+import sys
+
+if __name__ == "__main__":
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bootcamp.settings")
+
+ from django.core.management import execute_from_command_line
+
+ execute_from_command_line(sys.argv)
diff --git a/samples/development-frameworks/django/requirements.txt b/samples/development-frameworks/django/requirements.txt
new file mode 100644
index 00000000..61ae6b73
--- /dev/null
+++ b/samples/development-frameworks/django/requirements.txt
@@ -0,0 +1,11 @@
+Django==1.9.8
+dj-database-url==0.3.0
+dj-static==0.0.6
+gunicorn==19.6.0
+Unipath==1.0
+python-decouple==3
+Pillow==3.3.0
+Markdown==2.6.6
+bleach==1.4.3
+django-pyodbc-azure==1.9.6.0
+whitenoise==3.2
diff --git a/samples/development-frameworks/django/runtime.txt b/samples/development-frameworks/django/runtime.txt
new file mode 100644
index 00000000..fdf79660
--- /dev/null
+++ b/samples/development-frameworks/django/runtime.txt
@@ -0,0 +1 @@
+python-2.7.12
+ {{ comment.user.profile.get_screen_name }} + {{ comment.date|naturaltime }} +
+{{ comment.comment }}
+