mirror of
https://github.com/Microsoft/sql-server-samples.git
synced 2025-12-08 14:58:54 +00:00
Added django sample
This commit is contained in:
@@ -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
|
||||
|
||||
[](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
|
||||
+33
@@ -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();
|
||||
});
|
||||
+15
@@ -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>
|
||||
+30
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
+19
@@ -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 %}
|
||||
+41
@@ -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 %}
|
||||
+42
@@ -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 %}
|
||||
+27
@@ -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>
|
||||
+12
@@ -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>
|
||||
+23
@@ -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">×</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
|
||||
+32
@@ -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;
|
||||
}
|
||||
+30
@@ -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">×</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 %}
|
||||
+6
@@ -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">×</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">×</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">×</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">×</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 %}
|
||||
+44
@@ -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>
|
||||
+22
@@ -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;
|
||||
});
|
||||
});
|
||||
+32
@@ -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)
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
+24
@@ -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 %}
|
||||
+28
@@ -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 %}
|
||||
+23
@@ -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>
|
||||
+14
@@ -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
Reference in New Issue
Block a user