diff --git a/.travis.yml b/.travis.yml
index f18f5ced8..5142c38dd 100755
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,9 +1,10 @@
language: python
python:
- "3.6"
+ - "3.7"
env:
- - DJANGO_VERSION=2.0.7
- - DJANGO_VERSION=1.11.14
+ - DJANGO_VERSION=2.2.9
+ - DJANGO_VERSION=3.0.3
branches:
only:
- master
diff --git a/README.rst b/README.rst
index 5c960ac5d..0aefc68c8 100755
--- a/README.rst
+++ b/README.rst
@@ -28,15 +28,15 @@ The project has four basic apps:
* News (A Twitter-like microblog)
* Articles (A collaborative blog)
* Question & Answers (A Stack Overflow-like platform)
-* Messeger (A basic chat-a-like tool for asynchronous communication.)
+* Messenger (A basic chat-a-like tool for asynchronous communication.)
Technology Stack
----------------
* Python_ 3.6.x / 3.7.x
-* `Django Web Framework`_ 1.11.x / 2.0.x
+* `Django Web Framework`_ 2.2.x
* PostgreSQL_
-* `Redis 3.2`_
+* `Redis 5.0`_
* Daphne_
* Caddy_
* Docker_
@@ -52,7 +52,7 @@ Technology Stack
.. _Python: https://www.python.org/
.. _`Django Web Framework`: https://www.djangoproject.com/
.. _PostgreSQL: https://www.postgresql.org/
-.. _`Redis 3.2`: https://redis.io/documentation
+.. _`Redis 5.0`: https://redis.io/documentation
.. _Daphne: https://github.com/django/daphne/
.. _Caddy: https://caddyserver.com/docs
.. _Docker: https://docs.docker.com/
diff --git a/bootcamp/__init__.py b/bootcamp/__init__.py
index dc0c5c188..3a64db1dc 100755
--- a/bootcamp/__init__.py
+++ b/bootcamp/__init__.py
@@ -1,2 +1,7 @@
-__version__ = '2.0.0'
-__version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')])
+__version__ = "2.1.1"
+__version_info__ = tuple(
+ [
+ int(num) if num.isdigit() else num
+ for num in __version__.replace("-", ".", 1).split(".")
+ ]
+)
diff --git a/bootcamp/articles/admin.py b/bootcamp/articles/admin.py
index b91078594..d9023af88 100755
--- a/bootcamp/articles/admin.py
+++ b/bootcamp/articles/admin.py
@@ -4,5 +4,5 @@
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
- list_display = ('title', 'user', 'status')
- list_filter = ('user', 'status', 'timestamp')
+ list_display = ("title", "user", "status")
+ list_filter = ("user", "status", "timestamp")
diff --git a/bootcamp/articles/apps.py b/bootcamp/articles/apps.py
index 037a11955..479d6eb68 100755
--- a/bootcamp/articles/apps.py
+++ b/bootcamp/articles/apps.py
@@ -3,5 +3,5 @@
class ArticlesConfig(AppConfig):
- name = 'bootcamp.articles'
- verbose_name = _('Articles')
+ name = "bootcamp.articles"
+ verbose_name = _("Articles")
diff --git a/bootcamp/articles/forms.py b/bootcamp/articles/forms.py
index 8cf1e6a27..cc455be46 100755
--- a/bootcamp/articles/forms.py
+++ b/bootcamp/articles/forms.py
@@ -8,7 +8,8 @@
class ArticleForm(forms.ModelForm):
status = forms.CharField(widget=forms.HiddenInput())
edited = forms.BooleanField(
- widget=forms.HiddenInput(), required=False, initial=False)
+ widget=forms.HiddenInput(), required=False, initial=False
+ )
content = MarkdownxFormField()
class Meta:
diff --git a/bootcamp/articles/migrations/0001_initial.py b/bootcamp/articles/migrations/0001_initial.py
index 8345dd551..bb47a8e98 100755
--- a/bootcamp/articles/migrations/0001_initial.py
+++ b/bootcamp/articles/migrations/0001_initial.py
@@ -12,29 +12,66 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
- ('taggit', '0002_auto_20150616_2121'),
+ ("taggit", "0002_auto_20150616_2121"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
- name='Article',
+ name="Article",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('image', models.ImageField(upload_to='articles_pictures/%Y/%m/%d/', verbose_name='Featured image')),
- ('timestamp', models.DateTimeField(auto_now_add=True)),
- ('title', models.CharField(max_length=255, unique=True)),
- ('slug', models.SlugField(blank=True, max_length=80, null=True)),
- ('status', models.CharField(choices=[('D', 'Draft'), ('P', 'Published')], default='D', max_length=1)),
- ('content', markdownx.models.MarkdownxField()),
- ('edited', models.BooleanField(default=False)),
- ('tags', taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags')),
- ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='author', to=settings.AUTH_USER_MODEL)),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "image",
+ models.ImageField(
+ upload_to="articles_pictures/%Y/%m/%d/",
+ verbose_name="Featured image",
+ ),
+ ),
+ ("timestamp", models.DateTimeField(auto_now_add=True)),
+ ("title", models.CharField(max_length=255, unique=True)),
+ ("slug", models.SlugField(blank=True, max_length=80, null=True)),
+ (
+ "status",
+ models.CharField(
+ choices=[("D", "Draft"), ("P", "Published")],
+ default="D",
+ max_length=1,
+ ),
+ ),
+ ("content", markdownx.models.MarkdownxField()),
+ ("edited", models.BooleanField(default=False)),
+ (
+ "tags",
+ taggit.managers.TaggableManager(
+ help_text="A comma-separated list of tags.",
+ through="taggit.TaggedItem",
+ to="taggit.Tag",
+ verbose_name="Tags",
+ ),
+ ),
+ (
+ "user",
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="author",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
],
options={
- 'verbose_name': 'Article',
- 'verbose_name_plural': 'Articles',
- 'ordering': ('-timestamp',),
+ "verbose_name": "Article",
+ "verbose_name_plural": "Articles",
+ "ordering": ("-timestamp",),
},
- ),
+ )
]
diff --git a/bootcamp/articles/models.py b/bootcamp/articles/models.py
index 71ff509ce..44fdb22d9 100755
--- a/bootcamp/articles/models.py
+++ b/bootcamp/articles/models.py
@@ -27,8 +27,9 @@ def get_drafts(self):
def get_counted_tags(self):
tag_dict = {}
- query = self.filter(status='P').annotate(
- tagged=Count('tags')).filter(tags__gt=0)
+ query = (
+ self.filter(status="P").annotate(tagged=Count("tags")).filter(tags__gt=0)
+ )
for obj in query:
for tag in obj.tags.names():
if tag not in tag_dict:
@@ -43,16 +44,17 @@ def get_counted_tags(self):
class Article(models.Model):
DRAFT = "D"
PUBLISHED = "P"
- STATUS = (
- (DRAFT, _("Draft")),
- (PUBLISHED, _("Published")),
- )
+ STATUS = ((DRAFT, _("Draft")), (PUBLISHED, _("Published")))
user = models.ForeignKey(
- settings.AUTH_USER_MODEL, null=True, related_name="author",
- on_delete=models.SET_NULL)
+ settings.AUTH_USER_MODEL,
+ null=True,
+ related_name="author",
+ on_delete=models.SET_NULL,
+ )
image = models.ImageField(
- _('Featured image'), upload_to='articles_pictures/%Y/%m/%d/')
+ _("Featured image"), upload_to="articles_pictures/%Y/%m/%d/"
+ )
timestamp = models.DateTimeField(auto_now_add=True)
title = models.CharField(max_length=255, null=False, unique=True)
slug = models.SlugField(max_length=80, null=True, blank=True)
@@ -72,8 +74,9 @@ def __str__(self):
def save(self, *args, **kwargs):
if not self.slug:
- self.slug = slugify(f"{self.user.username}-{self.title}",
- to_lower=True, max_length=80)
+ self.slug = slugify(
+ f"{self.user.username}-{self.title}", lowercase=True, max_length=80
+ )
super().save(*args, **kwargs)
@@ -81,15 +84,13 @@ def get_markdown(self):
return markdownify(self.content)
-def notify_comment(**kwargs):
+def notify_comment(**kwargs): # pragma: no cover
"""Handler to be fired up upon comments signal to notify the author of a
given article."""
- actor = kwargs['request'].user
- receiver = kwargs['comment'].content_object.user
- obj = kwargs['comment'].content_object
- notification_handler(
- actor, receiver, Notification.COMMENTED, action_object=obj
- )
+ actor = kwargs["request"].user
+ receiver = kwargs["comment"].content_object.user
+ obj = kwargs["comment"].content_object
+ notification_handler(actor, receiver, Notification.COMMENTED, action_object=obj)
comment_was_posted.connect(receiver=notify_comment)
diff --git a/bootcamp/articles/tests/test_models.py b/bootcamp/articles/tests/test_models.py
index e8e4fe7f0..2620f6aa6 100755
--- a/bootcamp/articles/tests/test_models.py
+++ b/bootcamp/articles/tests/test_models.py
@@ -13,6 +13,7 @@ def setUp(self):
status="P",
user=self.user,
)
+ self.article.tags.add("test1", "test2")
self.not_p_article = Article.objects.create(
title="A really nice to-be title",
content="""This is a really good content, just if somebody
@@ -22,6 +23,7 @@ def setUp(self):
everybody always wants the real deal.""",
user=self.user,
)
+ self.not_p_article.tags.add("test1", "test2")
def test_object_instance(self):
assert isinstance(self.article, Article)
@@ -36,3 +38,13 @@ def test_return_values(self):
assert self.article in Article.objects.get_published()
assert Article.objects.get_published()[0].title == "A really nice title"
assert self.not_p_article in Article.objects.get_drafts()
+
+ def test_get_popular_tags(self):
+ correct_dict = {"test1": 1, "test2": 1}
+ assert Article.objects.get_counted_tags() == correct_dict.items()
+
+ def test_change_draft_title(self):
+ assert self.not_p_article.title == "A really nice to-be title"
+ self.not_p_article.title = "A really nice changed title"
+ self.not_p_article.save()
+ assert self.not_p_article.title == "A really nice changed title"
diff --git a/bootcamp/articles/tests/test_views.py b/bootcamp/articles/tests/test_views.py
index 512684ddd..69005dc75 100755
--- a/bootcamp/articles/tests/test_views.py
+++ b/bootcamp/articles/tests/test_views.py
@@ -53,29 +53,38 @@ def test_index_articles(self):
self.assertEqual(response.status_code, 200)
def test_error_404(self):
- response_no_art = self.client.get(reverse(
- "articles:article", kwargs={"slug": "no-slug"}))
+ response_no_art = self.client.get(
+ reverse("articles:article", kwargs={"slug": "no-slug"})
+ )
self.assertEqual(response_no_art.status_code, 404)
@override_settings(MEDIA_ROOT=tempfile.gettempdir())
def test_create_article(self):
- response = self.client.post(reverse("articles:write_new"),
- {"title": "A not that really nice title",
- "content": "Whatever works for you",
- "tags": "list, lists",
- "status": "P",
- "image": self.test_image})
+ response = self.client.post(
+ reverse("articles:write_new"),
+ {
+ "title": "A not that really nice title",
+ "content": "Whatever works for you",
+ "tags": "list, lists",
+ "status": "P",
+ "image": self.test_image,
+ },
+ )
assert response.status_code == 302
@override_settings(MEDIA_ROOT=tempfile.gettempdir())
def test_single_article(self):
current_count = Article.objects.count()
- response = self.client.post(reverse("articles:write_new"),
- {"title": "A not that really nice title",
- "content": "Whatever works for you",
- "tags": "list, lists",
- "status": "P",
- "image": self.test_image})
+ response = self.client.post(
+ reverse("articles:write_new"),
+ {
+ "title": "A not that really nice title",
+ "content": "Whatever works for you",
+ "tags": "list, lists",
+ "status": "P",
+ "image": self.test_image,
+ },
+ )
# response_art = self.client.get(
# reverse("articles:article",
# kwargs={"slug": "a-not-that-really-nice-title"}))
@@ -85,13 +94,40 @@ def test_single_article(self):
@override_settings(MEDIA_ROOT=tempfile.gettempdir())
def test_draft_article(self):
- response = self.client.post(reverse("articles:write_new"),
- {"title": "A not that really nice title",
- "content": "Whatever works for you",
- "tags": "list, lists",
- "status": "D",
- "image": self.test_image})
+ response = self.client.post(
+ reverse("articles:write_new"),
+ {
+ "title": "A not that really nice title",
+ "content": "Whatever works for you",
+ "tags": "list, lists",
+ "status": "D",
+ "image": self.test_image,
+ },
+ )
+ resp = self.client.get(reverse("articles:drafts"))
+ assert resp.status_code == 200
+ assert response.status_code == 302
+ assert (
+ resp.context["articles"][0].slug
+ == "first-user-a-not-that-really-nice-title"
+ )
+
+ @override_settings(MEDIA_ROOT=tempfile.gettempdir())
+ def test_draft_article_change(self):
+ response = self.client.post(
+ reverse("articles:edit_article", kwargs={"pk": self.not_p_article.id}),
+ {
+ "title": "A really nice changed title",
+ "content": "Whatever works for you",
+ "tags": "list, lists",
+ "status": "D",
+ "image": self.test_image,
+ },
+ )
resp = self.client.get(reverse("articles:drafts"))
assert resp.status_code == 200
assert response.status_code == 302
- assert resp.context["articles"][0].slug == "first-user-a-not-that-really-nice-title"
+ assert resp.context["articles"][0].title == "A really nice changed title"
+ assert (
+ resp.context["articles"][0].slug == "first-user-a-really-nice-to-be-title"
+ )
diff --git a/bootcamp/articles/urls.py b/bootcamp/articles/urls.py
index b73d3410b..74a286506 100755
--- a/bootcamp/articles/urls.py
+++ b/bootcamp/articles/urls.py
@@ -1,13 +1,18 @@
from django.conf.urls import url
-from bootcamp.articles.views import (ArticlesListView, DraftsListView,
- CreateArticleView, EditArticleView,
- DetailArticleView)
-app_name = 'articles'
+from bootcamp.articles.views import (
+ ArticlesListView,
+ DraftsListView,
+ CreateArticleView,
+ EditArticleView,
+ DetailArticleView,
+)
+
+app_name = "articles"
urlpatterns = [
- url(r'^$', ArticlesListView.as_view(), name='list'),
- url(r'^write-new-article/$', CreateArticleView.as_view(), name='write_new'),
- url(r'^drafts/$', DraftsListView.as_view(), name='drafts'),
- url(r'^edit/(?P\d+)/$', EditArticleView.as_view(), name='edit_article'),
- url(r'^(?P[-\w]+)/$', DetailArticleView.as_view(), name='article'),
+ url(r"^$", ArticlesListView.as_view(), name="list"),
+ url(r"^write-new-article/$", CreateArticleView.as_view(), name="write_new"),
+ url(r"^drafts/$", DraftsListView.as_view(), name="drafts"),
+ url(r"^edit/(?P\d+)/$", EditArticleView.as_view(), name="edit_article"),
+ url(r"^(?P[-\w]+)/$", DetailArticleView.as_view(), name="article"),
]
diff --git a/bootcamp/articles/views.py b/bootcamp/articles/views.py
index b6ac4e54c..c7013c717 100755
--- a/bootcamp/articles/views.py
+++ b/bootcamp/articles/views.py
@@ -11,13 +11,14 @@
class ArticlesListView(LoginRequiredMixin, ListView):
"""Basic ListView implementation to call the published articles list."""
+
model = Article
paginate_by = 15
context_object_name = "articles"
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
- context['popular_tags'] = Article.objects.get_counted_tags()
+ context["popular_tags"] = Article.objects.get_counted_tags()
return context
def get_queryset(self, **kwargs):
@@ -27,16 +28,18 @@ def get_queryset(self, **kwargs):
class DraftsListView(ArticlesListView):
"""Overriding the original implementation to call the drafts articles
list."""
+
def get_queryset(self, **kwargs):
return Article.objects.get_drafts()
class CreateArticleView(LoginRequiredMixin, CreateView):
"""Basic CreateView implementation to create new articles."""
+
model = Article
message = _("Your article has been created.")
form_class = ArticleForm
- template_name = 'articles/article_create.html'
+ template_name = "articles/article_create.html"
def form_valid(self, form):
form.instance.user = self.request.user
@@ -44,15 +47,16 @@ def form_valid(self, form):
def get_success_url(self):
messages.success(self.request, self.message)
- return reverse('articles:list')
+ return reverse("articles:list")
class EditArticleView(LoginRequiredMixin, AuthorRequiredMixin, UpdateView):
"""Basic EditView implementation to edit existing articles."""
+
model = Article
message = _("Your article has been updated.")
form_class = ArticleForm
- template_name = 'articles/article_update.html'
+ template_name = "articles/article_update.html"
def form_valid(self, form):
form.instance.user = self.request.user
@@ -60,9 +64,10 @@ def form_valid(self, form):
def get_success_url(self):
messages.success(self.request, self.message)
- return reverse('articles:list')
+ return reverse("articles:list")
class DetailArticleView(LoginRequiredMixin, DetailView):
"""Basic DetailView implementation to call an individual article."""
+
model = Article
diff --git a/bootcamp/contrib/sites/migrations/0001_initial.py b/bootcamp/contrib/sites/migrations/0001_initial.py
index a7639869f..304cd6d7c 100755
--- a/bootcamp/contrib/sites/migrations/0001_initial.py
+++ b/bootcamp/contrib/sites/migrations/0001_initial.py
@@ -9,23 +9,34 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
- name='Site',
+ name="Site",
fields=[
- ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
- ('domain', models.CharField(
- max_length=100, verbose_name='domain name', validators=[_simple_domain_name_validator]
- )),
- ('name', models.CharField(max_length=50, verbose_name='display name')),
+ (
+ "id",
+ models.AutoField(
+ verbose_name="ID",
+ serialize=False,
+ auto_created=True,
+ primary_key=True,
+ ),
+ ),
+ (
+ "domain",
+ models.CharField(
+ max_length=100,
+ verbose_name="domain name",
+ validators=[_simple_domain_name_validator],
+ ),
+ ),
+ ("name", models.CharField(max_length=50, verbose_name="display name")),
],
options={
- 'ordering': ('domain',),
- 'db_table': 'django_site',
- 'verbose_name': 'site',
- 'verbose_name_plural': 'sites',
+ "ordering": ("domain",),
+ "db_table": "django_site",
+ "verbose_name": "site",
+ "verbose_name_plural": "sites",
},
bases=(models.Model,),
- managers=[
- ('objects', django.contrib.sites.models.SiteManager()),
- ],
- ),
+ managers=[("objects", django.contrib.sites.models.SiteManager())],
+ )
]
diff --git a/bootcamp/contrib/sites/migrations/0002_alter_domain_unique.py b/bootcamp/contrib/sites/migrations/0002_alter_domain_unique.py
index 6a26ebcde..2c8d6dac0 100755
--- a/bootcamp/contrib/sites/migrations/0002_alter_domain_unique.py
+++ b/bootcamp/contrib/sites/migrations/0002_alter_domain_unique.py
@@ -4,17 +4,17 @@
class Migration(migrations.Migration):
- dependencies = [
- ('sites', '0001_initial'),
- ]
+ dependencies = [("sites", "0001_initial")]
operations = [
migrations.AlterField(
- model_name='site',
- name='domain',
+ model_name="site",
+ name="domain",
field=models.CharField(
- max_length=100, unique=True, validators=[django.contrib.sites.models._simple_domain_name_validator],
- verbose_name='domain name'
+ max_length=100,
+ unique=True,
+ validators=[django.contrib.sites.models._simple_domain_name_validator],
+ verbose_name="domain name",
),
- ),
+ )
]
diff --git a/bootcamp/contrib/sites/migrations/0003_set_site_domain_and_name.py b/bootcamp/contrib/sites/migrations/0003_set_site_domain_and_name.py
index c93f8e1d9..4291e1d5d 100755
--- a/bootcamp/contrib/sites/migrations/0003_set_site_domain_and_name.py
+++ b/bootcamp/contrib/sites/migrations/0003_set_site_domain_and_name.py
@@ -9,34 +9,26 @@
def update_site_forward(apps, schema_editor):
"""Set site domain and name."""
- Site = apps.get_model('sites', 'Site')
+ Site = apps.get_model("sites", "Site")
Site.objects.update_or_create(
id=settings.SITE_ID,
defaults={
- 'domain': 'vitor@freitas.com trybootcamp.vitorfs.com',
- 'name': 'Bootcamp'
- }
+ "domain": "vitor@freitas.com trybootcamp.vitorfs.com",
+ "name": "Bootcamp",
+ },
)
def update_site_backward(apps, schema_editor):
"""Revert site domain and name to default."""
- Site = apps.get_model('sites', 'Site')
+ Site = apps.get_model("sites", "Site")
Site.objects.update_or_create(
- id=settings.SITE_ID,
- defaults={
- 'domain': 'example.com',
- 'name': 'example.com'
- }
+ id=settings.SITE_ID, defaults={"domain": "example.com", "name": "example.com"}
)
class Migration(migrations.Migration):
- dependencies = [
- ('sites', '0002_alter_domain_unique'),
- ]
+ dependencies = [("sites", "0002_alter_domain_unique")]
- operations = [
- migrations.RunPython(update_site_forward, update_site_backward),
- ]
+ operations = [migrations.RunPython(update_site_forward, update_site_backward)]
diff --git a/bootcamp/helpers.py b/bootcamp/helpers.py
index 34858da68..bca72b6c2 100755
--- a/bootcamp/helpers.py
+++ b/bootcamp/helpers.py
@@ -1,7 +1,14 @@
+import re
+from urllib.parse import urljoin, urlparse
+
from django.core.exceptions import PermissionDenied
-from django.http import HttpResponseBadRequest
-from django.views.generic import View
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
+from django.http import HttpResponseBadRequest, JsonResponse
+from django.utils.translation import ugettext_lazy as _
+from django.views.generic import View
+
+import bs4
+import requests
def paginate_data(qs, page_size, page, paginated_type, **kwargs):
@@ -23,12 +30,13 @@ def paginate_data(qs, page_size, page, paginated_type, **kwargs):
has_next=page_obj.has_next(),
has_prev=page_obj.has_previous(),
objects=page_obj.object_list,
- **kwargs
+ **kwargs,
)
def ajax_required(f):
"""Not a mixin, but a nice decorator to validate than a request is AJAX"""
+
def wrap(request, *args, **kwargs):
if not request.is_ajax():
return HttpResponseBadRequest()
@@ -43,9 +51,130 @@ def wrap(request, *args, **kwargs):
class AuthorRequiredMixin(View):
"""Mixin to validate than the loggedin user is the creator of the object
to be edited or updated."""
+
def dispatch(self, request, *args, **kwargs):
obj = self.get_object()
if obj.user != self.request.user:
raise PermissionDenied
return super().dispatch(request, *args, **kwargs)
+
+
+def is_owner(obj, username):
+ """
+ Checks if model instance belongs to a user
+ Args:
+ obj: A model instance
+ username(str): User's username
+ Returns:
+ boolean: True is model instance belongs to user else False
+ """
+ if obj.user.username == username:
+ return True
+ return False
+
+
+def update_votes(obj, user, value):
+ """
+ Updates votes for either a question or answer
+ Args:
+ obj: Question or Answer model instance
+ user: User model instance voting an anwser or question
+ value(str): 'U' for an up vote or 'D' for down vote
+ """
+ obj.votes.update_or_create(
+ user=user, defaults={"value": value},
+ )
+ obj.count_votes()
+
+
+def fetch_metadata(text):
+ """Method to consolidate workflow to recover the metadata of a page of the first URL found a in
+ a given text block.
+ :requieres:
+
+ :param text: Block of text of any lenght
+ """
+ urls = get_urls(text)
+ try:
+ return get_metadata(urls[0])
+
+ except IndexError:
+ return None
+
+
+def get_urls(text):
+ """Method to look for all URLs in a given text, extract them and return them as a tuple of urls.
+ :requires:
+
+ :param text: A valid block of text of any lenght.
+
+ :returns:
+ A tuple of valid URLs extracted from the text.
+ """
+ regex = (
+ regex
+ ) = r"(?i)\b((?:https?:(?:\/{1,3}|[a-z0-9%])|[a-z0-9.\-]+[.](?:com|net|org|edu|gov|mil|aero|asia|biz|cat|coop|info|int|jobs|mobi|museum|name|post|pro|tel|travel|xxx|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cs|cu|cv|cx|cy|cz|dd|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|Ja|sk|sl|sm|sn|so|sr|ss|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw)\/)(?:[^\s()<>{}\[\]]+|\([^\s()]*?\([^\s()]+\)[^\s()]*?\)|\([^\s]+?\))+(?:\([^\s()]*?\([^\s()]+\)[^\s()]*?\)|\([^\s]+?\)|[^\s`!()\[\]{};:\'\".,<>?«»“”‘’])|(?:(? 0:
+ data["image"] = urljoin(url, images[0].get("src"))
+
+ if not data.get("description"):
+ data["description"] = ""
+ for text in soup.body.find_all(string=True):
+ if (
+ text.parent.name != "script"
+ and text.parent.name != "style"
+ and not isinstance(text, bs4.Comment)
+ ):
+ data["description"] += text
+
+ data["description"] = re.sub("\n|\r|\t", " ", data["description"])
+ data["description"] = re.sub(" +", " ", data["description"])
+ data["description"] = data["description"].strip()[:255]
+
+ return data
diff --git a/bootcamp/messager/apps.py b/bootcamp/messager/apps.py
index 493d73a62..c0885accb 100755
--- a/bootcamp/messager/apps.py
+++ b/bootcamp/messager/apps.py
@@ -3,5 +3,5 @@
class MessagerConfig(AppConfig):
- name = 'bootcamp.messager'
- verbose_name = _('Messager')
+ name = "bootcamp.messager"
+ verbose_name = _("Messager")
diff --git a/bootcamp/messager/consumers.py b/bootcamp/messager/consumers.py
index 457d126d4..e713a9da9 100755
--- a/bootcamp/messager/consumers.py
+++ b/bootcamp/messager/consumers.py
@@ -6,6 +6,7 @@
class MessagerConsumer(AsyncWebsocketConsumer):
"""Consumer to manage WebSocket connections for the Messager app.
"""
+
async def connect(self):
"""Consumer Connect implementation, to validate user status and prevent
non authenticated user to take advante from the connection."""
@@ -15,13 +16,17 @@ async def connect(self):
else:
# Accept the connection
- await self.channel_layer.group_add(f"{self.scope['user'].username}", self.channel_name)
+ await self.channel_layer.group_add(
+ f"{self.scope['user'].username}", self.channel_name
+ )
await self.accept()
async def disconnect(self, close_code):
"""Consumer implementation to leave behind the group at the moment the
closes the connection."""
- await self.channel_layer.group_discard(f"{self.scope['user'].username}", self.channel_name)
+ await self.channel_layer.group_discard(
+ f"{self.scope['user'].username}", self.channel_name
+ )
async def receive(self, text_data):
"""Receive method implementation to redirect any new message received
diff --git a/bootcamp/messager/migrations/0001_initial.py b/bootcamp/messager/migrations/0001_initial.py
index 380bb6382..74a7e1651 100755
--- a/bootcamp/messager/migrations/0001_initial.py
+++ b/bootcamp/messager/migrations/0001_initial.py
@@ -10,25 +10,50 @@ class Migration(migrations.Migration):
initial = True
- dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ]
+ dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
operations = [
migrations.CreateModel(
- name='Message',
+ name="Message",
fields=[
- ('uuid_id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('timestamp', models.DateTimeField(auto_now_add=True)),
- ('message', models.TextField(blank=True, max_length=1000)),
- ('unread', models.BooleanField(db_index=True, default=True)),
- ('recipient', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='received_messages', to=settings.AUTH_USER_MODEL, verbose_name='Recipient')),
- ('sender', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sent_messages', to=settings.AUTH_USER_MODEL, verbose_name='Sender')),
+ (
+ "uuid_id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ ("timestamp", models.DateTimeField(auto_now_add=True)),
+ ("message", models.TextField(blank=True, max_length=1000)),
+ ("unread", models.BooleanField(db_index=True, default=True)),
+ (
+ "recipient",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="received_messages",
+ to=settings.AUTH_USER_MODEL,
+ verbose_name="Recipient",
+ ),
+ ),
+ (
+ "sender",
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="sent_messages",
+ to=settings.AUTH_USER_MODEL,
+ verbose_name="Sender",
+ ),
+ ),
],
options={
- 'verbose_name': 'Message',
- 'verbose_name_plural': 'Messages',
- 'ordering': ('-timestamp',),
+ "verbose_name": "Message",
+ "verbose_name_plural": "Messages",
+ "ordering": ("-timestamp",),
},
- ),
+ )
]
diff --git a/bootcamp/messager/models.py b/bootcamp/messager/models.py
index 654a18ec1..b8645a5b1 100755
--- a/bootcamp/messager/models.py
+++ b/bootcamp/messager/models.py
@@ -4,6 +4,7 @@
from django.contrib.auth import get_user_model
from django.db import models
from django.utils.translation import ugettext_lazy as _
+from django.db import transaction
from asgiref.sync import async_to_sync
@@ -17,7 +18,7 @@ def get_conversation(self, sender, recipient):
"""Returns all the messages sent between two users."""
qs_one = self.filter(sender=sender, recipient=recipient)
qs_two = self.filter(sender=recipient, recipient=sender)
- return qs_one.union(qs_two).order_by('timestamp')
+ return qs_one.union(qs_two).order_by("timestamp")
def get_most_recent_conversation(self, recipient):
"""Returns the most recent conversation counterpart's username."""
@@ -41,14 +42,23 @@ def mark_conversation_as_read(self, sender, recipient):
class Message(models.Model):
"""A private message sent between users."""
- uuid_id = models.UUIDField(
- primary_key=True, default=uuid.uuid4, editable=False)
+
+ uuid_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
sender = models.ForeignKey(
- settings.AUTH_USER_MODEL, related_name='sent_messages',
- verbose_name=_("Sender"), null=True, on_delete=models.SET_NULL)
+ settings.AUTH_USER_MODEL,
+ related_name="sent_messages",
+ verbose_name=_("Sender"),
+ null=True,
+ on_delete=models.SET_NULL,
+ )
recipient = models.ForeignKey(
- settings.AUTH_USER_MODEL, related_name='received_messages', null=True,
- blank=True, verbose_name=_("Recipient"), on_delete=models.SET_NULL)
+ settings.AUTH_USER_MODEL,
+ related_name="received_messages",
+ null=True,
+ blank=True,
+ verbose_name=_("Recipient"),
+ on_delete=models.SET_NULL,
+ )
timestamp = models.DateTimeField(auto_now_add=True)
message = models.TextField(max_length=1000, blank=True)
unread = models.BooleanField(default=True, db_index=True)
@@ -57,7 +67,7 @@ class Message(models.Model):
class Meta:
verbose_name = _("Message")
verbose_name_plural = _("Messages")
- ordering = ("-timestamp", )
+ ordering = ("-timestamp",)
def __str__(self):
return self.message
@@ -79,17 +89,17 @@ def send_message(sender, recipient, message):
actual message.
"""
new_message = Message.objects.create(
- sender=sender,
- recipient=recipient,
- message=message
+ sender=sender, recipient=recipient, message=message
)
channel_layer = get_channel_layer()
payload = {
- 'type': 'receive',
- 'key': 'message',
- 'message_id': new_message.uuid_id,
- 'sender': sender,
- 'recipient': recipient
- }
- async_to_sync(channel_layer.group_send)(recipient.username, payload)
+ "type": "receive",
+ "key": "message",
+ "message_id": str(new_message.uuid_id),
+ "sender": str(sender),
+ "recipient": str(recipient),
+ }
+ transaction.on_commit(
+ lambda: async_to_sync(channel_layer.group_send)(recipient.username, payload)
+ )
return new_message
diff --git a/bootcamp/messager/schema.py b/bootcamp/messager/schema.py
index 92bdd3e02..ca4b14d04 100755
--- a/bootcamp/messager/schema.py
+++ b/bootcamp/messager/schema.py
@@ -15,6 +15,7 @@ class Meta:
class MessageQuery(object):
"""Abstract object to register in the root schema, allowing to query the
model."""
+
conversation = graphene.List(MessageType)
def resolve_conversation(self, info, **kwargs):
@@ -24,7 +25,7 @@ def resolve_conversation(self, info, **kwargs):
return Message.objects.get_conversation(sender, recipient)
def resolve_message(self, info, **kwargs):
- uuid_id = kwargs.get('uuid_id')
+ uuid_id = kwargs.get("uuid_id")
if uuid_id is not None:
return Message.objects.get(uuid_id=uuid_id)
diff --git a/bootcamp/messager/tests/test_models.py b/bootcamp/messager/tests/test_models.py
index 196222af8..a081b7d0d 100755
--- a/bootcamp/messager/tests/test_models.py
+++ b/bootcamp/messager/tests/test_models.py
@@ -10,17 +10,13 @@ def setUp(self):
self.first_message = Message.objects.create(
sender=self.user,
recipient=self.other_user,
- message="A not that long message."
+ message="A not that long message.",
)
self.second_message = Message.objects.create(
- sender=self.user,
- recipient=self.other_user,
- message="A follow up message."
+ sender=self.user, recipient=self.other_user, message="A follow up message."
)
self.third_message = Message.objects.create(
- sender=self.other_user,
- recipient=self.user,
- message="An answer message."
+ sender=self.other_user, recipient=self.user, message="An answer message."
)
def test_object_instance(self):
@@ -31,13 +27,11 @@ def test_return_values(self):
assert self.first_message.message == "A not that long message."
def test_conversation(self):
- conversation = Message.objects.get_conversation(
- self.user, self.other_user)
+ conversation = Message.objects.get_conversation(self.user, self.other_user)
assert conversation.last().message == "An answer message."
def test_recent_conversation(self):
- active_user = Message.objects.get_most_recent_conversation(
- self.user)
+ active_user = Message.objects.get_most_recent_conversation(self.user)
assert active_user == self.other_user
def test_single_marking_as_read(self):
@@ -47,7 +41,5 @@ def test_single_marking_as_read(self):
def test_sending_new_message(self):
initial_count = Message.objects.count()
- Message.send_message(
- self.other_user, self.user, "A follow up answer message."
- )
+ Message.send_message(self.other_user, self.user, "A follow up answer message.")
assert Message.objects.count() == initial_count + 1
diff --git a/bootcamp/messager/tests/test_views.py b/bootcamp/messager/tests/test_views.py
index b78197c24..609ceb399 100755
--- a/bootcamp/messager/tests/test_views.py
+++ b/bootcamp/messager/tests/test_views.py
@@ -17,17 +17,13 @@ def setUp(self):
self.first_message = Message.objects.create(
sender=self.user,
recipient=self.other_user,
- message="A not that long message."
+ message="A not that long message.",
)
self.second_message = Message.objects.create(
- sender=self.user,
- recipient=self.other_user,
- message="A follow up message."
+ sender=self.user, recipient=self.other_user, message="A follow up message."
)
self.third_message = Message.objects.create(
- sender=self.other_user,
- recipient=self.user,
- message="An answer message."
+ sender=self.other_user, recipient=self.user, message="An answer message."
)
def test_user_messages(self):
@@ -37,50 +33,61 @@ def test_user_messages(self):
def test_user_conversation(self):
response = self.client.get(
- reverse("messager:conversation_detail",
- kwargs={"username": self.user.username}))
+ reverse(
+ "messager:conversation_detail", kwargs={"username": self.user.username}
+ )
+ )
assert response.status_code == 200
assert str(response.context["active"]) == "first_user"
def test_send_message_view(self):
message_count = Message.objects.count()
- request = self.client.post(reverse("messager:send_message"),
- {"to": "second_user",
- "message": "A new short message"},
- HTTP_X_REQUESTED_WITH="XMLHttpRequest")
+ request = self.client.post(
+ reverse("messager:send_message"),
+ {"to": "second_user", "message": "A new short message"},
+ HTTP_X_REQUESTED_WITH="XMLHttpRequest",
+ )
assert request.status_code == 200
new_msm_count = Message.objects.count()
assert new_msm_count == message_count + 1
def test_wrong_requests_send_message(self):
- get_request = self.client.get(reverse("messager:send_message"),
- {"to": "second_user",
- "message": "A new short message"},
- HTTP_X_REQUESTED_WITH="XMLHttpRequest")
- no_ajax_request = self.client.get(reverse("messager:send_message"),
- {"to": "second_user",
- "message": "A new short message"})
- same_user_request = self.client.post(reverse("messager:send_message"),
- {"to": "first_user",
- "message": "A new short message"},
- HTTP_X_REQUESTED_WITH="XMLHttpRequest")
- no_lenght_request = self.client.post(reverse("messager:send_message"),
- {"to": "second_user",
- "message": ""},
- HTTP_X_REQUESTED_WITH="XMLHttpRequest")
+ get_request = self.client.get(
+ reverse("messager:send_message"),
+ {"to": "second_user", "message": "A new short message"},
+ HTTP_X_REQUESTED_WITH="XMLHttpRequest",
+ )
+ no_ajax_request = self.client.get(
+ reverse("messager:send_message"),
+ {"to": "second_user", "message": "A new short message"},
+ )
+ same_user_request = self.client.post(
+ reverse("messager:send_message"),
+ {"to": "first_user", "message": "A new short message"},
+ HTTP_X_REQUESTED_WITH="XMLHttpRequest",
+ )
+ no_lenght_request = self.client.post(
+ reverse("messager:send_message"),
+ {"to": "second_user", "message": ""},
+ HTTP_X_REQUESTED_WITH="XMLHttpRequest",
+ )
assert get_request.status_code == 405
assert no_ajax_request.status_code == 400
assert same_user_request.status_code == 200
assert no_lenght_request.status_code == 200
def test_message_reception_view(self):
- request = self.client.get(reverse("messager:receive_message"),
- {"message_id": self.third_message.uuid_id},
- HTTP_X_REQUESTED_WITH="XMLHttpRequest")
+ request = self.client.get(
+ reverse("messager:receive_message"),
+ {"message_id": self.third_message.uuid_id},
+ HTTP_X_REQUESTED_WITH="XMLHttpRequest",
+ )
assert b"An answer message." in request.content
def test_wrong_request_recieve_message_view(self):
- request = self.client.post(reverse("messager:receive_message"),
- {"message_id": self.third_message.uuid_id},
- HTTP_X_REQUESTED_WITH="XMLHttpRequest")
+ request = self.client.post(
+ reverse("messager:receive_message"),
+ {"message_id": self.third_message.uuid_id},
+ HTTP_X_REQUESTED_WITH="XMLHttpRequest",
+ )
assert request.status_code == 405
diff --git a/bootcamp/messager/urls.py b/bootcamp/messager/urls.py
index 3c76f6225..5dcb79605 100755
--- a/bootcamp/messager/urls.py
+++ b/bootcamp/messager/urls.py
@@ -2,12 +2,14 @@
from bootcamp.messager import views
-app_name = 'messager'
+app_name = "messager"
urlpatterns = [
- url(r'^$', views.MessagesListView.as_view(), name='messages_list'),
- url(r'^send-message/$', views.send_message, name='send_message'),
- url(r'^receive-message/$',
- views.receive_message, name='receive_message'),
- url(r'^(?P[\w.@+-]+)/$', views.ConversationListView.as_view(),
- name='conversation_detail'),
+ url(r"^$", views.MessagesListView.as_view(), name="messages_list"),
+ url(r"^send-message/$", views.send_message, name="send_message"),
+ url(r"^receive-message/$", views.receive_message, name="receive_message"),
+ url(
+ r"^(?P[\w.@+-]+)/$",
+ views.ConversationListView.as_view(),
+ name="conversation_detail",
+ ),
]
diff --git a/bootcamp/messager/views.py b/bootcamp/messager/views.py
index 7e64c7ffc..a07210ae0 100755
--- a/bootcamp/messager/views.py
+++ b/bootcamp/messager/views.py
@@ -6,6 +6,7 @@
from django.views.decorators.http import require_http_methods
from django.views.generic import ListView
+
from bootcamp.messager.models import Message
from bootcamp.helpers import ajax_required
@@ -14,38 +15,41 @@ class MessagesListView(LoginRequiredMixin, ListView):
"""CBV to render the inbox, showing by default the most recent
conversation as the active one.
"""
+
model = Message
paginate_by = 50
template_name = "messager/message_list.html"
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
- context['users_list'] = get_user_model().objects.filter(
- is_active=True).exclude(
- username=self.request.user).order_by('username')
+ context["users_list"] = (
+ get_user_model()
+ .objects.filter(is_active=True)
+ .exclude(username=self.request.user)
+ .order_by("username")
+ )
last_conversation = Message.objects.get_most_recent_conversation(
self.request.user
)
- context['active'] = last_conversation.username
+ context["active"] = last_conversation.username
return context
def get_queryset(self):
- active_user = Message.objects.get_most_recent_conversation(
- self.request.user)
+ active_user = Message.objects.get_most_recent_conversation(self.request.user)
return Message.objects.get_conversation(active_user, self.request.user)
class ConversationListView(MessagesListView):
"""CBV to render the inbox, showing an specific conversation with a given
user, who requires to be active too."""
+
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
- context['active'] = self.kwargs["username"]
+ context["active"] = self.kwargs["username"]
return context
def get_queryset(self):
- active_user = get_user_model().objects.get(
- username=self.kwargs["username"])
+ active_user = get_user_model().objects.get(username=self.kwargs["username"])
return Message.objects.get_conversation(active_user, self.request.user)
@@ -57,16 +61,15 @@ def send_message(request):
and create the new message and return the new data to be attached to the
conversation stream."""
sender = request.user
- recipient_username = request.POST.get('to')
+ recipient_username = request.POST.get("to")
recipient = get_user_model().objects.get(username=recipient_username)
- message = request.POST.get('message')
+ message = request.POST.get("message")
if len(message.strip()) == 0:
return HttpResponse()
if sender != recipient:
msg = Message.send_message(sender, recipient, message)
- return render(request, 'messager/single_message.html',
- {'message': msg})
+ return render(request, "messager/single_message.html", {"message": msg})
return HttpResponse()
@@ -77,7 +80,10 @@ def send_message(request):
def receive_message(request):
"""Simple AJAX functional view to return a rendered single message on the
receiver side providing realtime connections."""
- message_id = request.GET.get('message_id')
- message = Message.objects.get(pk=message_id)
- return render(request,
- 'messager/single_message.html', {'message': message})
+ message_id = request.GET.get("message_id")
+ try:
+ message = Message.objects.get(pk=message_id)
+ except Message.DoesNotExist as e:
+ raise e
+
+ return render(request, "messager/single_message.html", {"message": message})
diff --git a/bootcamp/news/admin.py b/bootcamp/news/admin.py
index f6e609d36..2bfe2797f 100755
--- a/bootcamp/news/admin.py
+++ b/bootcamp/news/admin.py
@@ -4,5 +4,5 @@
@admin.register(News)
class NewseAdmin(admin.ModelAdmin):
- list_display = ('content', 'user', 'reply')
- list_filter = ('timestamp', 'reply')
+ list_display = ("content", "user", "reply")
+ list_filter = ("timestamp", "reply")
diff --git a/bootcamp/news/apps.py b/bootcamp/news/apps.py
index 0086fbb18..b2a4c66ac 100755
--- a/bootcamp/news/apps.py
+++ b/bootcamp/news/apps.py
@@ -3,5 +3,5 @@
class NewsConfig(AppConfig):
- name = 'bootcamp.news'
+ name = "bootcamp.news"
verbose_name = _("News")
diff --git a/bootcamp/news/migrations/0001_initial.py b/bootcamp/news/migrations/0001_initial.py
index 7ae2f338a..fdee0f4a4 100755
--- a/bootcamp/news/migrations/0001_initial.py
+++ b/bootcamp/news/migrations/0001_initial.py
@@ -10,26 +10,59 @@ class Migration(migrations.Migration):
initial = True
- dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ]
+ dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
operations = [
migrations.CreateModel(
- name='News',
+ name="News",
fields=[
- ('timestamp', models.DateTimeField(auto_now_add=True)),
- ('uuid_id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('content', models.TextField(max_length=280)),
- ('reply', models.BooleanField(default=False, verbose_name='Is a reply?')),
- ('liked', models.ManyToManyField(blank=True, related_name='liked_news', to=settings.AUTH_USER_MODEL)),
- ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='thread', to='news.News')),
- ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='publisher', to=settings.AUTH_USER_MODEL)),
+ ("timestamp", models.DateTimeField(auto_now_add=True)),
+ (
+ "uuid_id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ ("content", models.TextField(max_length=280)),
+ (
+ "reply",
+ models.BooleanField(default=False, verbose_name="Is a reply?"),
+ ),
+ (
+ "liked",
+ models.ManyToManyField(
+ blank=True,
+ related_name="liked_news",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ (
+ "parent",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="thread",
+ to="news.News",
+ ),
+ ),
+ (
+ "user",
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="publisher",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
],
options={
- 'verbose_name': 'News',
- 'verbose_name_plural': 'News',
- 'ordering': ('-timestamp',),
+ "verbose_name": "News",
+ "verbose_name_plural": "News",
+ "ordering": ("-timestamp",),
},
- ),
+ )
]
diff --git a/bootcamp/news/migrations/0002_auto_20200405_1227.py b/bootcamp/news/migrations/0002_auto_20200405_1227.py
new file mode 100644
index 000000000..9cb20b084
--- /dev/null
+++ b/bootcamp/news/migrations/0002_auto_20200405_1227.py
@@ -0,0 +1,38 @@
+# Generated by Django 3.0.3 on 2020-04-05 12:27
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("news", "0001_initial"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="news",
+ name="meta_description",
+ field=models.TextField(max_length=255, null=True),
+ ),
+ migrations.AddField(
+ model_name="news",
+ name="meta_image",
+ field=models.CharField(max_length=255, null=True),
+ ),
+ migrations.AddField(
+ model_name="news",
+ name="meta_title",
+ field=models.CharField(max_length=255, null=True),
+ ),
+ migrations.AddField(
+ model_name="news",
+ name="meta_type",
+ field=models.CharField(max_length=255, null=True),
+ ),
+ migrations.AddField(
+ model_name="news",
+ name="meta_url",
+ field=models.CharField(max_length=2048, null=True),
+ ),
+ ]
diff --git a/bootcamp/news/models.py b/bootcamp/news/models.py
index 0f08a0889..bccfef3bd 100755
--- a/bootcamp/news/models.py
+++ b/bootcamp/news/models.py
@@ -10,23 +10,34 @@
from channels.layers import get_channel_layer
from bootcamp.notifications.models import Notification, notification_handler
+from bootcamp.helpers import fetch_metadata
class News(models.Model):
"""News model to contain small information snippets in the same manner as
Twitter does."""
+
user = models.ForeignKey(
- settings.AUTH_USER_MODEL, null=True, related_name="publisher",
- on_delete=models.SET_NULL)
- parent = models.ForeignKey("self", blank=True,
- null=True, on_delete=models.CASCADE, related_name="thread")
+ settings.AUTH_USER_MODEL,
+ null=True,
+ related_name="publisher",
+ on_delete=models.SET_NULL,
+ )
+ parent = models.ForeignKey(
+ "self", blank=True, null=True, on_delete=models.CASCADE, related_name="thread"
+ )
timestamp = models.DateTimeField(auto_now_add=True)
- uuid_id = models.UUIDField(
- primary_key=True, default=uuid.uuid4, editable=False)
+ uuid_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
content = models.TextField(max_length=280)
- liked = models.ManyToManyField(settings.AUTH_USER_MODEL,
- blank=True, related_name="liked_news")
+ liked = models.ManyToManyField(
+ settings.AUTH_USER_MODEL, blank=True, related_name="liked_news"
+ )
reply = models.BooleanField(verbose_name=_("Is a reply?"), default=False)
+ meta_url = models.CharField(max_length=2048, null=True)
+ meta_type = models.CharField(max_length=255, null=True)
+ meta_title = models.CharField(max_length=255, null=True)
+ meta_description = models.TextField(max_length=255, null=True)
+ meta_image = models.CharField(max_length=255, null=True)
class Meta:
verbose_name = _("News")
@@ -37,16 +48,24 @@ def __str__(self):
return str(self.content)
def save(self, *args, **kwargs):
+ # extract metada from content url
+ data = fetch_metadata(self.content)
+ if data:
+ self.meta_url = data.get("url")
+ self.meta_type = data.get("type", "website")
+ self.meta_title = data.get("title")
+ self.meta_description = data.get("description")
+ self.meta_image = data.get("image")
+
super().save(*args, **kwargs)
if not self.reply:
channel_layer = get_channel_layer()
payload = {
- "type": "receive",
- "key": "additional_news",
- "actor_name": self.user.username
-
- }
- async_to_sync(channel_layer.group_send)('notifications', payload)
+ "type": "receive",
+ "key": "additional_news",
+ "actor_name": self.user.username,
+ }
+ async_to_sync(channel_layer.group_send)("notifications", payload)
def get_absolute_url(self):
return reverse("news:detail", kwargs={"uuid_id": self.uuid})
@@ -57,10 +76,14 @@ def switch_like(self, user):
else:
self.liked.add(user)
- notification_handler(user, self.user,
- Notification.LIKED, action_object=self,
- id_value=str(self.uuid_id),
- key='social_update')
+ notification_handler(
+ user,
+ self.user,
+ Notification.LIKED,
+ action_object=self,
+ id_value=str(self.uuid_id),
+ key="social_update",
+ )
def get_parent(self):
if self.parent:
@@ -80,14 +103,16 @@ def reply_this(self, user, text):
"""
parent = self.get_parent()
reply_news = News.objects.create(
- user=user,
- content=text,
- reply=True,
- parent=parent
+ user=user, content=text, reply=True, parent=parent
)
notification_handler(
- user, parent.user, Notification.REPLY, action_object=reply_news,
- id_value=str(parent.uuid_id), key='social_update')
+ user,
+ parent.user,
+ Notification.REPLY,
+ action_object=reply_news,
+ id_value=str(parent.uuid_id),
+ key="social_update",
+ )
def get_thread(self):
parent = self.get_parent()
diff --git a/bootcamp/news/schema.py b/bootcamp/news/schema.py
index 1d1151815..51ea37e41 100755
--- a/bootcamp/news/schema.py
+++ b/bootcamp/news/schema.py
@@ -8,6 +8,7 @@
class NewsType(DjangoObjectType):
"""DjangoObjectType to acces the News model."""
+
count_thread = graphene.Int()
count_likers = graphene.Int()
@@ -18,12 +19,19 @@ def resolve_count_thread(self, info, **kwargs):
return self.get_thread().count()
def resolve_count_likers(self, info, **kwargs):
- return self.liked_news.count()
+ return self.get_likers().count()
+
+ def resolve_get_thread(self, info, **kwargs):
+ return self.get_thread()
+
+ def resolve_get_likers(self, info, **kwargs):
+ return self.get_likers()
class NewsPaginatedType(graphene.ObjectType):
"""A paginated type generic object to provide pagination to the news
graph."""
+
page = graphene.Int()
pages = graphene.Int()
has_next = graphene.Boolean()
@@ -47,9 +55,26 @@ def resolve_paginated_news(self, info, page):
return paginate_data(qs, page_size, page, NewsPaginatedType)
def resolve_news(self, info, **kwargs):
- uuid_id = kwargs.get('uuid_id')
+ uuid_id = kwargs.get("uuid_id")
if uuid_id is not None:
return News.objects.get(uuid_id=uuid_id)
return None
+
+
+class NewsMutation(graphene.Mutation):
+ """Mutation to create news objects on a efective way."""
+
+ class Arguments:
+ content = graphene.String()
+ user = graphene.ID()
+ parent = graphene.ID()
+
+ content = graphene.String()
+ user = graphene.ID()
+ parent = graphene.ID()
+ news = graphene.Field(lambda: News)
+
+ def mutate(self, **kwargs):
+ print(kwargs)
diff --git a/bootcamp/news/templatetags/__init__.py b/bootcamp/news/templatetags/__init__.py
new file mode 100755
index 000000000..e69de29bb
diff --git a/bootcamp/news/templatetags/urlize_target_blank.py b/bootcamp/news/templatetags/urlize_target_blank.py
new file mode 100755
index 000000000..5463a845f
--- /dev/null
+++ b/bootcamp/news/templatetags/urlize_target_blank.py
@@ -0,0 +1,12 @@
+from django import template
+from django.template.defaultfilters import stringfilter
+from django.utils.safestring import mark_safe
+from django.utils.html import urlize as urlize_impl
+
+register = template.Library()
+
+
+@register.filter(is_safe=True, needs_autoescape=True)
+@stringfilter
+def urlize_target_blank(value, autoescape=None):
+ return value.replace("[-\w]+)/$',
- views.NewsDeleteView.as_view(), name='delete_news'),
- url(r'^post-news/$', views.post_news, name='post_news'),
- url(r'^like/$', views.like, name='like_post'),
- url(r'^get-thread/$', views.get_thread, name='get_thread'),
- url(r'^post-comment/$', views.post_comment, name='post_comments'),
- url(r'^update-interactions/$', views.update_interactions, name='update_interactions'),
+ url(r"^$", views.NewsListView.as_view(), name="list"),
+ url(
+ r"^delete/(?P[-\w]+)/$", views.NewsDeleteView.as_view(), name="delete_news"
+ ),
+ url(r"^post-news/$", views.post_news, name="post_news"),
+ url(r"^like/$", views.like, name="like_post"),
+ url(r"^get-thread/$", views.get_thread, name="get_thread"),
+ url(r"^post-comment/$", views.post_comment, name="post_comments"),
+ url(
+ r"^update-interactions/$", views.update_interactions, name="update_interactions"
+ ),
]
diff --git a/bootcamp/news/views.py b/bootcamp/news/views.py
index a5babc798..737492549 100755
--- a/bootcamp/news/views.py
+++ b/bootcamp/news/views.py
@@ -13,6 +13,7 @@
class NewsListView(LoginRequiredMixin, ListView):
"""A really simple ListView, with some JS magic on the UI."""
+
model = News
paginate_by = 15
@@ -23,6 +24,7 @@ def get_queryset(self, **kwargs):
class NewsDeleteView(LoginRequiredMixin, AuthorRequiredMixin, DeleteView):
"""Implementation of the DeleteView overriding the delete method to
allow a no-redirect response to use with AJAX call."""
+
model = News
success_url = reverse_lazy("news:list")
@@ -34,25 +36,20 @@ def post_news(request):
"""A function view to implement the post functionality with AJAX allowing
to create News instances as parent ones."""
user = request.user
- post = request.POST['post']
+ post = request.POST["post"]
post = post.strip()
- if len(post) > 0 and len(post) <= 280:
- posted = News.objects.create(
- user=user,
- content=post,
- )
+ if 0 < len(post) <= 280:
+ posted = News.objects.create(user=user, content=post)
html = render_to_string(
- 'news/news_single.html',
- {
- 'news': posted,
- 'request': request
- })
+ "news/news_single.html", {"news": posted, "request": request}
+ )
return HttpResponse(html)
else:
- lenght = len(post) - 280
+ length = len(post) - 280
return HttpResponseBadRequest(
- content=_(f'Text is {lenght} characters longer than accepted.'))
+ content=_(f"Text is {length} characters longer than accepted.")
+ )
@login_required
@@ -61,7 +58,7 @@ def post_news(request):
def like(request):
"""Function view to receive AJAX, returns the count of likes a given news
has recieved."""
- news_id = request.POST['news']
+ news_id = request.POST["news"]
news = News.objects.get(pk=news_id)
user = request.user
news.switch_like(user)
@@ -73,16 +70,13 @@ def like(request):
@require_http_methods(["GET"])
def get_thread(request):
"""Returns a list of news with the given news as parent."""
- news_id = request.GET['news']
+ news_id = request.GET["news"]
news = News.objects.get(pk=news_id)
news_html = render_to_string("news/news_single.html", {"news": news})
thread_html = render_to_string(
- "news/news_thread.html", {"thread": news.get_thread()})
- return JsonResponse({
- "uuid": news_id,
- "news": news_html,
- "thread": thread_html,
- })
+ "news/news_thread.html", {"thread": news.get_thread(), "request": request}
+ )
+ return JsonResponse({"uuid": news_id, "news": news_html, "thread": thread_html})
@login_required
@@ -93,13 +87,13 @@ def post_comment(request):
News instances who happens to be the children and commenters of the root
post."""
user = request.user
- post = request.POST['reply']
- par = request.POST['parent']
+ post = request.POST["reply"]
+ par = request.POST["parent"]
parent = News.objects.get(pk=par)
post = post.strip()
if post:
parent.reply_this(user, post)
- return JsonResponse({'comments': parent.count_thread()})
+ return JsonResponse({"comments": parent.count_thread()})
else:
return HttpResponseBadRequest()
@@ -109,7 +103,7 @@ def post_comment(request):
@ajax_required
@require_http_methods(["POST"])
def update_interactions(request):
- data_point = request.POST['id_value']
+ data_point = request.POST["id_value"]
news = News.objects.get(pk=data_point)
- data = {'likes': news.count_likers(), 'comments': news.count_thread()}
+ data = {"likes": news.count_likers(), "comments": news.count_thread()}
return JsonResponse(data)
diff --git a/bootcamp/notifications/admin.py b/bootcamp/notifications/admin.py
index be7fa0b27..ce9e0f7fd 100755
--- a/bootcamp/notifications/admin.py
+++ b/bootcamp/notifications/admin.py
@@ -4,5 +4,5 @@
@admin.register(Notification)
class NotificationAdmin(admin.ModelAdmin):
- list_display = ('recipient', 'actor', 'verb', 'unread', )
- list_filter = ('recipient', 'unread', )
+ list_display = ("recipient", "actor", "verb", "unread")
+ list_filter = ("recipient", "unread")
diff --git a/bootcamp/notifications/apps.py b/bootcamp/notifications/apps.py
index 394767f2c..1dd04c24a 100755
--- a/bootcamp/notifications/apps.py
+++ b/bootcamp/notifications/apps.py
@@ -3,5 +3,5 @@
class NotificationsConfig(AppConfig):
- name = 'bootcamp.notifications'
- verbose_name = _('Notifications')
+ name = "bootcamp.notifications"
+ verbose_name = _("Notifications")
diff --git a/bootcamp/notifications/consumers.py b/bootcamp/notifications/consumers.py
index f27912e31..77b257019 100755
--- a/bootcamp/notifications/consumers.py
+++ b/bootcamp/notifications/consumers.py
@@ -7,6 +7,7 @@ class NotificationsConsumer(AsyncWebsocketConsumer):
"""Consumer to manage WebSocket connections for the Notification app,
called when the websocket is handshaking as part of initial connection.
"""
+
async def connect(self):
"""Consumer Connect implementation, to validate user status and prevent
non authenticated user to take advante from the connection."""
@@ -16,15 +17,13 @@ async def connect(self):
else:
# Accept the connection
- await self.channel_layer.group_add(
- 'notifications', self.channel_name)
+ await self.channel_layer.group_add("notifications", self.channel_name)
await self.accept()
async def disconnect(self, close_code):
"""Consumer implementation to leave behind the group at the moment the
closes the connection."""
- await self.channel_layer.group_discard(
- 'notifications', self.channel_name)
+ await self.channel_layer.group_discard("notifications", self.channel_name)
async def receive(self, text_data):
"""Receive method implementation to redirect any new message received
diff --git a/bootcamp/notifications/migrations/0001_initial.py b/bootcamp/notifications/migrations/0001_initial.py
index c98505d24..5af68e2dd 100755
--- a/bootcamp/notifications/migrations/0001_initial.py
+++ b/bootcamp/notifications/migrations/0001_initial.py
@@ -11,28 +11,82 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
- ('contenttypes', '0002_remove_content_type_name'),
+ ("contenttypes", "0002_remove_content_type_name"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
- name='Notification',
+ name="Notification",
fields=[
- ('unread', models.BooleanField(db_index=True, default=True)),
- ('timestamp', models.DateTimeField(auto_now_add=True)),
- ('uuid_id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('slug', models.SlugField(blank=True, max_length=210, null=True)),
- ('verb', models.CharField(choices=[('L', 'liked'), ('C', 'commented'), ('F', 'cavorited'), ('A', 'answered'), ('W', 'accepted'), ('E', 'edited'), ('K', 'also commented'), ('I', 'logged in'), ('O', 'logged out'), ('V', 'voted on'), ('S', 'shared'), ('U', 'created an account'), ('R', 'replied to')], max_length=1)),
- ('action_object_object_id', models.CharField(blank=True, max_length=50, null=True)),
- ('action_object_content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notify_action_object', to='contenttypes.ContentType')),
- ('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notify_actor', to=settings.AUTH_USER_MODEL)),
- ('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)),
+ ("unread", models.BooleanField(db_index=True, default=True)),
+ ("timestamp", models.DateTimeField(auto_now_add=True)),
+ (
+ "uuid_id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ ("slug", models.SlugField(blank=True, max_length=210, null=True)),
+ (
+ "verb",
+ models.CharField(
+ choices=[
+ ("L", "liked"),
+ ("C", "commented"),
+ ("F", "cavorited"),
+ ("A", "answered"),
+ ("W", "accepted"),
+ ("E", "edited"),
+ ("K", "also commented"),
+ ("I", "logged in"),
+ ("O", "logged out"),
+ ("V", "voted on"),
+ ("S", "shared"),
+ ("U", "created an account"),
+ ("R", "replied to"),
+ ],
+ max_length=1,
+ ),
+ ),
+ (
+ "action_object_object_id",
+ models.CharField(blank=True, max_length=50, null=True),
+ ),
+ (
+ "action_object_content_type",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="notify_action_object",
+ to="contenttypes.ContentType",
+ ),
+ ),
+ (
+ "actor",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="notify_actor",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ (
+ "recipient",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="notifications",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
],
options={
- 'verbose_name': 'Notification',
- 'verbose_name_plural': 'Notifications',
- 'ordering': ('-timestamp',),
+ "verbose_name": "Notification",
+ "verbose_name_plural": "Notifications",
+ "ordering": ("-timestamp",),
},
- ),
+ )
]
diff --git a/bootcamp/notifications/models.py b/bootcamp/notifications/models.py
index fbb198767..76229cc34 100755
--- a/bootcamp/notifications/models.py
+++ b/bootcamp/notifications/models.py
@@ -4,7 +4,6 @@
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
-from django.core import serializers
from django.db import models
from django.utils.translation import ugettext_lazy as _
@@ -46,23 +45,9 @@ def mark_all_as_unread(self, recipient=None):
return qs.update(unread=True)
- def serialize_latest_notifications(self, recipient=None):
- """Returns a serialized version of the most recent unread elements in
- the queryset"""
- qs = self.unread()[:5]
- if recipient:
- qs = qs.filter(recipient=recipient)[:5]
-
- notification_dic = serializers.serialize("json", qs)
- return notification_dic
-
- def get_most_recent(self, recipient=None):
+ def get_most_recent(self):
"""Returns the most recent unread elements in the queryset"""
- qs = self.unread()[:5]
- if recipient:
- qs = qs.filter(recipient=recipient)[:5]
-
- return qs
+ return self.unread()[:5]
class Notification(models.Model):
@@ -83,52 +68,60 @@ class Notification(models.Model):
<1 minute ago>
<2 hours ago>
"""
- LIKED = 'L'
- COMMENTED = 'C'
- FAVORITED = 'F'
- ANSWERED = 'A'
- ACCEPTED_ANSWER = 'W'
- EDITED_ARTICLE = 'E'
- ALSO_COMMENTED = 'K'
- LOGGED_IN = 'I'
- LOGGED_OUT = 'O'
- VOTED = 'V'
- SHARED = 'S'
- SIGNUP = 'U'
- REPLY = 'R'
+
+ LIKED = "L"
+ COMMENTED = "C"
+ FAVORITED = "F"
+ ANSWERED = "A"
+ ACCEPTED_ANSWER = "W"
+ EDITED_ARTICLE = "E"
+ ALSO_COMMENTED = "K"
+ LOGGED_IN = "I"
+ LOGGED_OUT = "O"
+ VOTED = "V"
+ SHARED = "S"
+ SIGNUP = "U"
+ REPLY = "R"
NOTIFICATION_TYPES = (
- (LIKED, _('liked')),
- (COMMENTED, _('commented')),
- (FAVORITED, _('cavorited')),
- (ANSWERED, _('answered')),
- (ACCEPTED_ANSWER, _('accepted')),
- (EDITED_ARTICLE, _('edited')),
- (ALSO_COMMENTED, _('also commented')),
- (LOGGED_IN, _('logged in')),
- (LOGGED_OUT, _('logged out')),
- (VOTED, _('voted on')),
- (SHARED, _('shared')),
- (SIGNUP, _('created an account')),
- (REPLY, _('replied to'))
- )
- actor = models.ForeignKey(settings.AUTH_USER_MODEL,
- related_name="notify_actor",
- on_delete=models.CASCADE)
- recipient = models.ForeignKey(settings.AUTH_USER_MODEL, blank=False,
- related_name="notifications", on_delete=models.CASCADE)
+ (LIKED, _("liked")),
+ (COMMENTED, _("commented")),
+ (FAVORITED, _("cavorited")),
+ (ANSWERED, _("answered")),
+ (ACCEPTED_ANSWER, _("accepted")),
+ (EDITED_ARTICLE, _("edited")),
+ (ALSO_COMMENTED, _("also commented")),
+ (LOGGED_IN, _("logged in")),
+ (LOGGED_OUT, _("logged out")),
+ (VOTED, _("voted on")),
+ (SHARED, _("shared")),
+ (SIGNUP, _("created an account")),
+ (REPLY, _("replied to")),
+ )
+ actor = models.ForeignKey(
+ settings.AUTH_USER_MODEL, related_name="notify_actor", on_delete=models.CASCADE
+ )
+ recipient = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ blank=False,
+ related_name="notifications",
+ on_delete=models.CASCADE,
+ )
unread = models.BooleanField(default=True, db_index=True)
timestamp = models.DateTimeField(auto_now_add=True)
- uuid_id = models.UUIDField(
- primary_key=True, default=uuid.uuid4, editable=False)
+ uuid_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
slug = models.SlugField(max_length=210, null=True, blank=True)
verb = models.CharField(max_length=1, choices=NOTIFICATION_TYPES)
- action_object_content_type = models.ForeignKey(ContentType,
- blank=True, null=True, related_name="notify_action_object",
- on_delete=models.CASCADE)
- action_object_object_id = models.CharField(
- max_length=50, blank=True, null=True)
+ action_object_content_type = models.ForeignKey(
+ ContentType,
+ blank=True,
+ null=True,
+ related_name="notify_action_object",
+ on_delete=models.CASCADE,
+ )
+ action_object_object_id = models.CharField(max_length=50, blank=True, null=True)
action_object = GenericForeignKey(
- "action_object_content_type", "action_object_object_id")
+ "action_object_content_type", "action_object_object_id"
+ )
objects = NotificationQuerySet.as_manager()
class Meta:
@@ -138,14 +131,17 @@ class Meta:
def __str__(self):
if self.action_object:
- return f'{self.actor} {self.get_verb_display()} {self.action_object} {self.time_since()} ago'
+ return f"{self.actor} {self.get_verb_display()} {self.action_object} {self.time_since()} ago"
- return f'{self.actor} {self.get_verb_display()} {self.time_since()} ago'
+ return f"{self.actor} {self.get_verb_display()} {self.time_since()} ago"
def save(self, *args, **kwargs):
if not self.slug:
- self.slug = slugify(f'{self.recipient} {self.uuid_id} {self.verb}',
- to_lower=True, max_length=200)
+ self.slug = slugify(
+ f"{self.recipient} {self.uuid_id} {self.verb}",
+ lowercase=True,
+ max_length=200,
+ )
super().save(*args, **kwargs)
@@ -162,32 +158,32 @@ def get_icon(self):
"""Model method to validate notification type and return the closest
icon to the verb.
"""
- if self.verb == 'C' or self.verb == 'A' or self.verb == 'K':
- return 'fa-comment'
+ if self.verb == "C" or self.verb == "A" or self.verb == "K":
+ return "fa-comment"
- elif self.verb == 'I' or self.verb == 'U' or self.verb == 'O':
- return 'fa-users'
+ elif self.verb == "I" or self.verb == "U" or self.verb == "O":
+ return "fa-users"
- elif self.verb == 'L':
- return 'fa-heart'
+ elif self.verb == "L":
+ return "fa-heart"
- elif self.verb == 'F':
- return 'fa-star'
+ elif self.verb == "F":
+ return "fa-star"
- elif self.verb == 'W':
- return 'fa-check-circle'
+ elif self.verb == "W":
+ return "fa-check-circle"
- elif self.verb == 'E':
- return 'fa-pencil'
+ elif self.verb == "E":
+ return "fa-pencil"
- elif self.verb == 'V':
- return 'fa-plus'
+ elif self.verb == "V":
+ return "fa-plus"
- elif self.verb == 'S':
- return 'fa-share-alt'
+ elif self.verb == "S":
+ return "fa-share-alt"
- elif self.verb == 'R':
- return 'fa-reply'
+ elif self.verb == "R":
+ return "fa-reply"
def mark_as_read(self):
if self.unread:
@@ -214,16 +210,16 @@ def notification_handler(actor, recipient, verb, **kwargs):
:param key: String defining what kind of notification is going to be created.
:param id_value: UUID value assigned to a specific element in the DOM.
"""
- key = kwargs.pop('key', 'notification')
- id_value = kwargs.pop('id_value', None)
- if recipient == 'global':
+ key = kwargs.pop("key", "notification")
+ id_value = kwargs.pop("id_value", None)
+ if recipient == "global":
users = get_user_model().objects.all().exclude(username=actor.username)
for user in users:
Notification.objects.create(
actor=actor,
recipient=user,
verb=verb,
- action_object=kwargs.pop('action_object', None)
+ action_object=kwargs.pop("action_object", None),
)
notification_broadcast(actor, key)
@@ -233,7 +229,7 @@ def notification_handler(actor, recipient, verb, **kwargs):
actor=actor,
recipient=get_user_model().objects.get(username=user),
verb=verb,
- action_object=kwargs.pop('action_object', None)
+ action_object=kwargs.pop("action_object", None),
)
elif isinstance(recipient, get_user_model()):
@@ -241,10 +237,11 @@ def notification_handler(actor, recipient, verb, **kwargs):
actor=actor,
recipient=recipient,
verb=verb,
- action_object=kwargs.pop('action_object', None)
+ action_object=kwargs.pop("action_object", None),
)
notification_broadcast(
- actor, key, id_value=id_value, recipient=recipient.username)
+ actor, key, id_value=id_value, recipient=recipient.username
+ )
else:
pass
@@ -264,13 +261,13 @@ def notification_broadcast(actor, key, **kwargs):
notified.
"""
channel_layer = get_channel_layer()
- id_value = kwargs.pop('id_value', None)
- recipient = kwargs.pop('recipient', None)
+ id_value = kwargs.pop("id_value", None)
+ recipient = kwargs.pop("recipient", None)
payload = {
- 'type': 'receive',
- 'key': key,
- 'actor_name': actor.username,
- 'id_value': id_value,
- 'recipient': recipient
- }
- async_to_sync(channel_layer.group_send)('notifications', payload)
+ "type": "receive",
+ "key": key,
+ "actor_name": actor.username,
+ "id_value": id_value,
+ "recipient": recipient,
+ }
+ async_to_sync(channel_layer.group_send)("notifications", payload)
diff --git a/bootcamp/notifications/tests/test_models.py b/bootcamp/notifications/tests/test_models.py
index 9ea96d226..57d914148 100755
--- a/bootcamp/notifications/tests/test_models.py
+++ b/bootcamp/notifications/tests/test_models.py
@@ -8,32 +8,46 @@ class NotificationsModelsTest(TestCase):
def setUp(self):
self.user = self.make_user("test_user")
self.other_user = self.make_user("other_test_user")
+ self.first_news = News.objects.create(
+ user=self.user, content="This is a short content."
+ )
+ self.second_news = News.objects.create(
+ user=self.other_user,
+ content="This is an answer to the first news.",
+ reply=True,
+ parent=self.first_news,
+ )
self.first_notification = Notification.objects.create(
- actor=self.user,
- recipient=self.other_user,
- verb="L"
- )
+ actor=self.user, recipient=self.other_user, verb="L"
+ )
self.second_notification = Notification.objects.create(
- actor=self.user,
- recipient=self.other_user,
- verb="C"
- )
+ actor=self.user, recipient=self.other_user, verb="C"
+ )
self.third_notification = Notification.objects.create(
- actor=self.other_user,
- recipient=self.user,
- verb="A"
- )
+ actor=self.other_user, recipient=self.user, verb="A"
+ )
+ self.fourth_notification = Notification.objects.create(
+ actor=self.other_user,
+ recipient=self.user,
+ action_object=self.first_news,
+ verb="A",
+ )
def test_return_values(self):
assert isinstance(self.first_notification, Notification)
assert isinstance(self.second_notification, Notification)
assert isinstance(self.third_notification, Notification)
+ assert isinstance(self.fourth_notification, Notification)
assert str(self.first_notification) == "test_user liked 0 minutes ago"
assert str(self.second_notification) == "test_user commented 0 minutes ago"
assert str(self.third_notification) == "other_test_user answered 0 minutes ago"
+ assert (
+ str(self.fourth_notification)
+ == "other_test_user answered This is a short content. 0 minutes ago"
+ )
def test_return_unread(self):
- assert Notification.objects.unread().count() == 3
+ assert Notification.objects.unread().count() == 4
assert self.first_notification in Notification.objects.unread()
def test_mark_as_read_and_return(self):
@@ -45,23 +59,20 @@ def test_mark_as_read_and_return(self):
def test_mark_all_as_read(self):
Notification.objects.mark_all_as_read()
- assert Notification.objects.read().count() == 3
+ assert Notification.objects.read().count() == 4
Notification.objects.mark_all_as_unread(self.other_user)
- assert Notification.objects.read().count() == 1
+ assert Notification.objects.read().count() == 2
Notification.objects.mark_all_as_unread()
- assert Notification.objects.unread().count() == 3
+ assert Notification.objects.unread().count() == 4
Notification.objects.mark_all_as_read(self.other_user)
assert Notification.objects.read().count() == 2
def test_get_most_recent(self):
- assert Notification.objects.get_most_recent().count() == 3
+ assert Notification.objects.get_most_recent().count() == 4
def test_single_notification(self):
Notification.objects.mark_all_as_read()
- obj = News.objects.create(
- user=self.user,
- content="This is a short content."
- )
+ obj = News.objects.create(user=self.user, content="This is a short content.")
notification_handler(self.user, self.other_user, "C", action_object=obj)
assert Notification.objects.unread().count() == 1
@@ -74,3 +85,73 @@ def test_list_notification(self):
Notification.objects.mark_all_as_read()
notification_handler(self.user, [self.user, self.other_user], "C")
assert Notification.objects.unread().count() == 2
+
+ def test_icon_comment(self):
+ notification_one = Notification.objects.create(
+ actor=self.user, recipient=self.other_user, verb="C"
+ )
+ notification_two = Notification.objects.create(
+ actor=self.user, recipient=self.other_user, verb="A"
+ )
+ notification_three = Notification.objects.create(
+ actor=self.user, recipient=self.other_user, verb="K"
+ )
+ assert notification_one.get_icon() == "fa-comment"
+ assert notification_two.get_icon() == "fa-comment"
+ assert notification_three.get_icon() == "fa-comment"
+
+ def test_icon_users(self):
+ notification_one = Notification.objects.create(
+ actor=self.user, recipient=self.other_user, verb="I"
+ )
+ notification_two = Notification.objects.create(
+ actor=self.user, recipient=self.other_user, verb="U"
+ )
+ notification_three = Notification.objects.create(
+ actor=self.user, recipient=self.other_user, verb="O"
+ )
+ assert notification_one.get_icon() == "fa-users"
+ assert notification_two.get_icon() == "fa-users"
+ assert notification_three.get_icon() == "fa-users"
+
+ def test_icon_hearth(self):
+ notification = Notification.objects.create(
+ actor=self.user, recipient=self.other_user, verb="L"
+ )
+ assert notification.get_icon() == "fa-heart"
+
+ def test_icon_star(self):
+ notification = Notification.objects.create(
+ actor=self.user, recipient=self.other_user, verb="F"
+ )
+ assert notification.get_icon() == "fa-star"
+
+ def test_icon_check_circle(self):
+ notification = Notification.objects.create(
+ actor=self.user, recipient=self.other_user, verb="W"
+ )
+ assert notification.get_icon() == "fa-check-circle"
+
+ def test_icon_pencil(self):
+ notification = Notification.objects.create(
+ actor=self.user, recipient=self.other_user, verb="E"
+ )
+ assert notification.get_icon() == "fa-pencil"
+
+ def test_icon_plus(self):
+ notification = Notification.objects.create(
+ actor=self.user, recipient=self.other_user, verb="V"
+ )
+ assert notification.get_icon() == "fa-plus"
+
+ def test_icon_share(self):
+ notification = Notification.objects.create(
+ actor=self.user, recipient=self.other_user, verb="S"
+ )
+ assert notification.get_icon() == "fa-share-alt"
+
+ def test_icon_reply(self):
+ notification = Notification.objects.create(
+ actor=self.user, recipient=self.other_user, verb="R"
+ )
+ assert notification.get_icon() == "fa-reply"
diff --git a/bootcamp/notifications/tests/test_views.py b/bootcamp/notifications/tests/test_views.py
index f96ac1fd5..89efcda8c 100755
--- a/bootcamp/notifications/tests/test_views.py
+++ b/bootcamp/notifications/tests/test_views.py
@@ -15,20 +15,14 @@ def setUp(self):
self.client.login(username="first_user", password="password")
self.other_client.login(username="second_user", password="password")
self.first_notification = Notification.objects.create(
- actor=self.user,
- recipient=self.other_user,
- verb="L"
- )
+ actor=self.user, recipient=self.other_user, verb="L"
+ )
self.second_notification = Notification.objects.create(
- actor=self.user,
- recipient=self.other_user,
- verb="C"
- )
+ actor=self.user, recipient=self.other_user, verb="C"
+ )
self.third_notification = Notification.objects.create(
- actor=self.other_user,
- recipient=self.user,
- verb="A"
- )
+ actor=self.other_user, recipient=self.user, verb="A"
+ )
def test_notification_list(self):
response = self.client.get(reverse("notifications:unread"))
@@ -37,12 +31,16 @@ def test_notification_list(self):
def test_mark_all_as_read(self):
response = self.client.get(reverse("notifications:mark_all_read"), follow=True)
- assert '/notifications/' in str(response.context["request"])
+ assert "/notifications/" in str(response.context["request"])
assert Notification.objects.unread().count() == 2
def test_mark_as_read(self):
response = self.client.get(
- reverse("notifications:mark_as_read", kwargs={"slug": self.first_notification.slug}))
+ reverse(
+ "notifications:mark_as_read",
+ kwargs={"slug": self.first_notification.slug},
+ )
+ )
assert response.status_code == 302
assert Notification.objects.unread().count() == 2
diff --git a/bootcamp/notifications/urls.py b/bootcamp/notifications/urls.py
index 813cce56f..3593fb82e 100755
--- a/bootcamp/notifications/urls.py
+++ b/bootcamp/notifications/urls.py
@@ -2,10 +2,14 @@
from bootcamp.notifications import views
-app_name = 'notifications'
+app_name = "notifications"
urlpatterns = [
- url(r'^$', views.NotificationUnreadListView.as_view(), name='unread'),
- url(r'^mark-as-read/(?P[-\w]+)/$', views.mark_as_read, name='mark_as_read'),
- url(r'^mark-all-as-read/$', views.mark_all_as_read, name='mark_all_read'),
- url(r'^latest-notifications/$', views.get_latest_notifications, name='latest_notifications'),
+ url(r"^$", views.NotificationUnreadListView.as_view(), name="unread"),
+ url(r"^mark-as-read/(?P[-\w]+)/$", views.mark_as_read, name="mark_as_read"),
+ url(r"^mark-all-as-read/$", views.mark_all_as_read, name="mark_all_read"),
+ url(
+ r"^latest-notifications/$",
+ views.get_latest_notifications,
+ name="latest_notifications",
+ ),
]
diff --git a/bootcamp/notifications/views.py b/bootcamp/notifications/views.py
index 9392081b0..7fcd3ef4b 100755
--- a/bootcamp/notifications/views.py
+++ b/bootcamp/notifications/views.py
@@ -11,9 +11,10 @@
class NotificationUnreadListView(LoginRequiredMixin, ListView):
"""Basic ListView implementation to show the unread notifications for
the actual user"""
+
model = Notification
- context_object_name = 'notification_list'
- template_name = 'notifications/notification_list.html'
+ context_object_name = "notification_list"
+ template_name = "notifications/notification_list.html"
def get_queryset(self, **kwargs):
return self.request.user.notifications.unread()
@@ -24,15 +25,17 @@ def mark_all_as_read(request):
"""View to call the model method which marks as read all the notifications
directed to the actual user."""
request.user.notifications.mark_all_as_read()
- _next = request.GET.get('next')
+ _next = request.GET.get("next")
messages.add_message(
- request, messages.SUCCESS,
- _(f'All notifications to {request.user.username} have been marked as read.'))
+ request,
+ messages.SUCCESS,
+ _(f"All notifications to {request.user.username} have been marked as read."),
+ )
if _next:
return redirect(_next)
- return redirect('notifications:unread')
+ return redirect("notifications:unread")
@login_required
@@ -44,19 +47,21 @@ def mark_as_read(request, slug=None):
notification.mark_as_read()
messages.add_message(
- request, messages.SUCCESS,
- _(f'The notification {notification.slug} has been marked as read.'))
- _next = request.GET.get('next')
+ request,
+ messages.SUCCESS,
+ _(f"The notification {notification.slug} has been marked as read."),
+ )
+ _next = request.GET.get("next")
if _next:
return redirect(_next)
- return redirect('notifications:unread')
+ return redirect("notifications:unread")
@login_required
def get_latest_notifications(request):
notifications = request.user.notifications.get_most_recent()
- return render(request,
- 'notifications/most_recent.html',
- {'notifications': notifications})
+ return render(
+ request, "notifications/most_recent.html", {"notifications": notifications}
+ )
diff --git a/bootcamp/qa/apps.py b/bootcamp/qa/apps.py
index 17909cc29..7a2617150 100755
--- a/bootcamp/qa/apps.py
+++ b/bootcamp/qa/apps.py
@@ -3,5 +3,5 @@
class QaConfig(AppConfig):
- name = 'bootcamp.qa'
- verbose_name = _('Q&A')
+ name = "bootcamp.qa"
+ verbose_name = _("Q&A")
diff --git a/bootcamp/qa/migrations/0001_initial.py b/bootcamp/qa/migrations/0001_initial.py
index 16ebf7c98..34849feea 100755
--- a/bootcamp/qa/migrations/0001_initial.py
+++ b/bootcamp/qa/migrations/0001_initial.py
@@ -13,78 +13,137 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
- ('taggit', '0002_auto_20150616_2121'),
+ ("taggit", "0002_auto_20150616_2121"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ('contenttypes', '0002_remove_content_type_name'),
+ ("contenttypes", "0002_remove_content_type_name"),
]
operations = [
migrations.CreateModel(
- name='Answer',
+ name="Answer",
fields=[
- ('content', markdownx.models.MarkdownxField()),
- ('uuid_id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('total_votes', models.IntegerField(default=0)),
- ('timestamp', models.DateTimeField(auto_now_add=True)),
- ('is_answer', models.BooleanField(default=False)),
+ ("content", markdownx.models.MarkdownxField()),
+ (
+ "uuid_id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ ("total_votes", models.IntegerField(default=0)),
+ ("timestamp", models.DateTimeField(auto_now_add=True)),
+ ("is_answer", models.BooleanField(default=False)),
],
options={
- 'verbose_name': 'Answer',
- 'verbose_name_plural': 'Answers',
- 'ordering': ['-is_answer', '-timestamp'],
+ "verbose_name": "Answer",
+ "verbose_name_plural": "Answers",
+ "ordering": ["-is_answer", "-timestamp"],
},
),
migrations.CreateModel(
- name='Question',
+ name="Question",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('title', models.CharField(max_length=200, unique=True)),
- ('timestamp', models.DateTimeField(auto_now_add=True)),
- ('slug', models.SlugField(blank=True, max_length=80, null=True)),
- ('status', models.CharField(choices=[('O', 'Open'), ('C', 'Closed'), ('D', 'Draft')], default='D', max_length=1)),
- ('content', markdownx.models.MarkdownxField()),
- ('has_answer', models.BooleanField(default=False)),
- ('total_votes', models.IntegerField(default=0)),
- ('tags', taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags')),
- ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("title", models.CharField(max_length=200, unique=True)),
+ ("timestamp", models.DateTimeField(auto_now_add=True)),
+ ("slug", models.SlugField(blank=True, max_length=80, null=True)),
+ (
+ "status",
+ models.CharField(
+ choices=[("O", "Open"), ("C", "Closed"), ("D", "Draft")],
+ default="D",
+ max_length=1,
+ ),
+ ),
+ ("content", markdownx.models.MarkdownxField()),
+ ("has_answer", models.BooleanField(default=False)),
+ ("total_votes", models.IntegerField(default=0)),
+ (
+ "tags",
+ taggit.managers.TaggableManager(
+ help_text="A comma-separated list of tags.",
+ through="taggit.TaggedItem",
+ to="taggit.Tag",
+ verbose_name="Tags",
+ ),
+ ),
+ (
+ "user",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
],
options={
- 'verbose_name': 'Question',
- 'verbose_name_plural': 'Questions',
- 'ordering': ['-timestamp'],
+ "verbose_name": "Question",
+ "verbose_name_plural": "Questions",
+ "ordering": ["-timestamp"],
},
),
migrations.CreateModel(
- name='Vote',
+ name="Vote",
fields=[
- ('uuid_id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('timestamp', models.DateTimeField(auto_now_add=True)),
- ('value', models.BooleanField(default=True)),
- ('object_id', models.CharField(blank=True, max_length=50, null=True)),
- ('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='votes_on', to='contenttypes.ContentType')),
- ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ (
+ "uuid_id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ ("timestamp", models.DateTimeField(auto_now_add=True)),
+ ("value", models.BooleanField(default=True)),
+ ("object_id", models.CharField(blank=True, max_length=50, null=True)),
+ (
+ "content_type",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="votes_on",
+ to="contenttypes.ContentType",
+ ),
+ ),
+ (
+ "user",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
],
- options={
- 'verbose_name': 'Vote',
- 'verbose_name_plural': 'Votes',
- },
+ options={"verbose_name": "Vote", "verbose_name_plural": "Votes"},
),
migrations.AddField(
- model_name='answer',
- name='question',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='qa.Question'),
+ model_name="answer",
+ name="question",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE, to="qa.Question"
+ ),
),
migrations.AddField(
- model_name='answer',
- name='user',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
+ model_name="answer",
+ name="user",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
+ ),
),
migrations.AlterUniqueTogether(
- name='vote',
- unique_together={('user', 'content_type', 'object_id')},
+ name="vote", unique_together={("user", "content_type", "object_id")}
),
migrations.AlterIndexTogether(
- name='vote',
- index_together={('content_type', 'object_id')},
+ name="vote", index_together={("content_type", "object_id")}
),
]
diff --git a/bootcamp/qa/models.py b/bootcamp/qa/models.py
index b043d28a2..a59b89f4e 100755
--- a/bootcamp/qa/models.py
+++ b/bootcamp/qa/models.py
@@ -18,18 +18,20 @@
class Vote(models.Model):
"""Model class to host every vote, made with ContentType framework to
allow a single model connected to Questions and Answers."""
- uuid_id = models.UUIDField(
- primary_key=True, default=uuid.uuid4, editable=False)
- user = models.ForeignKey(
- settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
+
+ uuid_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
+ user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
timestamp = models.DateTimeField(auto_now_add=True)
value = models.BooleanField(default=True)
- content_type = models.ForeignKey(ContentType,
- blank=True, null=True, related_name="votes_on", on_delete=models.CASCADE)
- object_id = models.CharField(
- max_length=50, blank=True, null=True)
- vote = GenericForeignKey(
- "content_type", "object_id")
+ content_type = models.ForeignKey(
+ ContentType,
+ blank=True,
+ null=True,
+ related_name="votes_on",
+ on_delete=models.CASCADE,
+ )
+ object_id = models.CharField(max_length=50, blank=True, null=True)
+ vote = GenericForeignKey("content_type", "object_id")
class Meta:
verbose_name = _("Vote")
@@ -54,7 +56,7 @@ def get_unanswered(self):
def get_counted_tags(self):
"""Returns a dict element with tags and its count to show on the UI."""
tag_dict = {}
- query = self.all().annotate(tagged=Count('tags')).filter(tags__gt=0)
+ query = self.all().annotate(tagged=Count("tags")).filter(tags__gt=0)
for obj in query:
for tag in obj.tags.names():
if tag not in tag_dict:
@@ -68,14 +70,11 @@ def get_counted_tags(self):
class Question(models.Model):
"""Model class to contain every question in the forum."""
+
OPEN = "O"
CLOSED = "C"
DRAFT = "D"
- STATUS = (
- (OPEN, _("Open")),
- (CLOSED, _("Closed")),
- (DRAFT, _("Draft")),
- )
+ STATUS = ((OPEN, _("Open")), (CLOSED, _("Closed")), (DRAFT, _("Draft")))
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
title = models.CharField(max_length=200, unique=True, blank=False)
timestamp = models.DateTimeField(auto_now_add=True)
@@ -95,8 +94,9 @@ class Meta:
def save(self, *args, **kwargs):
if not self.slug:
- self.slug = slugify(f"{self.title}-{self.id}",
- to_lower=True, max_length=80)
+ self.slug = slugify(
+ f"{self.title}-{self.id}", lowercase=True, max_length=80
+ )
super().save(*args, **kwargs)
@@ -135,11 +135,11 @@ def get_markdown(self):
class Answer(models.Model):
"""Model class to contain every answer in the forum and to link it
to its respective question."""
+
question = models.ForeignKey(Question, on_delete=models.CASCADE)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
content = MarkdownxField()
- uuid_id = models.UUIDField(
- primary_key=True, default=uuid.uuid4, editable=False)
+ uuid_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
total_votes = models.IntegerField(default=0)
timestamp = models.DateTimeField(auto_now_add=True)
is_answer = models.BooleanField(default=False)
@@ -160,7 +160,9 @@ def count_votes(self):
"""Method to update the sum of the total votes. Uses this complex query
to avoid race conditions at database level."""
dic = Counter(self.votes.values_list("value", flat=True))
- Answer.objects.filter(uuid_id=self.uuid_id).update(total_votes=dic[True] - dic[False])
+ Answer.objects.filter(uuid_id=self.uuid_id).update(
+ total_votes=dic[True] - dic[False]
+ )
self.refresh_from_db()
def get_upvoters(self):
diff --git a/bootcamp/qa/tests/test_models.py b/bootcamp/qa/tests/test_models.py
index 280c88a2a..5f8567788 100755
--- a/bootcamp/qa/tests/test_models.py
+++ b/bootcamp/qa/tests/test_models.py
@@ -8,10 +8,11 @@ def setUp(self):
self.user = self.make_user("test_user")
self.other_user = self.make_user("other_test_user")
self.question_one = Question.objects.create(
- user=self.user, title="This is a sample question",
+ user=self.user,
+ title="This is a sample question",
content="This is a sample question content",
- tags="test1, test2"
)
+ self.question_one.tags.add("test1", "test2")
self.question_two = Question.objects.create(
user=self.user,
title="A Short Title",
@@ -21,45 +22,49 @@ def setUp(self):
know than nobody wants to publish a test, just a test;
everybody always wants the real deal.""",
has_answer=True,
- tags="test1, test2"
)
+ self.question_two.tags.add("test1", "test2")
self.answer = Answer.objects.create(
user=self.user,
question=self.question_two,
content="A reaaaaally loooong content",
- is_answer=True
+ is_answer=True,
)
def test_can_vote_question(self):
self.question_one.votes.update_or_create(
- user=self.user, defaults={"value": True}, )
+ user=self.user, defaults={"value": True}
+ )
self.question_one.votes.update_or_create(
- user=self.other_user, defaults={"value": True})
+ user=self.other_user, defaults={"value": True}
+ )
self.question_one.count_votes()
assert self.question_one.total_votes == 2
def test_can_vote_answer(self):
+ self.answer.votes.update_or_create(user=self.user, defaults={"value": True})
self.answer.votes.update_or_create(
- user=self.user, defaults={"value": True}, )
- self.answer.votes.update_or_create(
- user=self.other_user, defaults={"value": True}, )
+ user=self.other_user, defaults={"value": True}
+ )
self.answer.count_votes()
assert self.answer.total_votes == 2
def test_get_question_voters(self):
self.question_one.votes.update_or_create(
- user=self.user, defaults={"value": True}, )
+ user=self.user, defaults={"value": True}
+ )
self.question_one.votes.update_or_create(
- user=self.other_user, defaults={"value": False})
+ user=self.other_user, defaults={"value": False}
+ )
self.question_one.count_votes()
assert self.user in self.question_one.get_upvoters()
assert self.other_user in self.question_one.get_downvoters()
def test_get_answern_voters(self):
+ self.answer.votes.update_or_create(user=self.user, defaults={"value": True})
self.answer.votes.update_or_create(
- user=self.user, defaults={"value": True}, )
- self.answer.votes.update_or_create(
- user=self.other_user, defaults={"value": False})
+ user=self.other_user, defaults={"value": False}
+ )
self.answer.count_votes()
assert self.user in self.answer.get_upvoters()
assert self.other_user in self.answer.get_downvoters()
@@ -83,6 +88,10 @@ def test_question_answer_count(self):
def test_question_accepted_answer(self):
assert self.question_two.get_accepted_answer() == self.answer
+ def test_get_popular_tags(self):
+ correct_dict = {"test1": 2, "test2": 2}
+ assert Question.objects.get_counted_tags() == correct_dict.items()
+
# Answer model tests
def test_answer_return_value(self):
assert str(self.answer) == "A reaaaaally loooong content"
@@ -91,17 +100,17 @@ def test_answer_accept_method(self):
answer_one = Answer.objects.create(
user=self.user,
question=self.question_one,
- content="A reaaaaally loooonger content"
+ content="A reaaaaally loooonger content",
)
answer_two = Answer.objects.create(
user=self.user,
question=self.question_one,
- content="A reaaaaally even loooonger content"
+ content="A reaaaaally even loooonger content",
)
answer_three = Answer.objects.create(
user=self.user,
question=self.question_one,
- content="Even a reaaaaally loooonger content"
+ content="Even a reaaaaally loooonger content",
)
self.assertFalse(answer_one.is_answer)
self.assertFalse(answer_two.is_answer)
diff --git a/bootcamp/qa/tests/test_views.py b/bootcamp/qa/tests/test_views.py
index 720f22cf5..9e6e889d3 100755
--- a/bootcamp/qa/tests/test_views.py
+++ b/bootcamp/qa/tests/test_views.py
@@ -15,9 +15,10 @@ def setUp(self):
self.client.login(username="first_user", password="password")
self.other_client.login(username="second_user", password="password")
self.question_one = Question.objects.create(
- user=self.user, title="This is a sample question",
+ user=self.user,
+ title="This is a sample question",
content="This is a sample question content",
- tags="test1, test2"
+ tags="test1, test2",
)
self.question_two = Question.objects.create(
user=self.user,
@@ -28,13 +29,13 @@ def setUp(self):
know than nobody wants to publish a test, just a test;
everybody always wants the real deal.""",
has_answer=True,
- tags="test1, test2"
+ tags="test1, test2",
)
self.answer = Answer.objects.create(
user=self.user,
question=self.question_two,
content="A reaaaaally loooong content",
- is_answer=True
+ is_answer=True,
)
def test_index_questions(self):
@@ -44,11 +45,15 @@ def test_index_questions(self):
def test_create_question_view(self):
current_count = Question.objects.count()
- response = self.client.post(reverse("qa:ask_question"),
- {"title": "Not much of a title",
- "content": "bablababla bablababla",
- "status": "O",
- "tags": "test, tag"})
+ response = self.client.post(
+ reverse("qa:ask_question"),
+ {
+ "title": "Not much of a title",
+ "content": "bablababla bablababla",
+ "status": "O",
+ "tags": "test, tag",
+ },
+ )
assert response.status_code == 302
new_question = Question.objects.first()
assert new_question.title == "Not much of a title"
@@ -67,9 +72,8 @@ def test_unanswered_questions(self):
def test_answer_question(self):
current_answer_count = Answer.objects.count()
response = self.client.post(
- reverse("qa:propose_answer",
- kwargs={"question_id": self.question_one.id}),
- {"content": "A reaaaaally loooong content"}
+ reverse("qa:propose_answer", kwargs={"question_id": self.question_one.id}),
+ {"content": "A reaaaaally loooong content"},
)
assert response.status_code == 302
assert Answer.objects.count() == current_answer_count + 1
@@ -78,33 +82,50 @@ def test_question_upvote(self):
response_one = self.client.post(
reverse("qa:question_vote"),
{"value": "U", "question": self.question_one.id},
- HTTP_X_REQUESTED_WITH="XMLHttpRequest")
+ HTTP_X_REQUESTED_WITH="XMLHttpRequest",
+ )
assert response_one.status_code == 200
def test_question_downvote(self):
response_one = self.client.post(
reverse("qa:question_vote"),
{"value": "D", "question": self.question_one.id},
- HTTP_X_REQUESTED_WITH="XMLHttpRequest")
+ HTTP_X_REQUESTED_WITH="XMLHttpRequest",
+ )
assert response_one.status_code == 200
def test_answer_upvote(self):
response_one = self.client.post(
reverse("qa:answer_vote"),
{"value": "U", "answer": self.answer.uuid_id},
- HTTP_X_REQUESTED_WITH="XMLHttpRequest")
+ HTTP_X_REQUESTED_WITH="XMLHttpRequest",
+ )
assert response_one.status_code == 200
def test_answer_downvote(self):
response_one = self.client.post(
reverse("qa:answer_vote"),
{"value": "D", "answer": self.answer.uuid_id},
- HTTP_X_REQUESTED_WITH="XMLHttpRequest")
+ HTTP_X_REQUESTED_WITH="XMLHttpRequest",
+ )
assert response_one.status_code == 200
def test_accept_answer(self):
response_one = self.client.post(
reverse("qa:accept_answer"),
{"answer": self.answer.uuid_id},
- HTTP_X_REQUESTED_WITH="XMLHttpRequest")
+ HTTP_X_REQUESTED_WITH="XMLHttpRequest",
+ )
+ assert response_one.status_code == 200
+
+ def test_owner_in_context(self):
+ response_one = self.client.get(
+ reverse("qa:question_detail", kwargs={"pk": self.question_one.id})
+ )
+ response_two = self.other_client.get(
+ reverse("qa:question_detail", kwargs={"pk": self.question_two.id})
+ )
assert response_one.status_code == 200
+ assert response_two.status_code == 200
+ assert response_one.context.get("is_question_owner") is True
+ assert response_two.context.get("is_question_owner") is False
diff --git a/bootcamp/qa/urls.py b/bootcamp/qa/urls.py
index 9067f7ecb..3b5d057b9 100755
--- a/bootcamp/qa/urls.py
+++ b/bootcamp/qa/urls.py
@@ -2,15 +2,23 @@
from bootcamp.qa import views
-app_name = 'qa'
+app_name = "qa"
urlpatterns = [
- url(r'^$', views.QuestionListView.as_view(), name='index_noans'),
- url(r'^answered/$', views.QuestionAnsListView.as_view(), name='index_ans'),
- url(r'^indexed/$', views.QuestionsIndexListView.as_view(), name='index_all'),
- url(r'^question-detail/(?P\d+)/$', views.QuestionDetailView.as_view(), name='question_detail'),
- url(r'^ask-question/$', views.CreateQuestionView.as_view(), name='ask_question'),
- url(r'^propose-answer/(?P\d+)/$', views.CreateAnswerView.as_view(), name='propose_answer'),
- url(r'^question/vote/$', views.question_vote, name='question_vote'),
- url(r'^answer/vote/$', views.answer_vote, name='answer_vote'),
- url(r'^accept-answer/$', views.accept_answer, name='accept_answer'),
+ url(r"^$", views.QuestionListView.as_view(), name="index_noans"),
+ url(r"^answered/$", views.QuestionAnsListView.as_view(), name="index_ans"),
+ url(r"^indexed/$", views.QuestionsIndexListView.as_view(), name="index_all"),
+ url(
+ r"^question-detail/(?P\d+)/$",
+ views.QuestionDetailView.as_view(),
+ name="question_detail",
+ ),
+ url(r"^ask-question/$", views.CreateQuestionView.as_view(), name="ask_question"),
+ url(
+ r"^propose-answer/(?P\d+)/$",
+ views.CreateAnswerView.as_view(),
+ name="propose_answer",
+ ),
+ url(r"^question/vote/$", views.question_vote, name="question_vote"),
+ url(r"^answer/vote/$", views.answer_vote, name="answer_vote"),
+ url(r"^accept-answer/$", views.accept_answer, name="accept_answer"),
]
diff --git a/bootcamp/qa/views.py b/bootcamp/qa/views.py
index e42d5da25..75236e940 100755
--- a/bootcamp/qa/views.py
+++ b/bootcamp/qa/views.py
@@ -11,10 +11,13 @@
from bootcamp.helpers import ajax_required
from bootcamp.qa.models import Question, Answer
from bootcamp.qa.forms import QuestionForm
+from bootcamp.helpers import is_owner
+from bootcamp.helpers import update_votes
class QuestionsIndexListView(LoginRequiredMixin, ListView):
"""CBV to render a list view with all the registered questions."""
+
model = Question
paginate_by = 20
context_object_name = "questions"
@@ -29,6 +32,7 @@ def get_context_data(self, *args, **kwargs):
class QuestionAnsListView(QuestionsIndexListView):
"""CBV to render a list view with all question which have been already
marked as answered."""
+
def get_queryset(self, **kwargs):
return Question.objects.get_answered()
@@ -41,6 +45,7 @@ def get_context_data(self, *args, **kwargs):
class QuestionListView(QuestionsIndexListView):
"""CBV to render a list view with all question which haven't been marked
as answered."""
+
def get_queryset(self, **kwargs):
return Question.objects.get_unanswered()
@@ -53,14 +58,26 @@ def get_context_data(self, *args, **kwargs):
class QuestionDetailView(LoginRequiredMixin, DetailView):
"""View to call a given Question object and to render all the details about
that Question."""
+
model = Question
context_object_name = "question"
+ def get_context_data(self, *args, **kwargs):
+ context = super().get_context_data(*args, **kwargs)
+ question = self.get_object()
+ if self.request.user.username == question.user.username:
+ is_question_owner = True
+ else:
+ is_question_owner = False
+ context["is_question_owner"] = is_question_owner
+ return context
+
class CreateQuestionView(LoginRequiredMixin, CreateView):
"""
View to handle the creation of a new question
"""
+
form_class = QuestionForm
template_name = "qa/question_form.html"
message = _("Your question has been created.")
@@ -78,8 +95,9 @@ class CreateAnswerView(LoginRequiredMixin, CreateView):
"""
View to create new answers for a given question
"""
+
model = Answer
- fields = ["content", ]
+ fields = ["content"]
message = _("Thank you! Your answer has been posted.")
def form_valid(self, form):
@@ -89,8 +107,7 @@ def form_valid(self, form):
def get_success_url(self):
messages.success(self.request, self.message)
- return reverse(
- "qa:question_detail", kwargs={"pk": self.kwargs["question_id"]})
+ return reverse("qa:question_detail", kwargs={"pk": self.kwargs["question_id"]})
@login_required
@@ -100,24 +117,28 @@ def question_vote(request):
"""Function view to receive AJAX call, returns the count of votes a given
question has recieved."""
question_id = request.POST["question"]
- value = None
- if request.POST["value"] == "U":
- value = True
+ question = Question.objects.get(pk=question_id)
+ is_question_owner = is_owner(question, request.user.username)
+ if is_question_owner:
+ return JsonResponse(
+ {
+ "message": _("You can't vote your own question."),
+ "is_owner": is_question_owner,
+ }
+ )
- else:
- value = False
+ value = True if request.POST["value"] == "U" else False
- question = Question.objects.get(pk=question_id)
try:
- question.votes.update_or_create(
- user=request.user, defaults={"value": value}, )
- question.count_votes()
- return JsonResponse({"votes": question.total_votes})
+ update_votes(question, request.user, value)
+ return JsonResponse(
+ {"votes": question.total_votes, "is_owner": is_question_owner}
+ )
except IntegrityError: # pragma: no cover
- return JsonResponse({'status': 'false',
- 'message': _("Database integrity error.")},
- status=500)
+ return JsonResponse(
+ {"status": "false", "message": _("Database integrity error.")}, status=500
+ )
@login_required
@@ -127,24 +148,26 @@ def answer_vote(request):
"""Function view to receive AJAX call, returns the count of votes a given
answer has recieved."""
answer_id = request.POST["answer"]
- value = None
- if request.POST["value"] == "U":
- value = True
+ answer = Answer.objects.get(uuid_id=answer_id)
+ is_answer_owner = is_owner(answer, request.user.username)
+ if is_answer_owner:
+ return JsonResponse(
+ {
+ "message": _("You can't vote your own answer."),
+ "is_owner": is_answer_owner,
+ }
+ )
- else:
- value = False
+ value = True if request.POST["value"] == "U" else False
- answer = Answer.objects.get(uuid_id=answer_id)
try:
- answer.votes.update_or_create(
- user=request.user, defaults={"value": value}, )
- answer.count_votes()
- return JsonResponse({"votes": answer.total_votes})
+ update_votes(answer, request.user, value)
+ return JsonResponse({"votes": answer.total_votes, "is_owner": is_answer_owner})
except IntegrityError: # pragma: no cover
- return JsonResponse({'status': 'false',
- 'message': _("Database integrity error.")},
- status=500)
+ return JsonResponse(
+ {"status": "false", "message": _("Database integrity error.")}, status=500
+ )
@login_required
@@ -156,4 +179,4 @@ def accept_answer(request):
answer_id = request.POST["answer"]
answer = Answer.objects.get(uuid_id=answer_id)
answer.accept_answer()
- return JsonResponse({'status': 'true'}, status=200)
+ return JsonResponse({"status": "true"}, status=200)
diff --git a/bootcamp/search/apps.py b/bootcamp/search/apps.py
index 535cc8173..e7ebada17 100755
--- a/bootcamp/search/apps.py
+++ b/bootcamp/search/apps.py
@@ -3,5 +3,5 @@
class SearchConfig(AppConfig):
- name = 'bootcamp.search'
- verbose_name = _('Search')
+ name = "bootcamp.search"
+ verbose_name = _("Search")
diff --git a/bootcamp/search/tests/test_views.py b/bootcamp/search/tests/test_views.py
index da5f0f383..61f8536c9 100755
--- a/bootcamp/search/tests/test_views.py
+++ b/bootcamp/search/tests/test_views.py
@@ -13,6 +13,7 @@ class SearchViewsTests(TestCase):
Includes tests for all the functionality
associated with Views
"""
+
def setUp(self):
self.user = self.make_user("first_user")
self.other_user = self.make_user("second_user")
@@ -27,16 +28,25 @@ def setUp(self):
wants to publish a test, just a test; everybody always wants the real
deal."""
self.article = Article.objects.create(
- user=self.user, title="A really nice first title",
- content=self.content, tags="list, lists", status="P")
- self.article_2 = Article.objects.create(user=self.other_user,
- title="A first bad title",
- content="First bad content",
- tags="bad", status="P")
+ user=self.user,
+ title="A really nice first title",
+ content=self.content,
+ tags="list, lists",
+ status="P",
+ )
+ self.article_2 = Article.objects.create(
+ user=self.other_user,
+ title="A first bad title",
+ content="First bad content",
+ tags="bad",
+ status="P",
+ )
self.question_one = Question.objects.create(
- user=self.user, title="This is the first sample question",
+ user=self.user,
+ title="This is the first sample question",
content="This is a sample question description for the first time",
- tags="test1,test2")
+ tags="test1,test2",
+ )
self.question_two = Question.objects.create(
user=self.user,
title="The first shortes title",
@@ -45,14 +55,15 @@ def setUp(self):
publish it first, because they know this is just a test, and you
know than nobody wants to publish a test, just a test;
everybody always wants the real deal.""",
- has_answer=True, tags="test1,test2"
+ has_answer=True,
+ tags="test1,test2",
+ )
+ self.news_one = News.objects.create(
+ user=self.user, content="This is the first lazy content."
)
- self.news_one = News.objects.create(user=self.user,
- content="This is the first lazy content.")
def test_news_search_results(self):
- response = self.client.get(
- reverse("search:results"), {'query': 'This is'})
+ response = self.client.get(reverse("search:results"), {"query": "This is"})
assert response.status_code == 200
assert self.news_one in response.context["news_list"]
assert self.question_one in response.context["questions_list"]
@@ -61,10 +72,12 @@ def test_news_search_results(self):
def test_questions_suggestions_results(self):
response = self.client.get(
- reverse("search:suggestions"), {'term': 'first'},
- HTTP_X_REQUESTED_WITH='XMLHttpRequest')
- assert response.json()[0]['value'] == "first_user"
- assert response.json()[1]['value'] == "A first bad title"
- assert response.json()[2]['value'] == "A really nice first title"
- assert response.json()[3]['value'] == "The first shortes title"
- assert response.json()[4]['value'] == "This is the first sample question"
+ reverse("search:suggestions"),
+ {"term": "first"},
+ HTTP_X_REQUESTED_WITH="XMLHttpRequest",
+ )
+ assert response.json()[0]["value"] == "first_user"
+ assert response.json()[1]["value"] == "A first bad title"
+ assert response.json()[2]["value"] == "A really nice first title"
+ assert response.json()[3]["value"] == "The first shortes title"
+ assert response.json()[4]["value"] == "This is the first sample question"
diff --git a/bootcamp/search/urls.py b/bootcamp/search/urls.py
index be661d33b..1d6633dd4 100755
--- a/bootcamp/search/urls.py
+++ b/bootcamp/search/urls.py
@@ -2,8 +2,8 @@
from bootcamp.search import views
-app_name = 'search'
+app_name = "search"
urlpatterns = [
- url(r'^$', views.SearchListView.as_view(), name='results'),
- url(r'^suggestions/$', views.get_suggestions, name='suggestions'),
+ url(r"^$", views.SearchListView.as_view(), name="results"),
+ url(r"^suggestions/$", views.get_suggestions, name="suggestions"),
]
diff --git a/bootcamp/search/views.py b/bootcamp/search/views.py
index 5064c3d62..6aeb828fa 100755
--- a/bootcamp/search/views.py
+++ b/bootcamp/search/views.py
@@ -15,34 +15,47 @@
class SearchListView(LoginRequiredMixin, ListView):
"""CBV to contain all the search results"""
+
model = News
template_name = "search/search_results.html"
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
query = self.request.GET.get("query")
- context["active"] = 'news'
+ context["active"] = "news"
context["hide_search"] = True
context["tags_list"] = Tag.objects.filter(name=query)
context["news_list"] = News.objects.filter(
- content__icontains=query, reply=False).distinct()
- context["articles_list"] = Article.objects.filter(Q(
- title__icontains=query) | Q(content__icontains=query) | Q(
- tags__name__icontains=query), status="P").distinct()
+ content__icontains=query, reply=False
+ ).distinct()
+ context["articles_list"] = Article.objects.filter(
+ Q(title__icontains=query)
+ | Q(content__icontains=query)
+ | Q(tags__name__icontains=query),
+ status="P",
+ ).distinct()
context["questions_list"] = Question.objects.filter(
- Q(title__icontains=query) | Q(content__icontains=query) | Q(
- tags__name__icontains=query)).distinct()
- context["users_list"] = get_user_model().objects.filter(
- Q(username__icontains=query) | Q(
- name__icontains=query)).distinct()
+ Q(title__icontains=query)
+ | Q(content__icontains=query)
+ | Q(tags__name__icontains=query)
+ ).distinct()
+ context["users_list"] = (
+ get_user_model()
+ .objects.filter(Q(username__icontains=query) | Q(name__icontains=query))
+ .distinct()
+ )
context["news_count"] = context["news_list"].count()
context["articles_count"] = context["articles_list"].count()
context["questions_count"] = context["questions_list"].count()
context["users_count"] = context["users_list"].count()
context["tags_count"] = context["tags_list"].count()
- context["total_results"] = context["news_count"] + \
- context["articles_count"] + context["questions_count"] + \
- context["users_count"] + context["tags_count"]
+ context["total_results"] = (
+ context["news_count"]
+ + context["articles_count"]
+ + context["questions_count"]
+ + context["users_count"]
+ + context["tags_count"]
+ )
return context
@@ -52,14 +65,27 @@ def get_context_data(self, *args, **kwargs):
def get_suggestions(request):
# Convert users, articles, questions objects into list to be
# represented as a single list.
- query = request.GET.get('term', '')
- users = list(get_user_model().objects.filter(
- Q(username__icontains=query) | Q(name__icontains=query)))
- articles = list(Article.objects.filter(
- Q(title__icontains=query) | Q(content__icontains=query) | Q(
- tags__name__icontains=query), status="P"))
- questions = list(Question.objects.filter(Q(title__icontains=query) | Q(
- content__icontains=query) | Q(tags__name__icontains=query)))
+ query = request.GET.get("term", "")
+ users = list(
+ get_user_model().objects.filter(
+ Q(username__icontains=query) | Q(name__icontains=query)
+ )
+ )
+ articles = list(
+ Article.objects.filter(
+ Q(title__icontains=query)
+ | Q(content__icontains=query)
+ | Q(tags__name__icontains=query),
+ status="P",
+ )
+ )
+ questions = list(
+ Question.objects.filter(
+ Q(title__icontains=query)
+ | Q(content__icontains=query)
+ | Q(tags__name__icontains=query)
+ )
+ )
# Add all the retrieved users, articles, questions to data_retrieved
# list.
data_retrieved = users
@@ -69,19 +95,19 @@ def get_suggestions(request):
for data in data_retrieved:
data_json = {}
if isinstance(data, get_user_model()):
- data_json['id'] = data.id
- data_json['label'] = data.username
- data_json['value'] = data.username
+ data_json["id"] = data.id
+ data_json["label"] = data.username
+ data_json["value"] = data.username
if isinstance(data, Article):
- data_json['id'] = data.id
- data_json['label'] = data.title
- data_json['value'] = data.title
+ data_json["id"] = data.id
+ data_json["label"] = data.title
+ data_json["value"] = data.title
if isinstance(data, Question):
- data_json['id'] = data.id
- data_json['label'] = data.title
- data_json['value'] = data.title
+ data_json["id"] = data.id
+ data_json["label"] = data.title
+ data_json["value"] = data.title
results.append(data_json)
diff --git a/bootcamp/static/css/news.css b/bootcamp/static/css/news.css
index 2c71eec3c..796fe5936 100755
--- a/bootcamp/static/css/news.css
+++ b/bootcamp/static/css/news.css
@@ -1,14 +1,14 @@
-.stream {
- margin: 2em 0 0 0;
- padding: 0;
-}
-
-.stream li {
+.infinite-container li {
list-style: none;
border-style: solid none solid none;
}
+.stream {
+ margin: 2em 0 0 0;
+ padding: 0;
+}
+
.stream li:last-child {
border-bottom: none;
}
@@ -35,11 +35,6 @@
border-radius: 50%;
}
-.interaction {
- padding-left: 1.5em;
- margin-bottom: 1em
-}
-
.interaction a {
margin-right: 1.3em;
font-size: 1.1em;
@@ -159,3 +154,38 @@
.remove-news:hover {
color: #333333;
}
+
+.meta.card {
+ background-color: #f8f8f8;
+ margin-bottom: 10px;
+ color: #6c757d;
+}
+
+.meta.card:hover {
+ background-color: #fff;
+}
+
+.meta .card-body {
+ padding: 0.8rem;
+}
+
+.meta .card-title, .meta .card-text, .meta .card-btn{
+ font-size: 0.8em;
+ line-height: 1.4em;
+ margin: 0;
+}
+
+.meta .card-img-top{
+ height: 100px;
+ background: center no-repeat #ccc;
+ background-size: cover;
+}
+
+.meta .card-text{
+ margin-bottom: 5px;
+}
+
+.meta:hover .btn{
+ background-color: #888;
+ color: #FFF;
+}
\ No newline at end of file
diff --git a/bootcamp/static/css/qa.css b/bootcamp/static/css/qa.css
index 4eea5245b..947cf54a0 100755
--- a/bootcamp/static/css/qa.css
+++ b/bootcamp/static/css/qa.css
@@ -46,7 +46,7 @@
margin-top: .4em;
}
-.options i.vote {
+.options i.vote, .options i.is-owner {
font-size: 2em;
color: #ddd;
}
diff --git a/bootcamp/static/js/articles.js b/bootcamp/static/js/articles.js
index a10deb9c8..4b7d675b4 100755
--- a/bootcamp/static/js/articles.js
+++ b/bootcamp/static/js/articles.js
@@ -1,18 +1,18 @@
$(function () {
- $(".publish").click(function () {
- $("input[name='status']").val("P");
- $("#article-form").submit();
- });
+ $(".publish").click(function () {
+ $("input[name='status']").val("P");
+ $("#article-form").submit();
+ });
- $(".update").click(function () {
- $("input[name='status']").val("P");
- //$("input[name='edited']").prop("checked");
- $("input[name='edited']").val("True");
- $("#article-form").submit();
- });
+ $(".update").click(function () {
+ $("input[name='status']").val("P");
+ //$("input[name='edited']").prop("checked");
+ $("input[name='edited']").val("True");
+ $("#article-form").submit();
+ });
- $(".draft").click(function () {
- $("input[name='status']").val("D");
- $("#article-form").submit();
- });
+ $(".draft").click(function () {
+ $("input[name='status']").val("D");
+ $("#article-form").submit();
+ });
});
diff --git a/bootcamp/static/js/bootcamp.js b/bootcamp/static/js/bootcamp.js
index 76dd1eaa2..016acf500 100755
--- a/bootcamp/static/js/bootcamp.js
+++ b/bootcamp/static/js/bootcamp.js
@@ -22,7 +22,7 @@ $('.form-group').removeClass('row');
/* Notifications JS basic client */
$(function () {
- let emptyMessage = 'You have no unread notification';
+ let emptyMessage = 'data-empty="true"';
function checkNotifications() {
$.ajax({
diff --git a/bootcamp/static/js/messager.js b/bootcamp/static/js/messager.js
index a1c1b8aa6..41298454e 100755
--- a/bootcamp/static/js/messager.js
+++ b/bootcamp/static/js/messager.js
@@ -1,113 +1,113 @@
$(function () {
- function setUserOnlineOffline(username, status) {
- /* This function enables the client to switch the user connection
- status, allowing to show if an user is connected or not.
- */
- var elem = $("online-stat-" + username);
- if (elem) {
- if (status === 'online') {
- elem.attr("class", "btn btn-success btn-circle");
- } else {
- elem.attr("class", "btn btn-danger btn-circle");
- };
- };
+ function setUserOnlineOffline(username, status) {
+ /* This function enables the client to switch the user connection
+ status, allowing to show if an user is connected or not.
+ */
+ var elem = $("online-stat-" + username);
+ if (elem) {
+ if (status === 'online') {
+ elem.attr("class", "btn btn-success btn-circle");
+ } else {
+ elem.attr("class", "btn btn-danger btn-circle");
+ };
};
+ };
- function addNewMessage(message_id) {
- /* This function calls the respective AJAX view, so it will be able to
- load the received message in a proper way.
- */
- $.ajax({
- url: '/messages/receive-message/',
- data: {'message_id': message_id},
- cache: false,
- success: function (data) {
- $(".send-message").before(data);
- scrollConversationScreen();
- }
- });
- };
+ function addNewMessage(message_id) {
+ /* This function calls the respective AJAX view, so it will be able to
+ load the received message in a proper way.
+ */
+ $.ajax({
+ url: '/messages/receive-message/',
+ data: { 'message_id': message_id },
+ cache: false,
+ success: function (data) {
+ $(".send-message").before(data);
+ scrollConversationScreen();
+ }
+ });
+ };
- function scrollConversationScreen() {
- /* Set focus on the input box from the form, and rolls to show the
- the most recent message.
- */
- $("input[name='message']").focus();
- $('.conversation').scrollTop($('.conversation')[0].scrollHeight);
- }
+ function scrollConversationScreen() {
+ /* Set focus on the input box from the form, and rolls to show the
+ the most recent message.
+ */
+ $("input[name='message']").focus();
+ $('.conversation').scrollTop($('.conversation')[0].scrollHeight);
+ }
- $("#send").submit(function () {
- $.ajax({
- url: '/messages/send-message/',
- data: $("#send").serialize(),
- cache: false,
- type: 'POST',
- success: function (data) {
- $(".send-message").before(data);
- $("input[name='message']").val('');
- scrollConversationScreen();
- }
- });
- return false;
+ $("#send").submit(function () {
+ $.ajax({
+ url: '/messages/send-message/',
+ data: $("#send").serialize(),
+ cache: false,
+ type: 'POST',
+ success: function (data) {
+ $(".send-message").before(data);
+ $("input[name='message']").val('');
+ scrollConversationScreen();
+ }
});
+ return false;
+ });
- // WebSocket connection management block.
- // Correctly decide between ws:// and wss://
- var ws_scheme = window.location.protocol == "https:" ? "wss" : "ws";
- var ws_path = ws_scheme + '://' + window.location.host + "/" + currentUser + "/";
- var webSocket = new channels.WebSocketBridge();
- webSocket.connect(ws_path);
+ // WebSocket connection management block.
+ // Correctly decide between ws:// and wss://
+ var ws_scheme = window.location.protocol == "https:" ? "wss" : "ws";
+ var ws_path = ws_scheme + '://' + window.location.host + "/" + currentUser + "/";
+ var webSocket = new channels.WebSocketBridge();
+ webSocket.connect(ws_path);
- window.onbeforeunload = function () {
- // Small function to run instruction just before closing the session.
- payload = {
- "type": "recieve",
- "sender": currentUser,
- "set_status": "offline"
- };
- webSocket.send(payload);
- }
+ window.onbeforeunload = function () {
+ // Small function to run instruction just before closing the session.
+ payload = {
+ "type": "recieve",
+ "sender": currentUser,
+ "set_status": "offline"
+ };
+ webSocket.send(payload);
+ }
- // Helpful debugging
- webSocket.socket.onopen = function () {
- console.log("Connected to inbox stream");
- // Commenting this block until I find a better way to manage how to
- // report the user status.
+ // Helpful debugging
+ webSocket.socket.onopen = function () {
+ console.log("Connected to inbox stream");
+ // Commenting this block until I find a better way to manage how to
+ // report the user status.
- /* payload = {
- "type": "recieve",
- "sender": currentUser,
- "set_status": "online"
- };
- webSocket.send(payload); */
+ /* payload = {
+ "type": "recieve",
+ "sender": currentUser,
+ "set_status": "online"
};
+ webSocket.send(payload); */
+ };
- webSocket.socket.onclose = function () {
- console.log("Disconnected from inbox stream");
- };
+ webSocket.socket.onclose = function () {
+ console.log("Disconnected from inbox stream");
+ };
- // onmessage management.
- webSocket.listen(function(event) {
- switch (event.key) {
- case "message":
- if (event.sender === activeUser) {
- addNewMessage(event.message_id);
- // I hope there is a more elegant way to work this out.
- setTimeout(function(){$("#unread-count").hide()}, 1);
- } else {
- $("#new-message-" + event.sender).show();
- }
- break;
+ // onmessage management.
+ webSocket.listen(function (event) {
+ switch (event.key) {
+ case "message":
+ if (event.sender === activeUser) {
+ addNewMessage(event.message_id);
+ // I hope there is a more elegant way to work this out.
+ setTimeout(function () { $("#unread-count").hide() }, 1);
+ } else {
+ $("#new-message-" + event.sender).show();
+ }
+ break;
- case "set_status":
- setUserOnlineOffline(event.sender, event.status);
- break;
+ case "set_status":
+ setUserOnlineOffline(event.sender, event.status);
+ break;
- default:
- console.log('error: ', event);
- console.log(typeof(event))
- break;
- }
- });
+ default:
+ console.log('error: ', event);
+ console.log(typeof (event))
+ break;
+ }
+ });
});
diff --git a/bootcamp/static/js/news.js b/bootcamp/static/js/news.js
index 79780a041..96e71a579 100755
--- a/bootcamp/static/js/news.js
+++ b/bootcamp/static/js/news.js
@@ -1,192 +1,192 @@
$(function () {
- function hide_stream_update() {
- $(".stream-update").hide();
- };
+ function hide_stream_update() {
+ $(".stream-update").hide();
+ };
- function getCookie(name) {
- // Function to get any cookie available in the session.
- var cookieValue = null;
- if (document.cookie && document.cookie !== '') {
- var cookies = document.cookie.split(';');
- for (var i = 0; i < cookies.length; i++) {
- var cookie = jQuery.trim(cookies[i]);
- // Does this cookie string begin with the name we want?
- if (cookie.substring(0, name.length + 1) === (name + '=')) {
- cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
- break;
- }
- }
+ function getCookie(name) {
+ // Function to get any cookie available in the session.
+ var cookieValue = null;
+ if (document.cookie && document.cookie !== '') {
+ var cookies = document.cookie.split(';');
+ for (var i = 0; i < cookies.length; i++) {
+ var cookie = jQuery.trim(cookies[i]);
+ // Does this cookie string begin with the name we want?
+ if (cookie.substring(0, name.length + 1) === (name + '=')) {
+ cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
+ break;
}
- return cookieValue;
- };
-
- function csrfSafeMethod(method) {
- // These HTTP methods do not require CSRF protection
- return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
+ }
}
+ return cookieValue;
+ };
- var csrftoken = getCookie('csrftoken');
- var page_title = $(document).attr("title");
- // This sets up every ajax call with proper headers.
- $.ajaxSetup({
- beforeSend: function(xhr, settings) {
- if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
- xhr.setRequestHeader("X-CSRFToken", csrftoken);
- }
- }
- });
+ function csrfSafeMethod(method) {
+ // These HTTP methods do not require CSRF protection
+ return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
+ }
- // Focus on the modal input by default.
- $('#newsFormModal').on('shown.bs.modal', function () {
- $('#newsInput').trigger('focus')
- });
+ var csrftoken = getCookie('csrftoken');
+ var page_title = $(document).attr("title");
+ // This sets up every ajax call with proper headers.
+ $.ajaxSetup({
+ beforeSend: function (xhr, settings) {
+ if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
+ xhr.setRequestHeader("X-CSRFToken", csrftoken);
+ }
+ }
+ });
- $('#newsThreadModal').on('shown.bs.modal', function () {
- $('#replyInput').trigger('focus')
- });
+ // Focus on the modal input by default.
+ $('#newsFormModal').on('shown.bs.modal', function () {
+ $('#newsInput').trigger('focus')
+ });
- // Counts textarea characters to provide data to user.
- $("#newsInput").keyup(function () {
- var charCount = $(this).val().length;
- $("#newsCounter").text(280 - charCount);
- });
+ $('#newsThreadModal').on('shown.bs.modal', function () {
+ $('#replyInput').trigger('focus')
+ });
- $("#replyInput").keyup(function () {
- var charCount = $(this).val().length;
- $("#replyCounter").text(280 - charCount);
- });
+ // Counts textarea characters to provide data to user.
+ $("#newsInput").keyup(function () {
+ var charCount = $(this).val().length;
+ $("#newsCounter").text(280 - charCount);
+ });
- $("input, textarea").attr("autocomplete", "off");
+ $("#replyInput").keyup(function () {
+ var charCount = $(this).val().length;
+ $("#replyCounter").text(280 - charCount);
+ });
- $("#postNews").click(function () {
- // Ajax call after pushing button, to register a News object.
- $.ajax({
- url: '/news/post-news/',
- data: $("#postNewsForm").serialize(),
- type: 'POST',
- cache: false,
- success: function (data) {
- $("ul.stream").prepend(data);
- $("#newsInput").val("");
- $("#newsFormModal").modal("hide");
- hide_stream_update();
- },
- error : function(data){
- alert(data.responseText);
- },
- });
+ $("input, textarea").attr("autocomplete", "off");
+
+ $("#postNews").click(function () {
+ // Ajax call after pushing button, to register a News object.
+ $.ajax({
+ url: '/news/post-news/',
+ data: $("#postNewsForm").serialize(),
+ type: 'POST',
+ cache: false,
+ success: function (data) {
+ $("ul.stream").prepend(data);
+ $("#newsInput").val("");
+ $("#newsFormModal").modal("hide");
+ hide_stream_update();
+ },
+ error: function (data) {
+ alert(data.responseText);
+ },
});
+ });
- $("#replyNews").click(function () {
- // Ajax call to register a reply to any given News object.
- $.ajax({
- url: '/news/post-comment/',
- data: $("#replyNewsForm").serialize(),
- type: 'POST',
- cache: false,
- success: function (data) {
- $("#replyInput").val("");
- $("#newsThreadModal").modal("hide");
- },
- error: function(data){
- alert(data.responseText);
- },
- });
+ $("#replyNews").click(function () {
+ // Ajax call to register a reply to any given News object.
+ $.ajax({
+ url: '/news/post-comment/',
+ data: $("#replyNewsForm").serialize(),
+ type: 'POST',
+ cache: false,
+ success: function (data) {
+ $("#replyInput").val("");
+ $("#newsThreadModal").modal("hide");
+ },
+ error: function (data) {
+ alert(data.responseText);
+ },
});
+ });
- $("ul.stream").on("click", ".like", function () {
- // Ajax call on action on like button.
- var li = $(this).closest("li");
- var news = $(li).attr("news-id");
- payload = {
- 'news': news,
- 'csrf_token': csrftoken
+ $("ul.stream").on("click", ".like", function () {
+ // Ajax call on action on like button.
+ var li = $(this).closest("li");
+ var news = $(li).attr("news-id");
+ payload = {
+ 'news': news,
+ 'csrf_token': csrftoken
+ }
+ $.ajax({
+ url: '/news/like/',
+ data: payload,
+ type: 'POST',
+ cache: false,
+ success: function (data) {
+ $(".like .like-count", li).text(data.likes);
+ if ($(".like .heart", li).hasClass("fa fa-heart")) {
+ $(".like .heart", li).removeClass("fa fa-heart");
+ $(".like .heart", li).addClass("fa fa-heart-o");
+ } else {
+ $(".like .heart", li).removeClass("fa fa-heart-o");
+ $(".like .heart", li).addClass("fa fa-heart");
}
- $.ajax({
- url: '/news/like/',
- data: payload,
- type: 'POST',
- cache: false,
- success: function (data) {
- $(".like .like-count", li).text(data.likes);
- if ($(".like .heart", li).hasClass("fa fa-heart")) {
- $(".like .heart", li).removeClass("fa fa-heart");
- $(".like .heart", li).addClass("fa fa-heart-o");
- } else {
- $(".like .heart", li).removeClass("fa fa-heart-o");
- $(".like .heart", li).addClass("fa fa-heart");
- }
- }
- });
- return false;
+ }
});
+ return false;
+ });
- $("ul.stream").on("click", ".comment", function () {
- // Ajax call to request a given News object detail and thread, and to
- // show it in a modal.
- var post = $(this).closest(".card");
- var news = $(post).closest("li").attr("news-id");
- $("#newsThreadModal").modal("show");
- $.ajax({
- url: '/news/get-thread/',
- data: {'news': news},
- cache: false,
- beforeSend: function () {
- $("#threadContent").html("");
- },
- success: function (data) {
- $("input[name=parent]").val(data.uuid)
- $("#newsContent").html(data.news);
- $("#threadContent").html(data.thread);
- }
- });
- return false;
+ $("ul.stream").on("click", ".comment", function () {
+ // Ajax call to request a given News object detail and thread, and to
+ // show it in a modal.
+ var post = $(this).closest(".card");
+ var news = $(post).closest("li").attr("news-id");
+ $("#newsThreadModal").modal("show");
+ $.ajax({
+ url: '/news/get-thread/',
+ data: { 'news': news },
+ cache: false,
+ beforeSend: function () {
+ $("#threadContent").html("");
+ },
+ success: function (data) {
+ $("input[name=parent]").val(data.uuid)
+ $("#newsContent").html(data.news);
+ $("#threadContent").html(data.thread);
+ }
});
+ return false;
+ });
});
/* Example query for the GraphQL endpoint.
- query{
- news(uuidId: "--insert here the required uuid_id value for the lookup"){
- uuidId
- content
- timestamp
- countThread
- countLikers
- user {
- name
- picture
- }
- liked {
- name
- }
- thread{
- content
- }
- }
- paginatedNews(page: 1){
- page
- pages
- hasNext
- hasPrev
- objects {
- uuidId
- content
- timestamp
- countThread
- countLikers
- user {
- name
- picture
- }
- liked {
- name
- }
- thread{
- content
- }
- }
- }
+ query{
+ news(uuidId: "--insert here the required uuid_id value for the lookup"){
+ uuidId
+ content
+ timestamp
+ countThread
+ countLikers
+ user {
+ name
+ picture
+ }
+ liked {
+ name
+ }
+ thread{
+ content
+ }
+ }
+ paginatedNews(page: 1){
+ page
+ pages
+ hasNext
+ hasPrev
+ objects {
+ uuidId
+ content
+ timestamp
+ countThread
+ countLikers
+ user {
+ name
+ picture
+ }
+ liked {
+ name
+ }
+ thread{
+ content
}
+ }
+ }
+ }
*/
diff --git a/bootcamp/static/js/qa.js b/bootcamp/static/js/qa.js
index 2427fb0ad..31dd35421 100755
--- a/bootcamp/static/js/qa.js
+++ b/bootcamp/static/js/qa.js
@@ -1,124 +1,131 @@
$(function () {
- function getCookie(name) {
- // Function to get any cookie available in the session.
- var cookieValue = null;
- if (document.cookie && document.cookie !== '') {
- var cookies = document.cookie.split(';');
- for (var i = 0; i < cookies.length; i++) {
- var cookie = jQuery.trim(cookies[i]);
- // Does this cookie string begin with the name we want?
- if (cookie.substring(0, name.length + 1) === (name + '=')) {
- cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
- break;
- }
- }
+ function getCookie(name) {
+ // Function to get any cookie available in the session.
+ var cookieValue = null;
+ if (document.cookie && document.cookie !== '') {
+ var cookies = document.cookie.split(';');
+ for (var i = 0; i < cookies.length; i++) {
+ var cookie = jQuery.trim(cookies[i]);
+ // Does this cookie string begin with the name we want?
+ if (cookie.substring(0, name.length + 1) === (name + '=')) {
+ cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
+ break;
}
- return cookieValue;
- };
+ }
+ }
+ return cookieValue;
+ };
+
+ function csrfSafeMethod(method) {
+ // These HTTP methods do not require CSRF protection
+ return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
+ }
- function csrfSafeMethod(method) {
- // These HTTP methods do not require CSRF protection
- return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
+ function toogleVote(voteIcon, vote, data, isAnswer) {
+ var idPrefix = isAnswer ? 'answer' : 'question';
+ var isOwner = data.is_owner;
+ if (isOwner === false) {
+ if (vote === "U") {
+ voteIcon.addClass('voted');
+ voteIcon.siblings(`#${idPrefix}DownVote`).removeClass('voted');
+ } else {
+ voteIcon.addClass('voted');
+ voteIcon.siblings(`#${idPrefix}UpVote`).removeClass('voted');
+ }
+ voteIcon.siblings(`#${idPrefix}Votes`).text(data.votes);
}
+ }
- var csrftoken = getCookie('csrftoken');
- var page_title = $(document).attr("title");
- // This sets up every ajax call with proper headers.
- $.ajaxSetup({
- beforeSend: function(xhr, settings) {
- if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
- xhr.setRequestHeader("X-CSRFToken", csrftoken);
- }
- }
- });
+ var csrftoken = getCookie('csrftoken');
+ var page_title = $(document).attr("title");
+ // This sets up every ajax call with proper headers.
+ $.ajaxSetup({
+ beforeSend: function (xhr, settings) {
+ if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
+ xhr.setRequestHeader("X-CSRFToken", csrftoken);
+ }
+ }
+ });
- $("#publish").click(function () {
- // function to operate the Publish button in the question form, marking
- // the question status as published.
- $("input[name='status']").val("O");
- $("#question-form").submit();
- });
+ $("#publish").click(function () {
+ // function to operate the Publish button in the question form, marking
+ // the question status as published.
+ $("input[name='status']").val("O");
+ $("#question-form").submit();
+ });
- $("#draft").click(function () {
- // Function to operate the Draft button in the question form, marking
- // the question status as draft.
- $("input[name='status']").val("D");
- $("#question-form").submit();
- });
+ $("#draft").click(function () {
+ // Function to operate the Draft button in the question form, marking
+ // the question status as draft.
+ $("input[name='status']").val("D");
+ $("#question-form").submit();
+ });
- $(".question-vote").click(function () {
- // Vote on a question.
- var span = $(this);
- var question = $(this).closest(".question").attr("question-id");
- vote = null;
- if ($(this).hasClass("up-vote")) {
- vote = "U";
- } else {
- vote = "D";
- }
- $.ajax({
- url: '/qa/question/vote/',
- data: {
- 'question': question,
- 'value': vote
- },
- type: 'post',
- cache: false,
- success: function (data) {
- $('.vote', span).removeClass('voted');
- if (vote === "U") {
- $(span).addClass('voted');
- }
- $("#questionVotes").text(data.votes);
- }
- });
+ $(".question-vote").click(function () {
+ // Vote on a question.
+ var voteIcon = $(this);
+ var question = $(this).closest(".question").attr("question-id");
+ if ($(this).hasClass("up-vote")) {
+ vote = "U";
+ } else {
+ $('#questionDownVote').addClass('voted');
+ $('#questionUpVote').removeClass('voted');
+ }
+ $.ajax({
+ url: '/qa/question/vote/',
+ data: {
+ 'question': question,
+ 'value': vote
+ },
+ type: 'post',
+ cache: false,
+ success: function (data) {
+ toogleVote(voteIcon, vote, data, false);
+ }
});
+ });
- $(".answer-vote").click(function () {
- // Vote on an answer.
- var span = $(this);
- var answer = $(this).closest(".answer").attr("answer-id");
- vote = null;
- if ($(this).hasClass("up-vote")) {
- vote = "U";
- } else {
- vote = "D";
- }
- $.ajax({
- url: '/qa/answer/vote/',
- data: {
- 'answer': answer,
- 'value': vote
- },
- type: 'post',
- cache: false,
- success: function (data) {
- $('.vote', span).removeClass('voted');
- if (vote === "U") {
- $(span).addClass('voted');
- }
- $("#answerVotes").text(data.votes);
- }
- });
+ $(".answer-vote").click(function () {
+ // Vote on an answer.
+ var voteIcon = $(this);
+ var answer = $(this).closest(".answer").attr("answer-id");
+ if ($(this).hasClass("up-vote")) {
+ vote = "U";
+ } else {
+ $('#answerDownVote').addClass('voted');
+ $('#answerUpVote').removeClass('voted');
+ }
+ $.ajax({
+ url: '/qa/answer/vote/',
+ data: {
+ 'answer': answer,
+ 'value': vote
+ },
+ type: 'post',
+ cache: false,
+ success: function (data) {
+ toogleVote(voteIcon, vote, data, true);
+ }
});
+ });
- $("#acceptAnswer").click(function () {
- // Mark an answer as accepted.
- var span = $(this);
- var answer = $(this).closest(".answer").attr("answer-id");
- $.ajax({
- url: '/qa/accept-answer/',
- data: {
- 'answer': answer
- },
- type: 'post',
- cache: false,
- success: function (data) {
- $("#acceptAnswer").removeClass("accepted");
- $("#acceptAnswer").prop("title", "Click to accept the answer");
- $("#acceptAnswer").addClass("accepted");
- $("#acceptAnswer").prop("title", "Click to unaccept the answer");
- }
- });
+ $("#acceptAnswer").click(function () {
+ // Mark an answer as accepted.
+ var span = $(this);
+ var answer = $(this).closest(".answer").attr("answer-id");
+ $.ajax({
+ url: '/qa/accept-answer/',
+ data: {
+ 'answer': answer
+ },
+ type: 'post',
+ cache: false,
+ success: function (data) {
+ $("#acceptAnswer").removeClass("accepted");
+ $("#acceptAnswer").prop("title", "Click to accept the answer");
+ $("#acceptAnswer").addClass("accepted");
+ $("#acceptAnswer").prop("title", "Click to unaccept the answer");
+ }
});
+ });
});
diff --git a/bootcamp/templates/articles/article_detail.html b/bootcamp/templates/articles/article_detail.html
index c0a39c30f..d5348719c 100755
--- a/bootcamp/templates/articles/article_detail.html
+++ b/bootcamp/templates/articles/article_detail.html
@@ -47,7 +47,12 @@
{% get_comment_form for article as form %}
@@ -67,7 +72,7 @@
{% endthumbnail %}
{{ comment.user.get_profile_name|title }}
- {{ comment }}
+ {{ comment.comment|linebreaks }}
{% endfor %}
diff --git a/bootcamp/templates/base.html b/bootcamp/templates/base.html
index 4d6b14c80..e88caaabf 100755
--- a/bootcamp/templates/base.html
+++ b/bootcamp/templates/base.html
@@ -113,9 +113,8 @@
{% endblock javascript %}
diff --git a/bootcamp/templates/news/news_single.html b/bootcamp/templates/news/news_single.html
index 9027d41b7..1dd5b5541 100755
--- a/bootcamp/templates/news/news_single.html
+++ b/bootcamp/templates/news/news_single.html
@@ -1,6 +1,7 @@
{% load i18n %}
{% load humanize static %}
{% load thumbnail %}
+{% load urlize_target_blank %}
-
diff --git a/bootcamp/templates/news/news_thread.html b/bootcamp/templates/news/news_thread.html
index e64f539e5..827cacdc8 100755
--- a/bootcamp/templates/news/news_thread.html
+++ b/bootcamp/templates/news/news_thread.html
@@ -1,6 +1,7 @@
{% load i18n %}
{% load humanize static %}
{% load thumbnail %}
+{% load urlize_target_blank %}
{% for reply in thread %}
@@ -24,22 +25,37 @@
{{ reply.user.get_profile_name|title }}
- {{ reply }}
- {% if reply.image %}
-
+ {{ reply|urlize|urlize_target_blank }}
+ {% if reply.meta_url %}
+
+ {% if reply.meta_image %}
+
+ {% endif %}
+
+ {% if reply.meta_title %}
+
{{ reply.meta_title }}
+ {% endif %}
+ {% if reply.meta_description %}
+
{{ reply.meta_description }}
+ {% endif %}
+ {% if reply.meta_url %}
+
{{ reply.meta_url }}
+ {% endif %}
+
+
{% endif %}
+
-
{% endfor %}
diff --git a/bootcamp/templates/notifications/most_recent.html b/bootcamp/templates/notifications/most_recent.html
index 849e74358..42eda4196 100755
--- a/bootcamp/templates/notifications/most_recent.html
+++ b/bootcamp/templates/notifications/most_recent.html
@@ -15,6 +15,6 @@
{% trans 'See all' %}
{% trans 'Mark all as read' %}
{% else %}
- {% trans 'You have no unread notification' %}
+ {% trans 'You have no unread notification' %}
{% endif %}
diff --git a/bootcamp/templates/qa/answer_sample.html b/bootcamp/templates/qa/answer_sample.html
index eda25b178..fe974750b 100755
--- a/bootcamp/templates/qa/answer_sample.html
+++ b/bootcamp/templates/qa/answer_sample.html
@@ -3,9 +3,9 @@
{% csrf_token %}
-
+
{{ answer.total_votes }}
-
+
{% if answer.is_answer and user == question.user %}
{% elif answer.is_answer %}
diff --git a/bootcamp/templates/qa/question_detail.html b/bootcamp/templates/qa/question_detail.html
index b5761287f..aa9106198 100755
--- a/bootcamp/templates/qa/question_detail.html
+++ b/bootcamp/templates/qa/question_detail.html
@@ -27,9 +27,9 @@
{{ question.title }}
{{ question.count_answers }}
{% trans 'Answers' %}
-
+
{{ question.total_votes }}
-
+
{% trans 'Votes' %}
@@ -55,7 +55,7 @@
{% trans 'Answers' %}
{% for answer in question.answer_set.all %}
- {% include 'qa/answer_sample.html' with answer=answer %}
+ {% include 'qa/answer_sample.html' with answer=answer username=request.user.username %}
{% empty %}
{% trans 'There are no answers yet.' %}
diff --git a/bootcamp/users/adapters.py b/bootcamp/users/adapters.py
index b31450aee..bc5b290f3 100755
--- a/bootcamp/users/adapters.py
+++ b/bootcamp/users/adapters.py
@@ -5,9 +5,9 @@
class AccountAdapter(DefaultAccountAdapter):
def is_open_for_signup(self, request):
- return getattr(settings, 'ACCOUNT_ALLOW_REGISTRATION', True)
+ return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True)
class SocialAccountAdapter(DefaultSocialAccountAdapter):
def is_open_for_signup(self, request, sociallogin):
- return getattr(settings, 'ACCOUNT_ALLOW_REGISTRATION', True)
+ return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True)
diff --git a/bootcamp/users/admin.py b/bootcamp/users/admin.py
index e9427e059..d395d5362 100755
--- a/bootcamp/users/admin.py
+++ b/bootcamp/users/admin.py
@@ -13,9 +13,9 @@ class Meta(UserChangeForm.Meta):
class MyUserCreationForm(UserCreationForm):
- error_message = UserCreationForm.error_messages.update({
- 'duplicate_username': 'This username has already been taken.'
- })
+ error_message = UserCreationForm.error_messages.update(
+ {"duplicate_username": "This username has already been taken."}
+ )
class Meta(UserCreationForm.Meta):
model = User
@@ -28,15 +28,13 @@ def clean_username(self):
except User.DoesNotExist:
return username
- raise forms.ValidationError(self.error_messages['duplicate_username'])
+ raise forms.ValidationError(self.error_messages["duplicate_username"])
@admin.register(User)
class MyUserAdmin(AuthUserAdmin):
form = MyUserChangeForm
add_form = MyUserCreationForm
- fieldsets = (
- ('User Profile', {'fields': ('name',)}),
- ) + AuthUserAdmin.fieldsets
- list_display = ('username', 'name', 'is_superuser')
- search_fields = ['name']
+ fieldsets = (("User Profile", {"fields": ("name",)}),) + AuthUserAdmin.fieldsets
+ list_display = ("username", "name", "is_superuser")
+ search_fields = ["name"]
diff --git a/bootcamp/users/apps.py b/bootcamp/users/apps.py
index 476a289b5..d64326584 100755
--- a/bootcamp/users/apps.py
+++ b/bootcamp/users/apps.py
@@ -3,7 +3,7 @@
class UsersConfig(AppConfig):
- name = 'bootcamp.users'
+ name = "bootcamp.users"
verbose_name = _("Users")
def ready(self):
diff --git a/bootcamp/users/migrations/0001_initial.py b/bootcamp/users/migrations/0001_initial.py
index 252c68e79..27f964e9a 100755
--- a/bootcamp/users/migrations/0001_initial.py
+++ b/bootcamp/users/migrations/0001_initial.py
@@ -10,46 +10,206 @@ class Migration(migrations.Migration):
initial = True
- dependencies = [
- ('auth', '0009_alter_user_last_name_max_length'),
- ]
+ dependencies = [("auth", "0009_alter_user_last_name_max_length")]
operations = [
migrations.CreateModel(
- name='User',
+ name="User",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('password', models.CharField(max_length=128, verbose_name='password')),
- ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
- ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
- ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
- ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')),
- ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
- ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
- ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
- ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
- ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
- ('name', models.CharField(blank=True, max_length=255, verbose_name="User's name")),
- ('picture', models.ImageField(blank=True, null=True, upload_to='profile_pics/', verbose_name='Profile picture')),
- ('location', models.CharField(blank=True, max_length=50, null=True, verbose_name='Location')),
- ('job_title', models.CharField(blank=True, max_length=50, null=True, verbose_name='Job title')),
- ('personal_url', models.URLField(blank=True, max_length=555, null=True, verbose_name='Personal URL')),
- ('facebook_account', models.URLField(blank=True, max_length=255, null=True, verbose_name='Facebook profile')),
- ('twitter_account', models.URLField(blank=True, max_length=255, null=True, verbose_name='Twitter account')),
- ('github_account', models.URLField(blank=True, max_length=255, null=True, verbose_name='GitHub profile')),
- ('linkedin_account', models.URLField(blank=True, max_length=255, null=True, verbose_name='LinkedIn profile')),
- ('short_bio', models.CharField(blank=True, max_length=60, null=True, verbose_name='Describe yourself')),
- ('bio', models.CharField(blank=True, max_length=280, null=True, verbose_name='Short bio')),
- ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
- ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("password", models.CharField(max_length=128, verbose_name="password")),
+ (
+ "last_login",
+ models.DateTimeField(
+ blank=True, null=True, verbose_name="last login"
+ ),
+ ),
+ (
+ "is_superuser",
+ models.BooleanField(
+ default=False,
+ help_text="Designates that this user has all permissions without explicitly assigning them.",
+ verbose_name="superuser status",
+ ),
+ ),
+ (
+ "username",
+ models.CharField(
+ error_messages={
+ "unique": "A user with that username already exists."
+ },
+ help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
+ max_length=150,
+ unique=True,
+ validators=[
+ django.contrib.auth.validators.UnicodeUsernameValidator()
+ ],
+ verbose_name="username",
+ ),
+ ),
+ (
+ "first_name",
+ models.CharField(
+ blank=True, max_length=30, verbose_name="first name"
+ ),
+ ),
+ (
+ "last_name",
+ models.CharField(
+ blank=True, max_length=150, verbose_name="last name"
+ ),
+ ),
+ (
+ "email",
+ models.EmailField(
+ blank=True, max_length=254, verbose_name="email address"
+ ),
+ ),
+ (
+ "is_staff",
+ models.BooleanField(
+ default=False,
+ help_text="Designates whether the user can log into this admin site.",
+ verbose_name="staff status",
+ ),
+ ),
+ (
+ "is_active",
+ models.BooleanField(
+ default=True,
+ help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
+ verbose_name="active",
+ ),
+ ),
+ (
+ "date_joined",
+ models.DateTimeField(
+ default=django.utils.timezone.now, verbose_name="date joined"
+ ),
+ ),
+ (
+ "name",
+ models.CharField(
+ blank=True, max_length=255, verbose_name="User's name"
+ ),
+ ),
+ (
+ "picture",
+ models.ImageField(
+ blank=True,
+ null=True,
+ upload_to="profile_pics/",
+ verbose_name="Profile picture",
+ ),
+ ),
+ (
+ "location",
+ models.CharField(
+ blank=True, max_length=50, null=True, verbose_name="Location"
+ ),
+ ),
+ (
+ "job_title",
+ models.CharField(
+ blank=True, max_length=50, null=True, verbose_name="Job title"
+ ),
+ ),
+ (
+ "personal_url",
+ models.URLField(
+ blank=True,
+ max_length=555,
+ null=True,
+ verbose_name="Personal URL",
+ ),
+ ),
+ (
+ "facebook_account",
+ models.URLField(
+ blank=True,
+ max_length=255,
+ null=True,
+ verbose_name="Facebook profile",
+ ),
+ ),
+ (
+ "twitter_account",
+ models.URLField(
+ blank=True,
+ max_length=255,
+ null=True,
+ verbose_name="Twitter account",
+ ),
+ ),
+ (
+ "github_account",
+ models.URLField(
+ blank=True,
+ max_length=255,
+ null=True,
+ verbose_name="GitHub profile",
+ ),
+ ),
+ (
+ "linkedin_account",
+ models.URLField(
+ blank=True,
+ max_length=255,
+ null=True,
+ verbose_name="LinkedIn profile",
+ ),
+ ),
+ (
+ "short_bio",
+ models.CharField(
+ blank=True,
+ max_length=60,
+ null=True,
+ verbose_name="Describe yourself",
+ ),
+ ),
+ (
+ "bio",
+ models.CharField(
+ blank=True, max_length=280, null=True, verbose_name="Short bio"
+ ),
+ ),
+ (
+ "groups",
+ models.ManyToManyField(
+ blank=True,
+ help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
+ related_name="user_set",
+ related_query_name="user",
+ to="auth.Group",
+ verbose_name="groups",
+ ),
+ ),
+ (
+ "user_permissions",
+ models.ManyToManyField(
+ blank=True,
+ help_text="Specific permissions for this user.",
+ related_name="user_set",
+ related_query_name="user",
+ to="auth.Permission",
+ verbose_name="user permissions",
+ ),
+ ),
],
options={
- 'verbose_name': 'user',
- 'verbose_name_plural': 'users',
- 'abstract': False,
+ "verbose_name": "user",
+ "verbose_name_plural": "users",
+ "abstract": False,
},
- managers=[
- ('objects', django.contrib.auth.models.UserManager()),
- ],
- ),
+ managers=[("objects", django.contrib.auth.models.UserManager())],
+ )
]
diff --git a/bootcamp/users/models.py b/bootcamp/users/models.py
index 0812ba152..42556bf17 100755
--- a/bootcamp/users/models.py
+++ b/bootcamp/users/models.py
@@ -11,31 +11,35 @@ class User(AbstractUser):
# First Name and Last Name do not cover name patterns around the globe.
name = models.CharField(_("User's name"), blank=True, max_length=255)
picture = models.ImageField(
- _('Profile picture'), upload_to='profile_pics/', null=True, blank=True)
- location = models.CharField(
- _('Location'), max_length=50, null=True, blank=True)
- job_title = models.CharField(
- _('Job title'), max_length=50, null=True, blank=True)
+ _("Profile picture"), upload_to="profile_pics/", null=True, blank=True
+ )
+ location = models.CharField(_("Location"), max_length=50, null=True, blank=True)
+ job_title = models.CharField(_("Job title"), max_length=50, null=True, blank=True)
personal_url = models.URLField(
- _('Personal URL'), max_length=555, blank=True, null=True)
+ _("Personal URL"), max_length=555, blank=True, null=True
+ )
facebook_account = models.URLField(
- _('Facebook profile'), max_length=255, blank=True, null=True)
+ _("Facebook profile"), max_length=255, blank=True, null=True
+ )
twitter_account = models.URLField(
- _('Twitter account'), max_length=255, blank=True, null=True)
+ _("Twitter account"), max_length=255, blank=True, null=True
+ )
github_account = models.URLField(
- _('GitHub profile'), max_length=255, blank=True, null=True)
+ _("GitHub profile"), max_length=255, blank=True, null=True
+ )
linkedin_account = models.URLField(
- _('LinkedIn profile'), max_length=255, blank=True, null=True)
+ _("LinkedIn profile"), max_length=255, blank=True, null=True
+ )
short_bio = models.CharField(
- _('Describe yourself'), max_length=60, blank=True, null=True)
- bio = models.CharField(
- _('Short bio'), max_length=280, blank=True, null=True)
+ _("Describe yourself"), max_length=60, blank=True, null=True
+ )
+ bio = models.CharField(_("Short bio"), max_length=280, blank=True, null=True)
def __str__(self):
return self.username
def get_absolute_url(self):
- return reverse('users:detail', kwargs={'username': self.username})
+ return reverse("users:detail", kwargs={"username": self.username})
def get_profile_name(self):
if self.name:
diff --git a/bootcamp/users/schema.py b/bootcamp/users/schema.py
index 83e4533c8..5bcb4560c 100755
--- a/bootcamp/users/schema.py
+++ b/bootcamp/users/schema.py
@@ -6,6 +6,7 @@
class UserType(DjangoObjectType):
"""DjangoObjectType to acces the User model."""
+
picture = graphene.String()
name = graphene.String()
@@ -33,7 +34,7 @@ def resolve_all_users(self, info, **kwargs):
return User.objects.all()
def resolve_user(self, info, **kwargs):
- id = kwargs.get('id')
+ id = kwargs.get("id")
if id is not None:
return User.objects.get(id=id)
diff --git a/bootcamp/users/tests/test_admin.py b/bootcamp/users/tests/test_admin.py
index a1ff0b846..1afcbfda3 100755
--- a/bootcamp/users/tests/test_admin.py
+++ b/bootcamp/users/tests/test_admin.py
@@ -4,32 +4,35 @@
class TestMyUserCreationForm(TestCase):
-
def setUp(self):
- self.user = self.make_user('notalamode', 'notalamodespassword')
+ self.user = self.make_user("notalamode", "notalamodespassword")
def test_clean_username_success(self):
# Instantiate the form with a new username
- form = MyUserCreationForm({
- 'username': 'alamode',
- 'password1': '7jefB#f@Cc7YJB]2v',
- 'password2': '7jefB#f@Cc7YJB]2v',
- })
+ form = MyUserCreationForm(
+ {
+ "username": "alamode",
+ "password1": "7jefB#f@Cc7YJB]2v",
+ "password2": "7jefB#f@Cc7YJB]2v",
+ }
+ )
# Run is_valid() to trigger the validation
valid = form.is_valid()
self.assertTrue(valid)
# Run the actual clean_username method
username = form.clean_username()
- self.assertEqual('alamode', username)
+ self.assertEqual("alamode", username)
def test_clean_username_false(self):
# Instantiate the form with the same username as self.user
- form = MyUserCreationForm({
- 'username': self.user.username,
- 'password1': 'notalamodespassword',
- 'password2': 'notalamodespassword',
- })
+ form = MyUserCreationForm(
+ {
+ "username": self.user.username,
+ "password1": "notalamodespassword",
+ "password2": "notalamodespassword",
+ }
+ )
# Run is_valid() to trigger the validation, which is going to fail
# because the username is already taken
valid = form.is_valid()
@@ -37,4 +40,4 @@ def test_clean_username_false(self):
# The form.errors dict should contain a single error called 'username'
self.assertTrue(len(form.errors) == 1)
- self.assertTrue('username' in form.errors)
+ self.assertTrue("username" in form.errors)
diff --git a/bootcamp/users/tests/test_models.py b/bootcamp/users/tests/test_models.py
index fcefea173..7c8e1c50e 100755
--- a/bootcamp/users/tests/test_models.py
+++ b/bootcamp/users/tests/test_models.py
@@ -2,21 +2,17 @@
class TestUser(TestCase):
-
def setUp(self):
self.user = self.make_user()
def test__str__(self):
self.assertEqual(
self.user.__str__(),
- "testuser" # This is the default username for self.make_user()
+ "testuser", # This is the default username for self.make_user()
)
def test_get_absolute_url(self):
- self.assertEqual(
- self.user.get_absolute_url(),
- "/users/testuser/"
- )
+ self.assertEqual(self.user.get_absolute_url(), "/users/testuser/")
def test_get_profile_name(self):
assert self.user.get_profile_name() == "testuser"
diff --git a/bootcamp/users/tests/test_urls.py b/bootcamp/users/tests/test_urls.py
index 4935b0f3e..6b072436d 100755
--- a/bootcamp/users/tests/test_urls.py
+++ b/bootcamp/users/tests/test_urls.py
@@ -11,41 +11,34 @@ def setUp(self):
def test_list_reverse(self):
"""users:list should reverse to /users/."""
- self.assertEqual(reverse('users:list'), '/users/')
+ self.assertEqual(reverse("users:list"), "/users/")
def test_list_resolve(self):
"""/users/ should resolve to users:list."""
- self.assertEqual(resolve('/users/').view_name, 'users:list')
+ self.assertEqual(resolve("/users/").view_name, "users:list")
def test_redirect_reverse(self):
"""users:redirect should reverse to /users/~redirect/."""
- self.assertEqual(reverse('users:redirect'), '/users/~redirect/')
+ self.assertEqual(reverse("users:redirect"), "/users/~redirect/")
def test_redirect_resolve(self):
"""/users/~redirect/ should resolve to users:redirect."""
- self.assertEqual(
- resolve('/users/~redirect/').view_name,
- 'users:redirect'
- )
+ self.assertEqual(resolve("/users/~redirect/").view_name, "users:redirect")
def test_detail_reverse(self):
"""users:detail should reverse to /users/testuser/."""
self.assertEqual(
- reverse('users:detail', kwargs={'username': 'testuser'}),
- '/users/testuser/'
+ reverse("users:detail", kwargs={"username": "testuser"}), "/users/testuser/"
)
def test_detail_resolve(self):
"""/users/testuser/ should resolve to users:detail."""
- self.assertEqual(resolve('/users/testuser/').view_name, 'users:detail')
+ self.assertEqual(resolve("/users/testuser/").view_name, "users:detail")
def test_update_reverse(self):
"""users:update should reverse to /users/~update/."""
- self.assertEqual(reverse('users:update'), '/users/~update/')
+ self.assertEqual(reverse("users:update"), "/users/~update/")
def test_update_resolve(self):
"""/users/~update/ should resolve to users:update."""
- self.assertEqual(
- resolve('/users/~update/').view_name,
- 'users:update'
- )
+ self.assertEqual(resolve("/users/~update/").view_name, "users:update")
diff --git a/bootcamp/users/tests/test_views.py b/bootcamp/users/tests/test_views.py
index 350034e9c..e4545a9f0 100755
--- a/bootcamp/users/tests/test_views.py
+++ b/bootcamp/users/tests/test_views.py
@@ -2,47 +2,38 @@
from test_plus.test import TestCase
-from ..views import (
- UserRedirectView,
- UserUpdateView
-)
+from ..views import UserRedirectView, UserUpdateView
class BaseUserTestCase(TestCase):
-
def setUp(self):
self.user = self.make_user()
self.factory = RequestFactory()
class TestUserRedirectView(BaseUserTestCase):
-
def test_get_redirect_url(self):
# Instantiate the view directly. Never do this outside a test!
view = UserRedirectView()
# Generate a fake request
- request = self.factory.get('/fake-url')
+ request = self.factory.get("/fake-url")
# Attach the user to the request
request.user = self.user
# Attach the request to the view
view.request = request
# Expect: '/users/testuser/', as that is the default username for
# self.make_user()
- self.assertEqual(
- view.get_redirect_url(),
- '/users/testuser/'
- )
+ self.assertEqual(view.get_redirect_url(), "/users/testuser/")
class TestUserUpdateView(BaseUserTestCase):
-
def setUp(self):
# call BaseUserTestCase.setUp()
super().setUp()
# Instantiate the view directly. Never do this outside a test!
self.view = UserUpdateView()
# Generate a fake request
- request = self.factory.get('/fake-url')
+ request = self.factory.get("/fake-url")
# Attach the user to the request
request.user = self.user
# Attach the request to the view
@@ -51,14 +42,8 @@ def setUp(self):
def test_get_success_url(self):
# Expect: '/users/testuser/', as that is the default username for
# self.make_user()
- self.assertEqual(
- self.view.get_success_url(),
- '/users/testuser/'
- )
+ self.assertEqual(self.view.get_success_url(), "/users/testuser/")
def test_get_object(self):
# Expect: self.user, as that is the request's user object
- self.assertEqual(
- self.view.get_object(),
- self.user
- )
+ self.assertEqual(self.view.get_object(), self.user)
diff --git a/bootcamp/users/urls.py b/bootcamp/users/urls.py
index 1e1618363..69414f47e 100755
--- a/bootcamp/users/urls.py
+++ b/bootcamp/users/urls.py
@@ -2,26 +2,14 @@
from . import views
-app_name = 'users'
+app_name = "users"
urlpatterns = [
+ url(regex=r"^$", view=views.UserListView.as_view(), name="list"),
+ url(regex=r"^~redirect/$", view=views.UserRedirectView.as_view(), name="redirect"),
+ url(regex=r"^~update/$", view=views.UserUpdateView.as_view(), name="update"),
url(
- regex=r'^$',
- view=views.UserListView.as_view(),
- name='list'
- ),
- url(
- regex=r'^~redirect/$',
- view=views.UserRedirectView.as_view(),
- name='redirect'
- ),
- url(
- regex=r'^~update/$',
- view=views.UserUpdateView.as_view(),
- name='update'
- ),
- url(
- regex=r'^(?P[\w.@+-]+)/$',
+ regex=r"^(?P[\w.@+-]+)/$",
view=views.UserDetailView.as_view(),
- name='detail'
+ name="detail",
),
]
diff --git a/bootcamp/users/views.py b/bootcamp/users/views.py
index 224eccb6c..d670fdcd2 100755
--- a/bootcamp/users/views.py
+++ b/bootcamp/users/views.py
@@ -8,28 +8,37 @@
class UserDetailView(LoginRequiredMixin, DetailView):
model = User
# These next two lines tell the view to index lookups by username
- slug_field = 'username'
- slug_url_kwarg = 'username'
+ slug_field = "username"
+ slug_url_kwarg = "username"
class UserRedirectView(LoginRequiredMixin, RedirectView):
permanent = False
def get_redirect_url(self):
- return reverse('users:detail',
- kwargs={'username': self.request.user.username})
+ return reverse("users:detail", kwargs={"username": self.request.user.username})
class UserUpdateView(LoginRequiredMixin, UpdateView):
- fields = ['name', 'email', 'picture', 'job_title', 'location', 'personal_url',
- 'facebook_account', 'twitter_account', 'github_account',
- 'linkedin_account', 'short_bio', 'bio', ]
+ fields = [
+ "name",
+ "email",
+ "picture",
+ "job_title",
+ "location",
+ "personal_url",
+ "facebook_account",
+ "twitter_account",
+ "github_account",
+ "linkedin_account",
+ "short_bio",
+ "bio",
+ ]
model = User
# send the user back to their own page after a successful update
def get_success_url(self):
- return reverse('users:detail',
- kwargs={'username': self.request.user.username})
+ return reverse("users:detail", kwargs={"username": self.request.user.username})
def get_object(self):
# Only get the User record for the user making the request
@@ -39,5 +48,5 @@ def get_object(self):
class UserListView(LoginRequiredMixin, ListView):
model = User
# These next two lines tell the view to index lookups by username
- slug_field = 'username'
- slug_url_kwarg = 'username'
+ slug_field = "username"
+ slug_url_kwarg = "username"
diff --git a/compose/production/postgres/Dockerfile b/compose/production/postgres/Dockerfile
index 4a848ee14..b80cdbe5a 100755
--- a/compose/production/postgres/Dockerfile
+++ b/compose/production/postgres/Dockerfile
@@ -1,4 +1,4 @@
-FROM postgres:10
+FROM postgres:11-alpine
COPY ./compose/production/postgres/backup.sh /usr/local/bin/backup
RUN chmod +x /usr/local/bin/backup
diff --git a/config/asgi.py b/config/asgi.py
index 9263f4251..658d2a2a0 100755
--- a/config/asgi.py
+++ b/config/asgi.py
@@ -7,6 +7,6 @@
import django
from channels.routing import get_default_application
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.production')
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")
django.setup()
application = get_default_application()
diff --git a/config/routing.py b/config/routing.py
index 5b9ccddd5..01a726c14 100755
--- a/config/routing.py
+++ b/config/routing.py
@@ -6,16 +6,21 @@
from bootcamp.messager.consumers import MessagerConsumer
from bootcamp.notifications.consumers import NotificationsConsumer
+
# from bootcamp.notifications.routing import notifications_urlpatterns
# from bootcamp.messager.routing import messager_urlpatterns
-application = ProtocolTypeRouter({
- "websocket": AllowedHostsOriginValidator(
- AuthMiddlewareStack(
- URLRouter([
- url(r'^notifications/$', NotificationsConsumer),
- url(r'^(?P[^/]+)/$', MessagerConsumer),
- ])
- ),
- ),
-})
+application = ProtocolTypeRouter(
+ {
+ "websocket": AllowedHostsOriginValidator(
+ AuthMiddlewareStack(
+ URLRouter(
+ [
+ url(r"^notifications/$", NotificationsConsumer),
+ url(r"^(?P[^/]+)/$", MessagerConsumer),
+ ]
+ )
+ )
+ )
+ }
+)
diff --git a/bootcamp/schema.py b/config/schema.py
similarity index 100%
rename from bootcamp/schema.py
rename to config/schema.py
diff --git a/config/settings/base.py b/config/settings/base.py
index cc925cc99..ecad77e50 100755
--- a/config/settings/base.py
+++ b/config/settings/base.py
@@ -4,11 +4,13 @@
import environ
-ROOT_DIR = environ.Path(__file__) - 3 # (bootcamp/config/settings/base.py - 3 = bootcamp/)
-APPS_DIR = ROOT_DIR.path('bootcamp')
+ROOT_DIR = (
+ environ.Path(__file__) - 3
+) # (bootcamp/config/settings/base.py - 3 = bootcamp/)
+APPS_DIR = ROOT_DIR.path("bootcamp")
env = environ.Env()
-env.read_env(str(ROOT_DIR.path('.env')))
+env.read_env(str(ROOT_DIR.path(".env")))
# READ_DOT_ENV_FILE = env.bool('DJANGO_READ_DOT_ENV_FILE', default=False)
# if READ_DOT_ENV_FILE:
@@ -18,14 +20,14 @@
# GENERAL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#debug
-DEBUG = env.bool('DJANGO_DEBUG', False)
+DEBUG = env.bool("DJANGO_DEBUG", False)
# Local time zone. Choices are
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# though not all of them may be available with every OS.
# In Windows, this must be set to your system time zone.
-TIME_ZONE = 'UTC'
+TIME_ZONE = "UTC"
# https://docs.djangoproject.com/en/dev/ref/settings/#language-code
-LANGUAGE_CODE = 'en-us'
+LANGUAGE_CODE = "en-us"
# https://docs.djangoproject.com/en/dev/ref/settings/#site-id
SITE_ID = 1
# https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n
@@ -38,144 +40,132 @@
# DATABASES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#databases
-DATABASES = {
- 'default': env.db('DATABASE_URL'),
-}
-DATABASES['default']['ATOMIC_REQUESTS'] = True
+DATABASES = {"default": env.db("DATABASE_URL")}
+DATABASES["default"]["ATOMIC_REQUESTS"] = True
# URLS
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#root-urlconf
-ROOT_URLCONF = 'config.urls'
+ROOT_URLCONF = "config.urls"
# https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application
-WSGI_APPLICATION = 'config.wsgi.application'
+WSGI_APPLICATION = "config.wsgi.application"
# APPS
# ------------------------------------------------------------------------------
DJANGO_APPS = [
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- 'django.contrib.sites',
- 'django.contrib.messages',
- 'django.contrib.staticfiles',
- 'django.contrib.humanize',
- 'django.contrib.admin',
- 'django.forms',
+ "django.contrib.auth",
+ "django.contrib.contenttypes",
+ "django.contrib.sessions",
+ "django.contrib.sites",
+ "django.contrib.messages",
+ "django.contrib.staticfiles",
+ "django.contrib.humanize",
+ "django.contrib.admin",
+ "django.forms",
]
THIRD_PARTY_APPS = [
- 'crispy_forms',
- 'sorl.thumbnail',
- 'allauth',
- 'allauth.account',
- 'allauth.socialaccount',
+ "crispy_forms",
+ "sorl.thumbnail",
+ "allauth",
+ "allauth.account",
+ "allauth.socialaccount",
# 'allauth.socialaccount.providers.amazon',
# 'allauth.socialaccount.providers.github',
# 'allauth.socialaccount.providers.google',
# 'allauth.socialaccount.providers.linkedin',
# 'allauth.socialaccount.providers.slack',
- 'channels',
- 'django_comments',
- 'graphene_django',
- 'markdownx',
- 'taggit',
+ "channels",
+ "django_comments",
+ "graphene_django",
+ "markdownx",
+ "taggit",
]
LOCAL_APPS = [
- 'bootcamp.users.apps.UsersConfig',
+ "bootcamp.users.apps.UsersConfig",
# Your stuff: custom apps go here
- 'bootcamp.articles.apps.ArticlesConfig',
- 'bootcamp.messager.apps.MessagerConfig',
- 'bootcamp.news.apps.NewsConfig',
- 'bootcamp.notifications.apps.NotificationsConfig',
- 'bootcamp.qa.apps.QaConfig',
- 'bootcamp.search.apps.SearchConfig'
+ "bootcamp.articles.apps.ArticlesConfig",
+ "bootcamp.messager.apps.MessagerConfig",
+ "bootcamp.news.apps.NewsConfig",
+ "bootcamp.notifications.apps.NotificationsConfig",
+ "bootcamp.qa.apps.QaConfig",
+ "bootcamp.search.apps.SearchConfig",
]
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
-FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'
+FORM_RENDERER = "django.forms.renderers.TemplatesSetting"
# MIGRATIONS
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#migration-modules
-MIGRATION_MODULES = {
- 'sites': 'bootcamp.contrib.sites.migrations'
-}
+MIGRATION_MODULES = {"sites": "bootcamp.contrib.sites.migrations"}
# AUTHENTICATION
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#authentication-backends
AUTHENTICATION_BACKENDS = [
- 'django.contrib.auth.backends.ModelBackend',
- 'allauth.account.auth_backends.AuthenticationBackend',
+ "django.contrib.auth.backends.ModelBackend",
+ "allauth.account.auth_backends.AuthenticationBackend",
]
# https://docs.djangoproject.com/en/dev/ref/settings/#auth-user-model
-AUTH_USER_MODEL = 'users.User'
+AUTH_USER_MODEL = "users.User"
# https://docs.djangoproject.com/en/dev/ref/settings/#login-redirect-url
-LOGIN_REDIRECT_URL = 'news:list'
+LOGIN_REDIRECT_URL = "news:list"
# https://docs.djangoproject.com/en/dev/ref/settings/#login-url
-LOGIN_URL = 'account_login'
+LOGIN_URL = "account_login"
# PASSWORDS
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers
PASSWORD_HASHERS = [
# https://docs.djangoproject.com/en/dev/topics/auth/passwords/#using-argon2-with-django
- 'django.contrib.auth.hashers.Argon2PasswordHasher',
- 'django.contrib.auth.hashers.PBKDF2PasswordHasher',
- 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
- 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
- 'django.contrib.auth.hashers.BCryptPasswordHasher',
+ "django.contrib.auth.hashers.Argon2PasswordHasher",
+ "django.contrib.auth.hashers.PBKDF2PasswordHasher",
+ "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
+ "django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
+ "django.contrib.auth.hashers.BCryptPasswordHasher",
]
# https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
- 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
- },
- {
- 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
- },
- {
- 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
- },
- {
- 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
},
+ {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
+ {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
+ {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
# MIDDLEWARE
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#middleware
MIDDLEWARE = [
- 'django.middleware.security.SecurityMiddleware',
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.middleware.common.CommonMiddleware',
- 'django.middleware.csrf.CsrfViewMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'django.contrib.messages.middleware.MessageMiddleware',
- 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+ "django.middleware.security.SecurityMiddleware",
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.middleware.common.CommonMiddleware",
+ "django.middleware.csrf.CsrfViewMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
+ "django.contrib.messages.middleware.MessageMiddleware",
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
]
# STATIC
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#static-root
-STATIC_ROOT = str(ROOT_DIR('staticfiles'))
+STATIC_ROOT = str(ROOT_DIR("staticfiles"))
# https://docs.djangoproject.com/en/dev/ref/settings/#static-url
-STATIC_URL = '/static/'
+STATIC_URL = "/static/"
# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS
-STATICFILES_DIRS = [
- str(APPS_DIR.path('static')),
-]
+STATICFILES_DIRS = [str(APPS_DIR.path("static"))]
# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders
STATICFILES_FINDERS = [
- 'django.contrib.staticfiles.finders.FileSystemFinder',
- 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
+ "django.contrib.staticfiles.finders.FileSystemFinder",
+ "django.contrib.staticfiles.finders.AppDirectoriesFinder",
]
# MEDIA
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#media-root
-MEDIA_ROOT = str(ROOT_DIR('media'))
+MEDIA_ROOT = str(ROOT_DIR("media"))
# https://docs.djangoproject.com/en/dev/ref/settings/#media-url
-MEDIA_URL = '/media/'
+MEDIA_URL = "/media/"
# TEMPLATES
# ------------------------------------------------------------------------------
@@ -183,53 +173,51 @@
TEMPLATES = [
{
# https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-TEMPLATES-BACKEND
- 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
# https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs
- 'DIRS': [
- str(APPS_DIR.path('templates')),
- ],
- 'OPTIONS': {
+ "DIRS": [str(APPS_DIR.path("templates"))],
+ "OPTIONS": {
# https://docs.djangoproject.com/en/dev/ref/settings/#template-debug
- 'debug': DEBUG,
+ "debug": DEBUG,
# https://docs.djangoproject.com/en/dev/ref/settings/#template-loaders
# https://docs.djangoproject.com/en/dev/ref/templates/api/#loader-types
- 'loaders': [
- 'django.template.loaders.filesystem.Loader',
- 'django.template.loaders.app_directories.Loader',
+ "loaders": [
+ "django.template.loaders.filesystem.Loader",
+ "django.template.loaders.app_directories.Loader",
],
# https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors
- 'context_processors': [
- 'django.template.context_processors.debug',
- 'django.template.context_processors.request',
- 'django.contrib.auth.context_processors.auth',
- 'django.template.context_processors.i18n',
- 'django.template.context_processors.media',
- 'django.template.context_processors.static',
- 'django.template.context_processors.tz',
- 'django.contrib.messages.context_processors.messages',
+ "context_processors": [
+ "django.template.context_processors.debug",
+ "django.template.context_processors.request",
+ "django.contrib.auth.context_processors.auth",
+ "django.template.context_processors.i18n",
+ "django.template.context_processors.media",
+ "django.template.context_processors.static",
+ "django.template.context_processors.tz",
+ "django.contrib.messages.context_processors.messages",
],
},
- },
+ }
]
# http://django-crispy-forms.readthedocs.io/en/latest/install.html#template-packs
-CRISPY_TEMPLATE_PACK = 'bootstrap4'
+CRISPY_TEMPLATE_PACK = "bootstrap4"
# FIXTURES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#fixture-dirs
-FIXTURE_DIRS = (
- str(APPS_DIR.path('fixtures')),
-)
+FIXTURE_DIRS = (str(APPS_DIR.path("fixtures")),)
# EMAIL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
-EMAIL_BACKEND = env('EMAIL_BACKEND', default='django.core.mail.backends.smtp.EmailBackend')
+EMAIL_BACKEND = env(
+ "EMAIL_BACKEND", default="django.core.mail.backends.smtp.EmailBackend"
+)
# ADMIN
# ------------------------------------------------------------------------------
# Django Admin URL regex.
-ADMIN_URL = r'^admin/'
+ADMIN_URL = r"^admin/"
# https://docs.djangoproject.com/en/dev/ref/settings/#admins
# ADMINS = [
# ("""Vitor Freitas""", 'vitor-freitas@example.com'),
@@ -240,17 +228,17 @@
# django-allauth
# ------------------------------------------------------------------------------
-ACCOUNT_ALLOW_REGISTRATION = env.bool('ACCOUNT_ALLOW_REGISTRATION', True)
+ACCOUNT_ALLOW_REGISTRATION = env.bool("ACCOUNT_ALLOW_REGISTRATION", True)
# https://django-allauth.readthedocs.io/en/latest/configuration.html
-ACCOUNT_AUTHENTICATION_METHOD = 'username'
+ACCOUNT_AUTHENTICATION_METHOD = "username"
# https://django-allauth.readthedocs.io/en/latest/configuration.html
ACCOUNT_EMAIL_REQUIRED = True
# https://django-allauth.readthedocs.io/en/latest/configuration.html
-ACCOUNT_EMAIL_VERIFICATION = 'mandatory'
+ACCOUNT_EMAIL_VERIFICATION = "mandatory"
# https://django-allauth.readthedocs.io/en/latest/configuration.html
-ACCOUNT_ADAPTER = 'bootcamp.users.adapters.AccountAdapter'
+ACCOUNT_ADAPTER = "bootcamp.users.adapters.AccountAdapter"
# https://django-allauth.readthedocs.io/en/latest/configuration.html
-SOCIALACCOUNT_ADAPTER = 'bootcamp.users.adapters.SocialAccountAdapter'
+SOCIALACCOUNT_ADAPTER = "bootcamp.users.adapters.SocialAccountAdapter"
# Your stuff...
@@ -260,18 +248,14 @@
REDIS_URL = f'{env("REDIS_URL", default="redis://127.0.0.1:6379")}/{0}'
# django-channels setup
-ASGI_APPLICATION = 'config.routing.application'
+ASGI_APPLICATION = "config.routing.application"
CHANNEL_LAYERS = {
- 'default': {
- 'BACKEND': 'channels_redis.core.RedisChannelLayer',
- 'CONFIG': {
- 'hosts': [REDIS_URL, ],
- },
+ "default": {
+ "BACKEND": "channels_redis.core.RedisChannelLayer",
+ "CONFIG": {"hosts": [REDIS_URL]},
}
}
# GraphQL settings
-GRAPHENE = {
- 'SCHEMA': 'bootcamp.schema.schema'
-}
+GRAPHENE = {"SCHEMA": "config.schema.schema"}
diff --git a/config/settings/local.py b/config/settings/local.py
index c269aeb68..c29246d1f 100755
--- a/config/settings/local.py
+++ b/config/settings/local.py
@@ -8,65 +8,65 @@
# GENERAL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#debug
-DEBUG = env.bool('DEBUG', default=True)
+DEBUG = env.bool("DEBUG", default=True)
# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
-SECRET_KEY = env('SECRET_KEY', default='fOqtAorZrVqWYbuMPOcZnTzw2D5bKeHGpXUwCaNBnvFUmO1njCQZGz05x1BhDG0E')
+SECRET_KEY = env(
+ "SECRET_KEY",
+ default="fOqtAorZrVqWYbuMPOcZnTzw2D5bKeHGpXUwCaNBnvFUmO1njCQZGz05x1BhDG0E",
+)
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
-ALLOWED_HOSTS = [
- "localhost",
- "0.0.0.0",
- "127.0.0.1",
-]
+ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1"]
# CACHES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#caches
CACHES = {
- 'default': {
- 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
- 'LOCATION': ''
+ "default": {
+ "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
+ "LOCATION": "",
}
}
# TEMPLATES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#templates
-TEMPLATES[0]['OPTIONS']['debug'] = DEBUG # noqa F405
+TEMPLATES[0]["OPTIONS"]["debug"] = DEBUG # noqa F405
# EMAIL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
-EMAIL_BACKEND = env('EMAIL_BACKEND', default='django.core.mail.backends.console.EmailBackend')
+EMAIL_BACKEND = env(
+ "EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend"
+)
# https://docs.djangoproject.com/en/dev/ref/settings/#email-host
-EMAIL_HOST = 'localhost'
+EMAIL_HOST = "localhost"
# https://docs.djangoproject.com/en/dev/ref/settings/#email-port
EMAIL_PORT = 1025
# django-debug-toolbar
# ------------------------------------------------------------------------------
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#prerequisites
-INSTALLED_APPS += ['debug_toolbar'] # noqa F405
+INSTALLED_APPS += ["debug_toolbar"] # noqa F405
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#middleware
-MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware'] # noqa F405
+MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # noqa F405
# https://django-debug-toolbar.readthedocs.io/en/latest/configuration.html#debug-toolbar-config
DEBUG_TOOLBAR_CONFIG = {
- 'DISABLE_PANELS': [
- 'debug_toolbar.panels.redirects.RedirectsPanel',
- ],
- 'SHOW_TEMPLATE_CONTEXT': True,
+ "DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"],
+ "SHOW_TEMPLATE_CONTEXT": True,
}
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#internal-ips
-INTERNAL_IPS = ['127.0.0.1', '10.0.2.2']
+INTERNAL_IPS = ["127.0.0.1", "10.0.2.2"]
import socket
import os
-if os.environ.get('USE_DOCKER') == 'yes':
+
+if os.environ.get("USE_DOCKER") == "yes":
hostname, _, ips = socket.gethostbyname_ex(socket.gethostname())
- INTERNAL_IPS += [ip[:-1] + '1' for ip in ips]
+ INTERNAL_IPS += [ip[:-1] + "1" for ip in ips]
# django-extensions
# ------------------------------------------------------------------------------
# https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration
-INSTALLED_APPS += ['django_extensions'] # noqa F405
+INSTALLED_APPS += ["django_extensions"] # noqa F405
# Your stuff...
# ------------------------------------------------------------------------------
diff --git a/config/settings/production.py b/config/settings/production.py
index fed6663bb..659eb800f 100755
--- a/config/settings/production.py
+++ b/config/settings/production.py
@@ -6,37 +6,39 @@
# GENERAL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
-SECRET_KEY = env('SECRET_KEY')
+SECRET_KEY = env("SECRET_KEY")
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
-ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=['vitor@freitas.com trybootcamp.vitorfs.com'])
+ALLOWED_HOSTS = env.list(
+ "ALLOWED_HOSTS", default=["vitor@freitas.com trybootcamp.vitorfs.com"]
+)
# DATABASES
# ------------------------------------------------------------------------------
-DATABASES['default'] = env.db('DATABASE_URL') # noqa F405
-DATABASES['default']['ATOMIC_REQUESTS'] = True # noqa F405
-DATABASES['default']['CONN_MAX_AGE'] = env.int('CONN_MAX_AGE', default=60) # noqa F405
+DATABASES["default"] = env.db("DATABASE_URL") # noqa F405
+DATABASES["default"]["ATOMIC_REQUESTS"] = True # noqa F405
+DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60) # noqa F405
# CACHES
# ------------------------------------------------------------------------------
CACHES = {
- 'default': {
- 'BACKEND': 'django_redis.cache.RedisCache',
- 'LOCATION': REDIS_URL,
- 'OPTIONS': {
- 'CLIENT_CLASS': 'django_redis.client.DefaultClient',
+ "default": {
+ "BACKEND": "django_redis.cache.RedisCache",
+ "LOCATION": REDIS_URL,
+ "OPTIONS": {
+ "CLIENT_CLASS": "django_redis.client.DefaultClient",
# Mimicing memcache behavior.
# http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior
- 'IGNORE_EXCEPTIONS': True,
- }
+ "IGNORE_EXCEPTIONS": True,
+ },
}
}
# SECURITY
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header
-SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
+SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-ssl-redirect
-SECURE_SSL_REDIRECT = env.bool('SECURE_SSL_REDIRECT', default=True)
+SECURE_SSL_REDIRECT = env.bool("SECURE_SSL_REDIRECT", default=True)
# https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-secure
SESSION_COOKIE_SECURE = True
# https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-httponly
@@ -50,26 +52,28 @@
# TODO: set this to 60 seconds first and then to 518400 once you prove the former works
SECURE_HSTS_SECONDS = 60
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-include-subdomains
-SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool('SECURE_HSTS_INCLUDE_SUBDOMAINS', default=True)
+SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool(
+ "SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True
+)
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-preload
-SECURE_HSTS_PRELOAD = env.bool('SECURE_HSTS_PRELOAD', default=True)
+SECURE_HSTS_PRELOAD = env.bool("SECURE_HSTS_PRELOAD", default=True)
# https://docs.djangoproject.com/en/dev/ref/middleware/#x-content-type-options-nosniff
-SECURE_CONTENT_TYPE_NOSNIFF = env.bool('SECURE_CONTENT_TYPE_NOSNIFF', default=True)
+SECURE_CONTENT_TYPE_NOSNIFF = env.bool("SECURE_CONTENT_TYPE_NOSNIFF", default=True)
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-browser-xss-filter
SECURE_BROWSER_XSS_FILTER = True
# https://docs.djangoproject.com/en/dev/ref/settings/#x-frame-options
-X_FRAME_OPTIONS = 'DENY'
+X_FRAME_OPTIONS = "DENY"
# STORAGES
# ------------------------------------------------------------------------------
# https://django-storages.readthedocs.io/en/latest/#installation
-INSTALLED_APPS += ['storages'] # noqa F405
+INSTALLED_APPS += ["storages"] # noqa F405
# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings
-AWS_ACCESS_KEY_ID = env('AWS_ACCESS_KEY_ID')
+AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID")
# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings
-AWS_SECRET_ACCESS_KEY = env('AWS_SECRET_ACCESS_KEY')
+AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY")
# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings
-AWS_STORAGE_BUCKET_NAME = env('AWS_STORAGE_BUCKET_NAME')
+AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME")
# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings
AWS_AUTO_CREATE_BUCKET = True
# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings
@@ -78,127 +82,124 @@
_AWS_EXPIRY = 60 * 60 * 24 * 7
# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings
AWS_S3_OBJECT_PARAMETERS = {
- 'CacheControl': f'max-age={_AWS_EXPIRY}, s-maxage={_AWS_EXPIRY}, must-revalidate',
+ "CacheControl": f"max-age={_AWS_EXPIRY}, s-maxage={_AWS_EXPIRY}, must-revalidate"
}
# STATIC
# ------------------------
-STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
+STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
# MEDIA
# ------------------------------------------------------------------------------
-DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
-MEDIA_URL = f'https://s3.amazonaws.com/{AWS_STORAGE_BUCKET_NAME}/'
+DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
+MEDIA_URL = f"https://s3.amazonaws.com/{AWS_STORAGE_BUCKET_NAME}/"
# TEMPLATES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#templates
-TEMPLATES[0]['OPTIONS']['loaders'] = [ # noqa F405
+TEMPLATES[0]["OPTIONS"]["loaders"] = [ # noqa F405
(
- 'django.template.loaders.cached.Loader',
+ "django.template.loaders.cached.Loader",
[
- 'django.template.loaders.filesystem.Loader',
- 'django.template.loaders.app_directories.Loader',
- ]
- ),
+ "django.template.loaders.filesystem.Loader",
+ "django.template.loaders.app_directories.Loader",
+ ],
+ )
]
# EMAIL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#default-from-email
DEFAULT_FROM_EMAIL = env(
- 'DEFAULT_FROM_EMAIL',
- default='Bootcamp '
+ "DEFAULT_FROM_EMAIL",
+ default="Bootcamp ",
)
# https://docs.djangoproject.com/en/dev/ref/settings/#server-email
-SERVER_EMAIL = env('SERVER_EMAIL', default=DEFAULT_FROM_EMAIL)
+SERVER_EMAIL = env("SERVER_EMAIL", default=DEFAULT_FROM_EMAIL)
# https://docs.djangoproject.com/en/dev/ref/settings/#email-subject-prefix
-EMAIL_SUBJECT_PREFIX = env('EMAIL_SUBJECT_PREFIX', default='[Bootcamp]')
+EMAIL_SUBJECT_PREFIX = env("EMAIL_SUBJECT_PREFIX", default="[Bootcamp]")
# ADMIN
# ------------------------------------------------------------------------------
# Django Admin URL regex.
-ADMIN_URL = env('ADMIN_URL')
+ADMIN_URL = env("ADMIN_URL")
# Anymail (Mailgun)
# ------------------------------------------------------------------------------
# https://anymail.readthedocs.io/en/stable/installation/#installing-anymail
-INSTALLED_APPS += ['anymail'] # noqa F405
-EMAIL_BACKEND = 'anymail.backends.mailgun.EmailBackend'
+INSTALLED_APPS += ["anymail"] # noqa F405
+EMAIL_BACKEND = "anymail.backends.mailgun.EmailBackend"
# https://anymail.readthedocs.io/en/stable/installation/#anymail-settings-reference
ANYMAIL = {
- 'MAILGUN_API_KEY': env('MAILGUN_API_KEY'),
- 'MAILGUN_SENDER_DOMAIN': env('MAILGUN_SENDER_DOMAIN')
+ "MAILGUN_API_KEY": env("MAILGUN_API_KEY"),
+ "MAILGUN_SENDER_DOMAIN": env("MAILGUN_SENDER_DOMAIN"),
}
# WhiteNoise
# ------------------------------------------------------------------------------
# http://whitenoise.evans.io/en/latest/django.html#enable-whitenoise
-MIDDLEWARE = ['whitenoise.middleware.WhiteNoiseMiddleware'] + MIDDLEWARE # noqa F405
+MIDDLEWARE = ["whitenoise.middleware.WhiteNoiseMiddleware"] + MIDDLEWARE # noqa F405
# raven
# ------------------------------------------------------------------------------
# https://docs.sentry.io/clients/python/integrations/django/
-INSTALLED_APPS += ['raven.contrib.django.raven_compat'] # noqa F405
-MIDDLEWARE = ['raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware'] + MIDDLEWARE
+INSTALLED_APPS += ["raven.contrib.django.raven_compat"] # noqa F405
+MIDDLEWARE = [
+ "raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware"
+] + MIDDLEWARE
# Sentry
# ------------------------------------------------------------------------------
-SENTRY_DSN = env('SENTRY_DSN')
-SENTRY_CLIENT = env('SENTRY_CLIENT', default='raven.contrib.django.raven_compat.DjangoClient')
+SENTRY_DSN = env("SENTRY_DSN")
+SENTRY_CLIENT = env(
+ "SENTRY_CLIENT", default="raven.contrib.django.raven_compat.DjangoClient"
+)
LOGGING = {
- 'version': 1,
- 'disable_existing_loggers': True,
- 'root': {
- 'level': 'WARNING',
- 'handlers': ['sentry'],
+ "version": 1,
+ "disable_existing_loggers": True,
+ "root": {"level": "WARNING", "handlers": ["sentry"]},
+ "formatters": {
+ "verbose": {
+ "format": "%(levelname)s %(asctime)s %(module)s "
+ "%(process)d %(thread)d %(message)s"
+ }
},
- 'formatters': {
- 'verbose': {
- 'format': '%(levelname)s %(asctime)s %(module)s '
- '%(process)d %(thread)d %(message)s'
+ "handlers": {
+ "sentry": {
+ "level": "ERROR",
+ "class": "raven.contrib.django.raven_compat.handlers.SentryHandler",
},
- },
- 'handlers': {
- 'sentry': {
- 'level': 'ERROR',
- 'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler',
+ "console": {
+ "level": "DEBUG",
+ "class": "logging.StreamHandler",
+ "formatter": "verbose",
},
- 'console': {
- 'level': 'DEBUG',
- 'class': 'logging.StreamHandler',
- 'formatter': 'verbose'
- }
},
- 'loggers': {
- 'django.db.backends': {
- 'level': 'ERROR',
- 'handlers': ['console'],
- 'propagate': False,
- },
- 'raven': {
- 'level': 'DEBUG',
- 'handlers': ['console'],
- 'propagate': False,
+ "loggers": {
+ "django.db.backends": {
+ "level": "ERROR",
+ "handlers": ["console"],
+ "propagate": False,
},
- 'sentry.errors': {
- 'level': 'DEBUG',
- 'handlers': ['console'],
- 'propagate': False,
+ "raven": {"level": "DEBUG", "handlers": ["console"], "propagate": False},
+ "sentry.errors": {
+ "level": "DEBUG",
+ "handlers": ["console"],
+ "propagate": False,
},
- 'django.security.DisallowedHost': {
- 'level': 'ERROR',
- 'handlers': ['console', 'sentry'],
- 'propagate': False,
+ "django.security.DisallowedHost": {
+ "level": "ERROR",
+ "handlers": ["console", "sentry"],
+ "propagate": False,
},
},
}
-SENTRY_CELERY_LOGLEVEL = env.int('SENTRY_LOG_LEVEL', logging.INFO)
+SENTRY_CELERY_LOGLEVEL = env.int("SENTRY_LOG_LEVEL", logging.INFO)
RAVEN_CONFIG = {
- 'CELERY_LOGLEVEL': env.int('SENTRY_LOG_LEVEL', logging.INFO),
- 'DSN': SENTRY_DSN
+ "CELERY_LOGLEVEL": env.int("SENTRY_LOG_LEVEL", logging.INFO),
+ "DSN": SENTRY_DSN,
}
# Your stuff...
diff --git a/config/settings/test.py b/config/settings/test.py
index 0f046befa..04b077503 100755
--- a/config/settings/test.py
+++ b/config/settings/test.py
@@ -10,47 +10,45 @@
# https://docs.djangoproject.com/en/dev/ref/settings/#debug
DEBUG = False
# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
-SECRET_KEY = env('SECRET_KEY', default='0wG2N60LqkDwM0Vi42p63bTekW3ac7Jt9w140')
+SECRET_KEY = env("SECRET_KEY", default="0wG2N60LqkDwM0Vi42p63bTekW3ac7Jt9w140")
# https://docs.djangoproject.com/en/dev/ref/settings/#test-runner
-TEST_RUNNER = 'django.test.runner.DiscoverRunner'
+TEST_RUNNER = "django.test.runner.DiscoverRunner"
# CACHES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#caches
CACHES = {
- 'default': {
- 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
- 'LOCATION': ''
+ "default": {
+ "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
+ "LOCATION": "",
}
}
# PASSWORDS
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers
-PASSWORD_HASHERS = [
- 'django.contrib.auth.hashers.MD5PasswordHasher',
-]
+PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
# TEMPLATES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#templates
-TEMPLATES[0]['OPTIONS']['debug'] = DEBUG # noqa F405
-TEMPLATES[0]['OPTIONS']['loaders'] = [ # noqa F405
+TEMPLATES[0]["OPTIONS"]["debug"] = DEBUG # noqa F405
+TEMPLATES[0]["OPTIONS"]["loaders"] = [ # noqa F405
(
- 'django.template.loaders.cached.Loader',
+ "django.template.loaders.cached.Loader",
[
- 'django.template.loaders.filesystem.Loader',
- 'django.template.loaders.app_directories.Loader',
+ "django.template.loaders.filesystem.Loader",
+ "django.template.loaders.app_directories.Loader",
],
- ),
+ )
]
# EMAIL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
-EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
+EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
# https://docs.djangoproject.com/en/dev/ref/settings/#email-host
-EMAIL_HOST = 'localhost'
+EMAIL_HOST = "localhost"
# https://docs.djangoproject.com/en/dev/ref/settings/#email-port
EMAIL_PORT = 1025
diff --git a/config/urls.py b/config/urls.py
index 2debc405c..1787dfb9a 100755
--- a/config/urls.py
+++ b/config/urls.py
@@ -8,42 +8,54 @@
from graphene_django.views import GraphQLView
urlpatterns = [
- url(r'^$',
- TemplateView.as_view(template_name='pages/home.html'), name='home'),
- url(r'^about/$',
- TemplateView.as_view(template_name='pages/about.html'), name='about'),
+ url(r"^$", TemplateView.as_view(template_name="pages/home.html"), name="home"),
+ url(
+ r"^about/$",
+ TemplateView.as_view(template_name="pages/about.html"),
+ name="about",
+ ),
# Django Admin, use {% url 'admin:index' %}
url(settings.ADMIN_URL, admin.site.urls),
# User management
- url(r'^users/', include('bootcamp.users.urls', namespace='users')),
- url(r'^accounts/', include('allauth.urls')),
+ url(r"^users/", include("bootcamp.users.urls", namespace="users")),
+ url(r"^accounts/", include("allauth.urls")),
# Third party apps here
- url(r'^comments/', include('django_comments.urls')),
- url(r'^graphql', GraphQLView.as_view(graphiql=True)),
- url(r'^markdownx/', include('markdownx.urls')),
+ url(r"^comments/", include("django_comments.urls")),
+ url(r"^graphql", GraphQLView.as_view(graphiql=True)),
+ url(r"^markdownx/", include("markdownx.urls")),
# Local apps here
- url(r'^notifications/',
- include('bootcamp.notifications.urls', namespace='notifications')),
- url(r'^articles/',
- include('bootcamp.articles.urls', namespace='articles')),
- url(r'^news/', include('bootcamp.news.urls', namespace='news')),
- url(r'^messages/',
- include('bootcamp.messager.urls', namespace='messager')),
- url(r'^qa/', include('bootcamp.qa.urls', namespace='qa')),
- url(r'^search/', include('bootcamp.search.urls', namespace='search')),
-
+ url(
+ r"^notifications/",
+ include("bootcamp.notifications.urls", namespace="notifications"),
+ ),
+ url(r"^articles/", include("bootcamp.articles.urls", namespace="articles")),
+ url(r"^news/", include("bootcamp.news.urls", namespace="news")),
+ url(r"^messages/", include("bootcamp.messager.urls", namespace="messager")),
+ url(r"^qa/", include("bootcamp.qa.urls", namespace="qa")),
+ url(r"^search/", include("bootcamp.search.urls", namespace="search")),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if settings.DEBUG:
# This allows the error pages to be debugged during development
urlpatterns += [
- url(r'^400/$', default_views.bad_request, kwargs={'exception': Exception('Bad Request!')}),
- url(r'^403/$', default_views.permission_denied, kwargs={'exception': Exception('Permission Denied')}),
- url(r'^404/$', default_views.page_not_found, kwargs={'exception': Exception('Page not Found')}),
- url(r'^500/$', default_views.server_error),
+ url(
+ r"^400/$",
+ default_views.bad_request,
+ kwargs={"exception": Exception("Bad Request!")},
+ ),
+ url(
+ r"^403/$",
+ default_views.permission_denied,
+ kwargs={"exception": Exception("Permission Denied")},
+ ),
+ url(
+ r"^404/$",
+ default_views.page_not_found,
+ kwargs={"exception": Exception("Page not Found")},
+ ),
+ url(r"^500/$", default_views.server_error),
]
- if 'debug_toolbar' in settings.INSTALLED_APPS:
+ if "debug_toolbar" in settings.INSTALLED_APPS:
import debug_toolbar
- urlpatterns = [
- url(r'^__debug__/', include(debug_toolbar.urls)),
- ] + urlpatterns
+
+ urlpatterns = [url(r"^__debug__/", include(debug_toolbar.urls))] + urlpatterns
diff --git a/config/wsgi.py b/config/wsgi.py
index ac31de273..b0edf189e 100755
--- a/config/wsgi.py
+++ b/config/wsgi.py
@@ -20,11 +20,12 @@
# This allows easy placement of apps within the interior
# bootcamp directory.
-app_path = os.path.abspath(os.path.join(
- os.path.dirname(os.path.abspath(__file__)), os.pardir))
-sys.path.append(os.path.join(app_path, 'bootcamp'))
+app_path = os.path.abspath(
+ os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir)
+)
+sys.path.append(os.path.join(app_path, "bootcamp"))
-if os.environ.get('DJANGO_SETTINGS_MODULE') == 'config.settings.production':
+if os.environ.get("DJANGO_SETTINGS_MODULE") == "config.settings.production":
from raven.contrib.django.raven_compat.middleware.wsgi import Sentry
# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
@@ -37,7 +38,7 @@
# file. This includes Django's development server, if the WSGI_APPLICATION
# setting points here.
application = get_wsgi_application()
-if os.environ.get('DJANGO_SETTINGS_MODULE') == 'config.settings.production':
+if os.environ.get("DJANGO_SETTINGS_MODULE") == "config.settings.production":
application = Sentry(application)
# Apply WSGI middleware here.
# from helloworld.wsgi import HelloWorldApplication
diff --git a/dev.yml b/dev.yml
index c64cbefab..4d00f8197 100755
--- a/dev.yml
+++ b/dev.yml
@@ -6,7 +6,7 @@ volumes:
services:
postgres:
- image: postgres:10
+ image: postgres:11-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
env_file: .env
@@ -14,6 +14,6 @@ services:
- '5432:5432'
redis:
- image: redis:4.0
+ image: redis:5.0
ports:
- '6379:6379'
diff --git a/local.yml b/local.yml
index dc66f7e93..9e7e710c2 100755
--- a/local.yml
+++ b/local.yml
@@ -29,6 +29,6 @@ services:
env_file: .env
redis:
- image: redis:4.0
+ image: redis:5.0
ports:
- '6379:6379'
diff --git a/manage.py b/manage.py
index 06ab82d75..4bc215c33 100755
--- a/manage.py
+++ b/manage.py
@@ -1,8 +1,8 @@
import os
import sys
-if __name__ == '__main__':
- os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.local')
+if __name__ == "__main__":
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
try:
from django.core.management import execute_from_command_line
@@ -25,6 +25,6 @@
# This allows easy placement of apps within the inner bootcamp directory.
current_path = os.path.dirname(os.path.abspath(__file__))
- sys.path.append(os.path.join(current_path, 'bootcamp'))
+ sys.path.append(os.path.join(current_path, "bootcamp"))
execute_from_command_line(sys.argv)
diff --git a/production.yml b/production.yml
index aa68ff9de..491f7cb75 100755
--- a/production.yml
+++ b/production.yml
@@ -48,4 +48,4 @@ services:
- "0.0.0.0:443:443"
redis:
- image: redis:4.0
+ image: redis:5.0
diff --git a/requirements/base.txt b/requirements/base.txt
index bc90885e1..a106ca342 100755
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -1,30 +1,33 @@
# Python
# ------------------------------------------------------------------------------
-argon2-cffi>=18.1.0 # https://github.com/hynek/argon2_cffi
-awesome-slugify>=1.6.5 # https://github.com/dimka665/awesome-slugify
-Pillow>=5.2.0 # https://github.com/python-pillow/Pillow
-psycopg2>=2.7.5 --no-binary psycopg2 # https://github.com/psycopg/psycopg2
-pytz>=2018.5 # https://github.com/stub42/pytz
-redis>=2.10.5 # https://github.com/antirez/redis
-whitenoise>=3.3.1 # https://github.com/evansd/whitenoise
+argon2-cffi # https://github.com/hynek/argon2_cffi
+python-slugify # https://github.com/un33k/python-slugify
+Pillow # https://github.com/python-pillow/Pillow
+pytz # https://github.com/stub42/pytz
+redis # https://github.com/antirez/redis
+whitenoise # https://github.com/evansd/whitenoise
# Django
# ------------------------------------------------------------------------------
-django>=2.0.7 # pyup: < 2.1 # https://www.djangoproject.com/
-django-allauth>=0.35.0 # https://github.com/pennersr/django-allauth
-django-contrib-comments>=1.8.0 # https://github.com/django/django-contrib-comments
-django-crispy-forms>=1.7.2 # https://github.com/django-crispy-forms/django-crispy-forms
-django-environ>=0.4.4 # https://github.com/joke2k/django-environ
-django-markdownx>=2.0.23 # https://github.com/neutronX/django-markdownx
-django-redis>=4.9.0 # https://github.com/niwinz/django-redis
-django-taggit>=0.22.2 # https://github.com/alex/django-taggit
-sorl-thumbnail>=12.4.1 # https://github.com/jazzband/sorl-thumbnail
+django # pyup: < 2.1 # https://www.djangoproject.com/
+django-allauth # https://github.com/pennersr/django-allauth
+django-contrib-comments # https://github.com/django/django-contrib-comments
+django-crispy-forms # https://github.com/django-crispy-forms/django-crispy-forms
+django-environ # https://github.com/joke2k/django-environ
+django-markdownx # https://github.com/neutronX/django-markdownx
+django-redis # https://github.com/niwinz/django-redis
+django-taggit # https://github.com/alex/django-taggit
+sorl-thumbnail # https://github.com/jazzband/sorl-thumbnail
# Channels
# ------------------------------------------------------------------------------
-channels>=2.1.1 # https://github.com/django/channels
-channels-redis>=2.2.1 # https://github.com/django/channels_redis
+channels # https://github.com/django/channels
+channels-redis # https://github.com/django/channels_redis
# GraphQL API
# ------------------------------------------------------------------------------
-graphene-django>=2.0 # https://github.com/graphql-python/graphene-django
+graphene-django # https://github.com/graphql-python/graphene-django
+
+# Open Graph Protocol
+# ------------------------------------------------------------------------------
+beautifulsoup4 # https://www.crummy.com/software/BeautifulSoup/
diff --git a/requirements/local.txt b/requirements/local.txt
index e5b864716..bd322ceb2 100755
--- a/requirements/local.txt
+++ b/requirements/local.txt
@@ -1,27 +1,29 @@
-r ./base.txt
-ipdb>=0.11 # https://github.com/gotcha/ipdb
-Sphinx>=1.7.1 # https://github.com/sphinx-doc/sphinx
-Werkzeug>=0.14.1 # https://github.com/pallets/werkzeug
+ipdb # https://github.com/gotcha/ipdb
+Werkzeug # https://github.com/pallets/werkzeug
+psycopg2-binary # https://github.com/psycopg/psycopg2
# Testing
# ------------------------------------------------------------------------------
-pytest>=3.4.2 # https://github.com/pytest-dev/pytest
-pytest-cov>=2.5.1 # https://github.com/pytest-dev/pytest-cov
-pytest-sugar>=0.9.1 # https://github.com/Frozenball/pytest-sugar
+pytest # https://github.com/pytest-dev/pytest
+pytest-cov # https://github.com/pytest-dev/pytest-cov
+pytest-sugar # https://github.com/Frozenball/pytest-sugar
+pytest-django # https://github.com/pytest-dev/pytest-django
# Code quality
# ------------------------------------------------------------------------------
-coverage>=4.5.1 # https://github.com/nedbat/coveragepy
-flake8>=3.5.0 # https://github.com/PyCQA/flake8
-pylint-common>=0.2.5 # https://github.com/landscapeio/pylint-common
-pylint-django>=0.9.0 # https://github.com/PyCQA/pylint-django
-pylint>=1.8 # https://github.com/PyCQA/pylint
+coverage # https://github.com/nedbat/coveragepy
+bandit # https://github.com/PyCQA/bandit
+pylint-common # https://github.com/landscapeio/pylint-common
+pylint-django # https://github.com/PyCQA/pylint-django
+pylint # https://github.com/PyCQA/pylint
+black # https://github.com/psf/black
+flake8 # https://github.com/pycqa/flake8
# Django
# ------------------------------------------------------------------------------
-django-coverage-plugin>=1.5.0 # https://github.com/nedbat/django_coverage_plugin
-django-debug-toolbar>=1.9.1 # https://github.com/jazzband/django-debug-toolbar
-django-extensions>=2.0.5 # https://github.com/django-extensions/django-extensions
-django-test-plus>=1.0.22 # https://github.com/revsys/django-test-plus
-pytest-django>=3.1.2 # https://github.com/pytest-dev/pytest-django
+django-coverage-plugin # https://github.com/nedbat/django_coverage_plugin
+django-debug-toolbar # https://github.com/jazzband/django-debug-toolbar
+django-extensions # https://github.com/django-extensions/django-extensions
+django-test-plus # https://github.com/revsys/django-test-plus
diff --git a/requirements/production.txt b/requirements/production.txt
index 53acf8c37..174bef27b 100755
--- a/requirements/production.txt
+++ b/requirements/production.txt
@@ -2,11 +2,12 @@
-r base.txt
-boto3>=1.6.2 # pyup: update minor # https://github.com/boto/boto3
-gevent>=1.2.2 # https://github.com/gevent/gevent
-raven>=6.6.0 # https://github.com/getsentry/raven-python
+boto3 # pyup: update minor # https://github.com/boto/boto3
+gevent # https://github.com/gevent/gevent
+raven # https://github.com/getsentry/raven-python
+psycopg2 --no-binary psycopg2 # https://github.com/psycopg/psycopg2
# Django
# ------------------------------------------------------------------------------
-django-anymail>=2.0 # https://github.com/anymail/django-anymail
-django-storages>=1.6.5 # https://github.com/jschneier/django-storages
+django-anymail # https://github.com/anymail/django-anymail
+django-storages # https://github.com/jschneier/django-storages
diff --git a/setup.cfg b/setup.cfg
index 6df81d26f..8fce85910 100755
--- a/setup.cfg
+++ b/setup.cfg
@@ -58,7 +58,7 @@ confidence=
# either give multiple identifier separated by comma (,) or put this option
# multiple time. See also the "--disable" option for examples.
disable=all
-enable=import-error,import-self,reimported,wildcard-import,misplaced-future,deprecated-module,unpacking-non-sequence,invalid-all-object,undefined-all-variable,used-before-assignment,cell-var-from-loop,global-variable-undefined,redefine-in-handler,unused-import,unused-wildcard-import,global-variable-not-assigned,undefined-loop-variable,global-statement,global-at-module-level,bad-open-mode,redundant-unittest-assert,boolean-datetimedeprecated-method,anomalous-unicode-escape-in-string,anomalous-backslash-in-string,not-in-loop,continue-in-finally,abstract-class-instantiated,star-needs-assignment-target,duplicate-argument-name,return-in-init,too-many-star-expressions,nonlocal-and-global,return-outside-function,return-arg-in-generator,invalid-star-assignment-target,bad-reversed-sequence,nonexistent-operator,yield-outside-function,init-is-generator,nonlocal-without-binding,lost-exception,assert-on-tuple,dangerous-default-value,duplicate-key,useless-else-on-loopexpression-not-assigned,confusing-with-statement,unnecessary-lambda,pointless-statement,pointless-string-statement,unnecessary-pass,unreachable,eval-used,exec-used,using-constant-test,bad-super-call,missing-super-argument,slots-on-old-class,super-on-old-class,property-on-old-class,not-an-iterable,not-a-mapping,format-needs-mapping,truncated-format-string,missing-format-string-key,mixed-format-string,too-few-format-args,bad-str-strip-call,too-many-format-args,bad-format-character,format-combined-specification,bad-format-string-key,bad-format-string,missing-format-attribute,missing-format-argument-key,unused-format-string-argumentunused-format-string-key,invalid-format-index,bad-indentation,mixed-indentation,unnecessary-semicolon,lowercase-l-suffix,invalid-encoded-data,unpacking-in-except,import-star-module-level,long-suffix,old-octal-literal,old-ne-operator,backtick,old-raise-syntax,metaclass-assignment,next-method-called,dict-iter-method,dict-view-method,indexing-exception,raising-string,using-cmp-argument,cmp-method,coerce-method,delslice-method,getslice-method,hex-method,nonzero-method,t-method,setslice-method,old-division,logging-format-truncated,logging-too-few-args,logging-too-many-args,logging-unsupported-format,logging-format-interpolation,invalid-unary-operand-type,unsupported-binary-operation,not-callable,redundant-keyword-arg,assignment-from-no-return,assignment-from-none,not-context-manager,repeated-keyword,missing-kwoa,no-value-for-parameter,invalid-sequence-index,invalid-slice-index,unexpected-keyword-arg,unsupported-membership-test,unsubscriptable-object,access-member-before-definition,method-hidden,assigning-non-slot,duplicate-bases,inconsistent-mro,inherit-non-class,invalid-slots,invalid-slots-object,no-method-argument,no-self-argument,unexpected-special-method-signature,non-iterator-returned,arguments-differ,signature-differs,bad-staticmethod-argument,non-parent-init-called,bad-except-order,catching-non-exception,bad-exception-context,notimplemented-raised,raising-bad-type,raising-non-exception,misplaced-bare-raise,duplicate-except,broad-except,nonstandard-exception,binary-op-exception,bare-except,not-async-context-manager,yield-inside-async-function
+enable=import-error,import-self,reimported,wildcard-import,misplaced-future,deprecated-module,unpacking-non-sequence,invalid-all-object,undefined-all-variable,used-before-assignment,cell-var-from-loop,global-variable-undefined,redefine-in-handler,unused-import,unused-wildcard-import,global-variable-not-assigned,undefined-loop-variable,global-statement,global-at-module-level,bad-open-mode,redundant-unittest-assert,boolean-datetimedeprecated-method,anomalous-unicode-escape-in-string,anomalous-backslash-in-string,not-in-loop,continue-in-finally,abstract-class-instantiated,star-needs-assignment-target,duplicate-argument-name,return-in-init,too-many-star-expressions,nonlocal-and-global,return-outside-function,return-arg-in-generator,invalid-star-assignment-target,bad-reversed-sequence,nonexistent-operator,yield-outside-function,init-is-generator,nonlocal-without-binding,lost-exception,assert-on-tuple,dangerous-default-value,duplicate-key,useless-else-on-loopexpression-not-assigned,confusing-with-statement,unnecessary-lambda,pointless-statement,pointless-string-statement,unnecessary-pass,unreachable,eval-used,exec-used,using-constant-test,bad-super-call,missing-super-argument,slots-on-old-class,super-on-old-class,property-on-old-class,not-an-iterable,not-a-mapping,format-needs-mapping,truncated-format-string,missing-format-string-key,mixed-format-string,too-few-format-args,bad-str-strip-call,too-many-format-args,bad-format-character,format-combined-specification,bad-format-string-key,bad-format-string,missing-format-attribute,missing-format-argument-key,unused-format-string-argumentunused-format-string-key,invalid-format-index,bad-indentation,mixed-indentation,unnecessary-semicolon,lowercase-l-suffix,invalid-encoded-data,unpacking-in-except,import-star-module-level,long-suffix,old-octal-literal,old-ne-operator,backtick,old-raise-syntax,metaclass-assignment,next-method-called,dict-iter-method,dict-view-method,indexing-exception,raising-string,using-cmp-argument,cmp-method,coerce-method,delslice-method,getslice-method,hex-method,nonzero-method,t-method,setslice-method,old-division,logging-format-truncated,logging-too-few-args,logging-too-many-args,logging-unsupported-format,logging-format-interpolation,invalid-unary-operand-type,unsupported-binary-operation,not-callable,redundant-keyword-arg,assignment-from-no-return,assignment-from-none,not-context-manager,repeated-keyword,missing-kwoa,no-value-for-parameter,invalid-sequence-index,invalid-slice-index,unexpected-keyword-arg,unsupported-membership-test,unsubscriptable-object,access-member-before-definition,method-hidden,assigning-non-slot,duplicate-bases,inconsistent-mro,inherit-non-class,invalid-slots,invalid-slots-object,no-method-argument,no-self-argument,unexpected-special-method-signature,non-iterator-returned,signature-differs,bad-staticmethod-argument,non-parent-init-called,bad-except-order,catching-non-exception,bad-exception-context,notimplemented-raised,raising-bad-type,raising-non-exception,misplaced-bare-raise,duplicate-except,broad-except,nonstandard-exception,binary-op-exception,bare-except,not-async-context-manager,yield-inside-async-function
# Needs investigation:
# abstract-method (might be indicating a bug? probably not though)
# protected-access (requires some refactoring)