Added django sample

This commit is contained in:
Meet Bhagdev
2016-11-08 13:10:49 -08:00
parent 4befe49622
commit 6a6d061e92
147 changed files with 9054 additions and 0 deletions
@@ -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.
@@ -0,0 +1 @@
web: gunicorn bootcamp.wsgi --log-file -
@@ -0,0 +1,87 @@
# Bootcamp
[![Build Status](https://travis-ci.org/vitorfs/bootcamp.svg?branch=master)](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/
@@ -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',
},
),
]
@@ -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'<a href="/{0}/">{1}</a> liked your post: <a href="/feeds/{2}/">{3}</a>'
_COMMENTED_TEMPLATE = u'<a href="/{0}/">{1}</a> commented on your post: <a href="/feeds/{2}/">{3}</a>'
_FAVORITED_TEMPLATE = u'<a href="/{0}/">{1}</a> favorited your question: <a href="/questions/{2}/">{3}</a>'
_ANSWERED_TEMPLATE = u'<a href="/{0}/">{1}</a> answered your question: <a href="/questions/{2}/">{3}</a>'
_ACCEPTED_ANSWER_TEMPLATE = u'<a href="/{0}/">{1}</a> accepted your answer: <a href="/questions/{2}/">{3}</a>'
_EDITED_ARTICLE_TEMPLATE = u'<a href="/{0}/">{1}</a> edited your article: <a href="/article/{2}/">{3}</a>'
_ALSO_COMMENTED_TEMPLATE = u'<a href="/{0}/">{1}</a> also commentend on the post: <a href="/feeds/{2}/">{3}</a>'
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
@@ -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;
}
@@ -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("<div style='text-align:center'><img src='/static/img/loading.gif'></div>");
$("#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();
});
@@ -0,0 +1,15 @@
{% load i18n %}
{% load humanize %}
<ul>
{% for notification in notifications %}
<li class="clearfix">
<img src="{{ notification.from_user.profile.get_picture }}" class="user-picture">
<p>{{ notification|safe }}</p>
<p><small>{{ notification.date|naturaltime }}</small></p>
</li>
{% empty %}
<li style="text-align: center">{% trans 'You have no unread notification' %}</li>
{% endfor %}
<li style="text-align: center"><a href="{% url 'notifications' %}">{% trans 'See all' %}</a></li>
</ul>
@@ -0,0 +1,30 @@
{% extends 'base.html' %}
{% load staticfiles %}
{% load i18n %}
{% load humanize %}
{% block title %} Notifications {% endblock %}
{% block head %}
<link href="{% static 'css/notifications.css' %}" rel="stylesheet">
{% endblock head %}
{% block main %}
<div class="page-header">
<h1>Notifications</h1>
</div>
<ul class="all-notifications">
{% for notification in notifications %}
<li class="clearfix">
<a href="{% url 'profile' notification.from_user.username %}"><img src="{{ notification.from_user.profile.get_picture }}" class="user-picture"></a>
<div>
<small>{{ notification.date|naturaltime }}</small>
<p>{{ notification|safe }}</p>
</div>
</li>
{% empty %}
<li>{% trans 'You have no notification' %}</li>
{% endfor %}
</ul>
{% endblock main %}
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
@@ -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))
@@ -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']
@@ -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')]),
),
]
@@ -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)
@@ -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;
}
@@ -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("<div style='text-align: center; padding-top: 1em'><img src='/static/img/loading.gif'></div>");
},
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();
}
});
}
});
});
@@ -0,0 +1,19 @@
{% extends 'base.html' %}
{% load staticfiles %}
{% load i18n %}
{% block title %}{{ article.title }}{% endblock %}
{% block head %}
<link href="{% static 'css/articles.css' %}" rel="stylesheet">
<script src="{% static 'js/articles.js' %}"></script>
{% endblock head %}
{% block main %}
<ol class="breadcrumb">
<li><a href="{% url 'articles' %}">{% trans 'Articles' %}</a></li>
<li class="active">{% trans 'Article' %}</li>
</ol>
{% include 'articles/partial_article.html' with article=article %}
{% include 'articles/partial_article_comments.html' with article=article %}
{% endblock main %}
@@ -0,0 +1,41 @@
{% extends 'base.html' %}
{% load staticfiles %}
{% load i18n %}
{% block title %} {% trans 'Articles' %} {% endblock %}
{% block head %}
<link href="{% static 'css/articles.css' %}" rel="stylesheet">
{% endblock head %}
{% block main %}
<div class="page-header">
<a href="{% url 'write' %}" class="btn btn-primary pull-right">
<span class="glyphicon glyphicon-pencil"></span> {% trans 'Write Article' %}
</a>
<a href="{% url 'drafts' %}" class="btn btn-default pull-right" style="margin-right: .8em">{% trans 'Drafts' %}</a>
<h1>{% trans 'Articles' %}</h1>
</div>
<div class="row">
<div class="col-md-10">
<section class="articles">
{% for article in articles %}
{% include 'articles/partial_article.html' with article=article %}
{% empty %}
<h4 class="no-data">{% trans 'There is no published article yet' %}. <a href="{% url 'write' %}">{% trans 'Be the first one to publish' %}!</a></h4>
{% endfor %}
</section>
</div>
<div class="col-md-2 popular-tags">
<h4>{% trans 'Popular Tags' %}</h4>
{% for tag, count in popular_tags %}
<a href="{% url 'tag' tag %}"><span class="label label-default">{{ count }} {{ tag }}</span></a>
{% endfor %}
</div>
</div>
<div class="row">
<div class="col-md-12">
{% include 'paginator.html' with paginator=articles %}
</div>
</div>
{% endblock main %}
@@ -0,0 +1,42 @@
{% extends 'base.html' %}
{% load staticfiles %}
{% load i18n %}
{% block head %}
<script src="{% static 'js/articles.js' %}"></script>
{% endblock head %}
{% block main %}
<ol class="breadcrumb">
<li><a href="{% url 'articles' %}">{% trans 'Articles' %}</a></li>
<li class="active">{% trans 'Drafts' %}</li>
</ol>
<table class="table table-striped">
<thead>
<tr>
<th>{% trans 'Title' %}</th>
<th>{% trans 'Content' %}</th>
<th>{% trans 'Tags' %}</th>
</tr>
</thead>
<tbody>
{% for article in drafts %}
<tr>
<td><a href="{% url 'edit_article' article.pk %}">{{ article.title }}</a></td>
<td>{{ article.get_summary_as_markdown|safe }}</td>
<td>
{% for tag in article.get_tags %}
<span class="label label-default">{{ tag }}</span>
{% endfor %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="4" style="text-align: center">
{% trans 'No draft to display' %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock main %}
@@ -0,0 +1,39 @@
{% extends 'base.html' %}
{% load staticfiles %}
{% load i18n %}
{% block head %}
<script src="{% static 'js/articles.js' %}"></script>
{% endblock head %}
{% block main %}
<ol class="breadcrumb">
<li><a href="{% url 'articles' %}">{% trans 'Articles' %}</a></li>
<li><a href="{% url 'drafts' %}">{% trans 'Drafts' %}</a></li>
<li class="active">{% trans 'Edit' %}</li>
</ol>
<form action="{% url 'edit_article' form.instance.pk %}" method="post" role="form">
{% csrf_token %}
{{ form.status }}
{% for field in form.visible_fields %}
<div class="form-group{% if field.errors %} has-error{% endif %}">
<label for="{{ field.label }}" class="control-label">{{ field.label }}</label>
{% if field.label == 'Content' %}
{% include 'markdown_editor.html' with textarea='id_content' %}
{% endif %}
{{ field }}
{% if field.help_text %}
<span class="help-block">{{ field.help_text }}</span>
{% endif %}
{% for error in field.errors %}
<label class="control-label">{{ error }}</label>
{% endfor %}
</div>
{% endfor %}
<div class="form-group">
<button type="button" class="btn btn-primary publish">{% trans 'Publish' %}</button>
<button type="button" class="btn btn-default draft">{% trans 'Save Draft' %}</button>
<a href="{% url 'drafts' %}" class="btn btn-default">{% trans 'Cancel' %}</a>
</div>
</form>
{% endblock main %}
@@ -0,0 +1,27 @@
<article>
<h2><a href="{% url 'article' article.slug %}">{{ article.title }}</a></h2>
<div class="info">
<span class="date">
<span class="glyphicon glyphicon-calendar"></span>
{{ article.create_date }}
</span>
<span class="user">
<a href="{% url 'profile' article.create_user.username %}"><img src="{{ article.create_user.profile.get_picture }}"></a>
<a href="{% url 'profile' article.create_user.username %}">{{ article.create_user.profile.get_screen_name }}</a>
</span>
<span class="comments">
<span class="glyphicon glyphicon-comment"></span>
{{ article.get_comments.count }} Comments
</span>
</div>
<div class="content">
{{ article.get_content_as_markdown|safe }}
</div>
{% if article.get_tags %}
<div class="tags">
{% for tag in article.get_tags %}
<a href="{% url 'tag' tag.tag %}"><span class="label label-default">{{ tag }}</span></a>
{% endfor %}
</div>
{% endif %}
</article>
@@ -0,0 +1,12 @@
{% load humanize %}
<div class="comment">
<a href="{% url 'profile' comment.user.username %}"><img src="{{ comment.user.profile.get_picture }}" class="comment-portrait"></a>
<div class="comment-text">
<h5>
<a href="{% url 'profile' comment.user.username %}">{{ comment.user.profile.get_screen_name }}</a>
<small>{{ comment.date|naturaltime }}</small>
</h5>
<p>{{ comment.comment }}</p>
</div>
</div>
@@ -0,0 +1,23 @@
{% load i18n %}
<hr>
<span class="pull-right text-muted" id="comment-helper" style="display: none"><small>{% trans 'Press Ctrl + Enter to post' %}</small></span>
<h4><span class="comment-count">{{ article.get_comments.count }}</span> {% trans 'Comments' %}</h4>
<div class="post-comment clearfix">
<form role="form" id="comment-form">
{% csrf_token %}
<input type="hidden" name="article" value="{{ article.pk }}">
<div class="user-portrait clearfix">
<img src="{{ user.profile.get_picture }}">
</div>
<div class="comment-input clearfix">
<textarea class="form-control" rows="1" placeholder="{% trans 'Write a comment...' %}" name="comment" id="comment"></textarea>
</div>
</form>
</div>
<div class="well well-sm" id="comment-list">
{% for comment in article.get_comments %}
{% include 'articles/partial_article_comment.html' with comment=comment %}
{% empty %}
<div style="padding: .6em 0">{% trans 'Be the first one to comment!' %}</div>
{% endfor %}
</div>
@@ -0,0 +1,55 @@
{% extends 'base.html' %}
{% load staticfiles %}
{% load i18n %}
{% block head %}
<script src="{% static 'js/articles.js' %}"></script>
{% endblock head %}
{% block main %}
<ol class="breadcrumb">
<li><a href="{% url 'articles' %}">{% trans 'Articles' %}</a></li>
<li class="active">{% trans 'Write Article' %}</li>
</ol>
<form action="{% url 'write' %}" method="post" role="form">
{% csrf_token %}
{{ form.status }}
{% for field in form.visible_fields %}
<div class="form-group{% if field.errors %} has-error{% endif %}">
<label for="{{ field.label }}" class="control-label">{{ field.label }}</label>
{% if field.label == 'Content' %}
{% include 'markdown_editor.html' with textarea='id_content' %}
{% endif %}
{{ field }}
{% if field.help_text %}
<span class="help-block">{{ field.help_text }}</span>
{% endif %}
{% for error in field.errors %}
<label class="control-label">{{ error }}</label>
{% endfor %}
</div>
{% endfor %}
<div class="form-group">
<button type="button" class="btn btn-primary publish">{% trans 'Publish' %}</button>
<button type="button" class="btn btn-default draft">{% trans 'Save Draft' %}</button>
<button type="button" class="btn btn-default preview" data-toggle="modal" data-target="#preview">{% trans 'Preview' %}</button>
<a href="{% url 'articles' %}" class="btn btn-default">{% trans 'Cancel' %}</a>
</div>
</form>
<div class="modal fade" id="preview">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Article Preview</h4>
</div>
<div class="modal-body"></div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% endblock main %}
@@ -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)
@@ -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<tag_name>.+)/$', views.tag, name='tag'),
url(r'^edit/(?P<id>\d+)/$', views.edit, name='edit_article'),
url(r'^(?P<slug>[-\w]+)/$', views.article, name='article'),
]
@@ -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()
@@ -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 <strong>alphanumeric</strong>, <strong>_</strong> and <strong>.</strong> 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
@@ -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',
},
),
]
@@ -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)
@@ -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;
}
@@ -0,0 +1,30 @@
{% extends 'base.html' %}
{% load staticfiles i18n %}
{% block head %}
<link href="{% static 'css/signup.css' %}" rel="stylesheet">
{% endblock head %}
{% block body %}
<h1 class="logo"><a href="{% url 'home' %}">Bootcamp</a></h1>
<div class="signup">
<h2>{% trans 'Sign up for Bootcamp' %}</h2>
<form action="{% url 'signup' %}" method="post" role="form">
{% csrf_token %}
{% for field in form.visible_fields %}
<div class="form-group{% if field.errors %} has-error{% endif %}">
<label for="{{ field.label }}">{{ field.label }}</label>
{{ field }}
{% if field.help_text %}
<span class="help-block">{{ field.help_text|safe }}</span>
{% endif %}
{% for error in field.errors %}
<label class="control-label">{{ error }}</label>
{% endfor %}
</div>
{% endfor %}
<button type="submit" class="btn btn-primary btn-lg">{% trans 'Create an account' %}</button>
</form>
</div>
{% endblock body %}
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
@@ -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()})
@@ -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
@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.
@@ -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;
}
@@ -0,0 +1,7 @@
.users {
margin-top: 2em;
}
.users .panel-heading img {
margin-right: .6em;
}
@@ -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;
}
@@ -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();
});
});
@@ -0,0 +1,46 @@
{% extends 'base.html' %}
{% load staticfiles %}
{% load i18n %}
{% block head %}
<link href="{% static 'css/cover.css' %}" rel="stylesheet">
{% endblock head %}
{% block body %}
<a href="https://github.com/vitorfs/bootcamp"><img style="position: absolute; top: 0; right: 0; border: 0;" src="https://camo.githubusercontent.com/e7bbb0521b397edbd5fe43e7f760759336b5e05f/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f677265656e5f3030373230302e706e67" alt="Fork me on GitHub" data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_green_007200.png"></a>
<div class="cover">
<h1 class="logo">Bootcamp</h1>
{% if form.non_field_errors %}
{% for error in form.non_field_errors %}
<div class="alert alert-danger alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
{{ error }}
</div>
{% endfor %}
{% endif %}
<div class="login">
<h2>{% trans 'Log in' %}</h2>
<form method="post" action="{% url 'login' %}" role="form">
{% csrf_token %}
<div class="form-group{% if form.username.errors %} has-error{% endif %}">
<label for="username">{% trans 'Username' %}</label>
<input type="text" class="form-control" id="username" name="username">
{% for error in form.username.errors %}
<label class="control-label">{{ error }}</label>
{% endfor %}
</div>
<div class="form-group{% if form.password.errors %} has-error{% endif %}">
<label for="password">{% trans 'Password' %}</label>
<input type="password" class="form-control" id="password" name="password">
{% for error in form.password.errors %}
<label class="control-label">{{ error }}</label>
{% endfor %}
</div>
<div class="form-group">
<button type="submit" class="btn btn-default">{% trans 'Log in' %}</button>
<a href="{% url 'signup' %}" class="btn btn-link">{% trans 'Sign up for Bootcamp' %}</a>
</div>
</form>
</div>
</div>
{% endblock body %}
@@ -0,0 +1,50 @@
{% extends 'base.html' %}
{% load staticfiles %}
{% load i18n %}
{% block title %}{% trans 'Network' %}{% endblock %}
{% block head %}
<link href="{% static 'css/network.css' %}" rel="stylesheet">
{% endblock head %}
{% block main %}
<div class="page-header">
<h1>Network</h1>
</div>
<div class="users">
<div class="row">
{% for user in users %}
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<img src="{{ user.profile.get_picture }}" style="width:20px">
<a href="{% url 'profile' user.username %}">{{ user.profile.get_screen_name }}</a>
</div>
<div class="panel-body">
{% if user.profile.job_title %}
<p><strong>{% trans 'Job Title' %}:</strong> {{ user.profile.job_title }}</p>
{% endif %}
<p><strong>{% trans 'Username' %}: </strong> {{ user.username }}</p>
{% if user.profile.location %}
<p><strong>{% trans 'Location' %}:</strong> {{ user.profile.location }}</p>
{% endif %}
{% if user.profile.url %}
<p><strong>{% trans 'Url' %}:</strong> {{ user.profile.get_url }}</p>
{% endif %}
</div>
</div>
</div>
{% if forloop.counter|divisibleby:3 %}</div><div class="row">{% endif %}
{% endfor %}
</div>
<div class="row">
<div class="col-md-12">
{% include 'paginator.html' with paginator=users %}
</div>
</div>
</div>
{% endblock main %}
@@ -0,0 +1,6 @@
{% load i18n %}
<div class="list-group">
<a href="{% url 'settings' %}" class="list-group-item{% if active == 'profile' %} active{% endif %}">{% trans 'Profile' %}</a>
<a href="{% url 'picture' %}" class="list-group-item{% if active == 'picture' %} active{% endif %}">{% trans 'Picture' %}</a>
<a href="{% url 'password' %}" class="list-group-item{% if active == 'password' %} active{% endif %}">{% trans 'Password' %}</a>
</div>
@@ -0,0 +1,49 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans 'Account Settings' %}{% endblock %}
{% block main %}
<div class="page-header">
<h1>{% trans 'Account Settings' %}</h1>
</div>
<div class="row" style="margin-top: 2em">
<div class="col-md-3">
{% include 'core/partial_settings_menu.html' with active='password' %}
</div>
<div class="col-md-9">
{% if messages %}
{% for message in messages %}
<div class="alert alert-success alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
{{ message }}
</div>
{% endfor %}
{% endif %}
<h3 style="margin-top: 0">{% trans 'Change Password' %}</h3>
<form role="form" class="form-horizontal" method="post" action="{% url 'password' %}">
{% csrf_token %}
{{ form.id }}
{% for field in form.visible_fields %}
<div class="form-group{% if field.errors %} has-error{% endif %}">
<label for="{{ field.label }}" class="col-sm-3 control-label">{{ field.label }}</label>
<div class="col-sm-9">
{{ field }}
{% if field.help_text %}
<span class="help-block">{{ field.help_text }}</span>
{% endif %}
{% for error in field.errors %}
<label class="control-label">{{ error }}</label>
{% endfor %}
</div>
</div>
{% endfor %}
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<button type="submit" class="btn btn-primary">{% trans 'Save' %}</button>
</div>
</div>
</form>
</div>
</div>
{% endblock main %}
@@ -0,0 +1,75 @@
{% extends 'base.html' %}
{% load i18n static %}
{% block title %}{% trans 'Account Settings' %}{% endblock %}
{% block head %}
<link href="{% static 'css/jquery.Jcrop.min.css' %}" rel="stylesheet">
<script src="{% static 'js/jquery.Jcrop.min.js' %}"></script>
<script src="{% static 'js/picture.js' %}"></script>
{% endblock head %}
{% block main %}
<div class="page-header">
<h1>{% trans 'Account Settings' %}</h1>
</div>
<div class="row" style="margin-top: 2em">
<div class="col-md-3">
{% include 'core/partial_settings_menu.html' with active='picture' %}
</div>
<div class="col-md-9">
{% if messages %}
{% for message in messages %}
<div class="alert alert-success alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
{{ message }}
</div>
{% endfor %}
{% endif %}
<h3 style="margin-top: 0">{% trans 'Change Picture' %}</h3>
<img src="{{ user.profile.get_picture }}" style="width: 150px; border-radius: 5px; margin-bottom: 1em;">
<form enctype="multipart/form-data" method="post" action="{% url 'upload_picture' %}" id="picture-upload-form">
{% csrf_token %}
<input type="file" name="picture" style="display: none">
<button type="button" class="btn btn-default" id="btn-upload-picture">{% trans 'Upload new picture' %}</button>
</form>
{% if uploaded_picture %}
<form method="post" action="{% url 'save_uploaded_picture' %}">
{% csrf_token %}
<div class="modal fade" id="modal-upload-picture">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">{% trans 'Crop Picture' %}</h4>
</div>
<div class="modal-body">
<div class="selected-picture">
<p>{% trans 'Crop the profile picture and then click on the' %} <strong>{% trans 'Save Picture' %}</strong> {% trans 'button' %}</p>
<img src="{% get_media_prefix %}profile_pictures/{{ user.username }}_tmp.jpg?_={% now 'U' %}" id="crop-picture">
<input type="hidden" id="x" name="x" />
<input type="hidden" id="y" name="y" />
<input type="hidden" id="w" name="w" />
<input type="hidden" id="h" name="h" />
</div>
<script type="text/javascript">
$(function () {
$("#modal-upload-picture").modal();
window.history.pushState("", "", "/settings/picture/");
});
</script>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{% trans 'Close' %}</button>
<button type="submit" class="btn btn-primary">{% trans 'Save changes' %}</button>
</div>
</div>
</div>
</div>
</form>
{% endif %}
</div>
</div>
{% endblock main %}
@@ -0,0 +1,55 @@
{% extends 'base.html' %}
{% load staticfiles %}
{% load i18n %}
{% block title %}{{ page_user.profile.get_screen_name }}{% endblock %}
{% block head %}
<link href="{% static 'css/profile.css' %}" rel="stylesheet">
<link href="{% static 'css/feeds.css' %}" rel="stylesheet">
<script src="{% static 'js/jquery.bullseye-1.0-min.js' %}"></script>
<script src="{% static 'js/feeds.js' %}"></script>
{% endblock head %}
{% block main %}
<div class="page-header">
<h1>{{ page_user.profile.get_screen_name }} {% if page_user.get_full_name %}<small>({{ page_user.username }})</small>{% endif %}</h1>
</div>
<div class="profile">
<div class="row">
<div class="col-md-3 user-profile">
<img src="{{ page_user.profile.get_picture }}" class="picture">
<ul>
{% if page_user.profile.job_title %}
<li><span class="glyphicon glyphicon-briefcase"></span> {{ page_user.profile.job_title }}</li>
{% endif %}
{% if page_user.profile.location %}
<li><span class="glyphicon glyphicon-map-marker"></span> {{ page_user.profile.location }}</li>
{% endif %}
{% if page_user.profile.url %}
<li><span class="glyphicon glyphicon-globe"></span> <a href="{{ page_user.profile.get_url }}" target="_blank">{{ page_user.profile.get_url }}</a></li>
{% endif %}
</ul>
</div>
<div class="col-md-9">
<h4>{% trans 'Last Feeds by' %} {{ page_user.profile.get_screen_name }}</h4>
<div class="stream-update">
<a href="#"><span class="new-posts"></span> new posts</a>
</div>
<ul class="stream">
{% for feed in feeds %}
{% include 'feeds/partial_feed.html' with feed=feed %}
{% endfor %}
</ul>
<div class="load">
<img src="{% static 'img/loading.gif' %}">
</div>
<form method="get" action="{% url 'load' %}" id="load_feed" autocomplete="off">
<input type="hidden" name="feed_source" id="feed_source" value="{{ page_user.pk }}">
<input type="hidden" name="from_feed" value="{{ from_feed }}">
<input type="hidden" name="page" value="{{ page }}">
</form>
</div>
</div>
</div>
{% endblock main %}
@@ -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 %}
<div class="page-header">
<h1>{% trans 'Account Settings' %}</h1>
</div>
<div class="row" style="margin-top: 2em">
<div class="col-md-3">
{% include 'core/partial_settings_menu.html' with active='profile' %}
</div>
<div class="col-md-9">
{% if messages %}
{% for message in messages %}
<div class="alert alert-success alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
{{ message }}
</div>
{% endfor %}
{% endif %}
<h3 style="margin-top: 0">{% trans 'Edit Profile' %}</h3>
<form role="form" class="form-horizontal" method="post" action="{% url 'settings' %}">
{% csrf_token %}
{% for field in form.visible_fields %}
<div class="form-group{% if field.errors %} has-error{% endif %}">
<label for="{{ field.label }}" class="col-sm-2 control-label">{{ field.label }}</label>
<div class="col-sm-10">
{{ field }}
{% if field.help_text %}
<span class="help-block">{{ field.help_text }}</span>
{% endif %}
{% for error in field.errors %}
<label class="control-label">{{ error }}</label>
{% endfor %}
</div>
</div>
{% endfor %}
{% comment %}
<div class="form-group">
<label for="language" class="col-sm-2 control-label">Language</label>
<div class="col-sm-10">
<select id="language" name="language" class="form-control">
{% for lang in LANGUAGES %}
<option value="{{ lang.0 }}">{{ lang.1 }}</option>
{% endfor %}
</select>
</div>
</div>
{% endcomment %}
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-primary">{% trans 'Save' %}</button>
</div>
</div>
</form>
</div>
</div>
{% endblock main %}
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
@@ -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/')
@@ -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
@@ -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',
},
),
]
@@ -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))
@@ -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;
}
@@ -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("<li class='loadcomment'><img src='/static/img/loading.gif'></li>");
},
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);
});
});
@@ -0,0 +1,22 @@
{% extends 'base.html' %}
{% load staticfiles %}
{% load i18n %}
{% load humanize %}
{% block title %} {% trans 'Feed' %} {% endblock %}
{% block head %}
<link href="{% static 'css/feeds.css' %}" rel="stylesheet">
<script src="{% static 'js/jquery.bullseye-1.0-min.js' %}"></script>
<script src="{% static 'js/feeds.js' %}"></script>
{% endblock head %}
{% block main %}
<div class="page-header">
<h1>Feed</h1>
</div>
<ul class="stream">
{% include 'feeds/partial_feed.html' with feed=feed %}
</ul>
{% endblock main %}
@@ -0,0 +1,70 @@
{% extends 'base.html' %}
{% load staticfiles %}
{% load i18n %}
{% block head %}
<link href="{% static 'css/feeds.css' %}?v=1" rel="stylesheet">
<script src="{% static 'js/jquery.bullseye-1.0-min.js' %}"></script>
<script src="{% static 'js/feeds.js' %}?v=1"></script>
{% endblock head %}
{% block main %}
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="page-header">
<button type="button" class="btn btn-primary pull-right btn-compose" title="{% trans 'Press Ctrl + P to compose' %}">
<span class="glyphicon glyphicon-share-alt"></span> {% trans 'Compose' %}
</button>
<h1>{% trans 'Feed' %}</h1>
</div>
<div class="panel panel-default panel-feed">
<div class="panel-heading">
<h3 class="panel-title">{% trans 'Latest posts' %}</h3>
</div>
<div class="panel-body">
<div class="compose">
<h2>{% trans "Compose a new post" %}</h2>
<form role="form" id="compose-form">
{% csrf_token %}
<input type="hidden" name="last_feed">
<div class="form-group">
<textarea class="form-control" rows="3" name="post"></textarea>
</div>
<div class="form-group">
<button type="button" class="btn btn-primary btn-post">
<span class="glyphicon glyphicon-send"></span> {% trans 'Post' %}
</button>
<button type="button" class="btn btn-default btn-cancel-compose">{% trans 'Cancel' %}</button>
<span class="help-block help-count pull-right">255</span>
</div>
</form>
</div>
<div class="stream-update">
<a href="#"><span class="new-posts"></span> {% trans 'new posts' %}</a>
</div>
<ul class="stream">
{% for feed in feeds %}
{% include 'feeds/partial_feed.html' with feed=feed %}
{% endfor %}
</ul>
<div class="load">
<img src="{% static 'img/loading.gif' %}">
</div>
<form method="get" action="{% url 'load' %}" id="load_feed" autocomplete="off">
<input type="hidden" name="feed_source" id="feed_source" value="all">
<input type="hidden" name="from_feed" value="{{ from_feed }}">
<input type="hidden" name="page" value="{{ page }}">
</form>
</div>
</div>
</div>
</div>
{% endblock main %}
@@ -0,0 +1,44 @@
{% load i18n %}
{% load humanize %}
<li feed-id="{{ feed.pk }}" csrf="{{ csrf_token }}">
<div class="feed-container">
<a href="{% url 'profile' feed.user.username %}"><img src="{{ feed.user.profile.get_picture }}" class="user"></a>
<div class="post">
{% if feed.user == user %}
<span class="glyphicon glyphicon-remove remove-feed" title="{% trans 'Click to remove this feed' %}"></span>
{% endif %}
<h3><a href="{% url 'profile' feed.user.username %}">{{ feed.user.profile.get_screen_name }}</a> <small>{{ feed.date|naturaltime }}</small></h3>
<p>{{ feed.linkfy_post|safe }}</p>
<div class="interaction">
{% if user in feed.get_likers %}
<a href="#" class="like unlike">
<span class="glyphicon glyphicon-thumbs-up"></span>
<span class="text">{% trans 'Unlike' %}</span>
(<span class="like-count">{{ feed.likes }}</span>)
</a>
{% else %}
<a href="#" class="like">
<span class="glyphicon glyphicon-thumbs-up"></span>
<span class="text">{% trans 'Like' %}</span>
(<span class="like-count">{{ feed.likes }}</span>)
</a>
{% endif %}
<a href="#" class="comment">
<span class="glyphicon glyphicon-comment"></span> {% trans 'Comment' %}
(<span class="comment-count">{{ feed.comments }}</span>)
</a>
</div>
<div class="comments">
<form role="form" method="post" action="{{ comment }}" onsubmit="return false">
{% csrf_token %}
<input type="hidden" name="feed" value="{{ feed.pk }}">
<input type="text" class="form-control input-sm" placeholder="{% trans 'Write a comment...' %}" name="post" maxlength="255">
</form>
<ol class="clearfix">
{% comment %} Place holder to load feed comments {% endcomment %}
</ol>
</div>
</div>
</div>
</li>
@@ -0,0 +1,22 @@
{% load humanize %}
{% load i18n %}
{% for comment in feed.get_comments %}
<li feed-id="{{ comment.pk }}" csrf="{{ csrf_token }}">
{% if comment.user == user %}
<span class="glyphicon glyphicon-remove remove-feed" title="{% trans 'Click to remove this comment' %}"></span>
{% endif %}
<a href="{% url 'profile' comment.user.username %}">
<img src="{{ comment.user.profile.get_picture }}" class="user-comment">
</a>
<h4>
<a href="{% url 'profile' comment.user.username %}">
{{ comment.user.profile.get_screen_name }}
</a>
<small>{{ comment.date|naturaltime }}</small>
</h4>
<div>{{ comment.linkfy_post|safe }}</div>
</li>
{% empty %}
<li class="empty">{% trans 'Be the first one to comment' %}</li>
{% endfor %}
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
@@ -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'),
]
@@ -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()
@@ -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 <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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"
@@ -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 <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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 ""
@@ -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 <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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"
@@ -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 <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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 "登出"
@@ -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',
},
),
]
@@ -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
@@ -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;
}
@@ -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();
});
@@ -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;
});
});
@@ -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)
});
}
});
});
@@ -0,0 +1,24 @@
{% extends 'base.html' %}
{% load staticfiles %}
{% block head %}
<link href="{% static 'css/messages.css' %}" rel="stylesheet">
<script src="{% static 'js/messages.js' %}"></script>
<script src="{% static 'js/jquery.typeahead.bundle.js' %}"></script>
{% endblock head %}
{% block main %}
<div class="page-header">
<a href="{% url 'new_message' %}" class="btn btn-primary pull-right">New message</a>
<h1>{% block page_header %}{% endblock %}</h1>
</div>
<div class="row" style="margin-top: 1em">
<div class="col-md-3">
{% include 'messenger/includes/partial_conversations.html' with conversations=conversations active=active %}
</div>
<div class="col-md-9">
{% block container %}
{% endblock container %}
</div>
</div>
{% endblock main %}
@@ -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 %}
<ul class="conversation">
{% for message in messages %}
{% include 'messenger/includes/partial_message.html' with message=message %}
{% endfor %}
<li class="send-message">
<img src="{{ user.profile.get_picture }}" class="picture">
<div style="margin-top: .3em">
<form role="form" method="post" action="{% url 'send_message' %}" id="send">
{% csrf_token %}
<input type="hidden" name="to" value="{{ active }}">
<input class="form-control" type="text" name="message" placeholder="Write a message..." maxlength="1000" autocomplete="off">
</form>
</div>
</li>
</ul>
{% else %}
<h4>Your inbox is empty!</h4>
{% endif %}
{% endblock container %}
@@ -0,0 +1,23 @@
{% load i18n %}
<div class="list-group">
{% comment %}
<a href="{% url 'inbox' %}" class="list-group-item{% if active == 'inbox' %} active{% endif %}">
<span>{% trans 'Inbox' %}</span>
<span class="badge">0</span>
</a>
{% endcomment %}
{% for conversation in conversations %}
<a href="{% url 'messages' conversation.user.username %}"
class="list-group-item{% if active == conversation.user.username %} active{% endif %}">
<img src="{{ conversation.user.profile.get_picture }}" class="conversation-portrait">
{{ conversation.user.profile.get_screen_name }}
{% if conversation.unread > 0 %}
<span class="badge pull-right">{{ conversation.unread }}</span>
{% endif %}
</a>
{% empty %}
<a href="#" class="list-group-item">{% trans 'Start a conversation' %}</a>
{% endfor %}
</div>
@@ -0,0 +1,14 @@
<li>
<img src="{{ message.from_user.profile.get_picture }}" class="picture">
<div>
<h5>
<small class="pull-right">
{{ message.date|date:'N d G:i' }}
</small>
<a href="{% url 'profile' message.from_user.username %}">
{{ message.from_user.profile.get_screen_name }}
</a>
</h5>
{{ message.message }}
</div>
</li>
@@ -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 %}
<form class="form-horizontal" role="form" method="post">
{% csrf_token %}
<div class="form-group">
<label for="to" class="col-sm-1 control-label">To</label>
<div class="col-sm-11">
<input class="form-control typeahead" type="text" id="to" name="to">
</div>
</div>
<div class="form-group">
<label for="to" class="col-sm-1 control-label">Message</label>
<div class="col-sm-11">
<textarea class="form-control" id="message" name="message" placeholder="Write a message" rows="4"></textarea>
</div>
</div>
<div class="form-group">
<div class="col-sm-11 col-sm-offset-1">
<button type="submit" class="btn btn-primary">Send</button>
</div>
</div>
</form>
<script src="{% static 'js/messages.typehead.js' %}"></script>
{% endblock container %}
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
@@ -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<username>[^/]+)/$', views.messages, name='messages'),
]
@@ -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)
@@ -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']
@@ -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')]),
),
]
@@ -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
@@ -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;
}
@@ -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);
}
});
});
});

Some files were not shown because too many files have changed in this diff Show More