├── .github
└── workflows
│ └── bootcamp.yml
├── .gitignore
├── LICENSE
├── README.md
├── bootcamp
├── __init__.py
├── activities
│ ├── __init__.py
│ ├── admin.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ ├── models.py
│ ├── static
│ │ ├── css
│ │ │ └── notifications.css
│ │ └── js
│ │ │ └── notifications.js
│ ├── templates
│ │ └── activities
│ │ │ ├── last_notifications.html
│ │ │ └── notifications.html
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── articles
│ ├── __init__.py
│ ├── admin.py
│ ├── forms.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ ├── models.py
│ ├── static
│ │ ├── css
│ │ │ └── articles.css
│ │ └── js
│ │ │ └── articles.js
│ ├── templates
│ │ └── articles
│ │ │ ├── article.html
│ │ │ ├── articles.html
│ │ │ ├── drafts.html
│ │ │ ├── edit.html
│ │ │ ├── partial_article.html
│ │ │ ├── partial_article_comment.html
│ │ │ ├── partial_article_comments.html
│ │ │ └── write.html
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── asgi.py
├── authentication
│ ├── __init__.py
│ ├── admin.py
│ ├── forms.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ ├── models.py
│ ├── schema.py
│ ├── static
│ │ └── css
│ │ │ └── signup.css
│ ├── templates
│ │ └── auth
│ │ │ └── signup.html
│ ├── tests.py
│ └── views.py
├── core
│ ├── __init__.py
│ ├── forms.py
│ ├── migrations
│ │ └── __init__.py
│ ├── static
│ │ ├── css
│ │ │ ├── cover.css
│ │ │ ├── network.css
│ │ │ └── profile.css
│ │ └── js
│ │ │ └── picture.js
│ ├── templates
│ │ └── core
│ │ │ ├── cover.html
│ │ │ ├── network.html
│ │ │ ├── partial_settings_menu.html
│ │ │ ├── password.html
│ │ │ ├── picture.html
│ │ │ ├── profile.html
│ │ │ └── settings.html
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── feeds
│ ├── __init__.py
│ ├── admin.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ ├── models.py
│ ├── schema.py
│ ├── static
│ │ ├── css
│ │ │ └── feeds.css
│ │ └── js
│ │ │ └── feeds.js
│ ├── templates
│ │ └── feeds
│ │ │ ├── feed.html
│ │ │ ├── feeds.html
│ │ │ ├── partial_feed.html
│ │ │ └── partial_feed_comments.html
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── locale
│ └── zh_Hans
│ │ └── LC_MESSAGES
│ │ └── django.po
├── messenger
│ ├── __init__.py
│ ├── admin.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ ├── models.py
│ ├── static
│ │ ├── css
│ │ │ └── messages.css
│ │ └── js
│ │ │ ├── check_messages.js
│ │ │ ├── messages.js
│ │ │ └── messages.typehead.js
│ ├── templates
│ │ └── messages
│ │ │ ├── base_messages.html
│ │ │ ├── inbox.html
│ │ │ ├── includes
│ │ │ ├── partial_conversations.html
│ │ │ └── partial_message.html
│ │ │ └── new.html
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── middleware.py
├── questions
│ ├── __init__.py
│ ├── admin.py
│ ├── forms.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ ├── models.py
│ ├── static
│ │ ├── css
│ │ │ └── questions.css
│ │ └── js
│ │ │ └── questions.js
│ ├── templates
│ │ └── questions
│ │ │ ├── ask.html
│ │ │ ├── partial_answer.html
│ │ │ ├── partial_question.html
│ │ │ ├── question.html
│ │ │ └── questions.html
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── schema.py
├── search
│ ├── __init__.py
│ ├── static
│ │ ├── css
│ │ │ └── search.css
│ │ └── js
│ │ │ └── search.js
│ ├── templates
│ │ └── search
│ │ │ ├── partial_articles_results.html
│ │ │ ├── partial_feed_results.html
│ │ │ ├── partial_questions_results.html
│ │ │ ├── partial_results_menu.html
│ │ │ ├── partial_users_results.html
│ │ │ ├── results.html
│ │ │ └── search.html
│ ├── tests.py
│ └── views.py
├── settings.py
├── static
│ ├── css
│ │ └── bootcamp.css
│ ├── img
│ │ ├── Jcrop.gif
│ │ ├── favicon.png
│ │ ├── loading.gif
│ │ └── user.png
│ └── js
│ │ ├── bootcamp.js
│ │ └── bootcamp.markdown.js
├── templates
│ ├── 403.html
│ ├── 404.html
│ ├── 500.html
│ ├── base.html
│ ├── markdown_editor.html
│ └── paginator.html
├── urls.py
└── wsgi.py
├── manage.py
└── requirements.txt
/.github/workflows/bootcamp.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v4
10 | - uses: actions/setup-python@v5
11 | with:
12 | python-version: '3.13'
13 | cache: 'pip'
14 | - name: Install dependencies
15 | run: |
16 | pip install -r requirements.txt
17 | pip install codecov ruff
18 | - name: Lint with ruff
19 | run: |
20 | ruff check bootcamp/
21 | - name: Test with codecov
22 | env:
23 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
24 | run: |
25 | coverage run manage.py test
26 | codecov
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 |
27 | # PyInstaller
28 | # Usually these files are written by a python script from a template
29 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
30 | *.manifest
31 | *.spec
32 |
33 | # Installer logs
34 | pip-log.txt
35 | pip-delete-this-directory.txt
36 |
37 | # Unit test / coverage reports
38 | htmlcov/
39 | .tox/
40 | .coverage
41 | .coverage.*
42 | .cache
43 | nosetests.xml
44 | coverage.xml
45 | *,cover
46 | .hypothesis/
47 | .ruff_cache/
48 |
49 | # Translations
50 | *.mo
51 | *.pot
52 |
53 | # Django stuff:
54 | *.log
55 | local_settings.py
56 |
57 | # Sphinx documentation
58 | docs/_build/
59 |
60 | # PyBuilder
61 | target/
62 |
63 | # IPython Notebook
64 | .ipynb_checkpoints
65 |
66 | # pyenv
67 | .python-version
68 |
69 | # celery beat schedule file
70 | celerybeat-schedule
71 |
72 | # dotenv
73 | .env
74 | .env.leave
75 |
76 | # virtualenv
77 | .venv/
78 | venv/
79 |
80 | # Rope project settings
81 | .ropeproject
82 |
83 | # Static files
84 | media/
85 | staticfiles/
86 |
87 | # PyCharm
88 | .idea/
89 |
90 | # Vim
91 | *.swp
92 | *.swa
93 |
94 | # SQLite
95 | db.sqlite3
96 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Vitor Freitas
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Bootcamp an public social network
2 |
3 | [](https://github.com/qulc/bootcamp/actions)
4 | [](https://codecov.io/gh/qulc/bootcamp)
5 |
6 |
7 | ### Technology Stack
8 |
9 | [](https://python.org)
10 | [](https://www.djangoproject.com/)
11 | [](https://github.com/graphql-python/graphene-django)
12 |
13 |
14 | ### Basic Commands
15 |
16 | To run the tests
17 |
18 | ```
19 | $ python manage.py test
20 | ```
21 |
22 | To run the server
23 |
24 | ```
25 | $ python manage.py migrate
26 | $ python manage.py runserver
27 | ```
28 |
29 | To compile i18n messages
30 |
31 | ```
32 | $ python manage.py makemessages -l zh_Hans
33 | $ python manage.py compilemessages -l zh_Hans
34 | ```
35 |
--------------------------------------------------------------------------------
/bootcamp/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qulc/bootcamp/de3b68240df225260f0da159269d4d66dfe1fe87/bootcamp/__init__.py
--------------------------------------------------------------------------------
/bootcamp/activities/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qulc/bootcamp/de3b68240df225260f0da159269d4d66dfe1fe87/bootcamp/activities/__init__.py
--------------------------------------------------------------------------------
/bootcamp/activities/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from .models import Activity, Notification
4 |
5 | admin.site.register(Activity)
6 | admin.site.register(Notification)
7 |
--------------------------------------------------------------------------------
/bootcamp/activities/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2 on 2023-04-06 10:01
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14 | ('questions', '0001_initial'),
15 | ('feeds', '0001_initial'),
16 | ('articles', '0001_initial'),
17 | ]
18 |
19 | operations = [
20 | migrations.CreateModel(
21 | name='Notification',
22 | fields=[
23 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
24 | ('date', models.DateTimeField(auto_now_add=True)),
25 | ('notification_type', models.CharField(choices=[('L', 'Liked'), ('C', 'Commented'), ('F', 'Favorited'), ('A', 'Answered'), ('W', 'Accepted Answer'), ('E', 'Edited Article'), ('S', 'Also Commented')], max_length=1)),
26 | ('is_read', models.BooleanField(default=False)),
27 | ('answer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='questions.answer')),
28 | ('article', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='articles.article')),
29 | ('feed', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='feeds.feed')),
30 | ('from_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL)),
31 | ('question', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='questions.question')),
32 | ('to_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL)),
33 | ],
34 | options={
35 | 'verbose_name': 'Notification',
36 | 'verbose_name_plural': 'Notifications',
37 | 'ordering': ('-date',),
38 | },
39 | ),
40 | migrations.CreateModel(
41 | name='Activity',
42 | fields=[
43 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
44 | ('activity_type', models.CharField(choices=[('F', 'Favorite'), ('L', 'Like'), ('U', 'Up Vote'), ('D', 'Down Vote')], max_length=1)),
45 | ('date', models.DateTimeField(auto_now_add=True)),
46 | ('feed', models.IntegerField(blank=True, null=True)),
47 | ('question', models.IntegerField(blank=True, null=True)),
48 | ('answer', models.IntegerField(blank=True, null=True)),
49 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
50 | ],
51 | options={
52 | 'verbose_name': 'Activity',
53 | 'verbose_name_plural': 'Activities',
54 | },
55 | ),
56 | ]
57 |
--------------------------------------------------------------------------------
/bootcamp/activities/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qulc/bootcamp/de3b68240df225260f0da159269d4d66dfe1fe87/bootcamp/activities/migrations/__init__.py
--------------------------------------------------------------------------------
/bootcamp/activities/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.contrib.auth.models import User
3 | from django.utils.html import escape
4 | from django.utils.translation import gettext_lazy as _
5 |
6 |
7 | class Activity(models.Model):
8 | FAVORITE = 'F'
9 | LIKE = 'L'
10 | UP_VOTE = 'U'
11 | DOWN_VOTE = 'D'
12 | ACTIVITY_TYPES = (
13 | (FAVORITE, 'Favorite'),
14 | (LIKE, 'Like'),
15 | (UP_VOTE, 'Up Vote'),
16 | (DOWN_VOTE, 'Down Vote'),
17 | )
18 |
19 | user = models.ForeignKey(User, on_delete=models.CASCADE)
20 | activity_type = models.CharField(max_length=1, choices=ACTIVITY_TYPES)
21 | date = models.DateTimeField(auto_now_add=True)
22 | feed = models.IntegerField(null=True, blank=True)
23 | question = models.IntegerField(null=True, blank=True)
24 | answer = models.IntegerField(null=True, blank=True)
25 |
26 | class Meta:
27 | verbose_name = 'Activity'
28 | verbose_name_plural = 'Activities'
29 |
30 | def __str__(self):
31 | return self.activity_type
32 |
33 |
34 | class Notification(models.Model):
35 | LIKED = 'L'
36 | COMMENTED = 'C'
37 | FAVORITED = 'F'
38 | ANSWERED = 'A'
39 | ACCEPTED_ANSWER = 'W'
40 | EDITED_ARTICLE = 'E'
41 | ALSO_COMMENTED = 'S'
42 |
43 | NOTIFICATION_TYPES = (
44 | (LIKED, 'Liked'),
45 | (COMMENTED, 'Commented'),
46 | (FAVORITED, 'Favorited'),
47 | (ANSWERED, 'Answered'),
48 | (ACCEPTED_ANSWER, 'Accepted Answer'),
49 | (EDITED_ARTICLE, 'Edited Article'),
50 | (ALSO_COMMENTED, 'Also Commented'),
51 | )
52 |
53 | _LIKED_TEMPLATE = (
54 | '{1} %s {3}'
55 | ) % _('liked your post:')
56 | _COMMENTED_TEMPLATE = (
57 | '{1} %s {3}'
58 | ) % _('commented on your post:')
59 | _FAVORITED_TEMPLATE = (
60 | '{1} %s {3}'
61 | ) % _('favorited your question:')
62 | _ANSWERED_TEMPLATE = (
63 | '{1} %s {3}'
64 | ) % _('answered your question:')
65 | _ACCEPTED_ANSWER_TEMPLATE = (
66 | '{1} %s {3}'
67 | ) % _('accepted your answer: ')
68 | _EDITED_ARTICLE_TEMPLATE = (
69 | '{1} %s {3}'
70 | ) % _('edited your article:')
71 | _ALSO_COMMENTED_TEMPLATE = (
72 | '{1} %s {3}'
73 | ) % _('also commentend on the post:')
74 |
75 | from_user = models.ForeignKey(User, related_name='+', on_delete=models.CASCADE)
76 | to_user = models.ForeignKey(User, related_name='+', on_delete=models.CASCADE)
77 | date = models.DateTimeField(auto_now_add=True)
78 | feed = models.ForeignKey('feeds.Feed', null=True, blank=True, on_delete=models.CASCADE)
79 | question = models.ForeignKey('questions.Question', null=True, blank=True, on_delete=models.CASCADE)
80 | answer = models.ForeignKey('questions.Answer', null=True, blank=True, on_delete=models.CASCADE)
81 | article = models.ForeignKey('articles.Article', null=True, blank=True, on_delete=models.CASCADE)
82 | notification_type = models.CharField(max_length=1,
83 | choices=NOTIFICATION_TYPES)
84 | is_read = models.BooleanField(default=False)
85 |
86 | class Meta:
87 | verbose_name = 'Notification'
88 | verbose_name_plural = 'Notifications'
89 | ordering = ('-date',)
90 |
91 | def __str__(self):
92 | if self.notification_type == self.LIKED:
93 | return self._LIKED_TEMPLATE.format(
94 | escape(self.from_user.username),
95 | escape(self.from_user.profile.get_screen_name()),
96 | self.feed.pk,
97 | escape(self.get_summary(self.feed.post))
98 | )
99 | elif self.notification_type == self.COMMENTED:
100 | return self._COMMENTED_TEMPLATE.format(
101 | escape(self.from_user.username),
102 | escape(self.from_user.profile.get_screen_name()),
103 | self.feed.pk,
104 | escape(self.get_summary(self.feed.post))
105 | )
106 | elif self.notification_type == self.FAVORITED:
107 | return self._FAVORITED_TEMPLATE.format(
108 | escape(self.from_user.username),
109 | escape(self.from_user.profile.get_screen_name()),
110 | self.question.pk,
111 | escape(self.get_summary(self.question.title))
112 | )
113 | elif self.notification_type == self.ANSWERED:
114 | return self._ANSWERED_TEMPLATE.format(
115 | escape(self.from_user.username),
116 | escape(self.from_user.profile.get_screen_name()),
117 | self.question.pk,
118 | escape(self.get_summary(self.question.title))
119 | )
120 | elif self.notification_type == self.ACCEPTED_ANSWER:
121 | return self._ACCEPTED_ANSWER_TEMPLATE.format(
122 | escape(self.from_user.username),
123 | escape(self.from_user.profile.get_screen_name()),
124 | self.answer.question.pk,
125 | escape(self.get_summary(self.answer.description))
126 | )
127 | elif self.notification_type == self.EDITED_ARTICLE:
128 | return self._EDITED_ARTICLE_TEMPLATE.format(
129 | escape(self.from_user.username),
130 | escape(self.from_user.profile.get_screen_name()),
131 | self.article.slug,
132 | escape(self.get_summary(self.article.title))
133 | )
134 | elif self.notification_type == self.ALSO_COMMENTED:
135 | return self._ALSO_COMMENTED_TEMPLATE.format(
136 | escape(self.from_user.username),
137 | escape(self.from_user.profile.get_screen_name()),
138 | self.feed.pk,
139 | escape(self.get_summary(self.feed.post))
140 | )
141 |
142 | return 'Ooops! Something went wrong.'
143 |
144 | def get_summary(self, value):
145 | summary_size = 50
146 | if len(value) > summary_size:
147 | return f'{value[:summary_size]}...'
148 |
149 | return value
150 |
--------------------------------------------------------------------------------
/bootcamp/activities/static/css/notifications.css:
--------------------------------------------------------------------------------
1 | ul.all-notifications {
2 | padding: 0;
3 | margin-top: 1em;
4 | }
5 |
6 | ul.all-notifications li {
7 | list-style: none;
8 | border-bottom: 1px solid #eeeeee;
9 | padding: .8em 0;
10 | }
11 |
12 | ul.all-notifications li:last-child {
13 | border-bottom: none;
14 | }
15 |
16 | ul.all-notifications li small {
17 | color: #cccccc;
18 | font-size: .8em;
19 | }
20 |
21 | ul.all-notifications li p {
22 | margin: 0;
23 | }
24 |
25 | ul.all-notifications li div {
26 | margin-left: 40px;
27 | padding-left: 1em;
28 | }
29 |
30 | ul.all-notifications .user-picture {
31 | width: 40px;
32 | float: left;
33 | }
--------------------------------------------------------------------------------
/bootcamp/activities/static/js/notifications.js:
--------------------------------------------------------------------------------
1 | $(function () {
2 | $('#notifications').popover({html: true, content: 'Loading...', trigger: 'manual'});
3 |
4 | $("#notifications").click(function () {
5 | if ($(".popover").is(":visible")) {
6 | $("#notifications").popover('hide');
7 | }
8 | else {
9 | $("#notifications").popover('show');
10 | $.ajax({
11 | url: '/notifications/last/',
12 | beforeSend: function () {
13 | $(".popover-content").html("

");
14 | $("#notifications").removeClass("new-notifications");
15 | },
16 | success: function (data) {
17 | $(".popover-content").html(data);
18 | }
19 | });
20 | }
21 | return false;
22 | });
23 |
24 | function check_notifications() {
25 | $.ajax({
26 | url: '/notifications/check/',
27 | cache: false,
28 | success: function (data) {
29 | if (data != "0") {
30 | $("#notifications").addClass("new-notifications");
31 | }
32 | else {
33 | $("#notifications").removeClass("new-notifications");
34 | }
35 | },
36 | complete: function () {
37 | window.setTimeout(check_notifications, 30000);
38 | }
39 | });
40 | };
41 | check_notifications();
42 | });
--------------------------------------------------------------------------------
/bootcamp/activities/templates/activities/last_notifications.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load humanize %}
3 |
4 |
16 |
--------------------------------------------------------------------------------
/bootcamp/activities/templates/activities/notifications.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load i18n %}
3 | {% load static %}
4 |
5 | {% load humanize %}
6 |
7 | {% block title %} {% trans 'Notifications' %} {% endblock %}
8 |
9 | {% block head %}
10 |
11 | {% endblock head %}
12 |
13 | {% block main %}
14 |
17 |
18 | {% for notification in notifications %}
19 | -
20 |
21 |
22 |
{{ notification.date|naturaltime }}
23 |
{{ notification|safe }}
24 |
25 |
26 | {% empty %}
27 | - {% trans 'You have no notification' %}
28 | {% endfor %}
29 |
30 | {% endblock main %}
31 |
--------------------------------------------------------------------------------
/bootcamp/activities/tests.py:
--------------------------------------------------------------------------------
1 | from http import HTTPStatus
2 |
3 | from django.test import TestCase, Client
4 | from django.contrib.auth.models import User
5 |
6 | from .models import Activity, Notification
7 | from bootcamp.feeds.models import Feed
8 |
9 |
10 | class NotificationMethodTests(TestCase):
11 | def setUp(self):
12 | from_user = User.objects.create_user(
13 | username='test_from_user',
14 | email='test@test.com',
15 | password='password'
16 | )
17 | to_user = User.objects.create_user(
18 | username='test_to_user',
19 | email='test_to@test.com',
20 | password='password'
21 | )
22 | feed = Feed.objects.create(user=from_user, post='test feed post')
23 | Activity.objects.create(user=to_user, activity_type=Activity.LIKE)
24 |
25 | self.notification = Notification.objects.create(
26 | notification_type=Notification.LIKED,
27 | from_user=from_user,
28 | to_user=to_user,
29 | feed=feed
30 | )
31 |
32 | def test_notification_str(self):
33 | str(self.notification)
34 |
35 |
36 | class ActivitiesViewsTests(TestCase):
37 | def setUp(self):
38 | self.client = Client(HTTP_X_REQUESTED_WITH='XMLHttpRequest')
39 | User.objects.create_user(
40 | username='test_user',
41 | email='lennon@thebeatles.com',
42 | password='test_password'
43 | )
44 | self.client.login(username='test_user', password='test_password')
45 |
46 | def test_notifications(self):
47 | response = self.client.get('/notifications/')
48 | self.assertEqual(response.status_code, HTTPStatus.OK)
49 |
50 | def test_ajax_notifications(self):
51 | response = self.client.get('/notifications/last/')
52 | self.assertEqual(response.status_code, HTTPStatus.OK)
53 |
54 | response = self.client.get('/notifications/check/')
55 | self.assertEqual(response.status_code, HTTPStatus.OK)
56 |
--------------------------------------------------------------------------------
/bootcamp/activities/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from .import views
4 |
5 | urlpatterns = [
6 | path('', views.notifications, name='notifications'),
7 | path('last/', views.last_notifications, name='last_notifications'),
8 | path('check/', views.check_notifications, name='check_notifications'),
9 | ]
10 |
--------------------------------------------------------------------------------
/bootcamp/activities/views.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render
2 | from django.http import HttpResponse
3 | from django.contrib.auth.decorators import login_required
4 |
5 | from .models import Notification
6 |
7 |
8 | @login_required
9 | def notifications(request):
10 | user = request.user
11 | all_notifications = Notification.objects.filter(to_user=user)
12 | all_notifications.update(is_read=True)
13 |
14 | context = {'notifications': all_notifications}
15 | return render(request, 'activities/notifications.html', context)
16 |
17 |
18 | @login_required
19 | def last_notifications(request):
20 | user = request.user
21 | last_unread_notifications = Notification.objects.filter(
22 | to_user=user, is_read=False)[:5]
23 |
24 | for unread_notification in last_unread_notifications:
25 | unread_notification.is_read = True
26 | unread_notification.save()
27 |
28 | context = {'notifications': last_unread_notifications}
29 | return render(request, 'activities/last_notifications.html', context)
30 |
31 |
32 | @login_required
33 | def check_notifications(request):
34 | user = request.user
35 | notifications_count = Notification.objects.filter(
36 | to_user=user, is_read=False).count()
37 | return HttpResponse(notifications_count)
38 |
--------------------------------------------------------------------------------
/bootcamp/articles/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qulc/bootcamp/de3b68240df225260f0da159269d4d66dfe1fe87/bootcamp/articles/__init__.py
--------------------------------------------------------------------------------
/bootcamp/articles/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from .models import Article, Tag, ArticleComment
4 |
5 | admin.site.register(Article)
6 | admin.site.register(Tag)
7 | admin.site.register(ArticleComment)
8 |
--------------------------------------------------------------------------------
/bootcamp/articles/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.utils.translation import gettext_lazy as _
3 |
4 | from .models import Article
5 |
6 |
7 | class ArticleForm(forms.ModelForm):
8 | status = forms.CharField(widget=forms.HiddenInput())
9 | title = forms.CharField(
10 | widget=forms.TextInput(attrs={'class': 'form-control'}),
11 | label=_('Title'),
12 | max_length=255)
13 | content = forms.CharField(
14 | widget=forms.Textarea(attrs={'class': 'form-control'}),
15 | max_length=4000,
16 | label=_('Content'),
17 | help_text=" ")
18 | tags = forms.CharField(
19 | widget=forms.TextInput(attrs={'class': 'form-control'}),
20 | max_length=255,
21 | required=False,
22 | label=_('Tags'),
23 | help_text=_(
24 | 'Use spaces to separate the tags, such as "Java Linux Python"'))
25 |
26 | class Meta:
27 | model = Article
28 | fields = ['title', 'content', 'tags', 'status']
29 |
--------------------------------------------------------------------------------
/bootcamp/articles/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2 on 2023-04-06 10:01
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='Article',
19 | fields=[
20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21 | ('title', models.CharField(max_length=255)),
22 | ('slug', models.SlugField(blank=True, max_length=255, null=True)),
23 | ('content', models.TextField(max_length=4000)),
24 | ('status', models.CharField(choices=[('D', 'Draft'), ('P', 'Published')], default='D', max_length=1)),
25 | ('create_date', models.DateTimeField(auto_now_add=True)),
26 | ('update_date', models.DateTimeField(blank=True, null=True)),
27 | ('create_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
28 | ('update_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL)),
29 | ],
30 | options={
31 | 'verbose_name': 'Article',
32 | 'verbose_name_plural': 'Articles',
33 | 'ordering': ('-create_date',),
34 | },
35 | ),
36 | migrations.CreateModel(
37 | name='Tag',
38 | fields=[
39 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
40 | ('tag', models.CharField(max_length=50)),
41 | ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='articles.article')),
42 | ],
43 | options={
44 | 'verbose_name': 'Tag',
45 | 'verbose_name_plural': 'Tags',
46 | },
47 | ),
48 | migrations.CreateModel(
49 | name='ArticleComment',
50 | fields=[
51 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
52 | ('comment', models.CharField(max_length=500)),
53 | ('date', models.DateTimeField(auto_now_add=True)),
54 | ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='articles.article')),
55 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
56 | ],
57 | options={
58 | 'verbose_name': 'Article Comment',
59 | 'verbose_name_plural': 'Article Comments',
60 | 'ordering': ('date',),
61 | },
62 | ),
63 | migrations.AddConstraint(
64 | model_name='tag',
65 | constraint=models.UniqueConstraint(fields=('tag', 'article'), name='unique_article_tag'),
66 | ),
67 | ]
68 |
--------------------------------------------------------------------------------
/bootcamp/articles/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qulc/bootcamp/de3b68240df225260f0da159269d4d66dfe1fe87/bootcamp/articles/migrations/__init__.py
--------------------------------------------------------------------------------
/bootcamp/articles/models.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from django.db import models
4 | from django.contrib.auth.models import User
5 | from django.template.defaultfilters import slugify
6 | from django.utils.translation import gettext_lazy as _
7 |
8 |
9 | class Article(models.Model):
10 | DRAFT = 'D'
11 | PUBLISHED = 'P'
12 | STATUS = (
13 | (DRAFT, 'Draft'),
14 | (PUBLISHED, 'Published'),
15 | )
16 |
17 | title = models.CharField(max_length=255)
18 | slug = models.SlugField(max_length=255, null=True, blank=True)
19 | content = models.TextField(max_length=4000)
20 | status = models.CharField(max_length=1, choices=STATUS, default=DRAFT)
21 | create_user = models.ForeignKey(User, on_delete=models.CASCADE)
22 | create_date = models.DateTimeField(auto_now_add=True)
23 | update_date = models.DateTimeField(blank=True, null=True)
24 | update_user = models.ForeignKey(User, null=True, blank=True,
25 | related_name="+", on_delete=models.CASCADE)
26 |
27 | class Meta:
28 | verbose_name = _("Article")
29 | verbose_name_plural = _("Articles")
30 | ordering = ("-create_date",)
31 |
32 | def __str__(self):
33 | return self.title
34 |
35 | def save(self, *args, **kwargs):
36 | if not self.pk:
37 | super(Article, self).save(*args, **kwargs)
38 | else:
39 | self.update_date = datetime.now()
40 | if not self.slug:
41 | slug_str = "%s %s" % (self.pk, self.title.lower())
42 | self.slug = slugify(slug_str)
43 | super(Article, self).save(*args, **kwargs)
44 |
45 | def get_content_as_markdown(self):
46 | return self.content
47 |
48 | @staticmethod
49 | def get_published():
50 | articles = Article.objects.filter(status=Article.PUBLISHED)
51 | return articles
52 |
53 | def create_tags(self, tags):
54 | tags = tags.strip()
55 | tag_list = tags.split(' ')
56 | for tag in tag_list:
57 | if tag:
58 | t, created = Tag.objects.get_or_create(tag=tag.lower(),
59 | article=self)
60 |
61 | def get_tags(self):
62 | return Tag.objects.filter(article=self)
63 |
64 | def get_summary(self):
65 | if len(self.content) > 255:
66 | return f'{self.content[:255]}...'
67 | else:
68 | return self.content
69 |
70 | def get_summary_as_markdown(self):
71 | return self.get_summary()
72 |
73 | def get_comments(self):
74 | return ArticleComment.objects.filter(article=self)
75 |
76 |
77 | class Tag(models.Model):
78 | tag = models.CharField(max_length=50)
79 | article = models.ForeignKey(Article, on_delete=models.CASCADE)
80 |
81 | class Meta:
82 | verbose_name = _('Tag')
83 | verbose_name_plural = _('Tags')
84 |
85 | constraints = [
86 | models.UniqueConstraint(fields=('tag', 'article'), name='unique_article_tag')
87 | ]
88 |
89 | def __str__(self):
90 | return self.tag
91 |
92 | @staticmethod
93 | def get_popular_tags():
94 | count = {}
95 | for tag in Tag.objects.all():
96 | if tag.article.status != Article.PUBLISHED:
97 | continue
98 | if tag.tag in count:
99 | count[tag.tag] += 1
100 | else:
101 | count[tag.tag] = 1
102 |
103 | sorted_count = sorted(list(count.items()), key=lambda t: t[1],
104 | reverse=True)
105 | return sorted_count[:20]
106 |
107 |
108 | class ArticleComment(models.Model):
109 | article = models.ForeignKey(Article, on_delete=models.CASCADE)
110 | comment = models.CharField(max_length=500)
111 | date = models.DateTimeField(auto_now_add=True)
112 | user = models.ForeignKey(User, on_delete=models.CASCADE)
113 |
114 | class Meta:
115 | verbose_name = _("Article Comment")
116 | verbose_name_plural = _("Article Comments")
117 | ordering = ("date",)
118 |
119 | def __str__(self):
120 | return f'{self.user.username} - {self.article.title}'
121 |
--------------------------------------------------------------------------------
/bootcamp/articles/static/css/articles.css:
--------------------------------------------------------------------------------
1 | .articles article {
2 | padding-bottom: 1.4em;
3 | border-bottom: 1px solid #eeeeee;
4 | margin-top: 2.4em;
5 | }
6 |
7 | .articles article p {
8 | font-size: 1.2em;
9 | }
10 |
11 | .articles article:last-child {
12 | border-bottom: none;
13 | }
14 |
15 | .info {
16 | margin-bottom: .5em;
17 | color: #a0a0a0;
18 | }
19 |
20 | .info a {
21 | color: #a0a0a0;
22 | }
23 |
24 | .info > span {
25 | margin-right: 1em;
26 | }
27 |
28 | .info .user img {
29 | width: 20px;
30 | border-radius: 3px;
31 | }
32 |
33 | article .tags {
34 | font-size: 1em;
35 | }
36 |
37 | .popular-tags h4 {
38 | margin-top: 2em;
39 | }
40 |
41 | .popular-tags .label {
42 | margin-top: .4em;
43 | font-size: .8em;
44 | line-height: 2;
45 | }
46 |
47 | .popular-tags a:hover, .tags a:hover {
48 | text-decoration: none;
49 | }
50 |
51 | .user-portrait img {
52 | border-radius: 4px;
53 | width: 34px;
54 | }
55 |
56 | .user-portrait {
57 | width: 40px;
58 | float: left;
59 | }
60 |
61 | .comment-input {
62 | float: left;
63 | width: -moz-calc(100% - 40px);
64 | width: -webkit-calc(100% - 40px);
65 | width: calc(100% - 40px);
66 | }
67 |
68 | .post-comment {
69 | margin-bottom: 1em;
70 | }
71 |
72 | .comment-portrait {
73 | width: 40px;
74 | border-radius: 5px;
75 | float: left;
76 | }
77 |
78 | .comment-text {
79 | margin-left: 50px;
80 | }
81 |
82 | .comment-text h5 {
83 | padding-top: .1em;
84 | margin: .2em 0;
85 | }
86 |
87 | .comment-text h5 small {
88 | margin-left: .6em;
89 | }
90 |
91 | .comment-text p {
92 | margin: 0;
93 | font-size: .9em;
94 | }
95 |
96 | .comment {
97 | border-bottom: 1px solid #e3e3e3;
98 | padding: .8em 0;
99 | }
100 |
101 | .comment:last-child {
102 | border-bottom: none;
103 | }
--------------------------------------------------------------------------------
/bootcamp/articles/static/js/articles.js:
--------------------------------------------------------------------------------
1 | $(function () {
2 | $(".publish").click(function () {
3 | $("input[name='status']").val("P");
4 | $("form").submit();
5 | });
6 |
7 | $(".draft").click(function () {
8 | $("input[name='status']").val("D");
9 | $("form").submit();
10 | });
11 |
12 | $(".preview").click(function () {
13 | $.ajax({
14 | url: '/articles/preview/',
15 | data: $("form").serialize(),
16 | cache: false,
17 | type: 'post',
18 | beforeSend: function () {
19 | $("#preview .modal-body").html("");
20 | },
21 | success: function (data) {
22 | $("#preview .modal-body").html(data);
23 | }
24 | });
25 | });
26 |
27 | $("#comment").focus(function () {
28 | $(this).attr("rows", "3");
29 | $("#comment-helper").fadeIn();
30 | });
31 |
32 | $("#comment").blur(function () {
33 | $(this).attr("rows", "1");
34 | $("#comment-helper").fadeOut();
35 | });
36 |
37 | $("#comment").keydown(function (evt) {
38 | var keyCode = evt.which?evt.which:evt.keyCode;
39 | if (evt.ctrlKey && (keyCode == 10 || keyCode == 13)) {
40 | $.ajax({
41 | url: '/articles/comment/',
42 | data: $("#comment-form").serialize(),
43 | cache: false,
44 | type: 'post',
45 | success: function (data) {
46 | $("#comment-list").html(data);
47 | var comment_count = $("#comment-list .comment").length;
48 | $(".comment-count").text(comment_count);
49 | $("#comment").val("");
50 | $("#comment").blur();
51 | }
52 | });
53 | }
54 | });
55 |
56 | });
--------------------------------------------------------------------------------
/bootcamp/articles/templates/articles/article.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load i18n %}
3 | {% load static %}
4 |
5 | {% block title %}{{ article.title }}{% endblock %}
6 |
7 | {% block head %}
8 |
9 |
10 | {% endblock head %}
11 |
12 | {% block main %}
13 |
14 | - {% trans 'Articles' %}
15 | - {% trans 'Article' %}
16 |
17 | {% include 'articles/partial_article.html' with article=article %}
18 | {% if not user.is_anonymous %}
19 | {% include 'articles/partial_article_comments.html' with article=article %}
20 | {% endif %}
21 | {% endblock main %}
22 |
--------------------------------------------------------------------------------
/bootcamp/articles/templates/articles/articles.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load i18n %}
3 | {% load static %}
4 |
5 | {% block title %} {% trans 'Articles' %} {% endblock %}
6 |
7 | {% block head %}
8 |
9 | {% endblock head %}
10 |
11 | {% block main %}
12 |
22 |
23 |
24 |
25 | {% for article in articles %}
26 | {% include 'articles/partial_article.html' with article=article %}
27 | {% empty %}
28 |
30 | {% endfor %}
31 |
32 |
33 |
39 |
40 |
41 |
42 | {% include 'paginator.html' with paginator=articles %}
43 |
44 |
45 | {% endblock main %}
46 |
--------------------------------------------------------------------------------
/bootcamp/articles/templates/articles/drafts.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load i18n %}
3 | {% load static %}
4 |
5 | {% block head %}
6 |
7 | {% endblock head %}
8 |
9 | {% block main %}
10 |
11 | - {% trans 'Articles' %}
12 | - {% trans 'Drafts' %}
13 |
14 |
15 |
16 |
17 | {% trans 'Title' %} |
18 | {% trans 'Content' %} |
19 | {% trans 'Tags' %} |
20 |
21 |
22 |
23 | {% for article in drafts %}
24 |
25 | {{ article.title }} |
26 | {{ article.get_summary_as_markdown|safe }} |
27 |
28 | {% for tag in article.get_tags %}
29 | {{ tag }}
30 | {% endfor %}
31 | |
32 |
33 | {% empty %}
34 |
35 |
36 | {% trans 'No draft to display' %}
37 | |
38 |
39 | {% endfor %}
40 |
41 |
42 | {% endblock main %}
43 |
--------------------------------------------------------------------------------
/bootcamp/articles/templates/articles/edit.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load i18n %}
3 | {% load static %}
4 |
5 | {% block head %}
6 |
7 | {% endblock head %}
8 |
9 | {% block main %}
10 |
11 | - {% trans 'Articles' %}
12 | - {% trans 'Drafts' %}
13 | - {% trans 'Edit' %}
14 |
15 |
39 | {% endblock main %}
40 |
--------------------------------------------------------------------------------
/bootcamp/articles/templates/articles/partial_article.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 |
4 |
18 |
19 | {{ article.get_content_as_markdown|safe }}
20 |
21 | {% if article.get_tags %}
22 |
27 | {% endif %}
28 |
29 |
--------------------------------------------------------------------------------
/bootcamp/articles/templates/articles/partial_article_comment.html:
--------------------------------------------------------------------------------
1 | {% load humanize %}
2 |
3 |
--------------------------------------------------------------------------------
/bootcamp/articles/templates/articles/partial_article_comments.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 |
4 | {% trans 'Comments' %}
5 |
17 |
--------------------------------------------------------------------------------
/bootcamp/articles/templates/articles/write.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load i18n %}
3 | {% load static %}
4 |
5 | {% block head %}
6 |
7 | {% endblock head %}
8 |
9 | {% block main %}
10 |
11 | - {% trans 'Articles' %}
12 | - {% trans 'Write Article' %}
13 |
14 |
39 |
40 |
54 |
55 | {% endblock main %}
56 |
--------------------------------------------------------------------------------
/bootcamp/articles/tests.py:
--------------------------------------------------------------------------------
1 | # Create your tests here.
2 |
--------------------------------------------------------------------------------
/bootcamp/articles/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from .import views
4 |
5 | urlpatterns = [
6 | path('', views.articles, name='articles'),
7 | path('write/', views.write, name='write'),
8 | path('preview/', views.preview, name='preview'),
9 | path('drafts/', views.drafts, name='drafts'),
10 | path('comment/', views.comment, name='comment'),
11 | path('tag//', views.tag, name='tag'),
12 | path('edit//', views.edit, name='edit_article'),
13 | path(')/', views.article, name='article'),
14 | ]
15 |
--------------------------------------------------------------------------------
/bootcamp/articles/views.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render, redirect
2 | from django.shortcuts import get_object_or_404
3 | from django.template.loader import render_to_string
4 | from django.utils.translation import gettext_lazy as _
5 | from django.contrib.auth.decorators import login_required
6 | from django.http import HttpResponseBadRequest, HttpResponse
7 | from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
8 |
9 | from bootcamp.articles.forms import ArticleForm
10 | from bootcamp.articles.models import Article, Tag, ArticleComment
11 |
12 |
13 | def _articles(request, articles):
14 | paginator = Paginator(articles, 10)
15 | page = request.GET.get('page')
16 | try:
17 | articles = paginator.page(page)
18 | except PageNotAnInteger:
19 | articles = paginator.page(1)
20 | except EmptyPage:
21 | articles = paginator.page(paginator.num_pages)
22 | popular_tags = Tag.get_popular_tags()
23 | return render(request, 'articles/articles.html',
24 | {'articles': articles, 'popular_tags': popular_tags})
25 |
26 |
27 | def articles(request):
28 | all_articles = Article.get_published()
29 | return _articles(request, all_articles)
30 |
31 |
32 | def article(request, slug):
33 | article = get_object_or_404(Article, slug=slug, status=Article.PUBLISHED)
34 | return render(request, 'articles/article.html', {'article': article})
35 |
36 |
37 | def tag(request, tag_name):
38 | tags = Tag.objects.filter(tag=tag_name)
39 | articles = []
40 | for tag in tags:
41 | if tag.article.status == Article.PUBLISHED:
42 | articles.append(tag.article)
43 | return _articles(request, articles)
44 |
45 |
46 | @login_required
47 | def write(request):
48 | if request.method == 'GET':
49 | form = ArticleForm()
50 | return render(request, 'articles/write.html', {'form': form})
51 |
52 | form = ArticleForm(request.POST)
53 |
54 | if form.is_valid():
55 | article = Article()
56 | article.create_user = request.user
57 | article.title = form.cleaned_data.get('title')
58 | article.content = form.cleaned_data.get('content')
59 | status = form.cleaned_data.get('status')
60 |
61 | if status in [Article.PUBLISHED, Article.DRAFT]:
62 | article.status = form.cleaned_data.get('status')
63 |
64 | article.save()
65 |
66 | tags = form.cleaned_data.get('tags')
67 | article.create_tags(tags)
68 | return redirect('/articles/')
69 |
70 | return render(request, 'articles/write.html', {'form': form})
71 |
72 |
73 | @login_required
74 | def drafts(request):
75 | drafts = Article.objects.filter(
76 | create_user=request.user, status=Article.DRAFT)
77 | return render(request, 'articles/drafts.html', {'drafts': drafts})
78 |
79 |
80 | @login_required
81 | def edit(request, article_id):
82 | tags = ''
83 | if not article_id:
84 | article = get_object_or_404(Article, pk=article_id)
85 |
86 | for tag in article.get_tags():
87 | tags = f'{tags} {tag.tag}'
88 | tags = tags.strip()
89 | else:
90 | article = Article(create_user=request.user)
91 |
92 | if request.POST:
93 | form = ArticleForm(request.POST, instance=article)
94 | if form.is_valid():
95 | form.save()
96 | return redirect('/articles/')
97 | else:
98 | form = ArticleForm(instance=article, initial={'tags': tags})
99 |
100 | return render(request, 'articles/edit.html', {'form': form})
101 |
102 |
103 | @login_required
104 | def preview(request):
105 | if request.method != 'POST':
106 | return HttpResponseBadRequest
107 |
108 | content = request.POST.get('content')
109 | html = _('Nothing to display :(')
110 |
111 | if len(content.strip()) > 0:
112 | html = content
113 |
114 | return HttpResponse(html)
115 |
116 |
117 | @login_required
118 | def comment(request):
119 | if request.method != 'POST':
120 | return HttpResponseBadRequest()
121 |
122 | article_id = request.POST.get('article')
123 | comment = request.POST.get('comment').strip()
124 |
125 | article = Article.objects.get(pk=article_id)
126 |
127 | if len(comment) > 0:
128 | ArticleComment.objects.create(
129 | user=request.user,
130 | article=article,
131 | comment=comment
132 | )
133 |
134 | html = ''
135 | for comment in article.get_comments():
136 | template = render_to_string('articles/partial_article_comment.html',
137 | {'comment': comment})
138 | html = f'{html}{template}'
139 |
140 | return HttpResponse(html)
141 |
--------------------------------------------------------------------------------
/bootcamp/asgi.py:
--------------------------------------------------------------------------------
1 | """
2 | ASGI config for mysite project.
3 |
4 | It exposes the ASGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.asgi import get_asgi_application
13 |
14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'bootcamp.settings')
15 |
16 | application = get_asgi_application()
17 |
--------------------------------------------------------------------------------
/bootcamp/authentication/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qulc/bootcamp/de3b68240df225260f0da159269d4d66dfe1fe87/bootcamp/authentication/__init__.py
--------------------------------------------------------------------------------
/bootcamp/authentication/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from .models import Profile
4 |
5 | admin.site.register(Profile)
6 |
--------------------------------------------------------------------------------
/bootcamp/authentication/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.conf import settings
3 | from django.contrib.auth.models import User
4 | from django.core.exceptions import ValidationError
5 | from django.utils.translation import gettext_lazy as _
6 |
7 |
8 | def signup_domain_validator(value):
9 | if '*' in settings.ALLOWED_SIGNUP_DOMAINS:
10 | return
11 |
12 | domain = value[value.index("@"):]
13 |
14 | if domain not in settings.ALLOWED_SIGNUP_DOMAINS:
15 | allowed_domain = ','.join(settings.ALLOWED_SIGNUP_DOMAINS)
16 | msg = _('Invalid domain. '
17 | 'Allowed domains on this network: {0}').format(allowed_domain)
18 | raise ValidationError(msg)
19 |
20 |
21 | def forbidden_username_validator(value):
22 | forbidden_usernames = {
23 | 'admin', 'settings', 'news', 'about', 'help', 'signin', 'signup',
24 | 'signout', 'terms', 'privacy', 'cookie', 'new', 'login', 'logout',
25 | 'administrator', 'join', 'account', 'username', 'root', 'blog',
26 | 'user', 'users', 'billing', 'subscribe', 'reviews', 'review', 'blog',
27 | 'blogs', 'edit', 'mail', 'email', 'home', 'job', 'jobs', 'contribute',
28 | 'newsletter', 'shop', 'profile', 'register', 'auth', 'authentication',
29 | 'campaign', 'config', 'delete', 'remove', 'forum', 'forums',
30 | 'download', 'downloads', 'contact', 'blogs', 'feed', 'feeds', 'faq',
31 | 'intranet', 'log', 'registration', 'search', 'explore', 'rss',
32 | 'support', 'status', 'static', 'media', 'setting', 'css', 'js',
33 | 'follow', 'activity', 'questions', 'articles', 'network', }
34 | if value.lower() in forbidden_usernames:
35 | raise ValidationError(_('This is a reserved word.'))
36 |
37 |
38 | def invalid_username_validator(value):
39 | if '@' in value or '+' in value or '-' in value:
40 | raise ValidationError(_('Enter a valid username.'))
41 |
42 |
43 | def unique_email_validator(value):
44 | if User.objects.filter(email__iexact=value).exists():
45 | raise ValidationError(_('User with this Email already exists.'))
46 |
47 |
48 | def unique_username_ignore_case_validator(value):
49 | if User.objects.filter(username__iexact=value).exists():
50 | raise ValidationError(_('User with this Username already exists.'))
51 |
52 |
53 | class SignUpForm(forms.ModelForm):
54 | username = forms.CharField(
55 | widget=forms.TextInput(attrs={'class': 'form-control'}),
56 | max_length=30,
57 | required=True,
58 | label=_('Username'),
59 | help_text=_('Usernames may contain alphanumeric, _ and . characters'))
60 | password = forms.CharField(
61 | widget=forms.PasswordInput(attrs={'class': 'form-control'}),
62 | label=_('Password'))
63 | confirm_password = forms.CharField(
64 | widget=forms.PasswordInput(attrs={'class': 'form-control'}),
65 | label=_('Confirm your password'),
66 | required=True)
67 | email = forms.CharField(
68 | widget=forms.EmailInput(attrs={'class': 'form-control'}),
69 | required=True,
70 | max_length=75,
71 | label=_('Email'))
72 |
73 | class Meta:
74 | model = User
75 | exclude = ['last_login', 'date_joined']
76 | fields = ['username', 'email', 'password', 'confirm_password', ]
77 |
78 | def __init__(self, *args, **kwargs):
79 | super(SignUpForm, self).__init__(*args, **kwargs)
80 | self.fields['username'].validators += [
81 | forbidden_username_validator, invalid_username_validator,
82 | unique_username_ignore_case_validator
83 | ]
84 | self.fields['email'].validators += [
85 | unique_email_validator, signup_domain_validator]
86 |
87 | def clean(self):
88 | super(SignUpForm, self).clean()
89 | password = self.cleaned_data.get('password')
90 | confirm_password = self.cleaned_data.get('confirm_password')
91 | if password and password != confirm_password:
92 | self._errors['password'] = self.error_class(
93 | [_('Passwords don\'t match')])
94 | return self.cleaned_data
95 |
--------------------------------------------------------------------------------
/bootcamp/authentication/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2 on 2023-04-06 10:01
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='Profile',
19 | fields=[
20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21 | ('url', models.CharField(blank=True, max_length=50, null=True)),
22 | ('location', models.CharField(blank=True, max_length=50, null=True)),
23 | ('job_title', models.CharField(blank=True, max_length=50, null=True)),
24 | ('picture_url', models.CharField(blank=True, max_length=120, null=True)),
25 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
26 | ],
27 | ),
28 | ]
29 |
--------------------------------------------------------------------------------
/bootcamp/authentication/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qulc/bootcamp/de3b68240df225260f0da159269d4d66dfe1fe87/bootcamp/authentication/migrations/__init__.py
--------------------------------------------------------------------------------
/bootcamp/authentication/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.contrib.auth.models import User
3 | from django.db.models.signals import post_save
4 |
5 | from bootcamp.activities.models import Notification
6 |
7 |
8 | class Profile(models.Model):
9 | user = models.OneToOneField(User, on_delete=models.CASCADE)
10 | url = models.CharField(max_length=50, null=True, blank=True)
11 | location = models.CharField(max_length=50, null=True, blank=True)
12 | job_title = models.CharField(max_length=50, null=True, blank=True)
13 | picture_url = models.CharField(max_length=120, null=True, blank=True)
14 |
15 | def get_url(self):
16 | url = self.url
17 | if not self.url.startswith("http://") \
18 | and not self.url.startswith("https://") \
19 | and len(self.url) > 0:
20 | url = "http://" + str(self.url)
21 | return url
22 |
23 | def get_picture(self):
24 | if not self.picture_url:
25 | no_picture = '/static/img/user.png'
26 | return no_picture
27 |
28 | return self.picture_url
29 |
30 | def get_screen_name(self):
31 | if self.user.get_full_name():
32 | return self.user.get_full_name()
33 |
34 | return self.user.username
35 |
36 | def notify_liked(self, feed):
37 | if self.user != feed.user:
38 | Notification.objects.create(
39 | notification_type=Notification.LIKED,
40 | from_user=self.user,
41 | to_user=feed.user,
42 | feed=feed
43 | )
44 |
45 | def unotify_liked(self, feed):
46 | if self.user != feed.user:
47 | Notification.objects.filter(
48 | notification_type=Notification.LIKED,
49 | from_user=self.user,
50 | to_user=feed.user,
51 | feed=feed
52 | ).delete()
53 |
54 | def notify_commented(self, feed):
55 | if self.user != feed.user:
56 | Notification(
57 | notification_type=Notification.COMMENTED,
58 | from_user=self.user,
59 | to_user=feed.user,
60 | feed=feed
61 | ).save()
62 |
63 | def notify_also_commented(self, feed):
64 | comments = feed.get_comments()
65 | users = set()
66 |
67 | for comment in comments:
68 | if comment.user != self.user and comment.user != feed.user:
69 | users.add(comment.user.pk)
70 |
71 | for user in users:
72 | Notification(
73 | notification_type=Notification.ALSO_COMMENTED,
74 | from_user=self.user,
75 | to_user=User(id=user),
76 | feed=feed
77 | ).save()
78 |
79 | def notify_favorited(self, question):
80 | if self.user != question.user:
81 | Notification(
82 | notification_type=Notification.FAVORITED,
83 | from_user=self.user,
84 | to_user=question.user,
85 | question=question
86 | ).save()
87 |
88 | def unotify_favorited(self, question):
89 | if self.user != question.user:
90 | Notification.objects.filter(
91 | notification_type=Notification.FAVORITED,
92 | from_user=self.user,
93 | to_user=question.user,
94 | question=question
95 | ).delete()
96 |
97 | def notify_answered(self, question):
98 | if self.user != question.user:
99 | Notification.objects.create(
100 | notification_type=Notification.ANSWERED,
101 | from_user=self.user,
102 | to_user=question.user,
103 | question=question
104 | )
105 |
106 | def notify_accepted(self, answer):
107 | if self.user != answer.user:
108 | Notification.objects.create(
109 | notification_type=Notification.ACCEPTED_ANSWER,
110 | from_user=self.user,
111 | to_user=answer.user,
112 | answer=answer
113 | )
114 |
115 | def unotify_accepted(self, answer):
116 | if self.user != answer.user:
117 | Notification.objects.filter(
118 | notification_type=Notification.ACCEPTED_ANSWER,
119 | from_user=self.user,
120 | to_user=answer.user,
121 | answer=answer
122 | ).delete()
123 |
124 |
125 | def create_user_profile(sender, instance, created, **kwargs):
126 | if created:
127 | Profile.objects.create(user=instance)
128 |
129 |
130 | def save_user_profile(sender, instance, **kwargs):
131 | instance.profile.save()
132 |
133 |
134 | post_save.connect(create_user_profile, sender=User)
135 | post_save.connect(save_user_profile, sender=User)
136 |
--------------------------------------------------------------------------------
/bootcamp/authentication/schema.py:
--------------------------------------------------------------------------------
1 | from django.core import signing
2 |
3 | import graphene
4 | from graphql.error import GraphQLError
5 | from django.contrib.auth.models import User
6 | from graphene_django import DjangoObjectType, DjangoConnectionField
7 |
8 | from ..authentication.models import Profile
9 |
10 |
11 | class UserObject(DjangoObjectType):
12 | class Meta:
13 | model = User
14 | interfaces = (graphene.Node,)
15 |
16 |
17 | class ProfileObject(DjangoObjectType):
18 | class Meta:
19 | model = Profile
20 |
21 |
22 | class UserQuery(graphene.ObjectType):
23 | profile = graphene.Field(ProfileObject)
24 |
25 | user = graphene.Node.Field(UserObject)
26 | users = DjangoConnectionField(UserObject)
27 |
28 | def resolve_users(self, info, **kwargs):
29 | return User.objects.all().select_related('profile')
30 |
31 |
32 | class AuthInput(graphene.InputObjectType):
33 | username = graphene.String(required=True)
34 | password = graphene.String(required=True)
35 |
36 |
37 | class CreateToken(graphene.Mutation):
38 | class Arguments:
39 | auth = AuthInput(required=True)
40 |
41 | token = graphene.String(required=True)
42 |
43 | @staticmethod
44 | def mutate(root, info, auth):
45 | user = User.objects.filter(username=auth.username).first()
46 | if not user or not user.check_password(auth.password):
47 | raise GraphQLError('username or password is invalid.')
48 |
49 | return CreateToken(token=signing.dumps(user.id))
50 |
--------------------------------------------------------------------------------
/bootcamp/authentication/static/css/signup.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: #006389;
3 | }
4 |
5 | .logo {
6 | margin: 0;
7 | font-size: 3em;
8 | font-weight: 200;
9 | font-family: "Audiowide", cursive;
10 | text-align: center;
11 | text-shadow: 1px 1px 0 #333333;
12 | line-height: 70px;
13 | }
14 |
15 | .logo a {
16 | color: #92c35e;
17 | }
18 |
19 | .logo a:hover, .logo a:focus {
20 | color: #92c35e;
21 | text-decoration: none;
22 | }
23 |
24 | .signup {
25 | background-color: #f6f6f6;
26 | width: 70%;
27 | border-radius: 7px;
28 | margin: 10px auto 0;
29 | }
30 |
31 | .signup {
32 | padding: 30px 20px;
33 | }
34 |
35 | .signup form {
36 | margin-top: 20px;
37 | }
38 |
39 | .signup h2 {
40 | margin: 0;
41 | }
--------------------------------------------------------------------------------
/bootcamp/authentication/templates/auth/signup.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load i18n %}
3 | {% load static %}
4 |
5 | {% block head %}
6 |
7 | {% endblock head %}
8 |
9 | {% block body %}
10 |
11 |
12 |
13 |
{% trans 'Sign up for Bootcamp' %}
14 |
30 |
31 |
32 | {% endblock body %}
33 |
--------------------------------------------------------------------------------
/bootcamp/authentication/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 | from django.contrib.auth.models import User
3 |
4 |
5 | class ProfileMethodTests(TestCase):
6 | def setUp(self):
7 | user = User.objects.create_user(
8 | username='lichun',
9 | email='i@lichun.me',
10 | password='lichun_password',
11 | )
12 | user.profile.url = 'https://lichun.me/'
13 | user.save()
14 |
15 | def test_get_profile(self):
16 | user = User.objects.get(username='lichun')
17 |
18 | url = user.profile.get_url()
19 | self.assertEqual(url, 'https://lichun.me/')
20 |
21 | picture_url = user.profile.get_picture()
22 | self.assertEqual(picture_url, '/static/img/user.png')
23 |
24 | screen_name = user.profile.get_screen_name()
25 | self.assertEqual(screen_name, 'lichun')
26 |
--------------------------------------------------------------------------------
/bootcamp/authentication/views.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth import authenticate, login
2 | from django.contrib.auth.models import User
3 | from django.shortcuts import render, redirect
4 | from django.utils.translation import gettext_lazy as _
5 |
6 | from bootcamp.feeds.models import Feed
7 | from .forms import SignUpForm
8 |
9 |
10 | def signup(request):
11 | if request.method != 'POST':
12 | return render(request, 'auth/signup.html', {'form': SignUpForm()})
13 |
14 | form = SignUpForm(request.POST)
15 |
16 | if not form.is_valid():
17 | return render(request, 'auth/signup.html', {'form': form})
18 |
19 | email = form.cleaned_data.get('email')
20 | username = form.cleaned_data.get('username')
21 | password = form.cleaned_data.get('password')
22 |
23 | User.objects.create_user(username=username, password=password,
24 | email=email)
25 | user = authenticate(username=username, password=password)
26 | login(request, user)
27 |
28 | welcome_post = _('{0} has joined the network.').format(user.username)
29 | Feed.objects.create(user=user, post=welcome_post)
30 |
31 | return redirect('/')
32 |
--------------------------------------------------------------------------------
/bootcamp/core/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qulc/bootcamp/de3b68240df225260f0da159269d4d66dfe1fe87/bootcamp/core/__init__.py
--------------------------------------------------------------------------------
/bootcamp/core/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.contrib.auth.models import User
3 | from django.utils.translation import gettext_lazy as _
4 |
5 |
6 | class ProfileForm(forms.ModelForm):
7 | first_name = forms.CharField(
8 | widget=forms.TextInput(attrs={'class': 'form-control'}),
9 | max_length=30, label=_('First Name'), required=False)
10 | last_name = forms.CharField(
11 | widget=forms.TextInput(attrs={'class': 'form-control'}),
12 | max_length=30, label=_('Last Name'), required=False)
13 | job_title = forms.CharField(
14 | widget=forms.TextInput(attrs={'class': 'form-control'}),
15 | max_length=50, label=_('Job Title'), required=False)
16 | email = forms.CharField(
17 | widget=forms.TextInput(attrs={'class': 'form-control'}),
18 | max_length=75, label=_('Email'), required=False)
19 | url = forms.CharField(
20 | widget=forms.TextInput(attrs={'class': 'form-control'}),
21 | max_length=50, label=_('Url'), required=False)
22 | location = forms.CharField(
23 | widget=forms.TextInput(attrs={'class': 'form-control'}),
24 | max_length=50, label=_('Location'), required=False)
25 |
26 | class Meta:
27 | model = User
28 | fields = ['first_name', 'last_name', 'job_title', 'email', 'url',
29 | 'location', ]
30 |
31 |
32 | class ChangePasswordForm(forms.ModelForm):
33 | id = forms.CharField(widget=forms.HiddenInput())
34 | old_password = forms.CharField(
35 | widget=forms.PasswordInput(attrs={'class': 'form-control'}),
36 | label=_("Old password"), required=True)
37 | new_password = forms.CharField(
38 | widget=forms.PasswordInput(attrs={'class': 'form-control'}),
39 | label=_("New password"), required=True)
40 | confirm_password = forms.CharField(
41 | widget=forms.PasswordInput(attrs={'class': 'form-control'}),
42 | label=_("Confirm new password"), required=True)
43 |
44 | class Meta:
45 | model = User
46 | fields = ['id', 'old_password', 'new_password', 'confirm_password']
47 |
48 | def clean(self):
49 | super(ChangePasswordForm, self).clean()
50 |
51 | old_password = self.cleaned_data.get('old_password')
52 | new_password = self.cleaned_data.get('new_password')
53 | confirm_password = self.cleaned_data.get('confirm_password')
54 |
55 | user_id = self.cleaned_data.get('id')
56 | user = User.objects.get(pk=user_id)
57 |
58 | if not user.check_password(old_password):
59 | self._errors['old_password'] = self.error_class(
60 | ['Old password don\'t match'])
61 |
62 | if new_password and new_password != confirm_password:
63 | self._errors['new_password'] = self.error_class(
64 | ['Passwords don\'t match'])
65 |
66 | return self.cleaned_data
67 |
68 |
69 | class SavePictureForm(forms.Form):
70 | x = forms.IntegerField(min_value=0)
71 | y = forms.IntegerField(min_value=0)
72 | width = forms.IntegerField(min_value=0)
73 | height = forms.IntegerField(min_value=0)
74 |
--------------------------------------------------------------------------------
/bootcamp/core/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qulc/bootcamp/de3b68240df225260f0da159269d4d66dfe1fe87/bootcamp/core/migrations/__init__.py
--------------------------------------------------------------------------------
/bootcamp/core/static/css/cover.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: #006389;
3 | }
4 |
5 | .cover {
6 | width: 400px;
7 | height: 400px;
8 | position: fixed;
9 | margin-left: -200px;
10 | margin-top: -200px;
11 | top: 50%;
12 | left: 50%;
13 | }
14 |
15 | .logo {
16 | margin: 0;
17 | font-size: 3em;
18 | font-weight: 200;
19 | font-family: "Audiowide", cursive;
20 | color: #92c35e;
21 | text-align: center;
22 | text-shadow: 1px 1px 0 #333333;
23 | line-height: 70px;
24 | }
25 |
26 | .login {
27 | background-color: #f6f6f6;
28 | width: 100%;
29 | height: 330px;
30 | border-radius: 7px;
31 | }
32 |
33 | .login {
34 | padding: 10px 20px;
35 | }
36 |
37 | .login form {
38 | margin-top: 20px;
39 | }
--------------------------------------------------------------------------------
/bootcamp/core/static/css/network.css:
--------------------------------------------------------------------------------
1 | .users {
2 | margin-top: 2em;
3 | }
4 |
5 | .users .panel-heading img {
6 | margin-right: .6em;
7 | }
--------------------------------------------------------------------------------
/bootcamp/core/static/css/profile.css:
--------------------------------------------------------------------------------
1 | .profile {
2 | margin-top: 1em;
3 | }
4 |
5 | .user-profile .picture {
6 | width: 200px;
7 | border-radius: 5px;
8 | }
9 |
10 | .user-profile ul {
11 | padding: 0;
12 | margin-top: .6em;
13 | }
14 |
15 | .user-profile ul li {
16 | list-style: none;
17 | }
--------------------------------------------------------------------------------
/bootcamp/core/static/js/picture.js:
--------------------------------------------------------------------------------
1 | $(function () {
2 |
3 | var jcrop_api,
4 | xsize = 200,
5 | ysize = 200;
6 |
7 | $("#crop-picture").Jcrop({
8 | aspectRatio: xsize / ysize,
9 | onSelect: updateCoords,
10 | setSelect: [0, 0, 200, 200]
11 | });
12 |
13 | function updateCoords(c) {
14 | $("#x").val(c.x);
15 | $("#y").val(c.y);
16 | $("#width").val(c.w);
17 | $("#height").val(c.h);
18 | };
19 |
20 | $("#btn-upload-picture").click(function () {
21 | $("#picture-upload-form input[name='picture']").click();
22 | });
23 |
24 | $("#picture-upload-form input[name='picture']").change(function () {
25 | $("#picture-upload-form").submit();
26 | });
27 |
28 | });
29 |
--------------------------------------------------------------------------------
/bootcamp/core/templates/core/cover.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load i18n %}
3 | {% load static %}
4 |
5 | {% block head %}
6 |
7 | {% endblock head %}
8 |
9 | {% block body %}
10 |
11 |
Bootcamp
12 | {% if form.non_field_errors %}
13 | {% for error in form.non_field_errors %}
14 |
15 |
16 | {{ error }}
17 |
18 | {% endfor %}
19 | {% endif %}
20 |
21 |
{% trans 'Log in' %}
22 |
43 |
44 |
45 | {% endblock body %}
46 |
--------------------------------------------------------------------------------
/bootcamp/core/templates/core/network.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load i18n %}
3 | {% load static %}
4 |
5 | {% block title %}{% trans 'Network' %}{% endblock %}
6 |
7 | {% block head %}
8 |
9 | {% endblock head %}
10 |
11 | {% block main %}
12 |
15 |
16 |
17 | {% for user in users %}
18 |
19 |
20 |
24 |
25 | {% if user.profile.job_title %}
26 |
{% trans 'Job Title' %}: {{ user.profile.job_title }}
27 | {% endif %}
28 |
{% trans 'Username' %}: {{ user.username }}
29 |
{% trans 'Email' %}: {{ user.email }}
30 | {% if user.profile.location %}
31 |
{% trans 'Location' %}: {{ user.profile.location }}
32 | {% endif %}
33 | {% if user.profile.url %}
34 |
{% trans 'Url' %}: {{ user.profile.get_url }}
35 | {% endif %}
36 |
37 |
38 |
39 | {% if forloop.counter|divisibleby:3 %}
{% endif %}
40 | {% endfor %}
41 |
42 |
43 | {% endblock main %}
44 |
--------------------------------------------------------------------------------
/bootcamp/core/templates/core/partial_settings_menu.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
7 |
--------------------------------------------------------------------------------
/bootcamp/core/templates/core/password.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load i18n %}
3 |
4 | {% block title %}{% trans 'Account Settings' %}{% endblock %}
5 |
6 | {% block main %}
7 |
10 |
11 |
12 | {% include 'core/partial_settings_menu.html' with active='password' %}
13 |
14 |
15 | {% if messages %}
16 | {% for message in messages %}
17 |
18 |
19 | {{ message }}
20 |
21 | {% endfor %}
22 | {% endif %}
23 |
{% trans 'Change Password' %}
24 |
47 |
48 |
49 | {% endblock main %}
50 |
--------------------------------------------------------------------------------
/bootcamp/core/templates/core/picture.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load i18n %}
3 | {% load static %}
4 |
5 | {% block title %}{% trans 'Account Settings' %}{% endblock %}
6 |
7 | {% block head %}
8 |
9 |
10 |
11 | {% endblock head %}
12 |
13 | {% block main %}
14 |
17 |
18 |
19 | {% include 'core/partial_settings_menu.html' with active='picture' %}
20 |
21 |
22 | {% if messages %}
23 | {% for message in messages %}
24 |
25 |
26 | {{ message }}
27 |
28 | {% endfor %}
29 | {% endif %}
30 |
{% trans 'Change Picture' %}
31 |

32 |
37 |
38 | {% if uploaded_picture %}
39 |
72 | {% endif %}
73 |
74 |
75 | {% endblock main %}
76 |
--------------------------------------------------------------------------------
/bootcamp/core/templates/core/profile.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load i18n %}
3 | {% load static %}
4 |
5 | {% block title %}{{ page_user.profile.get_screen_name }}{% endblock %}
6 |
7 | {% block head %}
8 |
9 |
10 |
11 | {% endblock head %}
12 |
13 | {% block main %}
14 |
18 |
19 |
20 |
21 |

22 |
23 | {% if page_user.profile.job_title %}
24 | - {{ page_user.profile.job_title }}
25 | {% endif %}
26 | {% if page_user.profile.location %}
27 | - {{ page_user.profile.location }}
28 | {% endif %}
29 | {% if page_user.email %}
30 | - {{ page_user.email }}
31 | {% endif %}
32 | {% if page_user.profile.url %}
33 | - {{ page_user.profile.get_url }}
35 |
36 | {% endif %}
37 |
38 |
39 |
40 |
{{ page_user.profile.get_screen_name }} {% trans 'Last Feeds by' %}
41 |
44 |
45 | {% for feed in feeds %}
46 | {% include 'feeds/partial_feed.html' with feed=feed %}
47 | {% endfor %}
48 |
49 |
50 |

51 |
52 |
57 |
58 |
59 |
60 | {% endblock main %}
61 |
--------------------------------------------------------------------------------
/bootcamp/core/templates/core/settings.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load i18n %}
3 | {% get_available_languages as LANGUAGES %}
4 | {% get_current_language as CURRENT_LANGUAGE %}
5 |
6 | {% block title %}{% trans 'Account Settings' %}{% endblock %}
7 |
8 | {% block main %}
9 |
12 |
13 |
14 | {% include 'core/partial_settings_menu.html' with active='profile' %}
15 |
16 |
17 | {% if messages %}
18 | {% for message in messages %}
19 |
20 |
21 | {{ message }}
22 |
23 | {% endfor %}
24 | {% endif %}
25 |
{% trans 'Edit Profile' %}
26 |
60 |
61 |
62 | {% endblock main %}
63 |
--------------------------------------------------------------------------------
/bootcamp/core/tests.py:
--------------------------------------------------------------------------------
1 | from http import HTTPStatus
2 |
3 | from django.test import TestCase, Client
4 | from django.contrib.auth.models import User
5 |
6 |
7 | class CoreViewsTest(TestCase):
8 | def setUp(self):
9 | self.client = Client()
10 | User.objects.create_user(
11 | username='test_user',
12 | email='lennon@thebeatles.com',
13 | password='test_password'
14 | )
15 | self.client.login(username='test_user', password='test_password')
16 |
17 | def test_home(self):
18 | response = self.client.get('/')
19 | self.assertEqual(response.status_code, HTTPStatus.OK)
20 |
21 | def test_network(self):
22 | response = self.client.get('/network/')
23 | self.assertEqual(response.status_code, HTTPStatus.OK)
24 |
25 | def test_profile(self):
26 | response = self.client.get('/test_user/')
27 | self.assertEqual(response.status_code, HTTPStatus.OK)
28 |
29 | def test_settings(self):
30 | response = self.client.get('/settings/')
31 | self.assertEqual(response.status_code, HTTPStatus.OK)
32 |
33 | def test_picture(self):
34 | response = self.client.get('/settings/picture/')
35 | self.assertEqual(response.status_code, HTTPStatus.OK)
36 |
37 | def test_password(self):
38 | response = self.client.get('/settings/password/')
39 | self.assertEqual(response.status_code, HTTPStatus.OK)
40 |
--------------------------------------------------------------------------------
/bootcamp/core/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from . import views
4 |
5 | urlpatterns = [
6 | path('', views.settings, name='settings'),
7 | path('picture/', views.picture, name='picture'),
8 | path('password/', views.password, name='password'),
9 | path('upload_picture/', views.upload_picture, name='upload_picture'),
10 | path('save_uploaded_picture/', views.save_uploaded_picture, name='save_uploaded_picture'),
11 | ]
12 |
--------------------------------------------------------------------------------
/bootcamp/core/views.py:
--------------------------------------------------------------------------------
1 | from django.contrib import messages
2 | from django.core.paginator import Paginator
3 | from django.contrib.auth.models import User
4 | from django.views.decorators.cache import cache_page
5 | from django.utils.translation import gettext_lazy as _
6 | from django.contrib.auth.decorators import login_required
7 | from django.shortcuts import render, redirect, get_object_or_404
8 |
9 | from bootcamp.feeds.models import Feed
10 | from bootcamp.feeds.views import feeds, FEEDS_NUM_PAGES
11 |
12 | from .forms import ProfileForm, ChangePasswordForm, SavePictureForm
13 |
14 |
15 | def home(request):
16 | return feeds(request)
17 |
18 |
19 | @cache_page(60 * 15)
20 | def network(request):
21 | users = User.objects.filter(is_active=True).order_by('username')
22 | return render(request, 'core/network.html', {'users': users})
23 |
24 |
25 | def profile(request, username):
26 | page_user = get_object_or_404(User, username=username)
27 | all_feeds = Feed.get_feeds().filter(user=page_user)
28 | paginator = Paginator(all_feeds, FEEDS_NUM_PAGES)
29 | feeds = paginator.page(1)
30 |
31 | from_feed = -1
32 |
33 | if feeds:
34 | from_feed = feeds[0].id
35 |
36 | context = {
37 | 'page_user': page_user, 'feeds': feeds,
38 | 'from_feed': from_feed, 'page': 1
39 | }
40 | return render(request, 'core/profile.html', context)
41 |
42 |
43 | @login_required
44 | def settings(request):
45 | user = request.user
46 | if request.method == 'POST':
47 | form = ProfileForm(request.POST)
48 | if form.is_valid():
49 | user.first_name = form.cleaned_data.get('first_name')
50 | user.last_name = form.cleaned_data.get('last_name')
51 | user.profile.job_title = form.cleaned_data.get('job_title')
52 | user.email = form.cleaned_data.get('email')
53 | user.profile.url = form.cleaned_data.get('url')
54 | user.profile.location = form.cleaned_data.get('location')
55 | user.save()
56 |
57 | message = _('Your profile were successfully edited.')
58 | messages.add_message(request, messages.SUCCESS, message)
59 | else:
60 | initial = {
61 | 'job_title': user.profile.job_title,
62 | 'url': user.profile.url,
63 | 'location': user.profile.location
64 | }
65 | form = ProfileForm(instance=user, initial=initial)
66 |
67 | return render(request, 'core/settings.html', {'form': form})
68 |
69 |
70 | @login_required
71 | def picture(request):
72 | uploaded_picture = False
73 | picture_url = None
74 |
75 | if request.GET.get('upload_picture') == 'uploaded':
76 | uploaded_picture = True
77 | picture_url = ""
78 |
79 | context = {
80 | 'uploaded_picture': uploaded_picture,
81 | 'picture_url': picture_url,
82 | }
83 | return render(request, 'core/picture.html', context)
84 |
85 |
86 | @login_required
87 | def password(request):
88 | user = request.user
89 |
90 | if request.method == 'POST':
91 | form = ChangePasswordForm(request.POST)
92 |
93 | if form.is_valid():
94 | new_password = form.cleaned_data.get('new_password')
95 | user.set_password(new_password)
96 | user.save()
97 |
98 | message = _('Your password were successfully changed.')
99 | messages.add_message(request, messages.SUCCESS, message)
100 | else:
101 | form = ChangePasswordForm(instance=user)
102 |
103 | return render(request, 'core/password.html', {'form': form})
104 |
105 |
106 | @login_required
107 | def upload_picture(request):
108 | # username = request.user.username
109 | # request.FILES['picture'], public_id=username, width=400, crop='limit'
110 | return redirect('/settings/picture/?upload_picture=uploaded')
111 |
112 |
113 | @login_required
114 | def save_uploaded_picture(request):
115 | form = SavePictureForm(request.POST)
116 | user = request.user
117 |
118 | if form.is_valid():
119 | form.cleaned_data.update(crop='crop')
120 | user.profile.picture_url = ""
121 | user.save()
122 |
123 | return redirect('/settings/picture/')
124 |
--------------------------------------------------------------------------------
/bootcamp/feeds/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qulc/bootcamp/de3b68240df225260f0da159269d4d66dfe1fe87/bootcamp/feeds/__init__.py
--------------------------------------------------------------------------------
/bootcamp/feeds/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from .models import Feed
4 |
5 | admin.site.register(Feed)
6 |
--------------------------------------------------------------------------------
/bootcamp/feeds/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2 on 2023-04-06 10:01
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='Feed',
19 | fields=[
20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21 | ('date', models.DateTimeField(auto_now_add=True)),
22 | ('post', models.TextField(max_length=255)),
23 | ('likes', models.IntegerField(default=0)),
24 | ('comments', models.IntegerField(default=0)),
25 | ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='feeds.feed')),
26 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
27 | ],
28 | options={
29 | 'verbose_name': 'Feed',
30 | 'verbose_name_plural': 'Feeds',
31 | 'ordering': ('-date',),
32 | },
33 | ),
34 | ]
35 |
--------------------------------------------------------------------------------
/bootcamp/feeds/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qulc/bootcamp/de3b68240df225260f0da159269d4d66dfe1fe87/bootcamp/feeds/migrations/__init__.py
--------------------------------------------------------------------------------
/bootcamp/feeds/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.contrib.auth.models import User
3 | from django.utils.translation import gettext_lazy as _
4 |
5 | from bootcamp.activities.models import Activity
6 |
7 |
8 | class Feed(models.Model):
9 | user = models.ForeignKey(User, on_delete=models.CASCADE)
10 | date = models.DateTimeField(auto_now_add=True)
11 | post = models.TextField(max_length=255)
12 | parent = models.ForeignKey('Feed', null=True, blank=True, on_delete=models.CASCADE)
13 | likes = models.IntegerField(default=0)
14 | comments = models.IntegerField(default=0)
15 |
16 | class Meta:
17 | verbose_name = _('Feed')
18 | verbose_name_plural = _('Feeds')
19 | ordering = ('-date',)
20 |
21 | def __str__(self):
22 | return self.post
23 |
24 | @staticmethod
25 | def get_feeds(from_feed=None):
26 | if from_feed is not None:
27 | feeds = Feed.objects.filter(parent=None, id__lte=from_feed)
28 | else:
29 | feeds = Feed.objects.filter(parent=None)
30 | return feeds.prefetch_related('user__profile')
31 |
32 | @staticmethod
33 | def get_feeds_after(feed):
34 | feeds = Feed.objects.filter(parent=None, id__gt=feed)
35 | return feeds
36 |
37 | def get_comments(self):
38 | return Feed.objects.filter(parent=self).order_by('date')
39 |
40 | def calculate_likes(self):
41 | likes = Activity.objects.filter(activity_type=Activity.LIKE,
42 | feed=self.pk).count()
43 | self.likes = likes
44 | self.save()
45 | return self.likes
46 |
47 | def get_likes(self):
48 | likes = Activity.objects.filter(activity_type=Activity.LIKE,
49 | feed=self.pk)
50 | return likes
51 |
52 | def get_likers(self):
53 | likes = self.get_likes()
54 | likers = []
55 | for like in likes:
56 | likers.append(like.user)
57 | return likers
58 |
59 | def calculate_comments(self):
60 | self.comments = Feed.objects.filter(parent=self).count()
61 | self.save()
62 | return self.comments
63 |
64 | def comment(self, user, post):
65 | feed_comment = Feed(user=user, post=post, parent=self)
66 | feed_comment.save()
67 | self.comments = Feed.objects.filter(parent=self).count()
68 | self.save()
69 | return feed_comment
70 |
71 | @classmethod
72 | def like(cls, feed_id, user):
73 | feed = Feed.objects.get(pk=feed_id)
74 | like = Activity.objects.filter(
75 | activity_type=Activity.LIKE, feed=feed_id, user=user)
76 |
77 | if like:
78 | user.profile.unotify_liked(feed)
79 | like.delete()
80 | else:
81 | Activity.objects.create(
82 | feed=feed_id,
83 | user=user,
84 | activity_type=Activity.LIKE
85 | )
86 | user.profile.notify_liked(feed)
87 |
88 | feed.calculate_likes()
89 | return feed
90 |
--------------------------------------------------------------------------------
/bootcamp/feeds/schema.py:
--------------------------------------------------------------------------------
1 | import graphene
2 |
3 | from graphql_relay import from_global_id
4 | from graphene_django import DjangoObjectType
5 | from graphene_django.fields import DjangoConnectionField
6 |
7 | from .models import Feed
8 |
9 |
10 | class FeedObject(DjangoObjectType):
11 | class Meta:
12 | model = Feed
13 | interfaces = [graphene.Node]
14 |
15 |
16 | class FeedQuery(graphene.ObjectType):
17 | feed = graphene.Node.Field(FeedObject)
18 | feeds = DjangoConnectionField(FeedObject, first=graphene.Int(default_value=5))
19 |
20 | def resolve_feeds(self, info, **kwargs):
21 | return Feed.objects.select_related(
22 | 'user', 'user__profile'
23 | ).prefetch_related('feed_set').filter(parent=None)
24 |
25 |
26 | class FeedInput(graphene.InputObjectType):
27 | post = graphene.String(required=True)
28 |
29 |
30 | class CreateFeed(graphene.Mutation):
31 | class Arguments:
32 | feed = FeedInput(required=True)
33 |
34 | feed = graphene.Field(lambda: FeedObject)
35 |
36 | @staticmethod
37 | def mutate(root, info, feed):
38 | feed = Feed.objects.create(
39 | user=info.context.user, post=feed.post)
40 | return CreateFeed(feed=feed, ok=True)
41 |
42 |
43 | class LikeFeed(graphene.Mutation):
44 | class Arguments:
45 | id = graphene.ID(required=True)
46 |
47 | feed = graphene.Field(lambda: FeedObject)
48 |
49 | @staticmethod
50 | def mutate(root, info, id):
51 | _, feed_id = from_global_id(id)
52 | feed = Feed.like(feed_id, info.context.user)
53 | return LikeFeed(feed=feed)
54 |
--------------------------------------------------------------------------------
/bootcamp/feeds/static/css/feeds.css:
--------------------------------------------------------------------------------
1 | ul.stream {
2 | margin: 0;
3 | padding: 0;
4 | }
5 |
6 | ul.stream li {
7 | list-style: none;
8 | border-bottom: 1px solid #eeeeee;
9 | padding: 1em 0;
10 | }
11 |
12 | ul.stream li:last-child {
13 | border-bottom: none;
14 | }
15 |
16 | ul.stream li a img.user {
17 | width: 60px;
18 | border-radius: 5px;
19 | float: left;
20 | }
21 |
22 | ul.stream li div.post {
23 | margin-left: 60px;
24 | padding: 0 0 0 1.2em;
25 | overflow-x: auto;
26 | }
27 |
28 | ul.stream li div.post h3 {
29 | font-size: 1em;
30 | margin: 0;
31 | margin-bottom: .2em;
32 | }
33 |
34 | ul.stream li div.post h3 small {
35 | margin-left: .3em;
36 | font-size: .8em;
37 | }
38 |
39 | ul.stream li div.post p {
40 | margin: 0;
41 | }
42 |
43 | ul.stream li div.post div.interaction {
44 | padding-top: .2em;
45 | }
46 |
47 | ul.stream li div.post div.interaction a {
48 | margin-right: .6em;
49 | font-size: .8em;
50 | }
51 |
52 | .stream-update {
53 | text-align: center;
54 | border-bottom: 1px solid #eeeeee;
55 | display: none;
56 | }
57 |
58 | .stream-update a {
59 | display: block;
60 | padding: .6em 0;
61 | background-color: #f5f8fa;
62 | }
63 |
64 | .stream-update a:hover {
65 | text-decoration: none;
66 | background-color: #e1e8ed;
67 | }
68 |
69 | .compose {
70 | display: none;
71 | border-bottom: 1px solid #eee;
72 | }
73 |
74 | .compose h2 {
75 | font-size: 1.4em;
76 | }
77 |
78 | .comments {
79 | margin-top: .6em;
80 | display: none;
81 | }
82 |
83 | .comments ol {
84 | margin: .8em 0 0;
85 | padding: .2em 0;
86 | background-color: #f4f4f4;
87 | border-radius: 3px;
88 | overflow-x: auto;
89 | }
90 |
91 | .comments ol li {
92 | list-style: none;
93 | padding: 0;
94 | }
95 |
96 | .comments ol li img.user-comment {
97 | width: 35px;
98 | border-radius: 4px;
99 | float: left;
100 | margin-left: 10px;
101 | }
102 |
103 | .comments ol li div {
104 | margin-left: 45px;
105 | padding: 0 .8em;
106 | font-size: .9em;
107 | }
108 |
109 | .comments ol li {
110 | padding: .6em .6em .6em 0;
111 | border-bottom: none;
112 | }
113 |
114 | .comments ol li h4 {
115 | margin: 0;
116 | margin-left: 45px;
117 | padding: 0 0 .2em .8em;
118 | font-size: .9em;
119 | }
120 |
121 | .comments ol li h4 small {
122 | margin-left: .3em;
123 | }
124 |
125 | .empty {
126 | margin: 0 .8em;
127 | font-size: .9em;
128 | }
129 |
130 | .load {
131 | text-align: center;
132 | padding-top: 1em;
133 | border-top: 1px solid #eeeeee;
134 | display: none;
135 | }
136 |
137 | .loadcomment {
138 | text-align: center;
139 | }
140 |
141 | .remove-feed {
142 | color: #bbbbbb;
143 | font-size: .8em;
144 | padding-top: .2em;
145 | float: right;
146 | display: none;
147 | cursor: pointer;
148 | }
149 |
150 | .remove-feed:hover {
151 | color: #333333;
152 | }
--------------------------------------------------------------------------------
/bootcamp/feeds/templates/feeds/feed.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% load i18n %}
4 | {% load humanize %}
5 | {% load static %}
6 |
7 | {% block title %}{% trans 'Feed' %}{% endblock %}
8 |
9 | {% block head %}
10 |
11 |
12 |
13 | {% endblock head %}
14 |
15 | {% block main %}
16 |
19 |
20 | {% include 'feeds/partial_feed.html' with feed=feed %}
21 |
22 | {% endblock main %}
23 |
--------------------------------------------------------------------------------
/bootcamp/feeds/templates/feeds/feeds.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load i18n %}
3 | {% load static %}
4 |
5 | {% block head %}
6 |
7 |
8 |
9 |
10 | {% endblock head %}
11 |
12 | {% block main %}
13 |
22 | {% if not user.is_anonymous %}
23 |
24 |
{% trans "Compose a new post" %}
25 |
26 |
41 |
42 | {% endif %}
43 |
46 |
47 | {% for feed in feeds %}
48 | {% include 'feeds/partial_feed.html' with feed=feed %}
49 | {% endfor %}
50 |
51 |
52 |

53 |
54 |
59 | {% endblock main %}
60 |
--------------------------------------------------------------------------------
/bootcamp/feeds/templates/feeds/partial_feed.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load humanize %}
3 |
4 |
5 |
6 |
7 |
8 |
9 | {% if feed.user == user or user.is_superuser %}
10 |
11 | {% endif %}
12 |
13 |
16 |
{{ feed.post|safe }}
17 |
18 | {% if not user.is_anonymous %}
19 |
38 |
49 | {% endif %}
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/bootcamp/feeds/templates/feeds/partial_feed_comments.html:
--------------------------------------------------------------------------------
1 | {% load humanize %}
2 | {% load i18n %}
3 |
4 | {% for comment in feed.get_comments %}
5 |
6 | {% if comment.user == user %}
7 |
9 | {% endif %}
10 |
11 |
12 |
13 |
19 | {{ comment.post|safe }}
20 |
21 | {% empty %}
22 | {% trans 'Be the first one to comment' %}
23 | {% endfor %}
24 |
--------------------------------------------------------------------------------
/bootcamp/feeds/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase, Client
2 | from django.contrib.auth.models import User
3 |
4 | from .models import Feed
5 |
6 |
7 | class FeedViewsTest(TestCase):
8 | def setUp(self):
9 | self.client = Client()
10 | user = User.objects.create_user(
11 | username='test_user',
12 | email='lennon@thebeatles.com',
13 | password='test_password'
14 | )
15 | self.feed = Feed.objects.create(user=user, post='test feed')
16 |
17 | def test_feeds(self):
18 | response = self.client.get('/feeds/')
19 | self.assertEqual(response.status_code, 200)
20 |
21 | def test_feed(self):
22 | response = self.client.get('/feeds/123/')
23 | self.assertEqual(response.status_code, 404)
24 |
25 | response = self.client.get(f'/feeds/{self.feed.pk}/')
26 | self.assertEqual(response.status_code, 200)
27 |
--------------------------------------------------------------------------------
/bootcamp/feeds/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from .import views
4 |
5 | urlpatterns = [
6 | path('', views.feeds, name='feeds'),
7 | path('/', views.feed, name='feed'),
8 | path('post/', views.post, name='post'),
9 | path('like/', views.like, name='like'),
10 | path('load/', views.load, name='load'),
11 | path('check/', views.check, name='check'),
12 | path('update/', views.update, name='update'),
13 | path('comment/', views.comment, name='comment'),
14 | path('remove/', views.remove, name='remove_feed'),
15 | path('load_new/', views.load_new, name='load_new'),
16 | path('track_comments/', views.track_comments, name='track_comments'),
17 | ]
18 |
--------------------------------------------------------------------------------
/bootcamp/feeds/views.py:
--------------------------------------------------------------------------------
1 | from django.http import (
2 | HttpResponse, JsonResponse,
3 | HttpResponseForbidden, HttpResponseBadRequest,
4 | )
5 | from django.template.loader import render_to_string
6 | from django.template.context_processors import csrf
7 | from django.shortcuts import render, get_object_or_404
8 | from django.contrib.auth.decorators import login_required
9 | from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
10 |
11 | from .models import Feed
12 |
13 | FEEDS_NUM_PAGES = 10
14 |
15 |
16 | def feeds(request):
17 | all_feeds = Feed.get_feeds()
18 | paginator = Paginator(all_feeds, FEEDS_NUM_PAGES).page(1)
19 | from_feed = -1
20 | if paginator:
21 | from_feed = paginator[0].id
22 | return render(request, 'feeds/feeds.html', {
23 | 'feeds': paginator,
24 | 'from_feed': from_feed,
25 | 'page': 1,
26 | })
27 |
28 |
29 | def feed(request, pk):
30 | feed = get_object_or_404(Feed, pk=pk)
31 | return render(request, 'feeds/feed.html', {'feed': feed})
32 |
33 |
34 | def load(request):
35 | page = request.GET.get('page')
36 | from_feed = request.GET.get('from_feed')
37 | feed_source = request.GET.get('feed_source')
38 | csrf_token = str(csrf(request)['csrf_token'])
39 |
40 | all_feeds = Feed.get_feeds(from_feed)
41 |
42 | if feed_source != 'all':
43 | all_feeds = all_feeds.filter(user__id=feed_source)
44 |
45 | paginator = Paginator(all_feeds, FEEDS_NUM_PAGES)
46 |
47 | try:
48 | feeds = paginator.page(page)
49 | except PageNotAnInteger:
50 | return HttpResponseBadRequest()
51 | except EmptyPage:
52 | feeds = []
53 |
54 | html = ''
55 | for feed in feeds:
56 | context = {
57 | 'feed': feed,
58 | 'user': request.user,
59 | 'csrf_token': csrf_token
60 | }
61 | template = render_to_string('feeds/partial_feed.html', context)
62 |
63 | html = f'{html}{template}'
64 |
65 | return HttpResponse(html)
66 |
67 |
68 | def _html_feeds(last_feed, user, csrf_token, feed_source='all'):
69 | feeds = Feed.get_feeds_after(last_feed)
70 |
71 | if feed_source != 'all':
72 | feeds = feeds.filter(user__id=feed_source)
73 |
74 | html = ''
75 |
76 | for feed in feeds:
77 | context = {'feed': feed, 'user': user, 'csrf_token': csrf_token}
78 | template = render_to_string('feeds/partial_feed.html', context)
79 | html = f'{html}{template}'
80 | return html
81 |
82 |
83 | def load_new(request):
84 | last_feed = request.GET.get('last_feed')
85 | user = request.user
86 | csrf_token = str(csrf(request)['csrf_token'])
87 | html = _html_feeds(last_feed, user, csrf_token)
88 | return HttpResponse(html)
89 |
90 |
91 | def check(request):
92 | last_feed = request.GET.get('last_feed')
93 | feed_source = request.GET.get('feed_source')
94 | feeds = Feed.get_feeds_after(last_feed)
95 |
96 | if feed_source != 'all':
97 | feeds = feeds.filter(user__id=feed_source)
98 |
99 | count = feeds.count()
100 | return HttpResponse(count)
101 |
102 |
103 | @login_required
104 | def post(request):
105 | last_feed = request.POST.get('last_feed')
106 | post = request.POST['post'].strip()[:255]
107 | user = request.user
108 |
109 | csrf_token = str(csrf(request)['csrf_token'])
110 |
111 | if len(post) > 0:
112 | Feed.objects.create(
113 | post=post,
114 | user=user
115 | )
116 | html = _html_feeds(last_feed, user, csrf_token)
117 | return HttpResponse(html)
118 |
119 |
120 | @login_required
121 | def like(request):
122 | user = request.user
123 | feed_id = request.POST['feed']
124 |
125 | feed = Feed.like(feed_id, user)
126 | return HttpResponse(feed.likes)
127 |
128 |
129 | @login_required
130 | def comment(request):
131 | if request.method == 'POST':
132 | feed_id = request.POST['feed']
133 | feed = Feed.objects.get(pk=feed_id)
134 | post = request.POST['post'].strip()
135 |
136 | if len(post) > 0:
137 | post = post[:255]
138 | user = request.user
139 | feed.comment(user=user, post=post)
140 | user.profile.notify_commented(feed)
141 | user.profile.notify_also_commented(feed)
142 |
143 | context = {'feed': feed}
144 | return render(request, 'feeds/partial_feed_comments.html', context)
145 |
146 | feed_id = request.GET.get('feed')
147 | feed = Feed.objects.get(pk=feed_id)
148 | return render(request, 'feeds/partial_feed_comments.html', {'feed': feed})
149 |
150 |
151 | @login_required
152 | def update(request):
153 | first_feed = request.GET.get('first_feed')
154 | last_feed = request.GET.get('last_feed')
155 | feed_source = request.GET.get('feed_source')
156 |
157 | feeds = Feed.get_feeds().filter(id__range=(last_feed, first_feed))
158 |
159 | if feed_source != 'all':
160 | feeds = feeds.filter(user__id=feed_source)
161 |
162 | dump = {}
163 |
164 | for feed in feeds:
165 | dump[feed.pk] = {'likes': feed.likes, 'comments': feed.comments}
166 |
167 | return JsonResponse(dump, safe=False)
168 |
169 |
170 | @login_required
171 | def track_comments(request):
172 | feed_id = request.GET.get('feed')
173 | feed = Feed.objects.get(pk=feed_id)
174 | return render(request, 'feeds/partial_feed_comments.html', {'feed': feed})
175 |
176 |
177 | @login_required
178 | def remove(request):
179 | feed_id = request.POST.get('feed')
180 |
181 | feed = Feed.objects.filter(pk=feed_id).first()
182 |
183 | if not feed:
184 | return HttpResponseBadRequest
185 |
186 | if feed.user == request.user or request.user.is_superuser:
187 | likes = feed.get_likes()
188 | parent = feed.parent
189 |
190 | for like in likes:
191 | like.delete()
192 |
193 | feed.delete()
194 | if parent:
195 | parent.calculate_comments()
196 |
197 | return HttpResponse()
198 |
199 | return HttpResponseForbidden()
200 |
--------------------------------------------------------------------------------
/bootcamp/messenger/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qulc/bootcamp/de3b68240df225260f0da159269d4d66dfe1fe87/bootcamp/messenger/__init__.py
--------------------------------------------------------------------------------
/bootcamp/messenger/admin.py:
--------------------------------------------------------------------------------
1 | # Register your models here.
2 | from django.contrib import admin
3 |
4 | from .models import Message
5 |
6 | admin.site.register(Message)
7 |
--------------------------------------------------------------------------------
/bootcamp/messenger/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2 on 2023-04-06 10:01
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='Message',
19 | fields=[
20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21 | ('message', models.TextField(blank=True, max_length=1000)),
22 | ('date', models.DateTimeField(auto_now_add=True)),
23 | ('is_read', models.BooleanField(default=False)),
24 | ('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL)),
25 | ('from_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL)),
26 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL)),
27 | ],
28 | options={
29 | 'verbose_name': 'Message',
30 | 'verbose_name_plural': 'Messages',
31 | 'ordering': ('date',),
32 | },
33 | ),
34 | ]
35 |
--------------------------------------------------------------------------------
/bootcamp/messenger/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qulc/bootcamp/de3b68240df225260f0da159269d4d66dfe1fe87/bootcamp/messenger/migrations/__init__.py
--------------------------------------------------------------------------------
/bootcamp/messenger/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.db.models import Max
3 | from django.contrib.auth.models import User
4 | from django.utils.translation import gettext_lazy as _
5 |
6 |
7 | class Message(models.Model):
8 | user = models.ForeignKey(User, related_name='+', on_delete=models.CASCADE)
9 | message = models.TextField(max_length=1000, blank=True)
10 | date = models.DateTimeField(auto_now_add=True)
11 | conversation = models.ForeignKey(User, related_name='+', on_delete=models.CASCADE)
12 | from_user = models.ForeignKey(User, related_name='+', on_delete=models.CASCADE)
13 | is_read = models.BooleanField(default=False)
14 |
15 | class Meta:
16 | ordering = ('date',)
17 | verbose_name = _('Message')
18 | verbose_name_plural = _('Messages')
19 |
20 | def __str__(self):
21 | return self.message
22 |
23 | @staticmethod
24 | def send_message(from_user, to_user, message):
25 | message = message[:1000]
26 | current_user_message = Message(
27 | from_user=from_user,
28 | message=message,
29 | user=from_user,
30 | conversation=to_user,
31 | is_read=True
32 | )
33 | current_user_message.save()
34 | Message.objects.create(
35 | from_user=from_user,
36 | conversation=from_user,
37 | message=message,
38 | user=to_user
39 | )
40 | return current_user_message
41 |
42 | @staticmethod
43 | def get_conversations(user):
44 | conversations = Message.objects.filter(user=user).values(
45 | 'conversation').annotate(last=Max('date')).order_by('-last')
46 |
47 | users = []
48 | for conversation in conversations:
49 | unread = Message.objects.filter(
50 | user=user,
51 | is_read=False,
52 | conversation__pk=conversation['conversation'],
53 | ).count(),
54 |
55 | users.append({
56 | 'user': User.objects.get(pk=conversation['conversation']),
57 | 'last': conversation['last'],
58 | 'unread': unread,
59 | })
60 |
61 | return users
62 |
--------------------------------------------------------------------------------
/bootcamp/messenger/static/css/messages.css:
--------------------------------------------------------------------------------
1 | .conversation {
2 | margin: 0;
3 | padding: 0;
4 | }
5 |
6 | .conversation li {
7 | list-style: none;
8 | padding: .6em 0;
9 | }
10 |
11 | .conversation li h5 {
12 | margin: 0;
13 | margin-bottom: .2em;
14 | }
15 |
16 | .conversation li div {
17 | margin-left: 50px;
18 | }
19 |
20 | .conversation .picture {
21 | width: 40px;
22 | border-radius: 5px;
23 | float: left;
24 | }
25 |
26 | .conversation-portrait {
27 | width: 20px;
28 | border-radius: 3px;
29 | margin-right: 5px;
30 | }
31 |
32 | .typeahead, .tt-query, .tt-hint {
33 | border: 1px solid #CCCCCC;
34 | border-radius: 5px;
35 | font-size: 1.2em;
36 | height: 35px;
37 | line-height: 35px;
38 | outline: medium none;
39 | padding: 4px 12px;
40 | width: 300px;
41 | }
42 | .typeahead {
43 | background-color: #FFFFFF;
44 | }
45 | .typeahead:focus {
46 | border: 2px solid #0097CF;
47 | }
48 | .tt-query {
49 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset;
50 | }
51 | .tt-hint {
52 | color: #999999;
53 | }
54 | .tt-dropdown-menu {
55 | background-color: #FFFFFF;
56 | border: 1px solid rgba(0, 0, 0, 0.2);
57 | border-radius: 8px;
58 | box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
59 | margin-top: 8px;
60 | padding: 8px 0;
61 | width: 300px;
62 | }
63 | .tt-suggestion {
64 | font-size: 1.2em;
65 | line-height: 24px;
66 | padding: 3px 20px;
67 | }
68 | .tt-suggestion.tt-cursor {
69 | background-color: #0097CF;
70 | color: #FFFFFF;
71 | }
72 | .tt-suggestion p {
73 | margin: 0;
74 | }
--------------------------------------------------------------------------------
/bootcamp/messenger/static/js/check_messages.js:
--------------------------------------------------------------------------------
1 | $(function () {
2 | function check_messages() {
3 | $.ajax({
4 | url: '/messages/check/',
5 | cache: false,
6 | success: function (data) {
7 | $("#unread-count").text(data);
8 | },
9 | complete: function () {
10 | window.setTimeout(check_messages, 60000);
11 | }
12 | });
13 | };
14 | check_messages();
15 | });
--------------------------------------------------------------------------------
/bootcamp/messenger/static/js/messages.js:
--------------------------------------------------------------------------------
1 | $(function () {
2 | $("#send").submit(function () {
3 | $.ajax({
4 | url: '/messages/send/',
5 | data: $("#send").serialize(),
6 | cache: false,
7 | type: 'post',
8 | success: function (data) {
9 | $(".send-message").before(data);
10 | $("input[name='message']").val('');
11 | $("input[name='message']").focus();
12 | }
13 | });
14 | return false;
15 | });
16 | });
--------------------------------------------------------------------------------
/bootcamp/messenger/static/js/messages.typehead.js:
--------------------------------------------------------------------------------
1 | $(function () {
2 | var substringMatcher = function(strs) {
3 | return function findMatches(q, cb) {
4 | var matches, substringRegex;
5 | matches = [];
6 | substrRegex = new RegExp(q, 'i');
7 | $.each(strs, function(i, str) {
8 | if (substrRegex.test(str)) {
9 | matches.push({ value: str });
10 | }
11 | });
12 | cb(matches);
13 | };
14 | };
15 |
16 | $.ajax({
17 | url: '/messages/users/',
18 | cache: false,
19 | success: function (data) {
20 | $('#to').typeahead({
21 | hint: true,
22 | highlight: true,
23 | minLength: 1
24 | },
25 | {
26 | name: 'data',
27 | displayKey: 'value',
28 | source: substringMatcher(data)
29 | });
30 | }
31 | });
32 | });
--------------------------------------------------------------------------------
/bootcamp/messenger/templates/messages/base_messages.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load i18n %}
3 | {% load static %}
4 |
5 | {% block head %}
6 |
7 |
8 |
9 |
10 | {% endblock head %}
11 |
12 |
13 | {% block main %}
14 |
18 |
19 |
20 | {% include 'messages/includes/partial_conversations.html' with conversations=conversations active=active %}
21 |
22 |
23 | {% block container %}
24 | {% endblock container %}
25 |
26 |
27 | {% endblock main %}
28 |
--------------------------------------------------------------------------------
/bootcamp/messenger/templates/messages/inbox.html:
--------------------------------------------------------------------------------
1 | {% extends 'messages/base_messages.html' %}
2 | {% load i18n %}
3 |
4 | {% block title %}{% trans 'Inbox' %}{% endblock %}
5 |
6 | {% block page_header %}{% trans 'Inbox' %}{% endblock %}
7 |
8 | {% block container %}
9 | {% if messages %}
10 |
25 | {% else %}
26 | {% trans 'Your inbox is empty!' %}
27 | {% endif %}
28 | {% endblock container %}
--------------------------------------------------------------------------------
/bootcamp/messenger/templates/messages/includes/partial_conversations.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 |
--------------------------------------------------------------------------------
/bootcamp/messenger/templates/messages/includes/partial_message.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 | {{ message.message }}
13 |
14 |
--------------------------------------------------------------------------------
/bootcamp/messenger/templates/messages/new.html:
--------------------------------------------------------------------------------
1 | {% extends 'messages/base_messages.html' %}
2 | {% load i18n %}
3 | {% load static %}
4 |
5 | {% block title %}{% trans 'New message' %}{% endblock %}
6 |
7 | {% block page_header %}{% trans 'New message' %}{% endblock %}
8 |
9 | {% block container %}
10 |
30 |
31 | {% endblock container %}
32 |
--------------------------------------------------------------------------------
/bootcamp/messenger/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase, Client
2 | from django.contrib.auth.models import User
3 |
4 |
5 | class MessengerViewsTest(TestCase):
6 | def setUp(self):
7 | self.client = Client()
8 | User.objects.create_user(
9 | username='test_user',
10 | email='lennon@thebeatles.com',
11 | password='test_password'
12 | )
13 | User.objects.create_user(
14 | username='test_user_1',
15 | email='lennon_1@thebeatles.com',
16 | password='test_password'
17 | )
18 | self.client.login(username='test_user', password='test_password')
19 |
20 | def test_inbox(self):
21 | response = self.client.get('/messages/')
22 | self.assertEqual(response.status_code, 200)
23 |
24 | def test_messages(self):
25 | response = self.client.get('/messages/no_user/')
26 | self.assertEqual(response.status_code, 200)
27 |
28 | response = self.client.get('/messages/test_user/')
29 | self.assertEqual(response.status_code, 200)
30 |
31 | def test_new_message(self):
32 | response = self.client.get('/messages/new/')
33 | self.assertEqual(response.status_code, 200)
34 |
35 | response = self.client.post('/messages/new/', {
36 | 'to': 'test_user_1',
37 | 'message': 'test message'
38 | })
39 | self.assertEqual(response.status_code, 302)
40 | self.assertEqual(response.url, '/messages/test_user_1/')
41 |
--------------------------------------------------------------------------------
/bootcamp/messenger/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from . import views
4 |
5 |
6 | urlpatterns = [
7 | path('', views.inbox, name='inbox'),
8 | path('new/', views.new, name='new_message'),
9 | path('send/', views.send, name='send_message'),
10 | path('delete/', views.delete, name='delete_message'),
11 | path('users/', views.users, name='users_message'),
12 | path('check/', views.check, name='check_message'),
13 | path('/', views.messages, name='messages'),
14 | ]
15 |
--------------------------------------------------------------------------------
/bootcamp/messenger/views.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import User
2 | from django.shortcuts import render, redirect
3 | from django.contrib.auth.decorators import login_required
4 | from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
5 |
6 | from .models import Message
7 |
8 |
9 | @login_required
10 | def inbox(request):
11 | messages = None
12 | active_conversation = None
13 |
14 | conversations = Message.get_conversations(user=request.user)
15 |
16 | if conversations:
17 | conversation = conversations[0]
18 | active_conversation = conversation['user'].username
19 |
20 | messages = Message.objects.filter(user=request.user,
21 | conversation=conversation['user'])
22 | messages.update(is_read=True)
23 |
24 | for conversation in conversations:
25 | if conversation['user'].username == active_conversation:
26 | conversation['unread'] = 0
27 | context = {
28 | 'messages': messages,
29 | 'active': active_conversation,
30 | 'conversations': conversations
31 | }
32 | return render(request, 'messages/inbox.html', context)
33 |
34 |
35 | @login_required
36 | def messages(request, username):
37 | active_conversation = username
38 | conversations = Message.get_conversations(user=request.user)
39 | messages = Message.objects.filter(user=request.user,
40 | conversation__username=username)
41 | messages.update(is_read=True)
42 | for conversation in conversations:
43 | if conversation['user'].username == username:
44 | conversation['unread'] = 0
45 |
46 | context = {
47 | 'messages': messages,
48 | 'conversations': conversations,
49 | 'active': active_conversation
50 | }
51 | return render(request, 'messages/inbox.html', context)
52 |
53 |
54 | @login_required
55 | def new(request):
56 | if request.method == 'POST':
57 | to_user_username = request.POST.get('to')
58 | message = request.POST.get('message')
59 |
60 | to_user = User.objects.filter(username=to_user_username).first()
61 |
62 | if not to_user:
63 | return redirect('/messages/new/')
64 |
65 | if len(message.strip()) == 0:
66 | return redirect('/messages/new/')
67 |
68 | from_user = request.user
69 | if from_user != to_user:
70 | Message.send_message(from_user, to_user, message)
71 |
72 | return redirect(f'/messages/{to_user_username}/')
73 |
74 | conversations = Message.get_conversations(user=request.user)
75 | context = {'conversations': conversations}
76 | return render(request, 'messages/new.html', context)
77 |
78 |
79 | @login_required
80 | def delete(request):
81 | return HttpResponse()
82 |
83 |
84 | @login_required
85 | def send(request):
86 | if request.method == 'POST':
87 | message = request.POST.get('message')
88 | to_user_username = request.POST.get('to')
89 |
90 | to_user = User.objects.get(username=to_user_username)
91 |
92 | if len(message.strip()) == 0:
93 | return HttpResponse()
94 |
95 | from_user = request.user
96 | if from_user != to_user:
97 | msg = Message.send_message(from_user, to_user, message)
98 | return render(request, 'messages/includes/partial_message.html',
99 | {'message': msg})
100 | return HttpResponse()
101 |
102 | return HttpResponseBadRequest()
103 |
104 |
105 | @login_required
106 | def users(request):
107 | users = User.objects.filter(is_active=True)
108 |
109 | dump = []
110 | template = '{0} ({1})'
111 |
112 | for user in users:
113 | if user.profile.get_screen_name() != user.username:
114 | screen_name = user.profile.get_screen_name()
115 | dump.append(template.format(screen_name, user.username))
116 | else:
117 | dump.append(user.username)
118 |
119 | return JsonResponse(dump, safe=False)
120 |
121 |
122 | @login_required
123 | def check(request):
124 | count = Message.objects.filter(user=request.user, is_read=False).count()
125 | return HttpResponse(count)
126 |
--------------------------------------------------------------------------------
/bootcamp/middleware.py:
--------------------------------------------------------------------------------
1 | from django.core import signing
2 | from django.contrib.auth.models import User
3 |
4 | from django.utils.deprecation import MiddlewareMixin
5 |
6 |
7 | class JWTAuthenticationAndCORSMiddleware(MiddlewareMixin):
8 | def process_request(self, request):
9 | authorization: str = request.META.get('HTTP_AUTHORIZATION')
10 | if not authorization or not authorization.startswith('JWT'):
11 | return
12 |
13 | _, token = authorization.split()
14 | try:
15 | user_id = signing.loads(token)
16 | except signing.BadSignature:
17 | return
18 |
19 | if not user_id or not isinstance(user_id, int):
20 | return
21 |
22 | request.user = User.objects.get(id=user_id)
23 |
24 | def process_response(self, request, response):
25 | response['Access-Control-Allow-Origin'] = '*'
26 | return response
27 |
28 |
--------------------------------------------------------------------------------
/bootcamp/questions/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qulc/bootcamp/de3b68240df225260f0da159269d4d66dfe1fe87/bootcamp/questions/__init__.py
--------------------------------------------------------------------------------
/bootcamp/questions/admin.py:
--------------------------------------------------------------------------------
1 | # Register your models here.
2 | from django.contrib import admin
3 |
4 | from .models import Question, Answer, Tag
5 |
6 | admin.site.register(Question)
7 | admin.site.register(Answer)
8 | admin.site.register(Tag)
9 |
--------------------------------------------------------------------------------
/bootcamp/questions/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.utils.translation import gettext_lazy as _
3 |
4 | from .models import Question, Answer
5 |
6 |
7 | class QuestionForm(forms.ModelForm):
8 | title = forms.CharField(
9 | max_length=255,
10 | label=_('Title'),
11 | widget=forms.TextInput(attrs={'class': 'form-control'})
12 | )
13 | description = forms.CharField(
14 | max_length=2000,
15 | label=_('Description'),
16 | widget=forms.Textarea(attrs={'class': 'form-control'}),
17 | help_text=' ',
18 | )
19 | tags = forms.CharField(
20 | max_length=255,
21 | required=False,
22 | label=_('Tags'),
23 | widget=forms.TextInput(attrs={'class': 'form-control'}),
24 | help_text=_('Use spaces to separate the tags, \
25 | such as "asp.net mvc5 javascript"')
26 | )
27 |
28 | class Meta:
29 | model = Question
30 | fields = ['title', 'description', 'tags']
31 |
32 |
33 | class AnswerForm(forms.ModelForm):
34 | question = forms.ModelChoiceField(widget=forms.HiddenInput(),
35 | queryset=Question.objects.all())
36 | description = forms.CharField(
37 | max_length=2000,
38 | widget=forms.Textarea(attrs={'class': 'form-control', 'rows': '4'})
39 | )
40 |
41 | class Meta:
42 | model = Answer
43 | fields = ['question', 'description']
44 |
--------------------------------------------------------------------------------
/bootcamp/questions/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2 on 2023-04-06 10:01
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='Question',
19 | fields=[
20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21 | ('title', models.CharField(max_length=255)),
22 | ('description', models.TextField(max_length=2000)),
23 | ('create_date', models.DateTimeField(auto_now_add=True)),
24 | ('update_date', models.DateTimeField(auto_now_add=True)),
25 | ('favorites', models.IntegerField(default=0)),
26 | ('has_accepted_answer', models.BooleanField(default=False)),
27 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
28 | ],
29 | options={
30 | 'verbose_name': 'Question',
31 | 'verbose_name_plural': 'Questions',
32 | 'ordering': ('-update_date',),
33 | },
34 | ),
35 | migrations.CreateModel(
36 | name='Tag',
37 | fields=[
38 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
39 | ('tag', models.CharField(max_length=50)),
40 | ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='questions.question')),
41 | ],
42 | options={
43 | 'verbose_name': 'Tag',
44 | 'verbose_name_plural': 'Tags',
45 | },
46 | ),
47 | migrations.CreateModel(
48 | name='Answer',
49 | fields=[
50 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
51 | ('description', models.TextField(max_length=2000)),
52 | ('create_date', models.DateTimeField(auto_now_add=True)),
53 | ('update_date', models.DateTimeField(blank=True, null=True)),
54 | ('votes', models.IntegerField(default=0)),
55 | ('is_accepted', models.BooleanField(default=False)),
56 | ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='questions.question')),
57 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
58 | ],
59 | options={
60 | 'verbose_name': 'Answer',
61 | 'verbose_name_plural': 'Answers',
62 | 'ordering': ('-is_accepted', '-votes', 'create_date'),
63 | },
64 | ),
65 | migrations.AddConstraint(
66 | model_name='tag',
67 | constraint=models.UniqueConstraint(fields=('tag', 'question'), name='unique_question_tag'),
68 | ),
69 | ]
70 |
--------------------------------------------------------------------------------
/bootcamp/questions/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qulc/bootcamp/de3b68240df225260f0da159269d4d66dfe1fe87/bootcamp/questions/migrations/__init__.py
--------------------------------------------------------------------------------
/bootcamp/questions/models.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from django.db import models
4 | from django.contrib.auth.models import User
5 |
6 | from bootcamp.activities.models import Activity
7 |
8 |
9 | class Question(models.Model):
10 | user = models.ForeignKey(User, on_delete=models.CASCADE)
11 | title = models.CharField(max_length=255)
12 | description = models.TextField(max_length=2000)
13 | create_date = models.DateTimeField(auto_now_add=True)
14 | update_date = models.DateTimeField(auto_now_add=True)
15 | favorites = models.IntegerField(default=0)
16 | has_accepted_answer = models.BooleanField(default=False)
17 |
18 | class Meta:
19 | verbose_name = 'Question'
20 | verbose_name_plural = 'Questions'
21 | ordering = ('-update_date',)
22 |
23 | def __str__(self):
24 | return self.title
25 |
26 | @staticmethod
27 | def get_unanswered():
28 | return Question.objects.filter(has_accepted_answer=False)
29 |
30 | @staticmethod
31 | def get_answered():
32 | return Question.objects.filter(has_accepted_answer=True)
33 |
34 | def get_answers(self):
35 | return Answer.objects.filter(question=self)
36 |
37 | def get_answers_count(self):
38 | return Answer.objects.filter(question=self).count()
39 |
40 | def get_accepted_answer(self):
41 | return Answer.objects.get(question=self, is_accepted=True)
42 |
43 | def get_description_as_markdown(self):
44 | return self.description
45 |
46 | def get_description_preview(self):
47 | preview_len = 255
48 |
49 | if len(self.description) > preview_len:
50 | return f'{self.description[:preview_len]}...'
51 |
52 | return self.description
53 |
54 | def get_description_preview_as_markdown(self):
55 | return self.get_description_preview()
56 |
57 | def calculate_favorites(self):
58 | favorites = Activity.objects.filter(
59 | question=self.pk,
60 | activity_type=Activity.FAVORITE
61 | ).count()
62 |
63 | self.favorites = favorites
64 | self.save()
65 | return self.favorites
66 |
67 | def get_favoriters(self):
68 | favorites = Activity.objects.filter(
69 | question=self.pk,
70 | activity_type=Activity.FAVORITE
71 | )
72 |
73 | favoriters = [favorite.user for favorite in favorites]
74 | return favoriters
75 |
76 | def create_tags(self, tags):
77 | tags = tags.strip()
78 | tag_list = tags.split(' ')
79 |
80 | for tag in tag_list:
81 | Tag.objects.get_or_create(tag=tag.lower(), question=self)
82 |
83 | def get_tags(self):
84 | return Tag.objects.filter(question=self)
85 |
86 |
87 | class Answer(models.Model):
88 | user = models.ForeignKey(User, on_delete=models.CASCADE)
89 | question = models.ForeignKey(Question, on_delete=models.CASCADE)
90 | description = models.TextField(max_length=2000)
91 | create_date = models.DateTimeField(auto_now_add=True)
92 | update_date = models.DateTimeField(null=True, blank=True)
93 | votes = models.IntegerField(default=0)
94 | is_accepted = models.BooleanField(default=False)
95 |
96 | class Meta:
97 | verbose_name = 'Answer'
98 | verbose_name_plural = 'Answers'
99 | ordering = ('-is_accepted', '-votes', 'create_date',)
100 |
101 | def __str__(self):
102 | return self.description
103 |
104 | def accept(self):
105 | answers = Answer.objects.filter(question=self.question)
106 |
107 | for answer in answers:
108 | answer.is_accepted = False
109 | answer.save()
110 |
111 | self.is_accepted = True
112 | self.save()
113 | self.question.has_accepted_answer = True
114 | self.question.save()
115 |
116 | def calculate_votes(self):
117 | up_votes = Activity.objects.filter(
118 | answer=self.pk,
119 | activity_type=Activity.UP_VOTE,
120 | ).count()
121 |
122 | down_votes = Activity.objects.filter(
123 | answer=self.pk,
124 | activity_type=Activity.DOWN_VOTE,
125 | ).count()
126 |
127 | self.votes = up_votes - down_votes
128 | self.save()
129 | return self.votes
130 |
131 | def get_up_voters(self):
132 | votes = Activity.objects.filter(answer=self.pk,
133 | activity_type=Activity.UP_VOTE,)
134 | voters = [vote.user for vote in votes]
135 | return voters
136 |
137 | def get_down_voters(self):
138 | votes = Activity.objects.filter(answer=self.pk,
139 | activity_type=Activity.DOWN_VOTE)
140 | voters = [vote.user for vote in votes]
141 | return voters
142 |
143 | def get_description_as_markdown(self):
144 | return self.description
145 |
146 |
147 | class Tag(models.Model):
148 | tag = models.CharField(max_length=50)
149 | question = models.ForeignKey(Question, on_delete=models.CASCADE)
150 |
151 | class Meta:
152 | verbose_name = 'Tag'
153 | verbose_name_plural = 'Tags'
154 |
155 | constraints = [
156 | models.UniqueConstraint(fields=('tag', 'question'), name='unique_question_tag')
157 | ]
158 |
159 | def __str__(self):
160 | return self.tag
161 |
--------------------------------------------------------------------------------
/bootcamp/questions/static/css/questions.css:
--------------------------------------------------------------------------------
1 | .questions {
2 | margin-top: 1em;
3 | }
4 |
5 | .questions .pagination {
6 | margin: 0;
7 | }
8 |
9 | .questions .user {
10 | width: 25px;
11 | border-radius: 3px;
12 | }
13 |
14 | .questions .asked {
15 | color: #aaaaaa;
16 | font-size: .8em;
17 | }
18 |
19 | .questions .username {
20 | margin-left: .3em;
21 | font-weight: 500;
22 | font-size: 0.8em;
23 | }
24 |
25 | .questions .panel-body:hover {
26 | background-color: #f5f8fa;
27 | cursor: pointer;
28 | }
29 |
30 | .questions .question-user {
31 | margin-bottom: .6em;
32 | }
33 |
34 | .options {
35 | text-align: center;
36 |
37 | }
38 |
39 | .options span {
40 | display: block;
41 | font-size: 1.2em;
42 | }
43 |
44 | .options span.favorite {
45 | font-size: 3em;
46 | margin-top: .4em;
47 | }
48 |
49 | .options span.vote {
50 | font-size: 2em;
51 | color: #ddd;
52 | }
53 |
54 | .options span.vote:hover {
55 | cursor: pointer;
56 | color: #428bca;
57 | }
58 |
59 | .options span.voted {
60 | color: #333;
61 | }
62 |
63 | .options span.accept {
64 | font-size: 2.6em;
65 | color: #dddddd;
66 | margin-top: .2em;
67 | }
68 |
69 | .options span.accept:hover {
70 | cursor: pointer;
71 | color: #5cb85c;
72 | }
73 |
74 | .options span.accepted {
75 | color: #5cb85c;
76 | }
77 |
78 | .answer {
79 | margin-top: 1em;
80 | }
81 |
82 | .answer .answer-user, .question .question-user {
83 | margin-bottom: .6em;
84 | }
85 |
86 | .answer .answer-user .user, .question .question-user .user {
87 | width: 40px;
88 | border-radius: 5px;
89 | }
90 |
91 | .answer .answer-user .username, .question .question-user .username {
92 | margin-left: .4em;
93 | }
94 |
95 | .answer .answered, .question .asked {
96 | color: #aaaaaa;
97 | margin-left: .6em;
98 | }
99 |
100 | .favorite {
101 | cursor: pointer;
102 | }
103 |
104 | .favorite:hover {
105 | color: #f0ad4e;
106 | }
107 |
108 | .favorited {
109 | color: #f0ad4e;
110 | }
111 |
112 | .question-info {
113 | text-align: center;
114 | float: right;
115 | padding: 0 1em;
116 | }
117 |
118 | .question-info h5 {
119 | margin: 0;
120 | margin-bottom: .2em;
121 | }
122 |
123 | .question-info .info:first-child {
124 | margin-bottom: 1.2em;
125 | }
--------------------------------------------------------------------------------
/bootcamp/questions/static/js/questions.js:
--------------------------------------------------------------------------------
1 | $(function () {
2 | $(".question .panel-body").click(function () {
3 | var question_id = $(this).closest(".question").attr("question-id");
4 | location.href = "/questions/" + question_id;
5 | });
6 |
7 | $(".accept").click(function () {
8 | var span = $(this);
9 | var question = $(".question").attr("question-id");
10 | var answer = $(this).closest(".answer").attr("answer-id");
11 | var csrf = $("input[name='csrfmiddlewaretoken']", $(this).closest(".answer")).val();
12 | $.ajax({
13 | url: '/questions/answer/accept/',
14 | data: {
15 | 'question': question,
16 | 'answer': answer,
17 | 'csrfmiddlewaretoken': csrf
18 | },
19 | type: 'post',
20 | cache: false,
21 | success: function (data) {
22 | $(".accept").removeClass("accepted");
23 | $(".accept").prop("title", "Click to accept the answer");
24 | $(span).addClass("accepted");
25 | $(span).prop("title", "Click to unaccept the answer");
26 | }
27 | });
28 | });
29 |
30 | $(".vote").click(function () {
31 | var span = $(this);
32 | var answer = $(this).closest(".answer").attr("answer-id");
33 | var csrf = $("input[name='csrfmiddlewaretoken']", $(this).closest(".answer")).val();
34 | var vote = "";
35 | if ($(this).hasClass("voted")) {
36 | var vote = "R";
37 | }
38 | else if ($(this).hasClass("up-vote")) {
39 | vote = "U";
40 | }
41 | else if ($(this).hasClass("down-vote")) {
42 | vote = "D";
43 | }
44 | $.ajax({
45 | url: '/questions/answer/vote/',
46 | data: {
47 | 'answer': answer,
48 | 'vote': vote,
49 | 'csrfmiddlewaretoken': csrf
50 | },
51 | type: 'post',
52 | cache: false,
53 | success: function (data) {
54 | var options = $(span).closest('.options');
55 | $('.vote', options).removeClass('voted');
56 | if (vote == 'U' || vote == 'D') {
57 | $(span).addClass('voted');
58 | }
59 | $('.votes', options).text(data);
60 | }
61 | });
62 | });
63 |
64 | $(".favorite").click(function () {
65 | var span = $(this);
66 | var question = $(this).closest(".question").attr("question-id");
67 | var csrf = $("input[name='csrfmiddlewaretoken']", $(this).closest(".question")).val();
68 |
69 | $.ajax({
70 | url: '/questions/favorite/',
71 | data: {
72 | 'question': question,
73 | 'csrfmiddlewaretoken': csrf
74 | },
75 | type: 'post',
76 | cache: false,
77 | success: function (data) {
78 | if ($(span).hasClass("favorited")) {
79 | $(span).removeClass("glyphicon-star")
80 | .removeClass("favorited")
81 | .addClass("glyphicon-star-empty");
82 | }
83 | else {
84 | $(span).removeClass("glyphicon-star-empty")
85 | .addClass("glyphicon-star")
86 | .addClass("favorited");
87 | }
88 | $(".favorite-count").text(data);
89 | }
90 | });
91 |
92 | });
93 | });
--------------------------------------------------------------------------------
/bootcamp/questions/templates/questions/ask.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load i18n %}
3 |
4 | {% block main %}
5 |
6 | - {% trans 'Questions' %}
7 | - {% trans 'Ask Question' %}
8 |
9 |
35 | {% endblock main %}
36 |
--------------------------------------------------------------------------------
/bootcamp/questions/templates/questions/partial_answer.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load humanize %}
3 |
4 |
5 | {% csrf_token %}
6 |
7 |
8 | {{ answer.votes }}
9 |
10 | {% if answer.is_accepted and user == question.user %}
11 |
12 | {% elif answer.is_accepted %}
13 |
14 | {% elif user == question.user %}
15 |
16 | {% endif %}
17 |
18 |
19 |
24 |
25 | {{ answer.get_description_as_markdown|safe }}
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/bootcamp/questions/templates/questions/partial_question.html:
--------------------------------------------------------------------------------
1 | {% load humanize %}
2 | {% load i18n %}
3 |
4 |
5 |
6 |
7 | {% if question.has_accepted_answer %}
8 |
9 | {% endif %}
10 | {{ question.title }}
11 |
12 |
13 |
14 |
15 |
16 |
{% trans 'Answers' %}
17 | {{ question.get_answers_count }}
18 |
19 |
20 |
{% trans 'Favorites' %}
21 | {{ question.favorites }}
22 |
23 |
24 |
29 |
30 | {{ question.get_description_preview_as_markdown|safe }}
31 |
32 | {% if question.get_tags %}
33 |
34 | {% for tag in question.get_tags %}
35 | {{ tag }}
36 | {% endfor %}
37 |
38 | {% endif %}
39 |
40 |
--------------------------------------------------------------------------------
/bootcamp/questions/templates/questions/question.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load i18n %}
3 | {% load static %}
4 |
5 | {% load humanize %}
6 |
7 | {% block head %}
8 |
9 |
10 | {% endblock head %}
11 |
12 | {% block main %}
13 |
14 | - {% trans "Questions" %}
15 | - {% trans "Question" %}
16 |
17 |
18 | {% csrf_token %}
19 |
20 | {% if user.is_anonymous %}
21 |
23 | {% elif user in question.get_favoriters %}
24 |
26 | {% else %}
27 |
29 | {% endif %}
30 | {{ question.favorites }}
31 |
32 |
33 |
{{ question.title }}
34 |
35 |
42 |
43 | {{ question.get_description_as_markdown|safe }}
44 |
45 | {% if question.get_tag_list %}
46 |
47 | {% for tag in question.get_tag_list %}
48 | {{ tag }}
49 | {% endfor %}
50 |
51 | {% endif %}
52 |
53 |
54 |
55 |
56 | {% for answer in question.get_answers %}
57 | {% include 'questions/partial_answer.html' with question=question answer=answer %}
58 | {% endfor %}
59 |
60 | {% if not user.is_anonymous %}
61 |
{% trans 'Your Answer' %}
62 |
63 |
74 | {% endif %}
75 |
76 | {% endblock main %}
77 |
--------------------------------------------------------------------------------
/bootcamp/questions/templates/questions/questions.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load i18n %}
3 | {% load static %}
4 |
5 | {% block title %}{% trans 'Questions' %}{% endblock %}
6 |
7 | {% block head %}
8 |
9 |
10 | {% endblock head %}
11 |
12 | {% block main %}
13 |
21 |
22 |
30 | {% for question in questions %}
31 | {% include 'questions/partial_question.html' with question=question %}
32 | {% empty %}
33 |
{% trans "No question to display" %}
34 | {% endfor %}
35 | {% include 'paginator.html' with paginator=questions %}
36 |
37 | {% endblock main %}
38 |
--------------------------------------------------------------------------------
/bootcamp/questions/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 | from django.contrib.auth.models import User
3 | from django.db.models.query import QuerySet
4 |
5 | from .models import Question, Answer
6 |
7 |
8 | class QuestionsMethodTests(TestCase):
9 | def setUp(self):
10 | user = User.objects.create_user(
11 | username='john',
12 | email='lennon@thebeatles.com',
13 | password='johnpassword'
14 | )
15 | self.question = Question.objects.create(
16 | user=user,
17 | title='test title',
18 | description='test decorators',
19 | )
20 | Answer.objects.create(
21 | user=user,
22 | question=self.question,
23 | description='test answers decorators',
24 | is_accepted=True,
25 | )
26 |
27 | def test_get_answers(self):
28 | answers = self.question.get_answers()
29 | self.assertIsInstance(answers, QuerySet)
30 |
31 | answers_count = self.question.get_answers_count()
32 | self.assertIsInstance(answers_count, int)
33 |
34 | accept_answer = self.question.get_accepted_answer()
35 | self.assertIsInstance(accept_answer, Answer)
36 |
37 | def test_get_favorites(self):
38 | favorites_count = self.question.calculate_favorites()
39 | self.assertEqual(favorites_count, 0)
40 |
41 | favoriters = self.question.get_favoriters()
42 | self.assertIsInstance(favoriters, list)
43 |
44 | def test_tags(self):
45 | tags = 'Python Tornado Async'
46 | self.question.create_tags(tags)
47 |
48 | tag_list = self.question.get_tags()
49 | self.assertIsInstance(tag_list, QuerySet)
50 |
51 | expect_tag = ('python', 'tornado', 'async')
52 | for tag in tag_list:
53 | self.assertIn(tag.tag, expect_tag)
54 |
55 |
56 | class AnswerMethodTests(TestCase):
57 | def setUp(self):
58 | user = User.objects.create_user(
59 | username='john_1',
60 | email='lennon_1@thebeatles.com',
61 | password='johnpassword'
62 | )
63 | question = Question.objects.create(
64 | user=user,
65 | title='test title 1',
66 | description='test decorators 1',
67 | )
68 | self.answer = Answer.objects.create(
69 | user=user,
70 | question=question,
71 | description='test answers decorators 1',
72 | is_accepted=True,
73 | )
74 |
75 | def test_get_vote(self):
76 | self.answer.accept()
77 |
78 | votes_count = self.answer.calculate_votes()
79 | self.assertIsInstance(votes_count, int)
80 |
81 | up_voters = self.answer.get_up_voters()
82 | self.assertIsInstance(up_voters, list)
83 |
84 | down_voters = self.answer.get_down_voters()
85 | self.assertIsInstance(down_voters, list)
86 |
--------------------------------------------------------------------------------
/bootcamp/questions/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from . import views
4 |
5 | urlpatterns = [
6 | path('', views.questions, name='questions'),
7 | path('/', views.question, name='question'),
8 | path('ask/', views.ask, name='ask'),
9 | path('all/', views.all_question, name='all'),
10 | path('answered/', views.answered, name='answered'),
11 | path('unanswered/', views.unanswered, name='unanswered'),
12 | path('favorite/', views.favorite, name='favorite'),
13 | path('answer/', views.answer, name='answer'),
14 | path('answer/accept/', views.accept, name='accept'),
15 | path('answer/vote/', views.vote, name='vote'),
16 | ]
17 |
--------------------------------------------------------------------------------
/bootcamp/questions/views.py:
--------------------------------------------------------------------------------
1 | from django.db.models import Q
2 | from django.contrib.auth.decorators import login_required
3 | from django.http import HttpResponse, HttpResponseForbidden
4 | from django.shortcuts import render, redirect, get_object_or_404
5 | from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
6 |
7 | from bootcamp.activities.models import Activity
8 |
9 | from .models import Question, Answer
10 | from .forms import QuestionForm, AnswerForm
11 |
12 |
13 | def _questions(request, questions, active):
14 | paginator = Paginator(questions, 10)
15 | page = request.GET.get('page')
16 |
17 | try:
18 | questions = paginator.page(page)
19 | except PageNotAnInteger:
20 | questions = paginator.page(1)
21 | except EmptyPage:
22 | questions = paginator.page(paginator.num_pages)
23 |
24 | context = {'questions': questions, 'active': active}
25 | return render(request, 'questions/questions.html', context)
26 |
27 |
28 | def questions(request):
29 | return unanswered(request)
30 |
31 |
32 | def answered(request):
33 | questions = Question.get_answered()
34 | return _questions(request, questions, 'answered')
35 |
36 |
37 | def unanswered(request):
38 | questions = Question.get_unanswered()
39 | return _questions(request, questions, 'unanswered')
40 |
41 |
42 | def all_question(request):
43 | questions = Question.objects.all()
44 | return _questions(request, questions, 'all')
45 |
46 |
47 | @login_required
48 | def ask(request):
49 | if request.method == 'POST':
50 | form = QuestionForm(request.POST)
51 | if not form.is_valid():
52 | return render(request, 'questions/ask.html', {'form': form})
53 |
54 | question = Question()
55 | question.user = request.user
56 | question.title = form.cleaned_data.get('title')
57 | question.description = form.cleaned_data.get('description')
58 | question.save()
59 |
60 | tags = form.cleaned_data.get('tags')
61 | question.create_tags(tags)
62 | return redirect('/questions/')
63 | else:
64 | form = QuestionForm()
65 |
66 | return render(request, 'questions/ask.html', {'form': form})
67 |
68 |
69 | def question(request, pk):
70 | question = get_object_or_404(Question, pk=pk)
71 | form = AnswerForm(initial={'question': question})
72 | context = {'question': question, 'form': form}
73 | return render(request, 'questions/question.html', context)
74 |
75 |
76 | @login_required
77 | def answer(request):
78 | if request.method == 'POST':
79 | form = AnswerForm(request.POST)
80 | if form.is_valid():
81 | user = request.user
82 | answer = Answer()
83 | answer.user = request.user
84 | answer.question = form.cleaned_data.get('question')
85 | answer.description = form.cleaned_data.get('description')
86 | answer.save()
87 | user.profile.notify_answered(answer.question)
88 | return redirect(f'/questions/{answer.question.pk}/')
89 | else:
90 | question = form.cleaned_data.get('question')
91 | context = {'question': question, 'form': form}
92 | return render(request, 'questions/question.html', context)
93 | else:
94 | return redirect('/questions/')
95 |
96 |
97 | @login_required
98 | def accept(request):
99 | answer_id = request.POST['answer']
100 | answer = Answer.objects.get(pk=answer_id)
101 | user = request.user
102 |
103 | # answer.accept cleans previous accepted answer
104 | user.profile.unotify_accepted(answer.question.get_accepted_answer())
105 |
106 | if answer.question.user == user:
107 | answer.accept()
108 | user.profile.notify_accepted(answer)
109 | return HttpResponse()
110 | else:
111 | return HttpResponseForbidden()
112 |
113 |
114 | @login_required
115 | def vote(request):
116 | answer_id = request.POST['answer']
117 | answer = Answer.objects.get(pk=answer_id)
118 | vote = request.POST['vote']
119 | user = request.user
120 |
121 | activity = Activity.objects.filter(
122 | Q(activity_type=Activity.UP_VOTE) |
123 | Q(activity_type=Activity.DOWN_VOTE),
124 | user=user,
125 | answer=answer_id,
126 | )
127 |
128 | if activity:
129 | activity.delete()
130 |
131 | if vote in [Activity.UP_VOTE, Activity.DOWN_VOTE]:
132 | activity = Activity(activity_type=vote, user=user, answer=answer_id)
133 | activity.save()
134 |
135 | return HttpResponse(answer.calculate_votes())
136 |
137 |
138 | @login_required
139 | def favorite(request):
140 | question_id = request.POST['question']
141 | question = Question.objects.get(pk=question_id)
142 | user = request.user
143 |
144 | activity = Activity.objects.filter(
145 | user=user,
146 | question=question_id,
147 | activity_type=Activity.FAVORITE,
148 | )
149 |
150 | if activity:
151 | activity.delete()
152 | user.profile.unotify_favorited(question)
153 | else:
154 | activity = Activity(
155 | user=user,
156 | question=question_id,
157 | activity_type=Activity.FAVORITE,
158 | )
159 | activity.save()
160 | user.profile.notify_favorited(question)
161 |
162 | return HttpResponse(question.calculate_favorites())
163 |
--------------------------------------------------------------------------------
/bootcamp/schema.py:
--------------------------------------------------------------------------------
1 | import graphene
2 |
3 | from .feeds.schema import FeedQuery, CreateFeed, LikeFeed
4 | from .authentication.schema import UserQuery, CreateToken
5 |
6 |
7 | class Query(FeedQuery, UserQuery, graphene.ObjectType):
8 | node = graphene.Node.Field()
9 |
10 |
11 | class Mutations(graphene.ObjectType):
12 | create_feed = CreateFeed.Field()
13 | like_feed = LikeFeed.Field()
14 | create_token = CreateToken.Field()
15 |
16 |
17 | schema = graphene.Schema(query=Query, mutation=Mutations)
18 |
--------------------------------------------------------------------------------
/bootcamp/search/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qulc/bootcamp/de3b68240df225260f0da159269d4d66dfe1fe87/bootcamp/search/__init__.py
--------------------------------------------------------------------------------
/bootcamp/search/static/css/search.css:
--------------------------------------------------------------------------------
1 | .results ul {
2 | padding: 0;
3 | }
4 |
5 | .results ul li {
6 | list-style: none;
7 | padding: .8em .4em;
8 | border-bottom: 1px solid #eeeeee;
9 | }
10 |
11 | .results ul li:last-child {
12 | border-bottom: none;
13 | }
14 |
15 | .results ul li img {
16 | width: 45px;
17 | border-radius: 5px;
18 | }
19 |
20 | .feed-results h3 {
21 | margin: 0;
22 | font-size: 1.2em;
23 | }
24 |
25 | .feed-results .post {
26 | margin-left: 55px;
27 | padding-top: .2em;
28 | }
29 |
30 | .feed-results .post p {
31 | margin: 0;
32 | margin-top: .2em;
33 | }
34 |
35 | [class$='-results'] li:hover {
36 | cursor: pointer;
37 | background-color: #f5f8fa;
38 | }
39 |
40 | .info {
41 | margin-bottom: .5em;
42 | color: #a0a0a0;
43 | }
44 |
45 | .info a {
46 | color: #a0a0a0;
47 | }
48 |
49 | .info > span {
50 | margin-right: 1em;
51 | }
52 |
53 | .info .user img {
54 | width: 20px;
55 | border-radius: 3px;
56 | }
57 |
58 | .no-result {
59 | margin-top: 2em;
60 | }
61 |
62 | .result-user {
63 | width: 20px;
64 | border-radius: 4px;
65 | margin-right: .2em;
66 | }
67 |
68 | .results {
69 | margin-top: 1em;
70 | }
71 |
72 | .article-content ul,
73 | .article-content ol,
74 | .question-description ul,
75 | .question-description ol {
76 | padding-left: 20px;
77 | }
78 |
79 | .article-content ul li,
80 | .question-description ul li {
81 | border-bottom: none;
82 | list-style: disc;
83 | padding: 0;
84 | }
85 |
86 | .article-content ol li,
87 | .question-description ol li {
88 | border-bottom: none;
89 | list-style: decimal;
90 | padding: 0;
91 | }
--------------------------------------------------------------------------------
/bootcamp/search/static/js/search.js:
--------------------------------------------------------------------------------
1 | $(function () {
2 | $(".feed-results li").click(function () {
3 | var feed = $(this).attr("feed-id");
4 | location.href = "/feeds/" + feed + "/";
5 | });
6 |
7 | $(".articles-results li").click(function () {
8 | var article = $(this).attr("article-slug");
9 | location.href = "/articles/" + article + "/";
10 | });
11 |
12 | $(".questions-results li").click(function () {
13 | var question = $(this).attr("question-id");
14 | location.href = "/questions/" + question + "/";
15 | });
16 |
17 | });
--------------------------------------------------------------------------------
/bootcamp/search/templates/search/partial_articles_results.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 | {% trans 'Articles' %}
4 | {% if results %}
5 |
6 | {% for article in results %}
7 | -
8 |
9 |
19 |
{{ article.get_summary_as_markdown|safe }}
20 |
21 | {% endfor %}
22 |
23 | {% else %}
24 | {% trans 'No article found' %} :(
25 | {% endif %}
--------------------------------------------------------------------------------
/bootcamp/search/templates/search/partial_feed_results.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load humanize %}
3 |
4 | {% trans 'Feed' %}
5 | {% if results %}
6 |
7 | {% for feed in results %}
8 | -
9 |
10 |
11 |
12 |
13 |
17 |
{{ feed.post|safe }}
18 |
19 |
20 | {% endfor %}
21 |
22 | {% else %}
23 | {% trans 'No feed found' %} :(
24 | {% endif %}
25 |
--------------------------------------------------------------------------------
/bootcamp/search/templates/search/partial_questions_results.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load humanize %}
3 |
4 | {% trans 'Questions' %}
5 | {% if results %}
6 |
21 | {% else %}
22 | {% trans 'No question found' %} :(
23 | {% endif %}
--------------------------------------------------------------------------------
/bootcamp/search/templates/search/partial_results_menu.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 |
--------------------------------------------------------------------------------
/bootcamp/search/templates/search/partial_users_results.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 | {% trans 'Users' %}
4 | {% if results %}
5 |
6 | {% for user_result in results %}
7 | -
8 |
15 |
16 | {% endfor %}
17 |
18 | {% else %}
19 | {% trans 'No user found' %} :(
20 | {% endif %}
--------------------------------------------------------------------------------
/bootcamp/search/templates/search/results.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% load i18n %}
4 | {% load humanize %}
5 | {% load static %}
6 |
7 | {% block title %} Search {% endblock %}
8 |
9 | {% block head %}
10 |
11 |
12 | {% endblock head %}
13 |
14 | {% block main %}
15 |
33 |
34 |
35 | {% include 'search/partial_results_menu.html' with active=active count=count querystring=querystring %}
36 |
37 |
38 | {% if active == 'feed' %}
39 | {% include 'search/partial_feed_results.html' with results=results %}
40 | {% elif active == 'articles' %}
41 | {% include 'search/partial_articles_results.html' with results=results %}
42 | {% elif active == 'questions' %}
43 | {% include 'search/partial_questions_results.html' with results=results %}
44 | {% elif active == 'users' %}
45 | {% include 'search/partial_users_results.html' with results=results %}
46 | {% endif %}
47 |
48 |
49 | {% endblock main %}
50 |
--------------------------------------------------------------------------------
/bootcamp/search/templates/search/search.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% load i18n %}
4 | {% load humanize %}
5 | {% load static %}
6 |
7 | {% block title %} Search {% endblock %}
8 |
9 | {% block head %}
10 |
11 | {% endblock head %}
12 |
13 | {% block main %}
14 |
17 |
18 |
19 |
20 |
{% trans "Search Feed, Articles, Questions and Users" %}
21 |
29 |
30 |
31 | {% endblock main %}
32 |
--------------------------------------------------------------------------------
/bootcamp/search/tests.py:
--------------------------------------------------------------------------------
1 | # Create your tests here.
2 |
--------------------------------------------------------------------------------
/bootcamp/search/views.py:
--------------------------------------------------------------------------------
1 | from django.db.models import Q
2 | from django.contrib.auth.models import User
3 | from django.shortcuts import render, redirect
4 |
5 | from bootcamp.feeds.models import Feed
6 | from bootcamp.articles.models import Article
7 | from bootcamp.questions.models import Question
8 |
9 |
10 | def search(request):
11 | if 'q' not in request.GET:
12 | return render(request, 'search/search.html', {'hide_search': True})
13 |
14 | querystring = request.GET.get('q').strip()
15 | search_type = request.GET.get('type')
16 |
17 | if len(querystring) == 0:
18 | return redirect('/search/')
19 |
20 | if search_type not in ['feed', 'articles', 'questions', 'users']:
21 | search_type = 'feed'
22 |
23 | results = {
24 | 'feed': Feed.objects.filter(
25 | post__icontains=querystring, parent=None),
26 | 'articles': Article.objects.filter(
27 | Q(title__icontains=querystring) |
28 | Q(content__icontains=querystring)),
29 | 'questions': Question.objects.filter(
30 | Q(title__icontains=querystring) |
31 | Q(description__icontains=querystring)),
32 | 'users': User.objects.filter(
33 | Q(username__icontains=querystring) |
34 | Q(first_name__icontains=querystring) |
35 | Q(last_name__icontains=querystring))
36 | }
37 | count = {
38 | 'feed': results['feed'].count(),
39 | 'users': results['users'].count(),
40 | 'articles': results['articles'].count(),
41 | 'questions': results['questions'].count()
42 | }
43 | context = {
44 | 'hide_search': True,
45 | 'querystring': querystring,
46 | 'active': search_type,
47 | 'count': count,
48 | 'results': results[search_type]
49 | }
50 | return render(request, 'search/results.html', context)
51 |
--------------------------------------------------------------------------------
/bootcamp/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for bootcamp project.
3 |
4 | Generated by 'django-admin startproject' using Django 5.1.4.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/4.1/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/4.1/ref/settings/
11 | """
12 |
13 | import os
14 | from pathlib import Path
15 |
16 | # Build paths inside the project like this: BASE_DIR / 'subdir'.
17 | PROJECT_ROOT = Path(__file__).resolve().parent
18 | BASE_DIR = PROJECT_ROOT.parent
19 |
20 |
21 | # Quick-start development settings - unsuitable for production
22 | # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
23 |
24 | # SECURITY WARNING: keep the secret key used in production secret!
25 | SECRET_KEY = 'django-insecure-9of492%_4i#f8gff&9o)ge&wz4n5hvk653a8)yo#-w=6!ehce_'
26 |
27 | # SECURITY WARNING: don't run with debug turned on in production!
28 | DEBUG = 'DEBUG' in os.environ
29 |
30 | ALLOWED_HOSTS = ['*']
31 |
32 | # Application definition
33 |
34 | INSTALLED_APPS = [
35 | 'django.contrib.admin',
36 | 'django.contrib.auth',
37 | 'django.contrib.contenttypes',
38 | 'django.contrib.sessions',
39 | 'django.contrib.messages',
40 | 'django.contrib.staticfiles',
41 | 'django.contrib.humanize',
42 |
43 | 'bootcamp.activities',
44 | 'bootcamp.articles',
45 | 'bootcamp.authentication',
46 | 'bootcamp.core',
47 | 'bootcamp.feeds',
48 | 'bootcamp.messenger',
49 | 'bootcamp.questions',
50 | 'bootcamp.search',
51 |
52 | 'graphene_django',
53 | ]
54 |
55 | MIDDLEWARE = [
56 | 'django.middleware.security.SecurityMiddleware',
57 | 'django.contrib.sessions.middleware.SessionMiddleware',
58 | 'django.middleware.common.CommonMiddleware',
59 | 'django.middleware.csrf.CsrfViewMiddleware',
60 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
61 | 'django.contrib.messages.middleware.MessageMiddleware',
62 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
63 | 'django.middleware.gzip.GZipMiddleware',
64 | 'django.middleware.http.ConditionalGetMiddleware',
65 | 'bootcamp.middleware.JWTAuthenticationAndCORSMiddleware'
66 | ]
67 |
68 | ROOT_URLCONF = 'bootcamp.urls'
69 |
70 | TEMPLATES = [
71 | {
72 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
73 | 'DIRS': [
74 | PROJECT_ROOT.joinpath('templates'),
75 | ],
76 | 'APP_DIRS': True,
77 | 'OPTIONS': {
78 | 'context_processors': [
79 | 'django.template.context_processors.debug',
80 | 'django.template.context_processors.request',
81 | 'django.contrib.auth.context_processors.auth',
82 | 'django.contrib.messages.context_processors.messages',
83 | ]
84 | },
85 | },
86 | ]
87 |
88 | WSGI_APPLICATION = 'bootcamp.wsgi.application'
89 |
90 | # Database
91 | # https://docs.djangoproject.com/en/5.1/ref/settings/#databases
92 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
93 |
94 | DATABASES = {
95 | 'default': {
96 | 'ENGINE': 'django.db.backends.sqlite3',
97 | 'NAME': BASE_DIR / 'db.sqlite3',
98 | }
99 | }
100 |
101 |
102 | if 'REDIS_URL' in os.environ:
103 | CACHES = {
104 | "default": {
105 | "BACKEND": "django.core.cache.backends.redis.RedisCache",
106 | "LOCATION": os.getenv('REDIS_URL'),
107 | }
108 | }
109 |
110 | # Password validation
111 | # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
112 |
113 | AUTH_PASSWORD_VALIDATORS = [
114 | {
115 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
116 | },
117 | {
118 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
119 | },
120 | {
121 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
122 | },
123 | {
124 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
125 | },
126 | ]
127 |
128 |
129 | # Internationalization
130 | # https://docs.djangoproject.com/en/5.1/topics/i18n/
131 |
132 | USE_TZ = True
133 | TIME_ZONE = 'Asia/Shanghai'
134 |
135 | USE_I18N = True
136 |
137 | LANGUAGE_CODE = 'zh-Hans'
138 |
139 | LANGUAGES = (
140 | ('en', 'English'),
141 | ('zh-Hans', 'Chinese')
142 | )
143 |
144 | LOCALE_PATHS = (
145 | PROJECT_ROOT.joinpath('locale'),
146 | )
147 |
148 | # Static files (CSS, JavaScript, Images)
149 | # https://docs.djangoproject.com/en/5.1/howto/static-files/
150 |
151 | STATIC_URL = '/static/'
152 | STATIC_ROOT = BASE_DIR.joinpath('staticfiles')
153 |
154 | STATICFILES_DIRS = (
155 | PROJECT_ROOT.joinpath('static'),
156 | )
157 |
158 | # Default primary key field type
159 | # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
160 |
161 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
162 |
163 | # Logging
164 | # https://docs.djangoproject.com/en/5.1/topics/logging/
165 | LOGGING = {
166 | 'version': 1,
167 | 'disable_existing_loggers': False,
168 | 'handlers': {
169 | 'console': {
170 | 'class': 'logging.StreamHandler',
171 | },
172 | },
173 | 'loggers': {
174 | 'django.db.backends': {
175 | 'handlers': ['console'],
176 | 'level': 'INFO',
177 | },
178 | },
179 | }
180 |
181 |
182 | LOGIN_URL = '/'
183 | LOGIN_REDIRECT_URL = '/feeds/'
184 |
185 | ALLOWED_SIGNUP_DOMAINS = ['*']
186 |
187 | SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
188 |
189 |
190 | # Where your Graphene schema lives
191 | GRAPHENE = {
192 | 'SCHEMA': 'bootcamp.schema.schema'
193 | }
194 |
195 |
--------------------------------------------------------------------------------
/bootcamp/static/css/bootcamp.css:
--------------------------------------------------------------------------------
1 | @import url(//fonts.googleapis.com/css?family=Audiowide);
2 |
3 | body {
4 | background-color: #f6f6f6;
5 | padding-top: 70px;
6 | }
7 |
8 | header .navbar-brand {
9 | font-size: 1.4em;
10 | font-weight: 200;
11 | font-family: "Audiowide", cursive;
12 | }
13 |
14 | main .container {
15 | margin-bottom: 2em;
16 | border: 1px solid #ddd;
17 | background-color: #ffffff;
18 | padding-top: 1.2em;
19 | padding-bottom: 1.2em;
20 | border-radius: .3em;
21 | box-shadow: 0 1px 3px #cccccc;
22 | }
23 |
24 | .page-header {
25 | margin: 0;
26 | }
27 |
28 | .page-header h1 {
29 | margin: 0;
30 | font-weight: 100;
31 | font-size: 2em;
32 | }
33 |
34 | .no-data {
35 | text-align: center;
36 | padding: 1em 0;
37 | }
38 |
39 | #notifications {
40 | font-size: 1.5em;
41 | padding: 13px;
42 | color: #dddddd;
43 | }
44 |
45 | #notifications.new-notifications {
46 | color: #428bca;
47 | }
48 |
49 | .popover {
50 | max-width: 350px;
51 | width: 350px;
52 | }
53 |
54 | .popover ul {
55 | padding: 0;
56 | margin: 0;
57 | }
58 |
59 | .popover ul li {
60 | list-style: none;
61 | border-bottom: 1px solid #eeeeee;
62 | padding: .4em 0;
63 | }
64 |
65 | .popover ul li:last-child {
66 | border-bottom: none;
67 | }
68 |
69 | .popover ul li .user-picture {
70 | width: 45px;
71 | float: left;
72 | }
73 |
74 | .popover ul li p {
75 | font-size: .9em;
76 | padding: 0 0 0 .6em;
77 | margin-left: 45px;
78 | margin-bottom: 0;
79 | }
80 |
81 | .markdown {
82 | margin-bottom: .8em;
83 | }
84 |
--------------------------------------------------------------------------------
/bootcamp/static/img/Jcrop.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qulc/bootcamp/de3b68240df225260f0da159269d4d66dfe1fe87/bootcamp/static/img/Jcrop.gif
--------------------------------------------------------------------------------
/bootcamp/static/img/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qulc/bootcamp/de3b68240df225260f0da159269d4d66dfe1fe87/bootcamp/static/img/favicon.png
--------------------------------------------------------------------------------
/bootcamp/static/img/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qulc/bootcamp/de3b68240df225260f0da159269d4d66dfe1fe87/bootcamp/static/img/loading.gif
--------------------------------------------------------------------------------
/bootcamp/static/img/user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qulc/bootcamp/de3b68240df225260f0da159269d4d66dfe1fe87/bootcamp/static/img/user.png
--------------------------------------------------------------------------------
/bootcamp/static/js/bootcamp.js:
--------------------------------------------------------------------------------
1 | $(function () {
2 | $.fn.count = function (limit) {
3 | var length = limit - $(this).val().length;
4 | var form = $(this).closest("form");
5 | if (length <= 0) {
6 | $(".form-group", form).addClass("has-error");
7 | }
8 | else {
9 | $(".form-group", form).removeClass("has-error");
10 | }
11 | $(".help-count", form).text(length);
12 | };
13 | });
--------------------------------------------------------------------------------
/bootcamp/static/js/bootcamp.markdown.js:
--------------------------------------------------------------------------------
1 | $(function () {
2 |
3 | $.fn.markdown = function () {
4 | var _textarea = $(this);
5 |
6 | $(".markdown .btn-group button").click(function (e) {
7 | e.preventDefault();
8 | var action = $(this).attr("ref");
9 | var selection = $(_textarea).selection();
10 |
11 | switch (action) {
12 | case "header":
13 | $(_textarea).selection("replace", {text: "# " + selection});
14 | break;
15 | case "bold":
16 | $(_textarea).selection("replace", {text: "**" + selection + "**"});
17 | break;
18 | case "italic":
19 | $(_textarea).selection("replace", {text: "_" + selection + "_"});
20 | break;
21 | case "list":
22 | var selection_list = selection.split("\n");
23 | var selection_list_result = "";
24 | for (var i = 0 ; i < selection_list.length ; i++) {
25 | selection_list_result += "* " + selection_list[i] + "\n";
26 | };
27 | if (selection_list_result.length > 0) {
28 | selection_list_result = selection_list_result.substring(0, selection_list_result.length - 1);
29 | }
30 | $(_textarea).selection("replace", {text: selection_list_result});
31 | break;
32 | case "link":
33 | $("#markdown_link_text").val("");
34 | $("#markdown_url").val("");
35 | $("#markdown_insert_link").modal("show");
36 | break;
37 | case "picture":
38 | $("#markdown_picture_url").val("");
39 | $("#markdown_alt_text").val("");
40 | $("#markdown_insert_picture").modal("show");
41 | break;
42 | case "indent-left":
43 | var selection_list = selection.split("\n");
44 | var selection_list_result = "";
45 | for (var i = 0; i < selection_list.length; i++) {
46 | selection_list_result += " " + selection_list[i] + "\n";
47 | };
48 | if (selection_list_result.length > 0) {
49 | selection_list_result = selection_list_result.substring(0, selection_list_result.length - 1);
50 | }
51 | $(_textarea).selection("replace", {text: selection_list_result});
52 | break;
53 | case "indent-right":
54 | var selection_list = selection.split("\n");
55 | var selection_list_result = "";
56 | for (var i = 0; i < selection_list.length ; i++) {
57 | selection_list_result += selection_list[i].trim() + "\n";
58 | };
59 | if (selection_list_result.length > 0) {
60 | selection_list_result = selection_list_result.substring(0, selection_list_result.length - 1);
61 | }
62 | $(_textarea).selection("replace", {text: selection_list_result});
63 | break;
64 | };
65 |
66 | });
67 |
68 | $(".add_link").click(function () {
69 | var selection = $(_textarea).selection();
70 | var text = $("#markdown_link_text").val();
71 | var url = $("#markdown_url").val();
72 | var link = "[" + text + "](" + url + ")";
73 | $(_textarea).selection("replace", {text: link});
74 | $("#markdown_insert_link").modal("hide")
75 | });
76 |
77 | $(".add_picture").click(function () {
78 | var selection = $(_textarea).selection();
79 | var url = $("#markdown_picture_url").val();
80 | var alt = $("#markdown_alt_text").val();
81 | var picture = "";
82 | $(_textarea).selection("replace", {text: picture});
83 | $("#markdown_insert_picture").modal("hide")
84 | });
85 |
86 | };
87 | });
--------------------------------------------------------------------------------
/bootcamp/templates/403.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load i18n %}
3 |
4 | {% block main %}
5 |
8 | {% trans 'Hey you!' %}
9 | {% endblock main %}
--------------------------------------------------------------------------------
/bootcamp/templates/404.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load i18n %}
3 |
4 | {% block title %} 404 {% endblock title %}
5 |
6 | {% block main %}
7 |
10 | {% trans 'We could not find the page you are looking for :(' %}
11 | {% endblock %}
--------------------------------------------------------------------------------
/bootcamp/templates/500.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load i18n %}
3 |
4 | {% block main %}
5 |
8 | {% trans "Ooops! Something went wrong. That's all we know." %}
9 | {% endblock main %}
--------------------------------------------------------------------------------
/bootcamp/templates/base.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load static %}
3 |
4 |
5 |
6 |
7 |
8 |
9 | {% block title %}Bootcamp{% endblock %}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | {% block head %} {% endblock head %}
18 |
19 |
20 | {% block body %}
21 |
22 |
82 |
83 |
84 |
85 | {% block main %}
86 | {% endblock main %}
87 |
88 |
89 |
90 |
91 | {% endblock body %}
92 |
93 |
94 |
--------------------------------------------------------------------------------
/bootcamp/templates/markdown_editor.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load static %}
3 |
4 |
5 |
6 |
7 |
8 |
32 |
56 |
57 |
58 |
59 |
63 |
64 |
{% trans 'You can learn more about markdown syntax' %} {% trans 'here' %}.
65 |
66 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/bootcamp/templates/paginator.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/bootcamp/urls.py:
--------------------------------------------------------------------------------
1 | """bootcamp URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/5.1/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: path('', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Import the include() function: from django.conf.urls import url, include
14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
15 | """
16 | from django.conf import settings
17 | from django.conf.urls import include
18 | from django.conf.urls.static import static
19 | from django.contrib import admin
20 | from django.contrib.auth import views as auth_views
21 | from django.urls import path
22 | from django.views.decorators.csrf import csrf_exempt
23 | from graphene_django.views import GraphQLView
24 |
25 | from .authentication import views as authentication_views
26 | from .core import views as core_views
27 | from .search import views as search_views
28 |
29 | urlpatterns = [
30 | path('admin/', admin.site.urls),
31 |
32 | path('', core_views.home, name='home'),
33 | path('login', auth_views.LoginView.as_view(template_name='core/cover.html'), name='login'),
34 | path('logout', auth_views.LogoutView.as_view(next_page='/'), name='logout'),
35 | path('signup/', authentication_views.signup, name='signup'),
36 |
37 | path('feeds/', include('bootcamp.feeds.urls')),
38 | path('settings/', include('bootcamp.core.urls')),
39 | path('articles/', include('bootcamp.articles.urls')),
40 | path('messages/', include('bootcamp.messenger.urls')),
41 | path('questions/', include('bootcamp.questions.urls')),
42 | path('notifications/', include('bootcamp.activities.urls')),
43 |
44 | path('search/', search_views.search, name='search'),
45 | path('network/', core_views.network, name='network'),
46 |
47 | path('graphql', csrf_exempt(GraphQLView.as_view(graphiql=True))),
48 |
49 | path('/', core_views.profile, name='profile'),
50 | ]
51 |
52 | if settings.DEBUG:
53 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
54 |
--------------------------------------------------------------------------------
/bootcamp/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for ab project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
8 | """
9 | import os
10 |
11 | from django.core.wsgi import get_wsgi_application
12 |
13 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bootcamp.settings")
14 |
15 | application = get_wsgi_application()
16 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Django's command-line utility for administrative tasks."""
3 | import os
4 | import sys
5 |
6 |
7 | def main():
8 | """Run administrative tasks."""
9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bootcamp.settings")
10 | try:
11 | from django.core.management import execute_from_command_line
12 | except ImportError as exc:
13 | raise ImportError(
14 | "Couldn't import Django. Are you sure it's installed and "
15 | "available on your PYTHONPATH environment variable? Did you "
16 | "forget to activate a virtual environment?"
17 | ) from exc
18 | execute_from_command_line(sys.argv)
19 |
20 |
21 | if __name__ == "__main__":
22 | main()
23 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Django
2 | graphene-django
3 |
--------------------------------------------------------------------------------
7 | {{ comment.user.profile.get_screen_name }} 8 | {{ comment.date|naturaltime }} 9 |
10 |{{ comment.comment }}
11 |