├── feed
├── __init__.py
├── tests
│ ├── __init__.py
│ ├── test_urls.py
│ └── test_views.py
├── migrations
│ ├── __init__.py
│ ├── 0002_alter_mumble_content.py
│ └── 0001_initial.py
├── apps.py
├── utils.py
├── urls.py
├── admin.py
├── serializers.py
├── signals.py
├── models.py
└── views.py
├── users
├── __init__.py
├── tests
│ ├── __init__.py
│ ├── test_urls.py
│ └── test_views.py
├── migrations
│ ├── __init__.py
│ ├── 0003_alter_userprofile_interests.py
│ ├── 0002_auto_20210517_1422.py
│ ├── 0004_auto_20210517_1436.py
│ └── 0001_initial.py
├── apps.py
├── signals.py
├── admin.py
├── models.py
├── urls.py
├── serializers.py
├── templates
│ ├── verify-email.html
│ └── forgotpwd-email.html
└── views.py
├── article
├── __init__.py
├── tests
│ ├── __init__.py
│ ├── test_views.py
│ ├── test_models.py
│ └── test_urls.py
├── migrations
│ ├── __init__.py
│ ├── 0002_alter_article_content.py
│ ├── 0003_auto_20210522_1337.py
│ └── 0001_initial.py
├── apps.py
├── urls.py
├── admin.py
├── serializers.py
├── models.py
└── views.py
├── discussion
├── __init__.py
├── tests
│ ├── __init__.py
│ ├── test_models.py
│ ├── test_urls.py
│ └── test_views.py
├── migrations
│ ├── __init__.py
│ ├── 0002_alter_discussion_content.py
│ ├── 0003_discussion_tags.py
│ └── 0001_initial.py
├── apps.py
├── urls.py
├── serializers.py
├── admin.py
├── models.py
└── views.py
├── message
├── __init__.py
├── tests
│ ├── __init__.py
│ ├── test_urls.py
│ └── test_views.py
├── migrations
│ ├── __init__.py
│ └── 0001_initial.py
├── admin.py
├── apps.py
├── urls.py
├── models.py
├── serializers.py
└── views.py
├── runtime.txt
├── mumblebackend
├── __init__.py
├── settings
│ ├── __init__.py
│ ├── dev.py
│ ├── prod.py
│ └── base.py
├── asgi.py
├── wsgi.py
├── views.py
└── urls.py
├── notification
├── __init__.py
├── tests
│ ├── __init__.py
│ ├── test_models.py
│ ├── test_urls.py
│ └── test_views.py
├── migrations
│ ├── __init__.py
│ ├── 0004_remove_notification_content_id.py
│ ├── 0002_alter_notification_notification_type.py
│ ├── 0001_initial.py
│ ├── 0003_auto_20210516_0431.py
│ └── 0005_auto_20210610_0037.py
├── urls.py
├── apps.py
├── admin.py
├── models.py
├── views.py
├── serializers.py
└── signals.py
├── static
└── images
│ ├── .gitignore
│ ├── 52.jpg
│ ├── Dennis.jpg
│ ├── cody.png
│ ├── mani.png
│ ├── mehdi.png
│ ├── peng.png
│ ├── zach.png
│ ├── abhijit.png
│ ├── default.png
│ ├── dennis1.jpg
│ ├── mohammad.png
│ ├── pravenn.jpg
│ ├── shahriar.png
│ ├── sulamita.png
│ ├── ujjawal.png
│ ├── Mumble-logo.png
│ ├── samthefam.png
│ ├── cody_yroWaNN.png
│ ├── mani_bcKdqs9.png
│ ├── mehdi_4xVrdj3.png
│ ├── peng_1Gl3Jf3.png
│ ├── web_dev_junki.png
│ ├── zach_5uhXnGn.png
│ ├── Dennis_UH5CQrc.jpg
│ ├── ParveeMalethia.jpg
│ ├── abhijit_J7YVC4M.png
│ ├── mohammad_Gm2N4lO.png
│ ├── mumble_profile.PNG
│ ├── shahriar_afma3DI.png
│ ├── sulamita_clzQaUD.png
│ ├── ujjawal_cJmtMpL.png
│ ├── dark-logo.1c6c40e2.png
│ ├── samthefam_CLtu9BX.png
│ ├── 2_years_of_coding_1.jpg
│ ├── Coding_Vampire_Praveen.png
│ ├── dark-logo.1c6c40e2_7t6hZtD.png
│ └── 14289931_293735227663385_3969000131895210120_o.jpg
├── requirements.txt
├── img
├── project-board.gif
├── activate-project.gif
├── drawSQL-MumbleApi.png
└── introducing-project-board1.PNG
├── Procfile
├── articledata.json
├── feeddata.json
├── manage.py
├── discussiondata.json
├── .github
├── ISSUE_TEMPLATE
│ ├── feature_report.md
│ └── bug_report.md
├── workflows
│ └── python.yml
└── pull_request_template.md
├── SECURITY.md
├── Project_Board.md
├── Reviewers.md
├── .gitignore
├── CodeOfConduct.md
├── Contributing.md
├── README.md
└── LICENSE
/feed/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/users/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/article/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/discussion/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/feed/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/message/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/runtime.txt:
--------------------------------------------------------------------------------
1 | python-3.8.5
--------------------------------------------------------------------------------
/users/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/article/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/article/tests/test_views.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/discussion/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/feed/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/message/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/message/tests/test_urls.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/message/tests/test_views.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/mumblebackend/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/notification/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/users/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/article/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/discussion/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/discussion/tests/test_models.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/discussion/tests/test_urls.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/discussion/tests/test_views.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/message/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/notification/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/notification/tests/test_models.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/notification/tests/test_urls.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/notification/tests/test_views.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/mumblebackend/settings/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/notification/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/images/.gitignore:
--------------------------------------------------------------------------------
1 | mumbleenv/
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/requirements.txt
--------------------------------------------------------------------------------
/img/project-board.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/img/project-board.gif
--------------------------------------------------------------------------------
/static/images/52.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/52.jpg
--------------------------------------------------------------------------------
/img/activate-project.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/img/activate-project.gif
--------------------------------------------------------------------------------
/static/images/Dennis.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/Dennis.jpg
--------------------------------------------------------------------------------
/static/images/cody.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/cody.png
--------------------------------------------------------------------------------
/static/images/mani.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/mani.png
--------------------------------------------------------------------------------
/static/images/mehdi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/mehdi.png
--------------------------------------------------------------------------------
/static/images/peng.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/peng.png
--------------------------------------------------------------------------------
/static/images/zach.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/zach.png
--------------------------------------------------------------------------------
/img/drawSQL-MumbleApi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/img/drawSQL-MumbleApi.png
--------------------------------------------------------------------------------
/static/images/abhijit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/abhijit.png
--------------------------------------------------------------------------------
/static/images/default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/default.png
--------------------------------------------------------------------------------
/static/images/dennis1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/dennis1.jpg
--------------------------------------------------------------------------------
/static/images/mohammad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/mohammad.png
--------------------------------------------------------------------------------
/static/images/pravenn.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/pravenn.jpg
--------------------------------------------------------------------------------
/static/images/shahriar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/shahriar.png
--------------------------------------------------------------------------------
/static/images/sulamita.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/sulamita.png
--------------------------------------------------------------------------------
/static/images/ujjawal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/ujjawal.png
--------------------------------------------------------------------------------
/static/images/Mumble-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/Mumble-logo.png
--------------------------------------------------------------------------------
/static/images/samthefam.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/samthefam.png
--------------------------------------------------------------------------------
/static/images/cody_yroWaNN.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/cody_yroWaNN.png
--------------------------------------------------------------------------------
/static/images/mani_bcKdqs9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/mani_bcKdqs9.png
--------------------------------------------------------------------------------
/static/images/mehdi_4xVrdj3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/mehdi_4xVrdj3.png
--------------------------------------------------------------------------------
/static/images/peng_1Gl3Jf3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/peng_1Gl3Jf3.png
--------------------------------------------------------------------------------
/static/images/web_dev_junki.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/web_dev_junki.png
--------------------------------------------------------------------------------
/static/images/zach_5uhXnGn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/zach_5uhXnGn.png
--------------------------------------------------------------------------------
/img/introducing-project-board1.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/img/introducing-project-board1.PNG
--------------------------------------------------------------------------------
/static/images/Dennis_UH5CQrc.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/Dennis_UH5CQrc.jpg
--------------------------------------------------------------------------------
/static/images/ParveeMalethia.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/ParveeMalethia.jpg
--------------------------------------------------------------------------------
/static/images/abhijit_J7YVC4M.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/abhijit_J7YVC4M.png
--------------------------------------------------------------------------------
/static/images/mohammad_Gm2N4lO.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/mohammad_Gm2N4lO.png
--------------------------------------------------------------------------------
/static/images/mumble_profile.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/mumble_profile.PNG
--------------------------------------------------------------------------------
/static/images/shahriar_afma3DI.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/shahriar_afma3DI.png
--------------------------------------------------------------------------------
/static/images/sulamita_clzQaUD.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/sulamita_clzQaUD.png
--------------------------------------------------------------------------------
/static/images/ujjawal_cJmtMpL.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/ujjawal_cJmtMpL.png
--------------------------------------------------------------------------------
/static/images/dark-logo.1c6c40e2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/dark-logo.1c6c40e2.png
--------------------------------------------------------------------------------
/static/images/samthefam_CLtu9BX.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/samthefam_CLtu9BX.png
--------------------------------------------------------------------------------
/static/images/2_years_of_coding_1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/2_years_of_coding_1.jpg
--------------------------------------------------------------------------------
/static/images/Coding_Vampire_Praveen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/Coding_Vampire_Praveen.png
--------------------------------------------------------------------------------
/static/images/dark-logo.1c6c40e2_7t6hZtD.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/dark-logo.1c6c40e2_7t6hZtD.png
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: gunicorn mumblebackend.wsgi --log-file -
2 | release: python manage.py migrate
3 | release: python manage.py migrate --database=message
--------------------------------------------------------------------------------
/message/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from .models import UserMessage , Thread
3 |
4 |
5 | admin.site.register(UserMessage)
6 | admin.site.register(Thread)
7 |
--------------------------------------------------------------------------------
/static/images/14289931_293735227663385_3969000131895210120_o.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/mumbleapi/HEAD/static/images/14289931_293735227663385_3969000131895210120_o.jpg
--------------------------------------------------------------------------------
/article/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class ArticleConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'article'
7 |
--------------------------------------------------------------------------------
/discussion/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class DiscussionConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'discussion'
7 |
--------------------------------------------------------------------------------
/feed/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class FeedConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'feed'
7 |
8 | def ready(self):
9 | import feed.signals
10 |
--------------------------------------------------------------------------------
/message/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class MessageConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'message'
7 |
8 | # def ready(self):
9 | # import notification.signals
--------------------------------------------------------------------------------
/notification/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from . import views
3 |
4 | urlpatterns = [
5 | path('/read/',views.read_notification,name='read-notification'),
6 | path('', views.get_notifications, name="get-notifications"),
7 | ]
8 |
--------------------------------------------------------------------------------
/notification/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class NotificationConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'notification'
7 |
8 | def ready(self):
9 | import notification.signals
--------------------------------------------------------------------------------
/users/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class UsersConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'users'
7 |
8 |
9 | def ready(self):
10 | import users.signals
11 |
12 |
--------------------------------------------------------------------------------
/articledata.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "model": "article.article",
4 | "pk": "c41dbffd-f671-4bd3-89b5-6a4f7f5ef648",
5 | "fields": {
6 | "user": 1,
7 | "title": "Dummy Article title",
8 | "content": "Dummy Article Content
",
9 | "created": "2021-06-22T03:07:19.795Z",
10 | "tags": []
11 | }
12 | }
13 | ]
14 |
--------------------------------------------------------------------------------
/notification/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from .models import Notification
3 |
4 |
5 | class AdminNotification(admin.ModelAdmin):
6 | search_fields = ('to_user',)
7 | list_filter = ('to_user', 'followed_by',)
8 | empty_value_display = '-empty field-'
9 |
10 |
11 |
12 | admin.site.register(Notification, AdminNotification)
13 |
--------------------------------------------------------------------------------
/message/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from . import views
3 |
4 | urlpatterns = [
5 | path('', views.get_messages, name="get-messages"),
6 | path('create-thread/', views.CreateThread,name="create-thread"),
7 | path('/read/', views.read_message, name="read-message"),
8 | path('create/', views.create_message, name="create-message"),
9 | ]
10 |
--------------------------------------------------------------------------------
/feeddata.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "model": "feed.mumble",
4 | "pk": "7e586962-c70f-4fa7-b536-6e7a24c6a587",
5 | "fields": {
6 | "parent": null,
7 | "remumble": null,
8 | "user": 1,
9 | "content": "Dummy Mumble Post
",
10 | "image": "",
11 | "vote_rank": 0,
12 | "comment_count": 0,
13 | "share_count": 0,
14 | "created": "2021-06-22T03:07:37.503Z"
15 | }
16 | }
17 | ]
18 |
--------------------------------------------------------------------------------
/notification/migrations/0004_remove_notification_content_id.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2 on 2021-05-17 12:52
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('notification', '0003_auto_20210516_0431'),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveField(
14 | model_name='notification',
15 | name='content_id',
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/mumblebackend/asgi.py:
--------------------------------------------------------------------------------
1 | """
2 | ASGI config for mumblebackend 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/3.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', 'mumblebackend.settings')
15 |
16 | application = get_asgi_application()
17 |
--------------------------------------------------------------------------------
/mumblebackend/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for mumblebackend 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/3.1/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mumblebackend.settings.dev')
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/feed/migrations/0002_alter_mumble_content.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2 on 2021-05-19 13:19
2 |
3 | import ckeditor.fields
4 | from django.db import migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('feed', '0001_initial'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='mumble',
16 | name='content',
17 | field=ckeditor.fields.RichTextField(blank=True, null=True),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/article/migrations/0002_alter_article_content.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2 on 2021-05-19 13:19
2 |
3 | import ckeditor.fields
4 | from django.db import migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('article', '0001_initial'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='article',
16 | name='content',
17 | field=ckeditor.fields.RichTextField(max_length=10000),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/discussion/migrations/0002_alter_discussion_content.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2 on 2021-05-19 13:19
2 |
3 | import ckeditor.fields
4 | from django.db import migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('discussion', '0001_initial'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='discussion',
16 | name='content',
17 | field=ckeditor.fields.RichTextField(max_length=10000),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/users/migrations/0003_alter_userprofile_interests.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2 on 2021-05-17 18:23
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('users', '0002_auto_20210517_1422'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='userprofile',
15 | name='interests',
16 | field=models.ManyToManyField(blank=True, related_name='topic_interests', to='users.TopicTag'),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/mumblebackend/settings/dev.py:
--------------------------------------------------------------------------------
1 | from mumblebackend.settings.base import *
2 | from .base import *
3 | import os
4 | # override base.py settings
5 |
6 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
7 |
8 |
9 | DATABASES = {
10 | 'default': {
11 | 'ENGINE': 'django.db.backends.sqlite3',
12 | 'NAME': BASE_DIR / 'db.sqlite3',
13 | },
14 | 'message': {
15 | 'ENGINE': 'django.db.backends.sqlite3',
16 | 'NAME': BASE_DIR / 'messages.sqlite3',
17 | }
18 | }
19 |
20 |
21 | DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
--------------------------------------------------------------------------------
/discussion/migrations/0003_discussion_tags.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2 on 2021-05-20 20:35
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('users', '0004_auto_20210517_1436'),
10 | ('discussion', '0002_alter_discussion_content'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='discussion',
16 | name='tags',
17 | field=models.ManyToManyField(blank=True, related_name='discussion_tags', to='users.TopicTag'),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/notification/migrations/0002_alter_notification_notification_type.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2 on 2021-05-06 16:50
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('notification', '0001_initial'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='notification',
15 | name='notification_type',
16 | field=models.CharField(choices=[('article', 'article'), ('mumble', 'mumble'), ('discussion', 'discussion'), ('follow', 'follow')], max_length=20),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/feed/utils.py:
--------------------------------------------------------------------------------
1 | #Updates comment count for parent posts
2 | def update_comment_counts(parent, action):
3 | if parent:
4 | if action == 'add':
5 | parent.comment_count += 1
6 | if action == 'delete':
7 | parent.comment_count -= 1
8 | parent.save()
9 | return update_comment_counts(parent.parent, action)
10 |
11 | #Gets triggered on post created and updates remumble count if shared or deleted
12 | def update_remumble_counts(parent, action):
13 |
14 | if action == 'add':
15 |
16 | parent.share_count += 1
17 |
18 | if action == 'delete':
19 | parent.share_count -= 1
20 |
21 | parent.save()
22 |
--------------------------------------------------------------------------------
/feed/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from . import views
3 |
4 | app_name = 'mumbles-api'
5 |
6 | urlpatterns = [
7 | path('', views.mumbles, name="mumbles"),
8 | path('create/', views.create_mumble, name="mumble-create"),
9 | path('edit//', views.edit_mumble, name="mumble-edit"),
10 | path('details//', views.mumble_details, name="mumble-details"),
11 | path('remumble/', views.remumble, name="mumble-remumble"),
12 | path('vote/', views.update_vote, name="posts-vote"),
13 | path('delete//', views.delete_mumble, name="delete-mumble"),
14 | path('/comments/', views.mumble_comments, name="mumble-comments"),
15 | ]
--------------------------------------------------------------------------------
/article/migrations/0003_auto_20210522_1337.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2 on 2021-05-22 13:37
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('users', '0004_auto_20210517_1436'),
10 | ('article', '0002_alter_article_content'),
11 | ]
12 |
13 | operations = [
14 | migrations.RemoveField(
15 | model_name='article',
16 | name='tags',
17 | ),
18 | migrations.AddField(
19 | model_name='article',
20 | name='tags',
21 | field=models.ManyToManyField(blank=True, related_name='article_tags', to='users.TopicTag'),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/article/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from . import views
3 |
4 | app_name = 'mumbles-api-articles'
5 |
6 | urlpatterns = [
7 | path('',views.articles,name='articles'),
8 | path('create/',views.create_article,name='create-article'),
9 | path('vote/',views.update_vote,name='article-vote'),
10 | path('/', views.get_article, name="get-article"),
11 | path('edit//', views.edit_article, name="edit-article"),
12 | path('delete//', views.delete_article, name="delete-article"),
13 | path('edit-comment//', views.edit_article_comment, name="edit-article-comment"),
14 | path('delete-comment//', views.delete_article_comment, name="delete-article-comment"),
15 | ]
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', 'mumblebackend.settings.dev')
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 |
--------------------------------------------------------------------------------
/discussiondata.json:
--------------------------------------------------------------------------------
1 | [{"model": "discussion.discussion", "pk": "b2de5c16-4d4a-414f-80f1-006ec7f108db", "fields": {"user": 1, "headline": "This is the Discussion headline", "content": "Here is some content for the discussion ( edited form postman )
", "created": "2021-04-29T04:47:28.386Z", "tags": ["tag1", "tag2"]}}, {"model": "discussion.discussioncomment", "pk": "e5b815bd-49a9-4951-865c-5e0f77c2fb24", "fields": {"discussion": "b2de5c16-4d4a-414f-80f1-006ec7f108db", "user": 1, "content": "This is a cool comment", "created": "2021-04-29T05:06:33.781Z"}}, {"model": "discussion.discussionvote", "pk": "fd236704-c803-4125-96c5-5277aeb37ca5", "fields": {"user": 1, "discussion": "b2de5c16-4d4a-414f-80f1-006ec7f108db", "comment": null, "value": 1, "created": "2021-04-29T04:56:49.299Z"}}]
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature/Enhancement request
3 | about: Suggest an idea for this project
4 | title: ""
5 | labels: feature, enhancement
6 | assignees: ""
7 | ---
8 |
9 | ### Is your feature request related to a problem? Please describe.
10 |
11 | A clear and concise description of what the problem is.
12 |
13 | #
14 |
15 | ### Describe the solution you'd like
16 |
17 | A clear and concise description of what you want to happen.
18 |
19 | #
20 |
21 | ### Describe alternatives you've considered
22 |
23 | A clear and concise description of any alternative solutions or features you've considered.
24 |
25 | #
26 |
27 | ### Additional context
28 |
29 | Add any other context such as screenshots, schematics, about the feature request here.
30 |
--------------------------------------------------------------------------------
/users/migrations/0002_auto_20210517_1422.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2 on 2021-05-17 18:22
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('users', '0001_initial'),
10 | ]
11 |
12 | operations = [
13 | migrations.CreateModel(
14 | name='TopicTag',
15 | fields=[
16 | ('name', models.CharField(max_length=150, primary_key=True, serialize=False)),
17 | ],
18 | ),
19 | migrations.AddField(
20 | model_name='userprofile',
21 | name='interests',
22 | field=models.ManyToManyField(blank=True, null=True, related_name='topic_interests', to='users.TopicTag'),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/discussion/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from . import views
3 |
4 | app_name = 'mumbles-api-discussions'
5 |
6 |
7 | urlpatterns = [
8 | path('',views.discussions,name='discussions'),
9 | path('create/',views.create_discussion,name='create-discussion'),
10 | path('vote/',views.update_vote,name='discussion-vote'),
11 | path('/', views.get_discussion, name="get-discussion"),
12 | path('edit//', views.edit_discussion, name="edit-discussion"),
13 | path('delete//', views.delete_discussion, name="delete-discussion"),
14 | path('edit-comment//', views.edit_discussion_comment, name="edit-discussion-comment"),
15 | path('delete-comment//', views.delete_discussion_comment, name="delete-discussion-comment"),
16 | ]
17 |
--------------------------------------------------------------------------------
/users/migrations/0004_auto_20210517_1436.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2 on 2021-05-17 18:36
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('users', '0003_alter_userprofile_interests'),
10 | ]
11 |
12 | operations = [
13 | migrations.CreateModel(
14 | name='SkillTag',
15 | fields=[
16 | ('name', models.CharField(max_length=150, primary_key=True, serialize=False)),
17 | ],
18 | ),
19 | migrations.AddField(
20 | model_name='userprofile',
21 | name='skills',
22 | field=models.ManyToManyField(blank=True, related_name='personal_skills', to='users.SkillTag'),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | #
2 |
3 |

4 |
5 |
6 | API
7 |
8 |
9 |
Security
10 |
11 |
12 | ### Reporting a Vulnerability :
13 |
14 | To report a security vulnerability, please :
15 |
16 |
17 | - DM the Mumble Bot in our Discord Server
18 | - Or tell us the problem at #security-vulnerabilities
19 |
20 |
21 |
22 | *You will receive a response from us *(Moderators and Git Repo Managers)* within 24 hours*
23 |
24 | #
25 |
26 | ### Join the Mumble Community :
27 |
28 | Join our Discord Server :
29 |
--------------------------------------------------------------------------------
/feed/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from datetime import timedelta
3 | from .models import Mumble, MumbleVote
4 |
5 |
6 | class AdminMumble(admin.ModelAdmin):
7 | list_display = ('user', 'vote_rank', 'created','get_utc')
8 | search_fields = ('user',)
9 | list_filter = ('created', 'vote_rank', 'user',)
10 | empty_value_display = '-empty field-'
11 |
12 | def get_utc(self, obj):
13 | return obj.created + timedelta(minutes=330)
14 |
15 | get_utc.short_description = 'Created (UTC)'
16 |
17 |
18 |
19 | class AdminMumbleVote(admin.ModelAdmin):
20 | list_display = ('user', 'mumble', 'value')
21 | search_fields = ('user',)
22 | list_filter = ('user',)
23 | empty_value_display = '-empty field-'
24 |
25 |
26 | admin.site.register(Mumble, AdminMumble)
27 | admin.site.register(MumbleVote, AdminMumbleVote)
28 |
--------------------------------------------------------------------------------
/.github/workflows/python.yml:
--------------------------------------------------------------------------------
1 | name: Python Build
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | pull_request:
7 | branches: [master]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 |
13 | strategy:
14 | matrix:
15 | db: [sqlite]
16 | python-version: [3.7]
17 |
18 | steps:
19 | - uses: actions/checkout@v2
20 |
21 | - name: Set up Python ${{ matrix.python-version }}
22 | uses: actions/setup-python@v2
23 | with:
24 | python-version: ${{ matrix.python-version }}
25 |
26 | - name: Install dependencies
27 | run: |
28 | python -m pip install --upgrade pip
29 | pip install -r requirements.txt
30 |
31 | - name: Run migrations
32 | run: |
33 | python manage.py migrate
34 |
35 | - name: Tests
36 | run: |
37 | python manage.py test
38 |
--------------------------------------------------------------------------------
/users/signals.py:
--------------------------------------------------------------------------------
1 | from django.db.models.signals import post_save, pre_save, post_delete
2 | from django.contrib.auth.models import User
3 | from .models import UserProfile
4 |
5 |
6 | def create_profile(sender, instance, created, **kwargs):
7 | if created:
8 | UserProfile.objects.create(
9 | user=instance,
10 | name=instance.username,
11 | username=instance.username,
12 | #email=instance.email,
13 | )
14 |
15 | print('Profile Created!')
16 |
17 |
18 | def update_profile(sender, instance, created, **kwargs):
19 | user_profile, _ = UserProfile.objects.get_or_create(user=instance)
20 | if created == False:
21 |
22 | user_profile.username = instance.username
23 |
24 | #instance.userprofile.email = instance.email
25 | user_profile.save()
26 | print('Profile updated!')
27 |
28 | post_save.connect(create_profile, sender=User)
29 | post_save.connect(update_profile, sender=User)
30 |
--------------------------------------------------------------------------------
/users/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from datetime import timedelta
3 | from .models import TopicTag, SkillTag, UserProfile
4 |
5 |
6 | class AdminTopicTag(admin.ModelAdmin):
7 | search_fields = ('name',)
8 | list_filter = ('name',)
9 | empty_value_display = '-empty field-'
10 |
11 |
12 | class AdminSkillTag(admin.ModelAdmin):
13 | search_fields = ('name',)
14 | list_filter = ('name',)
15 | empty_value_display = '-empty field-'
16 |
17 |
18 | class AdminUserProfile(admin.ModelAdmin):
19 | list_display = ('username','get_utc','email_verified')
20 | search_fields = ('user',)
21 | list_filter = ('user', 'email_verified',)
22 | empty_value_display = '-empty field-'
23 |
24 | def get_utc(self, obj):
25 | return obj.user.date_joined + timedelta(minutes=330)
26 |
27 | get_utc.short_description = 'Created (UTC)'
28 |
29 |
30 | admin.site.register(TopicTag, AdminTopicTag)
31 | admin.site.register(SkillTag, AdminSkillTag)
32 | admin.site.register(UserProfile, AdminUserProfile)
33 |
--------------------------------------------------------------------------------
/mumblebackend/views.py:
--------------------------------------------------------------------------------
1 | from rest_framework.decorators import api_view
2 | from rest_framework.response import Response
3 | from rest_framework.reverse import reverse, reverse_lazy
4 |
5 |
6 | @api_view(['GET'])
7 | def api_root(request):
8 |
9 | ''' Single entry point to mumble API (Does not include dynamic urls)'''
10 |
11 | return Response({
12 | # users endpoints
13 | 'users': reverse('users-api:users', request=request),
14 | 'users-recommended': reverse('users-api:users-recommended', request=request),
15 | 'register': reverse('users-api:register', request=request),
16 | 'login': reverse('users-api:login', request=request),
17 | 'profile_update': reverse('users-api:profile_update', request=request),
18 |
19 | # mumbles endpoints
20 | 'mumbles': reverse('mumbles-api:mumbles', request=request),
21 | 'mumble-create': reverse('mumbles-api:mumble-create', request=request),
22 | 'mumble-remumble': reverse('mumbles-api:mumble-remumble', request=request),
23 | 'posts-vote': reverse('mumbles-api:posts-vote', request=request),
24 | })
--------------------------------------------------------------------------------
/Project_Board.md:
--------------------------------------------------------------------------------
1 | #
2 |
18 |
19 | #
20 |
21 | ### Project Board
22 |
23 | In our repository, there is a project board named Tasks - Mumble Api, it helps moderators to see how is the work going.
24 |
25 |
26 | *Preview :*
27 |
28 |
29 |
30 |
31 | #
32 |
33 | ### So please, while submitting a PR or Issue, make sure to :
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/message/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from users.models import UserProfile
3 | import uuid
4 |
5 | class Thread(models.Model):
6 | id = models.UUIDField(default=uuid.uuid4, unique=True, primary_key=True, editable=False)
7 | sender = models.ForeignKey(UserProfile,on_delete=models.SET_NULL,null=True,related_name="sender")
8 | reciever = models.ForeignKey(UserProfile,on_delete=models.SET_NULL,null=True,related_name="reciever")
9 | updated = models.DateTimeField(auto_now=True)
10 | timestamp = models.DateTimeField(auto_now_add=True)
11 |
12 | def __str__(self):
13 | return str(self.sender.username + " and " + self.reciever.username)
14 |
15 |
16 | class UserMessage(models.Model):
17 | id = models.UUIDField(default=uuid.uuid4, unique=True, primary_key=True, editable=False)
18 | thread = models.ForeignKey(
19 | Thread, on_delete=models.CASCADE,related_name="messages")
20 | sender = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
21 | body = models.TextField(null=True,blank=True)
22 | is_read = models.BooleanField(default=False)
23 | timestamp = models.DateTimeField(auto_now_add=True)
24 |
25 | def __str__(self):
26 | return str(self.body)
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: "Use this template to report a bug you found in Mumble"
4 | title: ''
5 | labels: "bug"
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### Preflight Checklist
11 |
12 |
13 |
14 | [] I have searched the issue tracker for an issue that matches the one I want to file, without success.
15 |
16 | #
17 |
18 | ### Describe the bug
19 |
20 | A clear and concise description of what the bug is.
21 |
22 | #
23 |
24 | ### To Reproduce
25 |
26 | Steps to reproduce the behavior:
27 |
28 | 1. Go to '...'
29 | 2. Click on '....'
30 | 3. Scroll down to '....'
31 | 4. See error
32 |
33 | #
34 |
35 | ### What was expected ?
36 |
37 | A clear and concise description of what you expected to happen.
38 |
39 | #
40 |
41 | ### Screenshots
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | If applicable, add screenshots to help explain your problem.
50 |
51 | #
52 |
53 | ### Additional context
54 |
55 | Add any other context about the problem here (include commit numbers and branches if relevant)
56 |
--------------------------------------------------------------------------------
/message/serializers.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 | from .models import UserMessage , Thread
3 | from users.serializers import UserProfileSerializer
4 |
5 | class MessageSerializer(serializers.ModelSerializer):
6 | sender = UserProfileSerializer(read_only=True)
7 | class Meta:
8 | model = UserMessage
9 | fields = '__all__'
10 |
11 | class ThreadSerializer(serializers.ModelSerializer):
12 | chat_messages = serializers.SerializerMethodField(read_only=True)
13 | last_message = serializers.SerializerMethodField(read_only=True)
14 | un_read_count = serializers.SerializerMethodField(read_only=True)
15 | sender = UserProfileSerializer(read_only=True)
16 | reciever = UserProfileSerializer(read_only=True)
17 | class Meta:
18 | model = Thread
19 | fields = ['id','updated','timestamp','sender','reciever','chat_messages','last_message','un_read_count']
20 |
21 | def get_chat_messages(self,obj):
22 | messages = MessageSerializer(obj.messages.order_by('timestamp'),many=True)
23 | return messages.data
24 |
25 | def get_last_message(self,obj):
26 | serializer = MessageSerializer(obj.messages.order_by('timestamp').last(),many=False)
27 | return serializer.data
28 |
29 | def get_un_read_count(self,obj):
30 | messages = obj.messages.filter(is_read=False).count()
31 | return messages
32 |
--------------------------------------------------------------------------------
/article/tests/test_models.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import User
2 | from rest_framework.test import APIClient
3 | from django.urls import reverse , resolve
4 | from rest_framework import status
5 | from rest_framework.test import APITestCase
6 | from article.views import create_article
7 |
8 | class ArticleTestCases(APITestCase):
9 |
10 | def setUp(self):
11 | url = reverse('users-api:register')
12 | data = {
13 | 'username':'test',
14 | 'email':'test@gmail.com',
15 | 'password':'test@123'
16 | }
17 | response = self.client.post(url, data, format='json')
18 | self.assertEqual(response.status_code, status.HTTP_200_OK)
19 | self.assertEqual(User.objects.count(), 1)
20 | self.assertEqual(User.objects.get().username, 'test')
21 |
22 | def test_create_article(self):
23 | url = reverse('mumbles-api-articles:create-article')
24 | user = User.objects.get(username='test')
25 | client = APIClient()
26 | client.force_authenticate(user=user)
27 | data = {
28 | 'title':"Title of Article",
29 | 'content':"Content for article",
30 | 'tags':"Tags for article"
31 | }
32 | response = client.post(url,data, format='json')
33 | self.assertEqual(response.status_code, status.HTTP_200_OK)
34 | self.assertEqual(resolve(url).func,create_article)
--------------------------------------------------------------------------------
/notification/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2 on 2021-04-30 22:00
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 | import uuid
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | initial = True
12 |
13 | dependencies = [
14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15 | ]
16 |
17 | operations = [
18 | migrations.CreateModel(
19 | name='Notification',
20 | fields=[
21 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
22 | ('created', models.DateTimeField(auto_now_add=True)),
23 | ('content', models.CharField(max_length=255)),
24 | ('content_id', models.UUIDField(editable=False)),
25 | ('is_read', models.BooleanField(default=False)),
26 | ('notification_type', models.CharField(choices=[('article', 'article'), ('mumble', 'mumble')], max_length=20)),
27 | ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
28 | ('to_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='notifications', to=settings.AUTH_USER_MODEL)),
29 | ],
30 | ),
31 | ]
32 |
--------------------------------------------------------------------------------
/article/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from datetime import timedelta
3 | from .models import Article, ArticleComment, ArticleVote
4 |
5 |
6 | class AdminArticle(admin.ModelAdmin):
7 | list_display = ('title', 'user', 'get_utc',)
8 | search_fields = ('title',)
9 | list_filter = ('created',)
10 | empty_value_display = '-empty field-'
11 |
12 | def get_utc(self, obj):
13 | return obj.created + timedelta(minutes=330)
14 |
15 | get_utc.short_description = 'Created (UTC)'
16 |
17 |
18 | class AdminArticleComment(admin.ModelAdmin):
19 | list_display = ('article', 'user', 'get_utc',)
20 | search_fields = ('article',)
21 | list_filter = ('created',)
22 | empty_value_display = '-empty field-'
23 |
24 | def get_utc(self, obj):
25 | return obj.created + timedelta(minutes=330)
26 |
27 | get_utc.short_description = 'Created (UTC)'
28 |
29 |
30 | class AdminArticleVote(admin.ModelAdmin):
31 | list_display = ('article', 'user', 'value','get_utc')
32 | search_fields = ('article',)
33 | list_filter = ('created', 'value',)
34 | empty_value_display = '-empty field-'
35 |
36 | def get_utc(self, obj):
37 | return obj.created + timedelta(minutes=330)
38 |
39 | get_utc.short_description = 'Created (UTC)'
40 |
41 |
42 | admin.site.register(Article, AdminArticle)
43 | admin.site.register(ArticleComment, AdminArticleComment)
44 | admin.site.register(ArticleVote, AdminArticleVote)
45 |
--------------------------------------------------------------------------------
/notification/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.contrib.auth.models import User
3 | import uuid
4 |
5 | from article.models import Article
6 | from discussion.models import Discussion
7 | from feed.models import Mumble
8 |
9 |
10 | class Notification(models.Model):
11 |
12 | CHOICES = (
13 | ('article', 'article'),
14 | ('mumble', 'mumble'),
15 | ('discussion', 'discussion'),
16 | ('follow', 'follow'),
17 | )
18 |
19 | id = models.UUIDField(default=uuid.uuid4, unique=True, primary_key=True, editable=False)
20 | to_user = models.ForeignKey(User,on_delete=models.CASCADE, null=True, blank=True, related_name='notifications')
21 | created = models.DateTimeField(auto_now_add=True)
22 | created_by = models.ForeignKey(User,on_delete=models.CASCADE, null=True, blank=True)
23 | content = models.CharField(max_length=255)
24 | is_read = models.BooleanField(default=False)
25 | notification_type = models.CharField(max_length=20, choices=CHOICES)
26 | article = models.ForeignKey(Article,on_delete=models.CASCADE, null=True, blank=True)
27 | mumble = models.ForeignKey(Mumble,on_delete=models.CASCADE, null=True, blank=True)
28 | discussion = models.ForeignKey(Discussion,on_delete=models.CASCADE, null=True, blank=True)
29 | followed_by = models.ForeignKey(User,on_delete=models.CASCADE, null=True, blank=True, related_name='followed_by')
30 |
31 | def __str__(self):
32 | return self.content
33 |
--------------------------------------------------------------------------------
/notification/views.py:
--------------------------------------------------------------------------------
1 | from django.db.models import Q
2 | from django.shortcuts import render
3 | from rest_framework import status
4 | from rest_framework.decorators import api_view, permission_classes
5 | from rest_framework.permissions import IsAuthenticated
6 | from rest_framework.response import Response
7 |
8 | from users.models import UserProfile
9 |
10 | from .models import Notification
11 | from .serializers import NotificationSerializer
12 |
13 |
14 | @api_view(['PUT'])
15 | @permission_classes((IsAuthenticated,))
16 | def read_notification(request, pk):
17 | try:
18 | notification = Notification.objects.get(id=pk)
19 | if notification.to_user == request.user:
20 | notification.delete()
21 | return Response(status=status.HTTP_204_NO_CONTENT)
22 | else:
23 | return Response(status=status.HTTP_401_UNAUTHORIZED)
24 | except Exception as e:
25 | return Response({'details': f"{e}"},status=status.HTTP_204_NO_CONTENT)
26 |
27 |
28 | @api_view(['GET'])
29 | @permission_classes((IsAuthenticated,))
30 | def get_notifications(request):
31 | is_read = request.query_params.get('is_read')
32 | if is_read == None:
33 | notifications = request.user.notifications.order_by('-created')
34 | else:
35 | notifications = request.user.notifications.filter(is_read=is_read).order_by('-created')
36 | serializer = NotificationSerializer(notifications, many=True)
37 | return Response(serializer.data)
38 |
--------------------------------------------------------------------------------
/article/serializers.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 | from .models import (
3 | Article,
4 | ArticleComment,
5 | ArticleVote
6 | )
7 | from users.serializers import UserProfileSerializer , TopicTagSerializer
8 |
9 |
10 |
11 | class ArticleSerializer(serializers.ModelSerializer):
12 | user = serializers.SerializerMethodField(read_only=True)
13 | tags = TopicTagSerializer(many=True, read_only=True)
14 |
15 | class Meta:
16 | model = Article
17 | fields = '__all__'
18 |
19 | def get_user(self, obj):
20 | user = obj.user.userprofile
21 | serializer = UserProfileSerializer(user, many=False)
22 | return serializer.data
23 |
24 | class ArticleCommentSerializer(serializers.ModelSerializer):
25 | user = serializers.SerializerMethodField(read_only=True)
26 |
27 | class Meta:
28 | model = ArticleComment
29 | fields = '__all__'
30 |
31 | def get_user(self, obj):
32 | user = obj.user.userprofile
33 | serializer = UserProfileSerializer(user, many=False)
34 | return serializer.data
35 |
36 | class ArticleVoteSerializer(serializers.ModelSerializer):
37 | user = serializers.SerializerMethodField(read_only=True)
38 |
39 | class Meta:
40 | model = ArticleVote
41 | field = '__all__'
42 |
43 | def get_user(self, obj):
44 | user = obj.user.userprofile
45 | serializer = UserProfileSerializer(user, many=False)
46 | return serializer.data
47 |
--------------------------------------------------------------------------------
/discussion/serializers.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 | from .models import (
3 | Discussion,
4 | DiscussionComment,
5 | DiscussionVote
6 | )
7 | from users.serializers import UserProfileSerializer, TopicTagSerializer
8 |
9 |
10 |
11 | class DiscussionSerializer(serializers.ModelSerializer):
12 | user = serializers.SerializerMethodField(read_only=True)
13 | tags = TopicTagSerializer(many=True, read_only=True)
14 |
15 | class Meta:
16 | model = Discussion
17 | fields = '__all__'
18 |
19 | def get_user(self, obj):
20 | user = obj.user.userprofile
21 | serializer = UserProfileSerializer(user, many=False)
22 | return serializer.data
23 |
24 | class DiscussionCommentSerializer(serializers.ModelSerializer):
25 | user = serializers.SerializerMethodField(read_only=True)
26 |
27 | class Meta:
28 | model = DiscussionComment
29 | fields = '__all__'
30 |
31 | def get_user(self, obj):
32 | user = obj.user.userprofile
33 | serializer = UserProfileSerializer(user, many=False)
34 | return serializer.data
35 |
36 | class DiscussionVoteSerializer(serializers.ModelSerializer):
37 | user = serializers.SerializerMethodField(read_only=True)
38 |
39 | class Meta:
40 | model = DiscussionVote
41 | field = '__all__'
42 |
43 | def get_user(self, obj):
44 | user = obj.user.userprofile
45 | serializer = UserProfileSerializer(user, many=False)
46 | return serializer.data
47 |
--------------------------------------------------------------------------------
/users/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2 on 2021-05-03 18:14
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 | import uuid
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | initial = True
12 |
13 | dependencies = [
14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15 | ]
16 |
17 | operations = [
18 | migrations.CreateModel(
19 | name='UserProfile',
20 | fields=[
21 | ('name', models.CharField(max_length=200, null=True)),
22 | ('username', models.CharField(max_length=200, null=True)),
23 | ('profile_pic', models.ImageField(blank=True, default='default.png', null=True, upload_to='')),
24 | ('bio', models.TextField(null=True)),
25 | ('vote_ratio', models.IntegerField(blank=True, default=0, null=True)),
26 | ('followers_count', models.IntegerField(blank=True, default=0, null=True)),
27 | ('email_verified', models.BooleanField(default=False)),
28 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
29 | ('followers', models.ManyToManyField(blank=True, related_name='following', to=settings.AUTH_USER_MODEL)),
30 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
31 | ],
32 | ),
33 | ]
34 |
--------------------------------------------------------------------------------
/Reviewers.md:
--------------------------------------------------------------------------------
1 | #
2 |
3 |
17 |
18 |
19 |
20 | After submitting your PR, please tag reviewer(s) in your PR message. You can tag anyone below for the following.
21 |
22 |
23 |
24 | - **Markdown, Documentation, Email templates:**
25 |
26 | [@Mehdi - MidouWebDev](https://github.com/MidouWebDev)
27 |
28 | [@Abhi Vempati](https://github.com/abhivemp/)
29 |
30 | #
31 |
32 | - **API, Backend, Databases, Dependencies:**
33 |
34 | --> *Choose two reviewers :*
35 |
36 | [@Dennis Ivy](https://github.com/divanov11)
37 |
38 | [@Praveen Malethia](https://github.com/PraveenMalethia)
39 |
40 | [@Abhi Vempati](https://github.com/abhivemp)
41 |
42 | [@Bashiru Bukari](https://github.com/bashiru98)
43 |
44 | [@Cody Seibert](https://github.com/codyseibert)
45 |
46 | ### Need Help ?
47 |
48 | Join us in **[the Discord Server](https://discord.gg/9Du4KUY3dE)** and tag the Mumble Api Repo-Managers in the correct channel.
--------------------------------------------------------------------------------
/notification/migrations/0003_auto_20210516_0431.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2 on 2021-05-16 04:31
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 | dependencies = [
11 | ('feed', '0001_initial'),
12 | ('discussion', '0001_initial'),
13 | ('article', '0001_initial'),
14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15 | ('notification', '0002_alter_notification_notification_type'),
16 | ]
17 |
18 | operations = [
19 | migrations.AddField(
20 | model_name='notification',
21 | name='article',
22 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='article.article'),
23 | ),
24 | migrations.AddField(
25 | model_name='notification',
26 | name='discussion',
27 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='discussion.discussion'),
28 | ),
29 | migrations.AddField(
30 | model_name='notification',
31 | name='followed_by',
32 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='followed_by', to=settings.AUTH_USER_MODEL),
33 | ),
34 | migrations.AddField(
35 | model_name='notification',
36 | name='mumble',
37 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='feed.mumble'),
38 | ),
39 | ]
40 |
--------------------------------------------------------------------------------
/notification/serializers.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 | from .models import Notification
3 |
4 | from users.serializers import UserProfileSerializer
5 | from feed.serializers import MumbleSerializer
6 | from article.serializers import ArticleSerializer
7 | from discussion.serializers import DiscussionSerializer
8 |
9 | class NotificationSerializer(serializers.ModelSerializer):
10 | created_by = serializers.SerializerMethodField(read_only=True)
11 | followed_by = serializers.SerializerMethodField(read_only=True)
12 | mumble = serializers.SerializerMethodField(read_only=True)
13 | article = serializers.SerializerMethodField(read_only=True)
14 | discussion = serializers.SerializerMethodField(read_only=True)
15 |
16 | class Meta:
17 | model = Notification
18 | fields = '__all__'
19 |
20 | def get_created_by(self, obj):
21 | return UserProfileSerializer(obj.created_by.userprofile, many=False).data
22 |
23 | def get_followed_by(self, obj):
24 | if obj.notification_type == 'follow':
25 | return UserProfileSerializer(obj.followed_by.userprofile, many=False).data
26 | return None
27 |
28 | def get_mumble(self, obj):
29 | if obj.notification_type == 'mumble':
30 | return MumbleSerializer(obj.mumble, many=False).data
31 | return None
32 |
33 | def get_article(self, obj):
34 | if obj.notification_type == 'article':
35 | return ArticleSerializer(obj.article, many=False).data
36 | return None
37 |
38 | def get_discussion(self, obj):
39 | if obj.notification_type == 'discussion':
40 | return DiscussionSerializer(obj.discussion, many=False).data
41 | return None
42 |
--------------------------------------------------------------------------------
/discussion/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from datetime import timedelta
3 | from .models import (Discussion, DiscussionComment, DiscussionVote)
4 | from django.apps import apps
5 |
6 | models = apps.get_models()
7 |
8 |
9 |
10 | @admin.register(Discussion)
11 | class DiscussionAdmin(admin.ModelAdmin):
12 | list_display = ['id', 'headline', 'user', 'get_utc']
13 | list_filter = ['user']
14 | search_fields = ['user', 'headline']
15 | ordering = ['-created']
16 |
17 | def get_utc(self, obj):
18 | return obj.created + timedelta(minutes=330)
19 |
20 | get_utc.short_description = 'Created (UTC)'
21 |
22 |
23 | @admin.register(DiscussionComment)
24 | class DiscussionCommentAdmin(admin.ModelAdmin):
25 | list_display = ['id', 'discussion', 'user', 'get_utc']
26 | list_filter = ['user']
27 | search_fields = ['user', 'discussion']
28 | ordering = ['-created']
29 |
30 | def get_utc(self, obj):
31 | return obj.created + timedelta(minutes=330)
32 |
33 | get_utc.short_description = 'Created (UTC)'
34 |
35 |
36 | @admin.register(DiscussionVote)
37 | class DiscussionVoteAdmin(admin.ModelAdmin):
38 | list_display = ['id', 'discussion', 'user', 'get_utc']
39 | list_filter = ['user']
40 | search_fields = ['user', 'discussion']
41 | ordering = ['-created']
42 |
43 | def get_utc(self, obj):
44 | return obj.created + timedelta(minutes=330)
45 |
46 | get_utc.short_description = 'Created (UTC)'
47 |
48 |
49 | for model in models:
50 | if admin.sites.AlreadyRegistered:
51 | pass
52 | else:
53 | admin.site.register(model)
54 |
55 | # admin.site.register(Discussion)
56 | # admin.site.register(DiscussionComment)
57 | # admin.site.register(DiscussionVote)
58 |
--------------------------------------------------------------------------------
/article/tests/test_urls.py:
--------------------------------------------------------------------------------
1 | from django.test import SimpleTestCase
2 | from django.urls import reverse , resolve
3 | from article.views import *
4 |
5 | class TestUrls(SimpleTestCase):
6 |
7 | def test_articles_url_is_resolved(self):
8 | url = reverse('mumbles-api-articles:articles')
9 | self.assertEquals(resolve(url).func,articles)
10 |
11 | def test_articles_created_url_is_resolved(self):
12 | url = reverse('mumbles-api-articles:create-article')
13 | self.assertEquals(resolve(url).func,create_article)
14 |
15 | def test_articles_vote_url_is_resolved(self):
16 | url = reverse('mumbles-api-articles:article-vote')
17 | self.assertEquals(resolve(url).func,update_vote)
18 |
19 | def test_get_article_url_is_resolved(self):
20 | url = reverse('mumbles-api-articles:get-article',args=['sOmE-iD'])
21 | self.assertEquals(resolve(url).func,get_article)
22 |
23 | def test_edit_article_url_is_resolved(self):
24 | url = reverse('mumbles-api-articles:edit-article',args=['sOmE-iD'])
25 | self.assertEquals(resolve(url).func,edit_article)
26 |
27 | def test_delete_article_url_is_resolved(self):
28 | url = reverse('mumbles-api-articles:delete-article',args=['sOmE-iD'])
29 | self.assertEquals(resolve(url).func,delete_article)
30 |
31 | def test_edit_article_comment_url_is_resolved(self):
32 | url = reverse('mumbles-api-articles:edit-article-comment',args=['sOmE-iD'])
33 | self.assertEquals(resolve(url).func,edit_article_comment)
34 |
35 | def test_delete_article_comment_url_is_resolved(self):
36 | url = reverse('mumbles-api-articles:delete-article-comment',args=['sOmE-iD'])
37 | self.assertEquals(resolve(url).func,delete_article_comment)
--------------------------------------------------------------------------------
/message/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2 on 2021-07-21 03:35
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 | import uuid
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | ('users', '0004_auto_20210517_1436'),
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='Thread',
19 | fields=[
20 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
21 | ('updated', models.DateTimeField(auto_now=True)),
22 | ('timestamp', models.DateTimeField(auto_now_add=True)),
23 | ('reciever', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reciever', to='users.userprofile')),
24 | ('sender', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sender', to='users.userprofile')),
25 | ],
26 | ),
27 | migrations.CreateModel(
28 | name='UserMessage',
29 | fields=[
30 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
31 | ('body', models.TextField(blank=True, null=True)),
32 | ('is_read', models.BooleanField(default=False)),
33 | ('timestamp', models.DateTimeField(auto_now_add=True)),
34 | ('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.userprofile')),
35 | ('thread', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='message.thread')),
36 | ],
37 | ),
38 | ]
39 |
--------------------------------------------------------------------------------
/feed/serializers.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 | from django.contrib.auth.models import User
3 |
4 | from .models import Mumble
5 | from users.serializers import UserProfileSerializer, UserSerializer
6 |
7 |
8 | class MumbleSerializer(serializers.ModelSerializer):
9 | user = serializers.SerializerMethodField(read_only=True)
10 | original_mumble = serializers.SerializerMethodField(read_only=True)
11 | up_voters = serializers.SerializerMethodField(read_only=True)
12 | down_voters = serializers.SerializerMethodField(read_only=True)
13 |
14 | class Meta:
15 | model = Mumble
16 | fields = '__all__'
17 |
18 | def get_user(self, obj):
19 | user = obj.user.userprofile
20 | serializer = UserProfileSerializer(user, many=False)
21 | return serializer.data
22 |
23 |
24 | def get_original_mumble(self, obj):
25 | original = obj.remumble
26 | if original != None:
27 | serializer = MumbleSerializer(original, many=False)
28 | return serializer.data
29 | else:
30 | return None
31 |
32 | def get_up_voters(self, obj):
33 | # Returns list of users that upvoted post
34 | voters = obj.votes.through.objects.filter(mumble=obj, value='upvote').values_list('user', flat=True)
35 |
36 | voter_objects = obj.votes.filter(id__in=voters)
37 | serializer = UserSerializer(voter_objects, many=True)
38 | return serializer.data
39 |
40 | def get_down_voters(self, obj):
41 | # Returns list of users that upvoted post
42 | voters = obj.votes.through.objects.filter(mumble=obj, value='downvote').values_list('user', flat=True)
43 |
44 | voter_objects = obj.votes.filter(id__in=voters)
45 | serializer = UserSerializer(voter_objects, many=True)
46 | return serializer.data
47 |
--------------------------------------------------------------------------------
/mumblebackend/settings/prod.py:
--------------------------------------------------------------------------------
1 | from mumblebackend.settings.base import *
2 | from .base import *
3 | import django_heroku
4 | import sentry_sdk
5 | from sentry_sdk.integrations.django import DjangoIntegration
6 |
7 | SECRET_KEY = os.environ.get('SECRET_KEY')
8 |
9 | import os
10 |
11 |
12 | sentry_sdk.init(
13 | dsn="https://de808f6f605c4fd79120ddb21f073904@o599875.ingest.sentry.io/5743882",
14 | integrations=[DjangoIntegration()],
15 |
16 | # Set traces_sample_rate to 1.0 to capture 100%
17 | # of transactions for performance monitoring.
18 | # We recommend adjusting this value in production.
19 | traces_sample_rate=1.0,
20 |
21 | # If you wish to associate users to errors (assuming you are using
22 | # django.contrib.auth) you may enable sending PII data.
23 | send_default_pii=True
24 | )
25 |
26 |
27 | SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
28 | SECURE_SSL_REDIRECT = True
29 |
30 |
31 | DATABASES = {
32 | 'default': {
33 | 'ENGINE': 'django.db.backends.postgresql',
34 | 'NAME': os.environ.get('MUMBLE_DB_NAME'),
35 | 'USER': os.environ.get('MUMBLE_USER'),
36 | 'PASSWORD': os.environ.get('MUMBLE_DB_PASS'),
37 | 'HOST': os.environ.get('MUMBLE_HOST'),
38 | 'PORT': '5432',
39 | },
40 | 'message': {
41 | 'ENGINE': 'django.db.backends.sqlite3',
42 | 'NAME': BASE_DIR / 'messages.sqlite3',
43 | }
44 | }
45 |
46 | # EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
47 | # EMAIL_HOST = 'smtp.mailgun.org'
48 | # EMAIL_PORT = 587
49 | # EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')
50 | # EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')
51 | # EMAIL_USE_TLS = True
52 |
53 |
54 | DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
55 |
56 | django_heroku.settings(locals(), test_runner=False)
--------------------------------------------------------------------------------
/article/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.contrib.auth.models import User
3 | from ckeditor.fields import RichTextField
4 | from users.models import TopicTag
5 | import uuid
6 |
7 |
8 | class Article(models.Model):
9 | id = models.UUIDField(default=uuid.uuid4, unique=True, primary_key=True, editable=False)
10 | user = models.ForeignKey(User,on_delete=models.SET_NULL, null=True, blank=True)
11 | title = models.CharField(max_length=500, default="untitled")
12 | content = RichTextField(max_length=10000)
13 | # discussion tags from user model
14 | tags = models.ManyToManyField(TopicTag, related_name='article_tags', blank=True)
15 | created = models.DateTimeField(auto_now_add=True)
16 |
17 | def __str__(self):
18 | return str(self.title)
19 |
20 |
21 | class ArticleComment(models.Model):
22 | id = models.UUIDField(default=uuid.uuid4, unique=True, primary_key=True, editable=False)
23 | article = models.ForeignKey(Article,on_delete=models.CASCADE)
24 | user = models.ForeignKey(User,on_delete=models.SET_NULL, null=True, blank=True)
25 | content = models.TextField(max_length=1000)
26 | created = models.DateTimeField(auto_now_add=True)
27 |
28 | def __str__(self):
29 | return str(self.user.username)
30 |
31 |
32 | class ArticleVote(models.Model):
33 | id = models.UUIDField(default=uuid.uuid4, unique=True, primary_key=True, editable=False)
34 | user = models.ForeignKey(User,on_delete=models.SET_NULL, null=True, blank=True)
35 | article = models.ForeignKey(Article, on_delete=models.CASCADE)
36 | comment = models.ForeignKey(ArticleComment, on_delete=models.SET_NULL,null=True, blank=True)
37 | value = models.IntegerField(blank=True, null=True, default=0)
38 | created = models.DateTimeField(auto_now_add=True)
39 |
40 | def __str__(self):
41 | return f"{self.article} - count - {self.value}"
42 |
--------------------------------------------------------------------------------
/users/models.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import User
2 | from django.db import models
3 | import uuid
4 |
5 |
6 | # A topic tag is added to by the user so they can content on their feed with the
7 | # related tags that
8 | # They have selected
9 | class TopicTag(models.Model):
10 | name = models.CharField(primary_key=True, max_length=150, null=False, blank=False)
11 |
12 | def __str__(self):
13 | return self.name
14 |
15 |
16 | # Skills are added by teh user to indicate topics they are proficient in
17 | class SkillTag(models.Model):
18 | name = models.CharField(primary_key=True, max_length=150, null=False, blank=False)
19 |
20 | def __str__(self):
21 | return self.name
22 |
23 |
24 | class UserProfile(models.Model):
25 | user = models.OneToOneField(User, on_delete=models.CASCADE)
26 | name = models.CharField(max_length=200, null=True)
27 | username = models.CharField(max_length=200, null=True)
28 | profile_pic = models.ImageField(blank=True, null=True, default='default.png')
29 | bio = models.TextField(null=True)
30 | vote_ratio = models.IntegerField(blank=True, null=True, default=0)
31 | followers_count = models.IntegerField(blank=True, null=True, default=0)
32 | skills = models.ManyToManyField(SkillTag, related_name='personal_skills', blank=True)
33 | interests = models.ManyToManyField(TopicTag, related_name='topic_interests', blank=True)
34 | followers = models.ManyToManyField(User, related_name='following', blank=True)
35 | email_verified = models.BooleanField(default=False)
36 | id = models.UUIDField(default=uuid.uuid4, unique=True, primary_key=True, editable=False)
37 | """
38 | profile = UserProfile.objects.first()
39 | profile.followers.all() -> All users following this profile
40 | user.following.all() -> All user profiles I follow
41 | """
42 |
43 | def __str__(self):
44 | return str(self.user.username)
45 |
--------------------------------------------------------------------------------
/discussion/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.contrib.auth.models import User
3 | from ckeditor.fields import RichTextField
4 | from users.models import TopicTag
5 | import uuid
6 |
7 |
8 | class Discussion(models.Model):
9 | id = models.UUIDField(default=uuid.uuid4, unique=True, primary_key=True, editable=False)
10 | user = models.ForeignKey(User,on_delete=models.SET_NULL, null=True, blank=True)
11 | headline = models.CharField(max_length=500, default="no headline")
12 | content = RichTextField(max_length=10000)
13 | # discussion tags from user model
14 | tags = models.ManyToManyField(TopicTag, related_name='discussion_tags', blank=True)
15 | created = models.DateTimeField(auto_now_add=True)
16 |
17 | def __str__(self):
18 | return str(self.headline)
19 |
20 |
21 | class DiscussionComment(models.Model):
22 | id = models.UUIDField(default=uuid.uuid4, unique=True, primary_key=True, editable=False)
23 | discussion = models.ForeignKey(Discussion,on_delete=models.CASCADE)
24 | user = models.ForeignKey(User,on_delete=models.SET_NULL, null=True, blank=True)
25 | content = models.TextField(max_length=1000)
26 | created = models.DateTimeField(auto_now_add=True)
27 |
28 | def __str__(self):
29 | return str(self.user.username)
30 |
31 |
32 | class DiscussionVote(models.Model):
33 | id = models.UUIDField(default=uuid.uuid4, unique=True, primary_key=True, editable=False)
34 | user = models.ForeignKey(User,on_delete=models.SET_NULL, null=True, blank=True)
35 | discussion = models.ForeignKey(Discussion, on_delete=models.CASCADE)
36 | comment = models.ForeignKey(DiscussionComment, on_delete=models.SET_NULL,null=True, blank=True)
37 | value = models.IntegerField(blank=True, null=True, default=0)
38 | created = models.DateTimeField(auto_now_add=True)
39 |
40 | def __str__(self):
41 | return f"{self.discussion} - count - {self.value}"
42 |
--------------------------------------------------------------------------------
/feed/signals.py:
--------------------------------------------------------------------------------
1 | from django.db.models.signals import post_save, pre_save, post_delete
2 | from django.contrib.auth.models import User
3 | from users.models import UserProfile
4 | from .models import Mumble, MumbleVote
5 | from .utils import update_comment_counts, update_remumble_counts
6 |
7 | def update_mumble(sender, instance, created, **kwargs):
8 | #If a post is created & is a comment, them update the parent
9 |
10 | if created and instance.parent:
11 | update_comment_counts(instance.parent, 'add')
12 |
13 | if instance.remumble:
14 | parent = instance.remumble
15 | update_remumble_counts(parent, 'add')
16 |
17 |
18 | def delete_mumble_comments(sender, instance, **kwargs):
19 | #If a post is created & is a comment, them update the parent
20 |
21 | try:
22 | if instance.parent:
23 | update_comment_counts(instance.parent, 'delete')
24 | except Exception as e:
25 | print('mumble associated with comment was deleted')
26 |
27 | try:
28 | if instance.remumble:
29 | update_remumble_counts(instance.remumble, 'delete')
30 | except Exception as e:
31 | print('remumble associated with comment was deleted')
32 |
33 | post_save.connect(update_mumble, sender=Mumble)
34 | post_delete.connect(delete_mumble_comments, sender=Mumble)
35 |
36 |
37 | def vote_updated(sender, instance, **kwargs):
38 | try:
39 | mumble = instance.mumble
40 | up_votes = len(mumble.votes.through.objects.filter(mumble=mumble, value='upvote'))
41 | down_votes = len(mumble.votes.through.objects.filter(mumble=mumble, value='downvote'))
42 | mumble.vote_rank = (up_votes - down_votes)
43 | mumble.save()
44 | except Exception as e:
45 | print('mumble the vote was associated with was already deleted')
46 |
47 |
48 |
49 | post_save.connect(vote_updated, sender=MumbleVote)
50 | post_delete.connect(vote_updated, sender=MumbleVote)
51 |
--------------------------------------------------------------------------------
/users/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from . import views
3 |
4 | from rest_framework_simplejwt.views import (
5 | TokenRefreshView,
6 | )
7 |
8 |
9 | app_name = 'users-api'
10 |
11 | urlpatterns = [
12 | #api/users/
13 | path('', views.users, name='users'),
14 | path('recommended/', views.users_recommended, name="users-recommended"),
15 |
16 | path('profile/', views.profile, name='profile'),
17 | path('register/', views.RegisterView.as_view(), name='register'),
18 | path('login/', views.MyTokenObtainPairView.as_view(), name='login'),
19 | path('following/', views.following, name='following'),
20 | path('refresh_token/', TokenRefreshView.as_view(), name='refresh_token'),
21 |
22 | path('profile_update/', views.UserProfileUpdate.as_view(), name="profile_update"),
23 | path('profile_update/skills/', views.update_skills, name='update_skills'),
24 | path('profile_update/interests/', views.update_interests, name='update_interests'),
25 | path('profile_update/photo/', views.ProfilePictureUpdate.as_view(), name="profile_update_photo"),
26 | path('/follow/', views.follow_user, name="follow-user"),
27 | path('delete-profile/', views.delete_user, name="delete-user"),
28 | path('profile_update/delete/', views.ProfilePictureDelete, name="profile_delete_photo"),
29 | path('/', views.user, name="user"),
30 | path('skills/', views.users_by_skill, name="users-by-skill"),
31 | path('/mumbles/', views.user_mumbles, name="user-mumbles"),
32 | path('/articles/', views.user_articles, name="user-articles"),
33 |
34 | # Forget password or reset password
35 | path('password/change/',views.password_change,name="password-change"),
36 | # path('password/reset/',views.passwordReset,name="password-reset"),
37 |
38 | # email verification urls
39 | path('email/send-email-activation',views.send_activation_email,name='send-activation-email'),
40 | path('verify///',views.activate, name='verify'),
41 | ]
--------------------------------------------------------------------------------
/mumblebackend/urls.py:
--------------------------------------------------------------------------------
1 | """mumblebackend URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/3.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.urls import include, path
14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
15 | """
16 | from django.contrib import admin
17 | from django.urls import path, include
18 | from . import views
19 | from rest_framework.schemas import get_schema_view
20 | from rest_framework.documentation import include_docs_urls
21 | from django.conf import settings
22 | from django.conf.urls.static import static
23 |
24 | urlpatterns = [
25 | path('admin/', admin.site.urls),
26 | # commenting this because docs is added for better endpoint view
27 | # path('', views.api_root),
28 | path('api/users/', include('users.urls')),
29 | path('api/articles/', include('article.urls')),
30 | path('api/discussions/', include('discussion.urls')),
31 | path('api/messages/', include('message.urls')),
32 | path('api/notifications/', include('notification.urls')),
33 | path('api/mumbles/', include('feed.urls')),
34 | path('schema/', get_schema_view(
35 | title="MumbleAPI",
36 | description="API for the Mumble.dev",
37 | version="1.0.0"
38 | ), name="mumble-schema"),
39 | path('', include_docs_urls(
40 | title="MumbleAPI",
41 | description="API for the Mumble.dev",
42 | ), name="mumble-docs")
43 | ]
44 |
45 | if settings.DEBUG:
46 | urlpatterns += static(settings.STATIC_URL,document_root=settings.STATIC_ROOT)
47 | urlpatterns += static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT)
--------------------------------------------------------------------------------
/notification/signals.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import User
2 | from django.db.models.signals import post_delete, post_save, pre_save
3 |
4 | from article.models import Article
5 | from discussion.models import Discussion
6 | from feed.models import Mumble
7 | from users.models import UserProfile
8 |
9 | from .models import Notification
10 |
11 |
12 | def article_created(sender, instance, created, **kwargs):
13 | if not created: return
14 | followers = instance.user.userprofile.followers.all()
15 | for follower in followers:
16 | notification = Notification.objects.create(
17 | to_user=follower,
18 | created_by=instance.user,
19 | notification_type='article',
20 | article=instance,
21 | content=f"An article {instance.title} recently posted by {instance.user.userprofile.name}."
22 | )
23 |
24 |
25 | def mumble_created(sender, instance, created, **kwargs):
26 | if not created: return
27 | followers = instance.user.userprofile.followers.all()
28 | for follower in followers:
29 | notification = Notification.objects.create(
30 | to_user=follower,
31 | created_by=instance.user,
32 | notification_type='mumble',
33 | mumble=instance,
34 | content=f"{instance.user.userprofile.name} posted a new Mumble."
35 | )
36 |
37 |
38 |
39 | def discussion_created(sender, instance, created, **kwargs):
40 | if not created: return
41 | followers = instance.user.userprofile.followers.all()
42 | for follower in followers:
43 | notification = Notification.objects.create(
44 | to_user=follower,
45 | created_by=instance.user,
46 | notification_type='discussion',
47 | discussion=instance,
48 | content=f"A discussion was started by {instance.user.userprofile.name}."
49 | )
50 |
51 | post_save.connect(article_created, sender=Article)
52 | post_save.connect(mumble_created, sender=Mumble)
53 | post_save.connect(discussion_created, sender=Discussion)
--------------------------------------------------------------------------------
/notification/migrations/0005_auto_20210610_0037.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2 on 2021-06-10 00:37
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 | dependencies = [
11 | ('discussion', '0003_discussion_tags'),
12 | ('feed', '0002_alter_mumble_content'),
13 | ('article', '0003_auto_20210522_1337'),
14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15 | ('notification', '0004_remove_notification_content_id'),
16 | ]
17 |
18 | operations = [
19 | migrations.AlterField(
20 | model_name='notification',
21 | name='article',
22 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='article.article'),
23 | ),
24 | migrations.AlterField(
25 | model_name='notification',
26 | name='created_by',
27 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
28 | ),
29 | migrations.AlterField(
30 | model_name='notification',
31 | name='discussion',
32 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='discussion.discussion'),
33 | ),
34 | migrations.AlterField(
35 | model_name='notification',
36 | name='followed_by',
37 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='followed_by', to=settings.AUTH_USER_MODEL),
38 | ),
39 | migrations.AlterField(
40 | model_name='notification',
41 | name='mumble',
42 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='feed.mumble'),
43 | ),
44 | migrations.AlterField(
45 | model_name='notification',
46 | name='to_user',
47 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL),
48 | ),
49 | ]
50 |
--------------------------------------------------------------------------------
/feed/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.contrib.auth.models import User
3 | from ckeditor.fields import RichTextField
4 | import uuid
5 |
6 |
7 | #This needs to be shareable
8 | class Mumble(models.Model):
9 | parent =models.ForeignKey("self", on_delete=models.CASCADE, null=True, blank=True)
10 | #For re-mumble (Share) functionality
11 | remumble = models.ForeignKey("self", on_delete=models.CASCADE, related_name='remumbles', null=True, blank=True)
12 | user = models.ForeignKey(User, on_delete=models.CASCADE)
13 | #content is allowed to be plan for remumbles
14 | content = RichTextField(null=True, blank=True)
15 | image = models.ImageField(blank=True, null=True)
16 | vote_rank = models.IntegerField(blank=True, null=True, default=0)
17 | comment_count = models.IntegerField(blank=True, null=True, default=0)
18 | share_count = models.IntegerField(blank=True, null=True, default=0)
19 | created = models.DateTimeField(auto_now_add=True)
20 | votes = models.ManyToManyField(User, related_name='mumble_user', blank=True, through='MumbleVote')
21 | id = models.UUIDField(default=uuid.uuid4, unique=True, primary_key=True, editable=False)
22 |
23 | class Meta:
24 | ordering = ['-created']
25 |
26 | def __str__(self):
27 | try:
28 | content = self.content[0:80]
29 | except Exception:
30 | content = 'Remumbled: ' + str(self.remumble.content[0:80])
31 | return content
32 |
33 | @property
34 | def shares(self):
35 | queryset = self.remumbles.all()
36 | return queryset
37 |
38 | @property
39 | def comments(self):
40 | #Still need a way to get all sub elemsnts
41 | queryset = self.mumble_set.all()
42 | return queryset
43 |
44 |
45 |
46 | class MumbleVote(models.Model):
47 |
48 | CHOICES = (
49 | ('upvote', 'upvote'),
50 | ('downvote', 'downvote'),
51 | )
52 |
53 | user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
54 | mumble = models.ForeignKey(Mumble, on_delete=models.CASCADE, null=True, blank=True)
55 | value = models.CharField(max_length=20, choices=CHOICES)
56 | id = models.UUIDField(default=uuid.uuid4, unique=True, primary_key=True, editable=False)
57 |
58 | def __str__(self):
59 | return str(self.user) + ' ' + str(self.value) + '"' + str(self.mumble) + '"'
60 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | ### Describe your changes :
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | I worked on the ..... because ...
18 |
19 |
20 |
21 |
22 |
23 | #
24 |
25 | ### Type of change :
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | - [ ] Bug fix
35 | - [ ] New feature
36 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
37 |
38 | #
39 |
40 | ### Preview (Screenshots) :
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | If it is possible, please link screenshots of your changes preview !
49 |
50 |
51 | #
52 |
53 | ### Checklist:
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | - [ ] I have read the **Code Of Conduct** document.
64 | - [ ] I have read the **CONTRIBUTING** document.
65 | - [ ] I have performed a self-review of my own.
66 | - [ ] I have tagged my reviewers below.
67 | - [ ] I have commented my code, particularly in hard-to-understand areas.
68 | - [ ] My changes generate no new warnings.
69 | - [ ] I have added tests that prove my fix is effective or that my feature works.
70 | - [ ] All new and existing tests passed.
71 |
72 |
73 |
74 | #
75 |
76 | ### Reviewers
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/discussion/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2 on 2021-05-03 18:14
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 | import uuid
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | initial = True
12 |
13 | dependencies = [
14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15 | ]
16 |
17 | operations = [
18 | migrations.CreateModel(
19 | name='Discussion',
20 | fields=[
21 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
22 | ('headline', models.CharField(default='no headline', max_length=500)),
23 | ('content', models.TextField(max_length=10000)),
24 | ('created', models.DateTimeField(auto_now_add=True)),
25 | ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
26 | ],
27 | ),
28 | migrations.CreateModel(
29 | name='DiscussionComment',
30 | fields=[
31 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
32 | ('content', models.TextField(max_length=1000)),
33 | ('created', models.DateTimeField(auto_now_add=True)),
34 | ('discussion', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='discussion.discussion')),
35 | ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
36 | ],
37 | ),
38 | migrations.CreateModel(
39 | name='DiscussionVote',
40 | fields=[
41 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
42 | ('value', models.IntegerField(blank=True, default=0, null=True)),
43 | ('created', models.DateTimeField(auto_now_add=True)),
44 | ('comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='discussion.discussioncomment')),
45 | ('discussion', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='discussion.discussion')),
46 | ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
47 | ],
48 | ),
49 | ]
50 |
--------------------------------------------------------------------------------
/feed/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2 on 2021-05-06 17:28
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 | import uuid
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | initial = True
12 |
13 | dependencies = [
14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15 | ]
16 |
17 | operations = [
18 | migrations.CreateModel(
19 | name='Mumble',
20 | fields=[
21 | ('content', models.TextField(blank=True, null=True)),
22 | ('image', models.ImageField(blank=True, null=True, upload_to='')),
23 | ('vote_rank', models.IntegerField(blank=True, default=0, null=True)),
24 | ('comment_count', models.IntegerField(blank=True, default=0, null=True)),
25 | ('share_count', models.IntegerField(blank=True, default=0, null=True)),
26 | ('created', models.DateTimeField(auto_now_add=True)),
27 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
28 | ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='feed.mumble')),
29 | ('remumble', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='remumbles', to='feed.mumble')),
30 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
31 | ],
32 | options={
33 | 'ordering': ['-created'],
34 | },
35 | ),
36 | migrations.CreateModel(
37 | name='MumbleVote',
38 | fields=[
39 | ('value', models.CharField(choices=[('upvote', 'upvote'), ('downvote', 'downvote')], max_length=20)),
40 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
41 | ('mumble', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='feed.mumble')),
42 | ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
43 | ],
44 | ),
45 | migrations.AddField(
46 | model_name='mumble',
47 | name='votes',
48 | field=models.ManyToManyField(blank=True, related_name='mumble_user', through='feed.MumbleVote', to=settings.AUTH_USER_MODEL),
49 | ),
50 | ]
51 |
--------------------------------------------------------------------------------
/article/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2 on 2021-05-03 18:14
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 | import uuid
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | initial = True
12 |
13 | dependencies = [
14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15 | ]
16 |
17 | operations = [
18 | migrations.CreateModel(
19 | name='Article',
20 | fields=[
21 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
22 | ('title', models.CharField(default='untitled', max_length=500)),
23 | ('content', models.TextField(max_length=10000)),
24 | ('tags', models.CharField(max_length=100)),
25 | ('created', models.DateTimeField(auto_now_add=True)),
26 | ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
27 | ],
28 | ),
29 | migrations.CreateModel(
30 | name='ArticleComment',
31 | fields=[
32 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
33 | ('content', models.TextField(max_length=1000)),
34 | ('created', models.DateTimeField(auto_now_add=True)),
35 | ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='article.article')),
36 | ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
37 | ],
38 | ),
39 | migrations.CreateModel(
40 | name='ArticleVote',
41 | fields=[
42 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
43 | ('value', models.IntegerField(blank=True, default=0, null=True)),
44 | ('created', models.DateTimeField(auto_now_add=True)),
45 | ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='article.article')),
46 | ('comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='article.articlecomment')),
47 | ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
48 | ],
49 | ),
50 | ]
51 |
--------------------------------------------------------------------------------
/feed/tests/test_urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import reverse , resolve
2 | from rest_framework import status
3 | from django.contrib.auth.models import User
4 | from rest_framework.test import APITestCase
5 | from feed import views
6 | # Create your tests here.
7 |
8 | class FeedTestsUrls(APITestCase):
9 |
10 | def setUp(self):
11 | url = reverse('users-api:register')
12 | data = {
13 | 'username':'test',
14 | 'email':'test@gmail.com',
15 | 'password':'test@123'
16 | }
17 | response = self.client.post(url, data, format='json')
18 | self.assertEqual(response.status_code, status.HTTP_200_OK)
19 | self.assertEqual(User.objects.count(), 1)
20 | self.assertEqual(User.objects.get().username, 'test')
21 | self.test_user = User.objects.get(username='test')
22 | self.test_user_pwd = 'test@123'
23 |
24 | def test_mumbles_url(self):
25 | url = 'mumbles-api:mumbles'
26 | reversed_url = reverse(url)
27 | self.assertEqual(resolve(reversed_url).func,views.mumbles)
28 |
29 | def test_mumbles_create_url(self):
30 | url = 'mumbles-api:mumble-create'
31 | reversed_url = reverse(url)
32 | self.assertEqual(resolve(reversed_url).func,views.create_mumble)
33 |
34 | def test_mumbles_edit_url(self):
35 | url = 'mumbles-api:mumble-edit'
36 | reversed_url = reverse(url,args=['9812-3ehj9-238d39-8hd23h'])
37 | self.assertEqual(resolve(reversed_url).func,views.edit_mumble)
38 |
39 | def test_mumbles_detail_url(self):
40 | url = 'mumbles-api:mumble-details'
41 | reversed_url = reverse(url,args=['9812-3ehj9-238d39-8hd23h'])
42 | self.assertEqual(resolve(reversed_url).func,views.mumble_details)
43 |
44 | def test_mumbles_remumble_url(self):
45 | url = 'mumbles-api:mumble-remumble'
46 | reversed_url = reverse(url)
47 | self.assertEqual(resolve(reversed_url).func,views.remumble)
48 |
49 | def test_mumbles_vote_url(self):
50 | url = 'mumbles-api:posts-vote'
51 | reversed_url = reverse(url)
52 | self.assertEqual(resolve(reversed_url).func,views.update_vote)
53 |
54 | def test_mumbles_delete_url(self):
55 | url = 'mumbles-api:delete-mumble'
56 | reversed_url = reverse(url,args=['9812-3ehj9-238d39-8hd23h'])
57 | self.assertEqual(resolve(reversed_url).func,views.delete_mumble)
58 |
59 | def test_mumbles_comments_url(self):
60 | url = 'mumbles-api:mumble-comments'
61 | reversed_url = reverse(url,args=['9812-3ehj9-238d39-8hd23h'])
62 | self.assertEqual(resolve(reversed_url).func,views.mumble_comments)
63 |
--------------------------------------------------------------------------------
/users/serializers.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 | from django.contrib.auth.models import User
3 | from rest_framework_simplejwt.tokens import RefreshToken
4 |
5 | from .models import UserProfile, TopicTag, SkillTag
6 |
7 |
8 | class TopicTagSerializer(serializers.ModelSerializer):
9 | class Meta:
10 | model = TopicTag
11 | fields = '__all__'
12 |
13 | class SkillTagSerializer(serializers.ModelSerializer):
14 | class Meta:
15 | model = SkillTag
16 | fields = '__all__'
17 |
18 |
19 | class UserProfileSerializer(serializers.ModelSerializer):
20 | profile_pic = serializers.SerializerMethodField(read_only=True)
21 | interests = TopicTagSerializer(many=True, read_only=True)
22 | skills = SkillTagSerializer(many=True, read_only=True)
23 | class Meta:
24 | model = UserProfile
25 | fields = '__all__'
26 |
27 | def get_profile_pic(self, obj):
28 | try:
29 | pic = obj.profile_pic.url
30 | except:
31 | pic = None
32 | return pic
33 |
34 |
35 | class CurrentUserSerializer(serializers.ModelSerializer):
36 | profile = serializers.SerializerMethodField(read_only=True)
37 | class Meta:
38 | model = User
39 | fields = ['id', 'profile', 'username','email','is_superuser', 'is_staff']
40 |
41 | def get_profile(self, obj):
42 | profile = obj.userprofile
43 | serializer = UserProfileSerializer(profile, many=False)
44 | return serializer.data
45 |
46 | class UserSerializer(serializers.ModelSerializer):
47 | profile = serializers.SerializerMethodField(read_only=True)
48 | class Meta:
49 | model = User
50 | fields = ['id', 'profile', 'username', 'is_superuser', 'is_staff']
51 |
52 | def get_profile(self, obj):
53 | profile = obj.userprofile
54 | serializer = UserProfileSerializer(profile, many=False)
55 | return serializer.data
56 |
57 |
58 | class UserSerializerWithToken(UserSerializer):
59 | access = serializers.SerializerMethodField(read_only=True)
60 | refresh = serializers.SerializerMethodField(read_only=True)
61 |
62 | class Meta:
63 | model = User
64 | exclude = ['password']
65 |
66 | def get_access(self, obj):
67 | token = RefreshToken.for_user(obj)
68 |
69 | token['username'] = obj.username
70 | token['name'] = obj.userprofile.name
71 | token['profile_pic'] = obj.userprofile.profile_pic.url
72 | token['is_staff'] = obj.is_staff
73 | token['id'] = obj.id
74 | return str(token.access_token)
75 |
76 | def get_refresh(self, obj):
77 | token = RefreshToken.for_user(obj)
78 | return str(token)
--------------------------------------------------------------------------------
/feed/tests/test_views.py:
--------------------------------------------------------------------------------
1 | from django.urls import reverse
2 | from rest_framework import status
3 | from django.contrib.auth.models import User
4 | from rest_framework.test import APITestCase , APIClient
5 | import json
6 | # Create your tests here.
7 |
8 | class FeedTestsViews(APITestCase):
9 |
10 | def setUp(self):
11 | url = reverse('users-api:register')
12 | data = {
13 | 'username':'test',
14 | 'email':'test@gmail.com',
15 | 'password':'test@123'
16 | }
17 | response = self.client.post(url, data, format='json')
18 | self.assertEqual(response.status_code, status.HTTP_200_OK)
19 | self.assertEqual(User.objects.count(), 1)
20 | self.assertEqual(User.objects.get().username, 'test')
21 | self.test_user = User.objects.get(username='test')
22 | self.test_user_pwd = 'test@123'
23 | url = 'mumbles-api:mumble-create'
24 | reversed_url = reverse(url)
25 | data = {
26 | 'content':"Mumble Test Post"
27 | }
28 | client = APIClient()
29 | client.force_authenticate(user=self.test_user)
30 | response = client.post(reversed_url, data)
31 | self.mumble = json.loads(response.content.decode('utf-8'))
32 | self.assertEqual(response.status_code,status.HTTP_200_OK)
33 |
34 | def test_users_url(self):
35 | url = 'mumbles-api:mumbles'
36 | reversed_url = reverse(url)
37 | client = APIClient()
38 | client.force_authenticate(user=self.test_user)
39 | response = client.get(reversed_url)
40 | response_data = json.loads(response.content.decode('utf-8'))
41 | self.assertEqual(response.status_code, status.HTTP_200_OK)
42 | self.assertEqual(response_data.get('count'),1)
43 |
44 | def test_mumbles_edit_view(self):
45 | url = 'mumbles-api:mumble-edit'
46 | reversed_url = reverse(url,args=[self.mumble.get('id')])
47 | client = APIClient()
48 | client.force_authenticate(user=self.test_user)
49 | data = {
50 | 'content':"Mumble Post edited"
51 | }
52 | response = client.patch(reversed_url,data, format='json')
53 | response_data = json.loads(response.content.decode('utf-8'))
54 | self.assertEqual(response.status_code, status.HTTP_200_OK)
55 | self.assertEqual(response_data.get('content'),data.get('content'))
56 | self.mumble = response_data
57 |
58 | def test_mumbles_details_view(self):
59 | client = APIClient()
60 | client.force_authenticate(user=self.test_user)
61 | url = 'mumbles-api:mumble-details'
62 | reversed_url = reverse(url,args=[self.mumble.get('id')])
63 | response = client.get(reversed_url)
64 | self.assertEqual(response.status_code, status.HTTP_200_OK)
65 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 | /mumblebackend/settings/local
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | messages.sqlite3
63 | db.sqlite3-journal
64 |
65 | # Flask stuff:
66 | instance/
67 | .webassets-cache
68 |
69 | # Scrapy stuff:
70 | .scrapy
71 |
72 | # Sphinx documentation
73 | docs/_build/
74 |
75 | # PyBuilder
76 | .pybuilder/
77 | target/
78 |
79 | # Jupyter Notebook
80 | .ipynb_checkpoints
81 |
82 | # IPython
83 | profile_default/
84 | ipython_config.py
85 |
86 | # pyenv
87 | # For a library or package, you might want to ignore these files since the code is
88 | # intended to run in multiple environments; otherwise, check them in:
89 | # .python-version
90 |
91 | # pipenv
92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
95 | # install all needed dependencies.
96 | #Pipfile.lock
97 |
98 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
99 | __pypackages__/
100 |
101 | # Celery stuff
102 | celerybeat-schedule
103 | celerybeat.pid
104 |
105 | # SageMath parsed files
106 | *.sage.py
107 |
108 | # Environments
109 | .env
110 | .venv
111 | env/
112 | venv/
113 | ENV/
114 | env.bak/
115 | venv.bak/
116 |
117 | # Spyder project settings
118 | .spyderproject
119 | .spyproject
120 |
121 | # Rope project settings
122 | .ropeproject
123 |
124 | # mkdocs documentation
125 | /site
126 |
127 | # mypy
128 | .mypy_cache/
129 | .dmypy.json
130 | dmypy.json
131 |
132 | # Pyre type checker
133 | .pyre/
134 |
135 | # pytype static type analyzer
136 | .pytype/
137 |
138 | # Cython debug symbols
139 | cython_debug/
140 |
141 | .vscode
142 | .idea
--------------------------------------------------------------------------------
/message/views.py:
--------------------------------------------------------------------------------
1 | from rest_framework.decorators import api_view, permission_classes
2 | from rest_framework.permissions import IsAuthenticated
3 | from rest_framework.response import Response
4 | from rest_framework import status
5 | from users.models import UserProfile
6 | from .serializers import MessageSerializer , ThreadSerializer
7 | from .models import UserMessage , Thread
8 | from django.db.models import Q
9 |
10 | @api_view(['GET'])
11 | @permission_classes((IsAuthenticated,))
12 | def read_message(request, pk):
13 | try:
14 | thread = Thread.objects.get(id=pk)
15 | messages = thread.messages.all()
16 | un_read = thread.messages.filter(is_read=False)
17 | for msg in un_read:
18 | msg.is_read = True
19 | msg.save()
20 | serializer = MessageSerializer(messages, many=True)
21 | return Response(serializer.data)
22 | except Exception as e:
23 | return Response({'details': f"{e}"},status=status.HTTP_204_NO_CONTENT)
24 |
25 | @api_view(['POST'])
26 | @permission_classes((IsAuthenticated,))
27 | def CreateThread(request):
28 | sender = request.user.userprofile
29 | recipient_id = request.data.get('recipient_id')
30 | recipient = UserProfile.objects.get(id=recipient_id)
31 | if recipient_id is not None:
32 | try:
33 | thread,created = Thread.objects.get_or_create(sender=sender,reciever=recipient)
34 | serializer = ThreadSerializer(thread, many=False)
35 | return Response(serializer.data)
36 | except UserProfile.DoesNotExist:
37 | return Response({'detail':'User with that id doesnt not exists'})
38 | else:
39 | return Response({'details':'Recipient id not found'})
40 |
41 | @api_view(['GET'])
42 | @permission_classes((IsAuthenticated,))
43 | def get_messages(request):
44 | user = request.user.userprofile
45 | threads = Thread.objects.filter(Q(sender=user)|Q(reciever=user))
46 | serializer = ThreadSerializer(threads, many=True)
47 | return Response(serializer.data)
48 |
49 | @api_view(['POST'])
50 | @permission_classes((IsAuthenticated,))
51 | def create_message(request):
52 | sender = request.user.userprofile
53 | data = request.data
54 | thread_id = data.get('thread_id')
55 | if thread_id:
56 | message = data.get('message')
57 | thread= Thread.objects.get(id=thread_id)
58 | if thread:
59 | if message is not None:
60 | message = UserMessage.objects.create(thread=thread,sender=sender,body=message)
61 | message.save()
62 | serializer = ThreadSerializer(thread, many=False)
63 | return Response(serializer.data)
64 | else:
65 | return Response({'details':'Content for message required'})
66 | else:
67 | return Response({'details':'Thread not found'})
68 | else:
69 | return Response({'details':'Please provide other user id'})
--------------------------------------------------------------------------------
/users/tests/test_urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import reverse , resolve
2 | from rest_framework import status
3 | from rest_framework.test import APITestCase
4 | from users.views import (
5 | follow_user , users , UserProfileUpdate ,
6 | ProfilePictureUpdate , users_recommended ,
7 | user , user_mumbles, user_articles, password_change,
8 | send_activation_email, activate)
9 | # Create your tests here.
10 |
11 | class AccountTests(APITestCase):
12 |
13 | def setUp(self):
14 | pass
15 |
16 | def test_users_url(self):
17 | url = 'users-api:users'
18 | reversed_url = reverse(url)
19 | response = self.client.get('/api/users/')
20 | self.assertEqual(resolve(reversed_url).func,users)
21 | self.assertEqual(response.status_code, status.HTTP_200_OK)
22 |
23 | def test_users_follow_url(self):
24 | url = 'users-api:follow-user'
25 | reversed_url = reverse(url,args=['praveen'])
26 | self.assertEqual(resolve(reversed_url).func,follow_user)
27 |
28 | def test_user_profile_update_url(self):
29 | url = 'users-api:profile_update'
30 | reversed_url = reverse(url)
31 | self.assertEqual(resolve(reversed_url).func.view_class,UserProfileUpdate)
32 |
33 | def test_profile_update_photo_url(self):
34 | url = 'users-api:profile_update_photo'
35 | reversed_url = reverse(url)
36 | resolved = resolve(reversed_url).func
37 | self.assertEqual(resolved.view_class,ProfilePictureUpdate)
38 |
39 | def test_users_recommended_url(self):
40 | url = 'users-api:users-recommended'
41 | reversed_url = reverse(url)
42 | self.assertEqual(resolve(reversed_url).func,users_recommended)
43 |
44 | def test_user_url(self):
45 | url = 'users-api:user'
46 | reversed_url = reverse(url,args=['test'])
47 | self.assertEqual(resolve(reversed_url).func,user)
48 |
49 | def test_user_mumbles(self):
50 | url = 'users-api:user-mumbles'
51 | reversed_url = reverse(url,args=['test'])
52 | self.assertEqual(resolve(reversed_url).func,user_mumbles)
53 |
54 | def test_user_articles_url(self):
55 | url = 'users-api:user-articles'
56 | reversed_url = reverse(url,args=['test'])
57 | self.assertEqual(resolve(reversed_url).func,user_articles)
58 |
59 | def test_user_password_url(self):
60 | url = 'users-api:password-change'
61 | reversed_url = reverse(url)
62 | self.assertEqual(resolve(reversed_url).func,password_change)
63 |
64 | def test_send_activation_email_url(self):
65 | url = 'users-api:send-activation-email'
66 | reversed_url = reverse(url)
67 | self.assertEqual(resolve(reversed_url).func,send_activation_email)
68 |
69 | def test_active_user_account_url(self):
70 | url = 'users-api:verify'
71 | reversed_url = reverse(url,args=['903u924u934u598348943','*&6g83chruhrweriuj'])
72 | self.assertEqual(resolve(reversed_url).func,activate)
--------------------------------------------------------------------------------
/CodeOfConduct.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | API
6 |
7 |
8 |
9 | Code of Conduct
10 |
11 |
12 |
13 | ### Our Goal
14 |
15 | We *as Contributors and maintainers* want to make this experience **a harassment-free** for everyone, regardless of age, body
16 | size, disability, ethnicity, gender identity and expression, level of experience,
17 | education, socio-economic status, nationality, personal appearance, race,
18 | religion, or sexual identity and orientation.
19 |
20 | #
21 |
22 | ### How to act ?
23 |
24 |
25 | * Using **welcoming and inclusive language**
26 | * **Being respectful** of differing viewpoints and experiences
27 | * Focusing on what is best for the community
28 | * Gracefully **accepting constructive criticism**
29 | * **Showing empathy** towards other community members
30 |
31 | #
32 |
33 | ### We don't accept :
34 |
35 | * **The use** of sexualized language or imagery and **unwelcome** sexual attention or
36 | advances
37 | * **Trolling, insulting**/derogatory comments, and personal or political attacks
38 | * **Public or private harassment**
39 | * **Publishing others private information**, such as a physical or electronic
40 | address, **without explicit permission**
41 |
42 | #
43 |
44 |
45 |
46 | Be Kind !
47 |
48 |
49 |
50 | #
51 |
52 |
53 | ### Our Responsibilities
54 |
55 | *Project maintainers are responsible for clarifying* the standards of acceptable
56 | behavior and are expected to take *appropriate and fair corrective action* in
57 | *response to any instances* of **unacceptable** behavior.
58 |
59 | Project maintainers **have the right and responsibility** to *remove, edit, or
60 | reject comments, commits, code, wiki edits, issues, and other contributions
61 | that are not aligned to this Code of Conduct*, or to **ban temporarily or
62 | permanently any contributor for unacceptable behaviors that they deem inappropriate**,
63 | threatening, offensive, or harmful.
64 |
65 | #
66 |
67 | ### Scope
68 |
69 | This Code of Conduct applies both within project spaces and in public spaces
70 | when an individual is representing the project or its community. Examples of
71 | representing a project or community include using an official project e-mail
72 | address, posting via an official social media account, or acting as an appointed
73 | representative at an online or offline event. Representation of a project may be
74 | further defined and clarified by project maintainers.
75 |
76 | #
77 |
78 | ### Enforcement
79 |
80 | **Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team on the
**.
81 |
82 |
83 |
84 | All
85 | **complaints will be reviewed and investigated and will result in a response that
86 | is deemed necessary and appropriate to the circumstances.**
87 |
88 |
89 |
90 | The project team is
91 | obligated to maintain confidentiality with regard to the reporter of an incident.
92 | Further details of specific enforcement policies may be posted separately.
93 |
94 | Project maintainers who do not follow or enforce the Code of Conduct in good
95 | faith **may face temporary or permanent repercussions as determined by other
96 | members of the project's leadership.**
97 |
98 | #
99 |
100 |
101 | ### NB
102 |
103 |
104 | > ⚠ If you are victim of one of these unacceptable behaviors, DM the **Mumble Bot** in our
105 |
106 | **Moderators and the Repo Managers are there** to solve your problems !
107 |
--------------------------------------------------------------------------------
/users/templates/verify-email.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Email Verification
9 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | MUMBLE
51 |
52 | |
53 |
54 |
55 |
56 |
57 |
58 | Hi, 👋
59 |
60 |
61 | {{user.name}} !
62 |
63 |
64 |
65 |
66 | Thanks for signing up ! 🙏
67 |
68 |
69 |
70 |
71 |
You’re almost ready to start enjoying MUMBLE
72 |
73 |
74 | Please verify your email address by clicking the below button to join the community where
75 | you can start discussions, share your projects and more.. !
76 |
77 |
78 |
79 |
98 |
99 |
100 |
101 |
Thanks !
102 |
The Mumble Team
103 |
104 |
105 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/users/templates/forgotpwd-email.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Reset Password Email
9 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | MUMBLE
57 |
58 | |
59 |
60 |
61 |
62 |
63 |
64 | Hi, 👋
65 |
66 |
67 | {{user.name}} !
68 |
69 |
70 |
71 | Looks like you forgot your password ! 🙂
72 |
73 |
74 |
75 |
76 |
77 |
Don't worry, you are going to take control of your MUMBLE account soon.
78 |
79 |
80 | You’re almost ready to enjoy MUMBLE again !
81 |
82 |
83 | Please click the button below to reset the password and re-join the community where
84 | you can start discussions, share your projects and more.. !
85 |
86 |
87 |
88 |
107 |
108 |
109 |
110 |
Thanks !
111 |
The Mumble Team
112 |
113 |
114 |
123 |
124 |
--------------------------------------------------------------------------------
/mumblebackend/settings/base.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | import os
3 | from datetime import timedelta
4 | import django_heroku
5 |
6 | # Build paths inside the project like this: BASE_DIR / 'subdir'.
7 | BASE_DIR = Path(__file__).resolve().parent.parent.parent
8 |
9 |
10 | # Quick-start development settings - unsuitable for production
11 | # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
12 |
13 | # SECURITY WARNING: keep the secret key used in production secret!
14 | SECRET_KEY = 'my secret key for local testing'
15 |
16 | # SECURITY WARNING: don't run with debug turned on in production!
17 | DEBUG = True
18 |
19 | ALLOWED_HOSTS = ['mumbleapi.herokuapp.com', '127.0.0.1']
20 |
21 |
22 | # Application definition
23 |
24 | INSTALLED_APPS = [
25 | 'django.contrib.admin',
26 | 'django.contrib.auth',
27 | 'django.contrib.contenttypes',
28 | 'django.contrib.sessions',
29 | 'django.contrib.messages',
30 | 'django.contrib.staticfiles',
31 |
32 |
33 | 'users.apps.UsersConfig',
34 | 'feed.apps.FeedConfig',
35 | 'article.apps.ArticleConfig',
36 | 'discussion.apps.DiscussionConfig',
37 | 'message.apps.MessageConfig',
38 | 'notification.apps.NotificationConfig',
39 |
40 | 'rest_framework',
41 | 'corsheaders',
42 | 'ckeditor',
43 | ]
44 |
45 | REST_FRAMEWORK = {
46 | 'DEFAULT_AUTHENTICATION_CLASSES': (
47 | 'rest_framework_simplejwt.authentication.JWTAuthentication',
48 | ),
49 | 'DEFAULT_THROTTLE_CLASSES': [
50 | 'rest_framework.throttling.AnonRateThrottle',
51 | 'rest_framework.throttling.UserRateThrottle'
52 | ],
53 | 'DEFAULT_THROTTLE_RATES': {
54 | 'anon': '520/min',
55 | 'user': '520/min'
56 | },
57 | 'TEST_REQUEST_DEFAULT_FORMAT': 'json',
58 |
59 | # Added default schema class because by default django rest required this class
60 | 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema'
61 | }
62 |
63 |
64 | SIMPLE_JWT = {
65 | 'ACCESS_TOKEN_LIFETIME': timedelta(days=60),
66 | 'REFRESH_TOKEN_LIFETIME': timedelta(days=160),
67 | 'ROTATE_REFRESH_TOKENS': False,
68 | 'BLACKLIST_AFTER_ROTATION': True,
69 | 'UPDATE_LAST_LOGIN': False,
70 |
71 | 'ALGORITHM': 'HS256',
72 | 'SIGNING_KEY': SECRET_KEY,
73 | 'VERIFYING_KEY': None,
74 | 'AUDIENCE': None,
75 | 'ISSUER': None,
76 |
77 | 'AUTH_HEADER_TYPES': ('Bearer',),
78 | 'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
79 | 'USER_ID_FIELD': 'id',
80 | 'USER_ID_CLAIM': 'user_id',
81 |
82 | 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
83 | 'TOKEN_TYPE_CLAIM': 'token_type',
84 |
85 | 'JTI_CLAIM': 'jti',
86 |
87 | 'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
88 | 'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
89 | 'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
90 | }
91 |
92 |
93 | MIDDLEWARE = [
94 | 'django.middleware.security.SecurityMiddleware',
95 |
96 | 'corsheaders.middleware.CorsMiddleware',
97 | 'whitenoise.middleware.WhiteNoiseMiddleware',
98 |
99 |
100 | 'django.contrib.sessions.middleware.SessionMiddleware',
101 | 'django.middleware.common.CommonMiddleware',
102 | 'django.middleware.csrf.CsrfViewMiddleware',
103 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
104 | 'django.contrib.messages.middleware.MessageMiddleware',
105 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
106 | ]
107 |
108 | ROOT_URLCONF = 'mumblebackend.urls'
109 |
110 | TEMPLATES = [
111 | {
112 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
113 | 'DIRS': [],
114 | 'APP_DIRS': True,
115 | 'OPTIONS': {
116 | 'context_processors': [
117 | 'django.template.context_processors.debug',
118 | 'django.template.context_processors.request',
119 | 'django.contrib.auth.context_processors.auth',
120 | 'django.contrib.messages.context_processors.messages',
121 | ],
122 | },
123 | },
124 | ]
125 |
126 | WSGI_APPLICATION = 'mumblebackend.wsgi.application'
127 |
128 |
129 | # Password validation
130 | # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
131 |
132 | AUTH_PASSWORD_VALIDATORS = [
133 | {
134 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
135 | },
136 | {
137 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
138 | },
139 | {
140 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
141 | },
142 | {
143 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
144 | },
145 | ]
146 |
147 |
148 | # Internationalization
149 | # https://docs.djangoproject.com/en/3.1/topics/i18n/
150 |
151 | LANGUAGE_CODE = 'en-us'
152 |
153 | TIME_ZONE = 'UTC'
154 |
155 | USE_I18N = True
156 |
157 | USE_L10N = True
158 |
159 | USE_TZ = True
160 |
161 | CORS_ALLOW_ALL_ORIGINS = True
162 |
163 | # Static files (CSS, JavaScript, Images)
164 | # https://docs.djangoproject.com/en/3.1/howto/static-files/
165 |
166 | STATIC_URL = '/static/'
167 | MEDIA_URL = '/images/'
168 |
169 | STATICFILES_DIRS = [
170 | os.path.join(BASE_DIR, 'static')
171 | ]
172 |
173 | MEDIA_ROOT = os.path.join(BASE_DIR, 'static/images')
174 |
175 |
176 |
177 | LINODE_BUCKET = 'mumble'
178 | LINODE_BUCKET_REGION = 'us-east-1'
179 | LINODE_BUCKET_ACCESS_KEY = os.environ.get('MUMBLE_LINODE_BUCKET_ACCESS_KEY')
180 | LINODE_BUCKET_SECRET_KEY = os.environ.get('MUMBLE_LINODE_BUCKET_SECRET_KEY')
181 |
182 | AWS_QUERYSTRING_AUTH = True
183 | AWS_S3_FILE_OVERWRITE = False
184 |
185 | AWS_S3_ENDPOINT_URL = f'https://{LINODE_BUCKET_REGION}.linodeobjects.com'
186 | AWS_ACCESS_KEY_ID = LINODE_BUCKET_ACCESS_KEY
187 | AWS_SECRET_ACCESS_KEY = LINODE_BUCKET_SECRET_KEY
188 | AWS_STORAGE_BUCKET_NAME = LINODE_BUCKET
189 |
190 | django_heroku.settings(locals(), test_runner=False)
191 |
--------------------------------------------------------------------------------
/article/views.py:
--------------------------------------------------------------------------------
1 | from rest_framework.response import Response
2 | from rest_framework.decorators import api_view, permission_classes
3 | from rest_framework import status
4 | from django.db.models import Q
5 | from .models import Article , ArticleComment , ArticleVote
6 | from .serializers import ArticleSerializer , ArticleCommentSerializer
7 | from rest_framework.pagination import PageNumberPagination
8 | from rest_framework.permissions import IsAuthenticated
9 | from users.models import TopicTag
10 |
11 | @api_view(['GET'])
12 | def get_article(request, pk):
13 | try:
14 | article = Article.objects.get(id=pk)
15 | serializer = ArticleSerializer(article, many=False)
16 | return Response(serializer.data)
17 | except Exception as e:
18 | return Response({'details': f"{e}"},status=status.HTTP_204_NO_CONTENT)
19 |
20 | @api_view(['PUT'])
21 | @permission_classes((IsAuthenticated,))
22 | def edit_article(request,pk):
23 | try:
24 | article = Article.objects.get(id=pk)
25 | if article.user == request.user:
26 | data = request.data
27 | article.title = data.get('title')
28 | article.content = data.get('content')
29 | article.tags = data.get('tags')
30 | article.save()
31 | serializer = ArticleSerializer(article, many=False)
32 | return Response(serializer.data)
33 | else:
34 | return Response(status=status.HTTP_401_UNAUTHORIZED)
35 | except Exception as e:
36 | return Response({'details': f"{e}"},status=status.HTTP_204_NO_CONTENT)
37 |
38 | @api_view(['DELETE'])
39 | @permission_classes((IsAuthenticated,))
40 | def delete_article(request,pk):
41 | try:
42 | article = Article.objects.get(id=pk)
43 | if article.user == request.user:
44 | article.delete()
45 | return Response(status=status.HTTP_204_NO_CONTENT)
46 | else:
47 | return Response(status=status.HTTP_401_UNAUTHORIZED)
48 | except Exception as e:
49 | return Response({'details': f"{e}"},status=status.HTTP_204_NO_CONTENT)
50 |
51 | @api_view(['GET'])
52 | @permission_classes((IsAuthenticated,))
53 | def articles(request):
54 | query = request.query_params.get('q')
55 | if query == None:
56 | query = ''
57 | articles = Article.objects.filter(Q(content__icontains=query)|Q(title__icontains=query)).order_by("-created")
58 | paginator = PageNumberPagination()
59 | paginator.page_size = 10
60 | result_page = paginator.paginate_queryset(articles,request)
61 | serializer = ArticleSerializer(result_page, many=True)
62 | return paginator.get_paginated_response(serializer.data)
63 |
64 |
65 | @api_view(['PUT'])
66 | @permission_classes((IsAuthenticated,))
67 | def edit_article_comment(request,pk):
68 | try:
69 | comment = ArticleComment.objects.get(id=pk)
70 | if comment.user == request.user:
71 | serializer = ArticleCommentSerializer(comment,many=False)
72 | return Response(serializer.data)
73 | else:
74 | return Response(status=status.HTTP_401_UNAUTHORIZED)
75 | except Exception as e:
76 | return Response({'details': f"{e}"},status=status.HTTP_204_NO_CONTENT)
77 |
78 | @api_view(['DELETE'])
79 | @permission_classes((IsAuthenticated,))
80 | def delete_article_comment(request,pk):
81 | try:
82 | comment = ArticleComment.objects.get(id=pk)
83 | if comment.user == request.user:
84 | serializer = ArticleCommentSerializer(comment,many=False)
85 | comment.delete()
86 | return Response(serializer.data)
87 | else:
88 | return Response(status=status.HTTP_401_UNAUTHORIZED)
89 | except Exception as e:
90 | return Response({'details': f"{e}"},status=status.HTTP_204_NO_CONTENT)
91 |
92 |
93 | @api_view(['POST'])
94 | @permission_classes((IsAuthenticated,))
95 | def create_article(request):
96 | user = request.user
97 | data = request.data
98 | is_comment = data.get('isComment')
99 | if is_comment:
100 | article = Article.objects.get(id=data.get('postId'))
101 | comment = ArticleComment.objects.create(
102 | user=user,
103 | article=article,
104 | content=data.get('content'),
105 | )
106 | comment.save()
107 | serializer = ArticleCommentSerializer(comment,many=False)
108 | return Response(serializer.data)
109 | else:
110 | content = data.get('content')
111 | tags = data.get('tags')
112 | title = data.get('title')
113 | article = Article.objects.create(
114 | user=user,
115 | content=content,
116 | title=title,
117 | )
118 | if tags is not None:
119 | for tag_name in tags:
120 | tag_instance = TopicTag.objects.filter(name=tag_name).first()
121 | if not tag_instance:
122 | tag_instance = TopicTag.objects.create(name=tag_name)
123 | article.tags.add(tag_instance)
124 | article.save()
125 | serializer = ArticleSerializer(article, many=False)
126 | return Response(serializer.data)
127 |
128 | @api_view(['POST'])
129 | @permission_classes((IsAuthenticated,))
130 | def update_vote(request):
131 | user = request.user
132 | data = request.data
133 | article_id = data.get('postId')
134 | comment_id = data.get('commentId')
135 |
136 | article = Article.objects.get(id=article_id)
137 |
138 | if comment_id:
139 | comment = ArticleComment.objects.get(id=comment_id)
140 | vote, created = ArticleVote.objects.get_or_create(article=article,comment=comment,user=user,value=1)
141 | if not created:
142 | vote.delete()
143 | else:
144 | vote.save()
145 | else:
146 | vote, created = ArticleVote.objects.get_or_create(article=article,user=user,value=1)
147 | if not created:
148 | vote.delete()
149 | else:
150 | vote.save()
151 |
152 | serializer = ArticleSerializer(article, many=False)
153 | return Response(serializer.data)
154 |
--------------------------------------------------------------------------------
/discussion/views.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render
2 | from rest_framework.response import Response
3 | from rest_framework.decorators import api_view
4 | from rest_framework import status
5 | from django.db.models import Q
6 | from .models import Discussion, DiscussionComment , DiscussionVote
7 | from .serializers import DiscussionSerializer , DiscussionCommentSerializer
8 | from rest_framework.permissions import IsAuthenticated
9 | from rest_framework.decorators import api_view, permission_classes
10 | from rest_framework.pagination import PageNumberPagination
11 | from users.models import TopicTag
12 |
13 |
14 | @api_view(['GET'])
15 | def get_discussion(request, pk):
16 | try:
17 | discussion= Discussion.objects.get(id=pk)
18 | serializer = DiscussionSerializer(discussion, many=False)
19 | return Response(serializer.data)
20 | except Exception as e:
21 | return Response(status=status.HTTP_204_NO_CONTENT)
22 |
23 | @api_view(['PUT'])
24 | @permission_classes((IsAuthenticated,))
25 | def edit_discussion(request,pk):
26 | try:
27 | discussion= Discussion.objects.get(id=pk)
28 | if discussion.user == request.user:
29 | data = request.data
30 | discussion.headline = data.get('headline')
31 | discussion.content = data.get('content')
32 | # tags field will be included after issue 23 is resolved
33 | # discussion.tags = data.get('tags')
34 | discussion.save()
35 | serializer = DiscussionSerializer(discussion, many=False)
36 | return Response(serializer.data)
37 | else:
38 | return Response(status=status.HTTP_401_UNAUTHORIZED)
39 | except Exception as e:
40 | return Response({'detail':f'{e}'},status=status.HTTP_204_NO_CONTENT)
41 |
42 | @api_view(['DELETE'])
43 | @permission_classes((IsAuthenticated,))
44 | def delete_discussion(request,pk):
45 | try:
46 | discussion= Discussion.objects.get(id=pk)
47 | if discussion.user == request.user:
48 | discussion.delete()
49 | return Response(status=status.HTTP_204_NO_CONTENT)
50 | else:
51 | return Response(status=status.HTTP_401_UNAUTHORIZED)
52 | except Exception as e:
53 | return Response({'detail':f'{e}'},status=status.HTTP_204_NO_CONTENT)
54 |
55 | @api_view(['GET'])
56 | def discussions(request):
57 | query = request.query_params.get('q')
58 | if query == None:
59 | query = ''
60 | # Q objects is used to make complex query to search in discussion content and headline
61 | discussions = Discussion.objects.filter(Q(content__icontains=query)|Q(headline__icontains=query)).order_by("-created")
62 | paginator = PageNumberPagination()
63 | paginator.page_size = 10
64 | result_page = paginator.paginate_queryset(discussions,request)
65 | serializer = DiscussionSerializer(result_page, many=True)
66 | return paginator.get_paginated_response(serializer.data)
67 |
68 |
69 | @api_view(['PUT'])
70 | @permission_classes((IsAuthenticated,))
71 | def edit_discussion_comment(request,pk):
72 | try:
73 | comment = DiscussionComment.objects.get(id=pk)
74 | if comment.user == request.user:
75 | serializer = DiscussionCommentSerializer(comment,many=False)
76 | return Response(serializer.data)
77 | else:
78 | return Response(status=status.HTTP_401_UNAUTHORIZED)
79 | except Exception as e:
80 | return Response({'detail':f'{e}'},status=status.HTTP_204_NO_CONTENT)
81 |
82 | @api_view(['DELETE'])
83 | @permission_classes((IsAuthenticated,))
84 | def delete_discussion_comment(request,pk):
85 | try:
86 | comment = DiscussionComment.objects.get(id=pk)
87 | if comment.user == request.user:
88 | serializer = DiscussionCommentSerializer(comment,many=False)
89 | comment.delete()
90 | return Response(serializer.data)
91 | else:
92 | return Response(status=status.HTTP_401_UNAUTHORIZED)
93 | except Exception as e:
94 | return Response({'detail':f'{e}'},status=status.HTTP_204_NO_CONTENT)
95 |
96 |
97 | @api_view(['POST'])
98 | @permission_classes((IsAuthenticated,))
99 | def create_discussion(request):
100 | user = request.user
101 | data = request.data
102 | is_comment = data.get('isComment')
103 | if is_comment:
104 | discussion= Discussion.objects.get(id=data.get('postId'))
105 | comment = DiscussionComment.objects.create(
106 | user=user,
107 | discussion=discussion,
108 | content=data.get('content'),
109 | )
110 | comment.save()
111 | serializer = DiscussionCommentSerializer(comment,many=False)
112 | return Response(serializer.data)
113 | else:
114 | content = data.get('content')
115 | tags = data.get('tags')
116 | headline = data.get('headline')
117 | discussion= Discussion.objects.create(
118 | user=user,
119 | content=content,
120 | headline=headline,
121 | )
122 | if tags is not None:
123 | for tag_name in tags:
124 | tag_instance = TopicTag.objects.filter(name=tag_name).first()
125 | if not tag_instance:
126 | tag_instance = TopicTag.objects.create(name=tag_name)
127 | discussion.tags.add(tag_instance)
128 | discussion.save()
129 | serializer = DiscussionSerializer(discussion, many=False)
130 | return Response(serializer.data)
131 |
132 | @api_view(['POST'])
133 | @permission_classes((IsAuthenticated,))
134 | def update_vote(request):
135 | user = request.user
136 | data = request.data
137 | discussion_id = data.get('postId')
138 | comment_id = data.get('commentId')
139 |
140 | discussion= Discussion.objects.get(id=discussion_id)
141 |
142 | if comment_id:
143 | comment = DiscussionComment.objects.get(id=comment_id)
144 | vote, created = DiscussionVote.objects.get_or_create(discussion=discussion,comment=comment,user=user,value=1)
145 | if not created:
146 | vote.delete()
147 | else:
148 | vote.save()
149 | else:
150 | vote, created = DiscussionVote.objects.get_or_create(discussion=discussion,user=user,value=1)
151 | if not created:
152 | vote.delete()
153 | else:
154 | vote.save()
155 |
156 | serializer = DiscussionSerializer(discussion, many=False)
157 | return Response(serializer.data)
158 |
--------------------------------------------------------------------------------
/Contributing.md:
--------------------------------------------------------------------------------
1 | #
2 |
18 |
19 |
20 |
21 | A big welcome to Mumble !
22 |
23 | Thank you for considering contributing to Mumble !
24 |
25 | It’s because of people like you that open source projects emerge !
26 |
27 | Reading and following these guidelines will help us make the contribution process easy.
28 |
29 | > ⚠ Those who want to contribute on the repo, please refer to the [README.md](https://github.com/divanov11/mumbleapi/blob/master/README.md) and read the [Code Of Conduct](https://github.com/divanov11/mumbleapi/blob/master/CodeOfConduct.md) for more informations.
30 |
31 | #
32 |
33 | ### Table of contents
34 |
35 | - Contributing to Mumble
36 |
37 | - Code of Conduct
38 | - Getting Started
39 | - Issues
40 | - Pull Requests
41 | - Merging Pull Requests
42 | - Project board
43 |
44 | - NB
45 |
46 | - Fork-and-Pull
47 | - Getting Help
48 |
49 | #
50 |
51 | ### Code of Conduct
52 |
53 | We take our open source community seriously.
54 |
55 | So by participating and contributing to this project, you agree to our [Code of Conduct](https://github.com/divanov11/mumbleapi/blob/master/CodeOfConduct.md).
56 |
57 | #
58 |
59 | ### Getting Started
60 |
61 | Contributions are made to this repo via Issues and Pull Requests (PRs).
62 |
63 |
64 | To contribute :
65 |
66 | - Search for **existing Issues and PRs before creating your own**.
67 | - Describe your changes & issues very well by **following our PR & issues templates !**
68 |
69 | #
70 |
71 | ### Issues
72 |
73 | Issues are used to report problems with the library, request a new feature, or to discuss potential changes before a PR is created.
74 |
75 | If you find an issue that addresses the problem you're having, please complete this issue with comments.
76 |
77 | You can send screenshots to further explain the bug you are encountering.
78 |
79 | Before you make your changes, please open an issue using a [template](https://github.com/divanov11/mumbleapi/issues/new/choose). We'll use the issue to have a conversation about the feature or problem and how you want to go about it.
80 |
81 | **Please don't work on said issue until you have been assigned to it.**
82 |
83 | #
84 |
85 | ### Pull Requests
86 |
87 | PRs are always welcome !
88 |
89 |
90 | In general, PRs should:
91 |
92 | - Only fix/add the functionality in question **OR** address wide-spread whitespace/style issues, not both.
93 | - **Add unit or integration tests for fixed or changed functionality** (if a test suite already exists).
94 | - Address a single concern in the least number of **changed lines as possible**.
95 | - **Be accompanied by a complete Pull Request template (loaded automatically when a PR is created)**.
96 | - **Tag 2 [Reviewers](https://github.com/divanov11/mumbleapi/blob/master/Reviewers.md)**
97 | #
98 |
99 | ### Merging Pull Requests
100 |
101 |
102 | 1. It's mandatory that the PR author adds reviewers prior to submitting the PR. Tag reviewers in the message. A collaborator of the repo will officially add them in PR as reviewer(s).
103 | 2. All PRs will require the approval of both reviewers prior to the branch merge. Once the last reviewer approves the changes, they can merge the branch.
104 | 3. The PR author should **add two reviewers; unless the change is so minor (think documentation, code formatting)**. A collaborator will choose a label "Review: Needs 1" **OR** "Review: Needs 2" to further organize the repo and review system.
105 |
106 | #
107 |
108 |
109 | ### Project Board
110 |
111 | In our repository, there is a project board named Backend development, it helps moderators to see how is the work going.
112 |
113 |
114 | *Preview :*
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 | **So please, while submitting a PR or Issue, make sure to :**
123 |
124 |
125 |
126 |
127 |
128 | #
129 |
130 | ### NB
131 |
132 | In general, we follow the **fork-and-pull**
133 |
134 |
135 | #### Steps :
136 |
137 | **1. Fork the repository to your own Github account**
138 |
139 | **2. Clone the forked project to your machine**
140 |
141 | ```bash
142 | git clone https://github.com//mumbleapi.git
143 | ```
144 |
145 | **3.Add Upstream or the remote of the original project to your local repository**
146 |
147 | ```bash
148 | # check remotes
149 | git remote -v
150 | git remote add upstream https://github.com/divanov11/mumbleapi.git
151 | ```
152 |
153 | **4. Make sure you update the local repository**
154 |
155 | ```bash
156 | # Get updates
157 | git fetch upstream
158 | # switch to master branch
159 | git checkout master
160 | # Merge updates to local repository
161 | git merge upstream/master
162 | # Push to github repository
163 | git push origin master
164 | ```
165 |
166 | **5. Create a branch locally with a succinct but descriptive name**
167 |
168 | ```bash
169 | git checkout -b branch-name
170 | ```
171 |
172 | **6. Commit changes to the branch**
173 |
174 | ```bash
175 | # Stage changes for commit i.e add all modified files to commit
176 | git add .
177 | # You can also add specific files using
178 | # git add
179 | git commit -m "your commit message goes here"
180 | # check status
181 | git status
182 | ```
183 |
184 | **7.Following any formatting and testing guidelines specific to this repository**
185 |
186 | **8. Push changes to your fork**
187 |
188 | ```bash
189 | git push origin branch-name
190 | ```
191 |
192 | **9.Open a PR in our repository and follow the PR template so that we can efficiently review the changes.**
193 |
194 | **10. After the pull request was merged, fetch the upstream and update the default branch of your fork**
195 |
196 | #
197 |
198 | ### Getting Help
199 |
200 | Join us in **[the Discord Server](https://discord.gg/9Du4KUY3dE)** and post your question there in the correct category with a descriptive tag.
201 |
202 | #
203 |
--------------------------------------------------------------------------------
/feed/views.py:
--------------------------------------------------------------------------------
1 | from django.core.exceptions import ObjectDoesNotExist
2 | from django.db.models import Q
3 | from rest_framework import status
4 | from rest_framework.decorators import api_view, permission_classes
5 | from rest_framework.pagination import PageNumberPagination
6 | from rest_framework.permissions import IsAuthenticated
7 | from rest_framework.response import Response
8 |
9 | from .models import Mumble, MumbleVote
10 | from .serializers import MumbleSerializer
11 |
12 | # Create your views here.
13 |
14 |
15 | @api_view(['GET'])
16 | @permission_classes((IsAuthenticated,))
17 | def mumbles(request):
18 | query = request.query_params.get('q')
19 | if query == None:
20 | query = ''
21 |
22 | user = request.user
23 | following = user.following.select_related('user')
24 |
25 | following = user.following.all()
26 |
27 | ids = []
28 | ids = [i.user.id for i in following]
29 | ids.append(user.id)
30 | print('IDS:', ids)
31 |
32 | #Make sure parent==None is always on
33 | #Query 5 mumbles form users you follow | TOP PRIORITY
34 |
35 | mumbles = list(Mumble.objects.filter(parent=None, user__id__in=ids).order_by("-created"))[0:5]
36 | #mumbles = list(mumbles.filter(Q(user__userprofile__name__icontains=query) | Q(content__icontains=query)))
37 |
38 | recentMumbles = Mumble.objects.filter(Q(parent=None) & Q(vote_rank__gte=0) & Q(remumble=None)).order_by("-created")[0:5]
39 |
40 | #Query top ranked mumbles and attach to end of original queryset
41 | topMumbles = Mumble.objects.filter(Q(parent=None)).order_by("-vote_rank", "-created")
42 |
43 | #Add top ranked mumbles to feed after prioritizing follow list
44 | index = 0
45 | for mumble in recentMumbles:
46 | if mumble not in mumbles:
47 | mumbles.insert(index, mumble)
48 | index += 1
49 |
50 |
51 | #Add top ranked mumbles to feed after prioritizing follow list
52 | for mumble in topMumbles:
53 | if mumble not in mumbles:
54 | mumbles.append(mumble)
55 |
56 |
57 | paginator = PageNumberPagination()
58 | paginator.page_size = 10
59 | result_page = paginator.paginate_queryset(mumbles, request)
60 | serializer = MumbleSerializer(result_page, many=True)
61 | return paginator.get_paginated_response(serializer.data)
62 |
63 | @api_view(['GET'])
64 | @permission_classes((IsAuthenticated,))
65 | def mumble_details(request,pk):
66 | try:
67 | mumble = Mumble.objects.get(id=pk)
68 | serializer = MumbleSerializer(mumble, many=False)
69 | return Response(serializer.data)
70 | except:
71 | message = {
72 | 'detail':'Mumble doesn\'t exist'
73 | }
74 | return Response(message, status=status.HTTP_404_NOT_FOUND)
75 |
76 | @api_view(['POST'])
77 | @permission_classes((IsAuthenticated,))
78 | def create_mumble(request):
79 | user = request.user
80 | data = request.data
81 |
82 | is_comment = data.get('isComment')
83 | if is_comment:
84 | parent = Mumble.objects.get(id=data['postId'])
85 | mumble = Mumble.objects.create(
86 | parent=parent,
87 | user=user,
88 | content=data['content'],
89 | )
90 | else:
91 | mumble = Mumble.objects.create(
92 | user=user,
93 | content=data['content']
94 | )
95 |
96 | serializer = MumbleSerializer(mumble, many=False)
97 | return Response(serializer.data)
98 |
99 | @api_view(['PATCH'])
100 | @permission_classes((IsAuthenticated,))
101 | def edit_mumble(request,pk):
102 | user = request.user
103 | data = request.data
104 |
105 | try:
106 | mumble = Mumble.objects.get(id=pk)
107 | if user != mumble.user:
108 | return Response(status=status.HTTP_401_UNAUTHORIZED)
109 | else:
110 | serializer = MumbleSerializer(mumble,data = data)
111 | if serializer.is_valid():
112 | serializer.save()
113 | return Response(serializer.data,status=status.HTTP_200_OK)
114 | else:
115 | return Response(status=status.HTTP_406_NOT_ACCEPTABLE)
116 | except Exception as e:
117 | return Response(status=status.HTTP_204_NO_CONTENT)
118 |
119 | @api_view(['DELETE'])
120 | @permission_classes((IsAuthenticated,))
121 | def delete_mumble(request, pk):
122 | user = request.user
123 | try:
124 | mumble = Mumble.objects.get(id=pk)
125 | if user != mumble.user:
126 | return Response(status=status.HTTP_401_UNAUTHORIZED)
127 | else:
128 | mumble.delete()
129 | return Response(status=status.HTTP_204_NO_CONTENT)
130 | except Exception as e:
131 | return Response({'details': f"{e}"},status=status.HTTP_204_NO_CONTENT)
132 |
133 | @api_view(['GET'])
134 | def mumble_comments(request, pk):
135 | mumble = Mumble.objects.get(id=pk)
136 | comments = mumble.mumble_set.all()
137 | serializer = MumbleSerializer(comments, many=True)
138 | return Response(serializer.data)
139 |
140 |
141 | @api_view(['POST'])
142 | @permission_classes((IsAuthenticated,))
143 | def remumble(request):
144 | user = request.user
145 | data = request.data
146 | original_mumble = Mumble.objects.get(id=data['id'])
147 | if original_mumble.user == user:
148 | return Response({'detail':'You can not remumble your own mumble.'},status=status.HTTP_403_FORBIDDEN)
149 | try:
150 | mumble = Mumble.objects.filter(
151 | remumble=original_mumble,
152 | user=user,
153 | )
154 | if mumble.exists():
155 | return Response({'detail':'Already Mumbled'},status=status.HTTP_406_NOT_ACCEPTABLE)
156 | else:
157 | mumble = Mumble.objects.create(
158 | remumble=original_mumble,
159 | user=user,
160 | )
161 | serializer = MumbleSerializer(mumble, many=False)
162 | return Response(serializer.data)
163 | except Exception as e:
164 | return Response({'detail':f'{e}'},status=status.HTTP_403_FORBIDDEN)
165 |
166 |
167 | @api_view(['POST'])
168 | @permission_classes((IsAuthenticated,))
169 | def update_vote(request):
170 | user = request.user
171 | data = request.data
172 |
173 | mumble = Mumble.objects.get(id=data['post_id'])
174 | #What if user is trying to remove their vote?
175 | vote, created = MumbleVote.objects.get_or_create(mumble=mumble, user=user)
176 |
177 | if vote.value == data.get('value'):
178 | #If same value is sent, user is clicking on vote to remove it
179 | vote.delete()
180 | else:
181 |
182 | vote.value=data['value']
183 | vote.save()
184 |
185 | #We re-query the vote to get the latest vote rank value
186 | mumble = Mumble.objects.get(id=data['post_id'])
187 | serializer = MumbleSerializer(mumble, many=False)
188 |
189 | return Response(serializer.data)
190 |
--------------------------------------------------------------------------------
/users/tests/test_views.py:
--------------------------------------------------------------------------------
1 | import re
2 | from django.contrib.auth.models import User
3 | from rest_framework.test import APIClient
4 | import json
5 | from django.urls import reverse
6 | from rest_framework import status
7 | from rest_framework.test import APITestCase
8 | # Create your tests here.
9 |
10 | from ..models import SkillTag, TopicTag
11 |
12 | from users.views import email_validator
13 |
14 | class AccountTests(APITestCase):
15 |
16 | def setUp(self):
17 | url = reverse('users-api:register')
18 | data = {
19 | 'username':'test',
20 | 'email':'test@gmail.com',
21 | 'password':'test@123'
22 | }
23 | response = self.client.post(url, data, format='json')
24 | self.assertEqual(response.status_code, status.HTTP_200_OK)
25 | self.assertEqual(User.objects.count(), 1)
26 | self.assertEqual(User.objects.get().username, 'test')
27 | self.test_user = User.objects.get(username='test')
28 | self.test_user_pwd = 'test@123'
29 |
30 | # Creating another account to test following
31 |
32 | data = {
33 | 'username':'praveen',
34 | 'email':'praveen@gmail.com',
35 | 'password':'SomethingRandomPassword@123'
36 | }
37 | response = self.client.post(url, data, format='json')
38 | self.assertEqual(response.status_code, status.HTTP_200_OK)
39 | self.assertEqual(User.objects.count(), 2)
40 | self.assertEqual(User.objects.get(username='praveen').username,data.get('username'))
41 | self.another_user = User.objects.get(username='praveen')
42 |
43 |
44 | def test_users_follow_view(self):
45 | client = APIClient()
46 | # authenticating the user
47 | client.force_authenticate(user=self.test_user)
48 | # get following user count before follow
49 | user_followers_before = self.another_user.userprofile.followers.count()
50 | response = client.post('/api/users/praveen/follow/',args=[self.another_user.username])
51 | user_followers_after = self.another_user.userprofile.followers.count()
52 |
53 | # test if follow was successful
54 |
55 | self.assertEqual(user_followers_after,user_followers_before+1)
56 | self.assertEqual(response.status_code, status.HTTP_200_OK)
57 |
58 | def test_users_login(self):
59 | url = 'users-api:login'
60 | reversed_url = reverse(url)
61 | data = {
62 | 'username':'praveen',
63 | 'password':'SomethingRandomPassword@123'
64 | }
65 | client = APIClient()
66 | response = client.post(reversed_url, data, format='json')
67 | self.assertEqual(response.status_code, status.HTTP_200_OK)
68 | # response = json.loads(response.content.decode('UTF-8'))
69 | # if response[0]:
70 | # print("Login Token Being Generated Correctly")
71 | # if response[1]:
72 | # print("Rrefresh Token also genrerate Correctly")
73 | # if response.content.get("username") == data.get('username'):
74 | # print("Correct User fetched")
75 |
76 |
77 | def test_user_profile_update_view(self):
78 | url = 'users-api:profile_update'
79 | reversed_url = reverse(url)
80 | data = {
81 | 'username':'TEST'
82 | }
83 | client = APIClient()
84 | client.force_authenticate(user=self.test_user)
85 | response = client.patch(reversed_url,data, format='json')
86 | self.assertEqual(response.status_code, status.HTTP_200_OK)
87 |
88 |
89 | def test_user_email_is_valid(self):
90 | email = 'rshalem@gmail.com'
91 | self.assertEqual(email_validator(email), 'rshalem@gmail.com')
92 | print('PASSED EMAIL VERIFICATION TEST')
93 |
94 | def test_user_following_view(self):
95 | url = 'users-api:following'
96 | reversed_url = reverse(url)
97 | client = APIClient()
98 | client.force_authenticate(user=self.test_user)
99 | response = client.get(reversed_url)
100 | self.assertEqual(response.status_code, status.HTTP_200_OK)
101 |
102 | def test_user_mumbles_view(self):
103 | url = 'users-api:user-mumbles'
104 | reversed_url = reverse(url,args=[self.test_user.username])
105 | client = APIClient()
106 | client.force_authenticate(user=self.test_user)
107 | response = client.get(reversed_url)
108 | self.assertEqual(response.status_code, status.HTTP_200_OK)
109 |
110 | def test_user_articles_view(self):
111 | url = 'users-api:user-articles'
112 | reversed_url = reverse(url,args=[self.test_user.username])
113 | client = APIClient()
114 | client.force_authenticate(user=self.test_user)
115 | response = client.get(reversed_url)
116 | self.assertEqual(response.status_code, status.HTTP_200_OK)
117 |
118 | def test_user_password_change_view(self):
119 | url = 'users-api:password-change'
120 | reversed_url = reverse(url)
121 | client = APIClient()
122 | client.force_authenticate(user=self.test_user)
123 | data = {
124 | 'new_password':"Test@123",
125 | 'new_password_confirm':"Test@123"
126 | }
127 | response = client.post(reversed_url,data, format='json')
128 | self.assertEqual(response.status_code, status.HTTP_200_OK)
129 |
130 | def test_user_send_activate_email_view(self):
131 | url = 'users-api:send-activation-email'
132 | reversed_url = reverse(url)
133 | client = APIClient()
134 | client.force_authenticate(user=self.test_user)
135 | response = client.post(reversed_url)
136 | self.assertEqual(response.status_code, status.HTTP_200_OK)
137 |
138 |
139 | def test_update_skills(self):
140 | url = 'users-api:update_skills'
141 | reversed_url = reverse(url)
142 | self.client.force_authenticate(user=self.test_user)
143 | response = self.client.patch(reversed_url, [
144 | {'name': 'javascript'}
145 | ])
146 | response_json = json.loads(response.content)
147 | tag = SkillTag.objects.get(name='javascript')
148 | self.assertEqual(tag.name, 'javascript')
149 | self.assertEqual(response_json['skills'], [{'name': 'javascript'}])
150 | self.assertEqual(response.status_code, status.HTTP_200_OK)
151 |
152 |
153 | def test_update_interests(self):
154 | url = 'users-api:update_interests'
155 | reversed_url = reverse(url)
156 | self.client.force_authenticate(user=self.test_user)
157 | response = self.client.patch(reversed_url, [
158 | {'name': 'agile'}
159 | ])
160 | response_json = json.loads(response.content)
161 | tag = TopicTag.objects.get(name='agile')
162 | self.assertEqual(tag.name, 'agile')
163 | self.assertEqual(response_json['interests'], [{'name': 'agile'}])
164 | self.assertEqual(response.status_code, status.HTTP_200_OK)
165 |
166 | def test_users_follow_view(self):
167 | # test_user should be following 0 people at the start
168 | user_following_before = self.test_user.following.count()
169 | self.client.force_authenticate(user=self.test_user)
170 | response = self.client.post('/api/users/praveen/follow/',args=[self.another_user.username])
171 |
172 | # check the following endpoint to verify that test_user comes back
173 | url = 'users-api:following'
174 | reversed_url = reverse(url)
175 | self.client.force_authenticate(user=self.test_user)
176 | response = self.client.get(reversed_url)
177 | user_following_after = self.test_user.following.count()
178 | self.assertEqual(user_following_after,user_following_before + 1)
179 |
180 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #
2 |
18 |
19 |
20 |
21 | ## Getting Started
22 |
23 | If you are trying to use this project for the first time, you can get up and running by following these steps.
24 |
25 | To contribute to this project, please see the [contributing guidelines](https://github.com/divanov11/mumbleapi/blob/master/Contributing.md).
26 | > ⚠ Note, this step assumes you are using **github ssh keys** for the *git clone method*
27 |
28 |
29 |
30 | ## The Mumble Diagram
31 |
32 | --> *Preview :*
33 |
34 |
39 |
40 |
41 |
42 | --> *Full View:*
43 |
44 | You can see clearly the diagram at :
45 |
46 |
47 |
48 | ## Requirements
49 |
50 | | Technology | Version |
51 | | :----------------------------------------------------------: | :----------------: |
52 | | [**Python**](https://docs.python.org/3/) | **3.x** |
53 | | [**pip**](https://pypi.org/project/pip/) | **latest version** |
54 | | [**asgiref**](https://pypi.org/project/asgiref/) | **3.3.4** |
55 | | [**certifi**](https://pypi.org/project/certifi/) | **2020.12.5** |
56 | | [**chardet**](https://pypi.org/project/chardet/) | **4.0.0** |
57 | | [**coreapi**](https://pypi.org/project/coreapi/) | **2.3.3** |
58 | | [**coreschema**](https://pypi.org/project/coreschema/)| | **0.0.4** |
59 | | [**dj-database-url**](https://pypi.org/project/dj-database-url/) | **0.5.0** |
60 | | [**Django**](https://docs.djangoproject.com/en/3.2/) | **3.2** |
61 | | [**django-ckeditor**](https://pypi.org/project/django-ckeditor/) | **6.0.0** |
62 | | [**django-cors-headers**](https://pypi.org/project/django-cors-headers/) | **3.7.0** |
63 | | [**django-heroku**](https://pypi.org/project/django-heroku/) | **0.3.1** |
64 | | [**django-js-asset**](https://pypi.org/project/django-heroku/) | **1.2.2** |
65 | | [**djangorestframework**](https://www.django-rest-framework.org/) | **3.12.4** |
66 | | [**djangorestframework-simplejwt**](https://pypi.org/project/djangorestframework-simplejwt/) | **4.6.0** |
67 | | [**dnspython**](https://pypi.org/project/dnspython/) | **2.1.0** |
68 | | [**email-validator**](https://pypi.org/project/email-validator/) | **1.1.2** |
69 | | [**gunicorn**](https://pypi.org/project/gunicorn/) | **20.1.0** |
70 | | [*idna*](https://pypi.org/project/idna/) | **2.10**|
71 | | [*itypes*](https://pypi.org/project/itypes/) | **1.2.0**|
72 | | [*Jinja2*](https://pypi.org/project/Jinja2/) |**3.0.0**|
73 | | [*MarkupSafe*](https://pypi.org/project/MarkupSafe/) | **2.0.0**|
74 | | [**Pillow**](https://pypi.org/project/Pillow/) | **8.2.0** |
75 | | [**psycopg2**](https://pypi.org/project/psycopg2/) | **2.8.6** |
76 | | [**PyJWT**](https://pypi.org/project/PyJWT/) | **2.0.1** |
77 | | [**pytz**](https://pypi.org/project/pytz/) | **2021.1** |
78 | | [*PyYAML*](https://pypi.org/project/PyYAML/) | **5.4.1** |
79 | | [**requests**](https://pypi.org/project/requests/) | **2.25.1** |
80 | | [**sentry-sdk**](https://pypi.org/project/sentry-sdk/) | **1.0.0** |
81 | | [**six**](https://pypi.org/project/six/) | **1.15.0** |
82 | | [**sqlparse**](https://pypi.org/project/sqlparse/) | **0.4.1** |
83 | | [**typing-extension**](https://pypi.org/project/typing-extensions/) | **3.10.0.0** |
84 | | [**uritemplate**](https://pypi.org/project/uritemplate/) | **3.0.1** |
85 | | [**urllib3**](https://pypi.org/project/urllib3/) | **1.26.4** |
86 | | [**whitenoise**](https://pypi.org/project/whitenoise/) | **5.2.0** |
87 |
88 |
89 | ## Install and Run
90 |
91 | Make sure you have **Python 3.x** installed and **the latest version of pip** *installed* before running these steps.
92 |
93 | To contribute, please follow the [guidelines](https://github.com/divanov11/mumbleapi/blob/master/Contributing.md) process.
94 |
95 | Clone the repository using the following command
96 |
97 | ```bash
98 | git clone git@github.com:divanov11/mumbleapi.git
99 | # After cloning, move into the directory having the project files using the change directory command
100 | cd mumbleapi
101 | ```
102 | Create a virtual environment where all the required python packages will be installed
103 |
104 | ```bash
105 | # Use this on Windows
106 | python -m venv env
107 | # Use this on Linux and Mac
108 | python -m venv env
109 | ```
110 | Activate the virtual environment
111 |
112 | ```bash
113 | # Windows
114 | .\env\Scripts\activate
115 | # Linux and Mac
116 | source env/bin/activate
117 | ```
118 | Install all the project Requirements
119 | ```bash
120 | pip install -r requirements.txt
121 | ```
122 | -Apply migrations and create your superuser (follow the prompts)
123 |
124 | ```bash
125 | # apply migrations and create your database
126 | python manage.py migrate
127 |
128 | # Create a user with manage.py
129 | python manage.py createsuperuser
130 | ```
131 | Load test data to your database
132 |
133 | ```bash
134 |
135 | # load data for feed
136 | python manage.py loaddata feeddata.json
137 |
138 | # load data for article
139 | python manage.py loaddata articledata.json
140 |
141 | # load data for discussion
142 | python manage.py loaddata discussiondata.json
143 | ```
144 |
145 | Run the tests
146 |
147 | ```bash
148 | # run django tests for article app
149 | python manage.py test article
150 | ```
151 |
152 | ```bash
153 | # run django tests for discussion app
154 | python manage.py test discussion
155 | ```
156 |
157 | ```bash
158 | # run django tests for feed app
159 | python manage.py test feed
160 | ```
161 |
162 | ```bash
163 | # run django tests for users app
164 | python manage.py test users
165 | ```
166 |
167 | Run the development server
168 |
169 | ```bash
170 | # run django development server
171 | python manage.py runserver
172 | ```
173 | ## Reviewers
174 |
175 | After submitting your PR, please tag reviewer(s) in your PR message. You can tag anyone below for the following.
176 |
177 |
178 |
179 | - **Markdown, Documentation, Email templates:**
180 |
181 | [@Mehdi - MidouWebDev](https://github.com/MidouWebDev)
182 |
183 | [@Abhi Vempati](https://github.com/abhivemp/)
184 |
185 | #
186 |
187 | - **API, Backend, Databases, Dependencies:**
188 |
189 | --> *Choose two reviewers :*
190 |
191 | [@Dennis Ivy](https://github.com/divanov11)
192 |
193 | [@Praveen Malethia](https://github.com/PraveenMalethia)
194 |
195 | [@Abhi Vempati](https://github.com/abhivemp)
196 |
197 | [@Bashiru Bukari](https://github.com/bashiru98)
198 |
199 | [@Cody Seibert](https://github.com/codyseibert)
200 |
201 | ## Explore admin panel for model data or instances
202 |
203 | http://127.0.0.1:8000/admin or http://localhost:8000/admin
204 |
205 | ## Login with the user credentials (you created) using "createsuperuser" cmd
206 |
207 | > ⚠ If everything is good and has been done successfully, your **Django Rest API** should be hosted on port 8000 i.e http://127.0.0.1:8000/ or http://localhost:8000/
208 |
209 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | Copyright 2021 Dennis Ivy
179 |
180 | Licensed under the Apache License, Version 2.0 (the "License");
181 | you may not use this file except in compliance with the License.
182 | You may obtain a copy of the License at
183 |
184 | http://www.apache.org/licenses/LICENSE-2.0
185 |
186 | Unless required by applicable law or agreed to in writing, software
187 | distributed under the License is distributed on an "AS IS" BASIS,
188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
189 | See the License for the specific language governing permissions and
190 | limitations under the License.
191 |
--------------------------------------------------------------------------------
/users/views.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import uuid
3 | import random
4 | import os.path
5 |
6 | from django.contrib.auth.hashers import make_password
7 | from django.contrib.auth.models import User
8 | #email verification imports
9 | from django.contrib.auth.tokens import default_token_generator
10 | from django.core.files.storage import default_storage
11 | # from django.contrib.sites.shortcuts import get_current_site
12 | from django.core.mail import EmailMessage
13 | from django.db.models import Q , Count
14 | from django.template.loader import render_to_string
15 | from django.utils.encoding import force_bytes
16 | from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
17 | from email_validator import validate_email, EmailNotValidError
18 | from rest_framework import permissions, status
19 | from rest_framework.decorators import api_view, permission_classes
20 | from rest_framework.pagination import PageNumberPagination
21 | from rest_framework.parsers import FileUploadParser
22 | from rest_framework.permissions import IsAuthenticated
23 | from rest_framework.response import Response
24 | from rest_framework.views import APIView
25 | from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
26 | from rest_framework_simplejwt.views import TokenObtainPairView
27 |
28 | from article.serializers import ArticleSerializer
29 | from feed.serializers import MumbleSerializer
30 | from notification.models import Notification
31 |
32 | from .models import UserProfile, SkillTag, TopicTag
33 | from .serializers import (UserProfileSerializer, UserSerializer,
34 | UserSerializerWithToken, CurrentUserSerializer)
35 |
36 | # Create your views here.
37 | def email_validator(email):
38 | """validates & return the entered email if correct
39 | else returns an exception as string"""
40 | try:
41 | validated_email_data = validate_email(email)
42 | email_add = validated_email_data['email']
43 | return email_add
44 | except EmailNotValidError as e:
45 | return str(e)
46 |
47 | class RegisterView(APIView):
48 | permission_classes = [permissions.AllowAny]
49 | authentication_classes = []
50 |
51 | def post(self, request):
52 | data = request.data
53 | username = data.get('username')
54 | email = data.get('email')
55 | password = data.get('password')
56 | email_valid_check_result = email_validator(email)
57 | messages = {'errors':[]}
58 | if username == None:
59 | messages['errors'].append('username can\'t be empty')
60 | if email == None:
61 | messages['errors'].append('Email can\'t be empty')
62 | if not email_valid_check_result == email:
63 | messages['errors'].append(email_valid_check_result)
64 | if password == None:
65 | messages['errors'].append('Password can\'t be empty')
66 | if User.objects.filter(email=email).exists():
67 | messages['errors'].append("Account already exists with this email id.")
68 | if User.objects.filter(username__iexact=username).exists():
69 | messages['errors'].append("Account already exists with this username.")
70 | if len(messages['errors']) > 0:
71 | return Response({"detail":messages['errors']},status=status.HTTP_400_BAD_REQUEST)
72 | try:
73 | user = User.objects.create(
74 | username=username,
75 | email=email,
76 | password=make_password(password)
77 | )
78 | serializer = UserSerializerWithToken(user, many=False)
79 | except Exception as e:
80 | print(e)
81 | return Response({'detail':f'{e}'},status=status.HTTP_400_BAD_REQUEST)
82 | return Response(serializer.data)
83 |
84 | class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
85 |
86 | @classmethod
87 | def get_token(cls, user):
88 | token = super().get_token(user)
89 |
90 | token['username'] = user.username
91 | token['name'] = user.userprofile.name
92 | token['profile_pic'] = 'static' + user.userprofile.profile_pic.url
93 | token['is_staff'] = user.is_staff
94 | token['id'] = user.id
95 |
96 | return token
97 |
98 | def validate(self, attrs):
99 | data = super().validate(attrs)
100 |
101 | serializer = UserSerializerWithToken(self.user).data
102 | for k, v in serializer.items():
103 | data[k] = v
104 |
105 | return data
106 |
107 |
108 | class MyTokenObtainPairView(TokenObtainPairView):
109 | serializer_class = MyTokenObtainPairSerializer
110 |
111 |
112 | @api_view(['GET'])
113 | def users(request):
114 | query = request.query_params.get('q') or ''
115 | users = User.objects.filter(
116 | Q(userprofile__name__icontains=query) |
117 | Q(userprofile__username__icontains=query)
118 | ).order_by('-userprofile__followers_count')
119 | paginator = PageNumberPagination()
120 | paginator.page_size = 10
121 | result_page = paginator.paginate_queryset(users,request)
122 | serializer = UserSerializer(result_page, many=True)
123 | return paginator.get_paginated_response(serializer.data)
124 |
125 |
126 | @api_view(['GET'])
127 | def users_by_skill(request, skill):
128 | try:
129 | skill = SkillTag.objects.get(name=skill)
130 | print(skill)
131 | users = User.objects.filter(
132 | Q(userprofile__skills__in=[skill])
133 | ).order_by('-userprofile__followers_count')
134 | paginator = PageNumberPagination()
135 | paginator.page_size = 10
136 | result_page = paginator.paginate_queryset(users,request)
137 | serializer = UserSerializer(result_page, many=True)
138 | return paginator.get_paginated_response(serializer.data)
139 | except Exception as e:
140 | return Response({'detail':f'{e}'},status=status.HTTP_400_BAD_REQUEST)
141 |
142 |
143 | @api_view(['GET'])
144 | @permission_classes((IsAuthenticated,))
145 | def users_recommended(request):
146 | user = request.user
147 | users = User.objects.annotate(followers_count=Count('userprofile__followers')).order_by('followers_count').reverse().exclude(id=user.id)[0:5]
148 | serializer = UserSerializer(users, many=True)
149 | return Response(serializer.data)
150 |
151 | @api_view(['GET'])
152 | def user(request, username):
153 | user = User.objects.get(username=username)
154 |
155 | if(request.user.username == username):
156 | serializer = CurrentUserSerializer(user, many=False)
157 | return Response(serializer.data)
158 |
159 | serializer = UserSerializer(user, many=False)
160 | return Response(serializer.data)
161 |
162 | @api_view(['GET'])
163 | def user_mumbles(request, username):
164 | try:
165 | user = User.objects.get(username=username)
166 | mumbles = user.mumble_set.filter(parent=None)
167 | serializer = MumbleSerializer(mumbles, many=True)
168 | return Response(serializer.data)
169 | except Exception as e:
170 | return Response({'detail':f'{e}'},status=status.HTTP_404_NOT_FOUND)
171 |
172 | @api_view(['GET'])
173 | def user_articles(request, username):
174 | user = User.objects.get(username=username)
175 | articles = user.article_set
176 | serializer = ArticleSerializer(articles, many=True)
177 | return Response(serializer.data)
178 |
179 |
180 | @api_view(['GET'])
181 | @permission_classes((IsAuthenticated,))
182 | def following(request):
183 | user = request.user
184 | following = user.following.all()
185 | serializer = UserProfileSerializer(following, many=True)
186 | return Response(serializer.data)
187 |
188 | @api_view(['GET'])
189 | @permission_classes((IsAuthenticated,))
190 | def profile(request):
191 | user = request.user
192 | serializer = UserSerializer(user, many=False)
193 | return Response(serializer.data)
194 |
195 | @api_view(['PATCH'])
196 | @permission_classes((IsAuthenticated,))
197 | def update_skills(request):
198 | user_profile = request.user.userprofile
199 | skills = request.data
200 | user_profile.skills.set(
201 | SkillTag.objects.get_or_create(name=skill['name'])[0] for skill in skills
202 | )
203 | user_profile.save()
204 | serializer = UserProfileSerializer(user_profile, many=False)
205 | return Response(serializer.data)
206 |
207 | @api_view(['PATCH'])
208 | @permission_classes((IsAuthenticated,))
209 | def update_interests(request):
210 | user_profile = request.user.userprofile
211 | interests = request.data
212 | user_profile.interests.set(
213 | TopicTag.objects.get_or_create(name=interest['name'])[0] for interest in interests
214 | )
215 | user_profile.save()
216 | serializer = UserProfileSerializer(user_profile, many=False)
217 | return Response(serializer.data)
218 |
219 | @api_view(['POST'])
220 | @permission_classes((IsAuthenticated,))
221 | def follow_user(request, username):
222 | user = request.user
223 | try:
224 | user_to_follow = User.objects.get(username=username)
225 | user_to_follow_profile = user_to_follow.userprofile
226 |
227 | if user == user_to_follow:
228 | return Response('You can not follow yourself')
229 |
230 | if user in user_to_follow_profile.followers.all():
231 | user_to_follow_profile.followers.remove(user)
232 | user_to_follow_profile.followers_count = user_to_follow_profile.followers.count()
233 | user_to_follow_profile.save()
234 | return Response('User unfollowed')
235 | else:
236 | user_to_follow_profile.followers.add(user)
237 | user_to_follow_profile.followers_count = user_to_follow_profile.followers.count()
238 | user_to_follow_profile.save()
239 | # doing this as a signal is much more difficult and hacky
240 | Notification.objects.create(
241 | to_user=user_to_follow,
242 | created_by=user,
243 | notification_type='follow',
244 | followed_by=user,
245 | content=f"{user.userprofile.name} started following you."
246 | )
247 | return Response('User followed')
248 | except Exception as e:
249 | message = {'detail':f'{e}'}
250 | return Response(message,status=status.HTTP_204_NO_CONTENT)
251 |
252 |
253 | class UserProfileUpdate(APIView):
254 | permission_classes = [IsAuthenticated]
255 | serializer_class = UserProfileSerializer
256 | #http_method_names = ['patch', 'head']
257 |
258 |
259 | def patch(self, *args, **kwargs):
260 | profile = self.request.user.userprofile
261 | serializer = self.serializer_class(
262 | profile, data=self.request.data, partial=True)
263 | if serializer.is_valid():
264 | user = serializer.save().user
265 | new_email = self.request.data.get('email')
266 | user = self.request.user
267 | if new_email is not None:
268 | user.email = new_email
269 | profile.email_verified = False
270 | user.save()
271 | profile.save()
272 | return Response({'success': True, 'message': 'successfully updated your info',
273 | 'user': UserSerializer(user).data,'updated_email': new_email}, status=200)
274 | else:
275 | response = serializer.errors
276 | return Response(response, status=401)
277 |
278 |
279 | class ProfilePictureUpdate(APIView):
280 | permission_classes=[IsAuthenticated]
281 | serializer_class=UserProfileSerializer
282 | parser_class=(FileUploadParser,)
283 |
284 | def patch(self, *args, **kwargs):
285 | rd = random.Random()
286 | profile_pic=self.request.FILES['profile_pic']
287 | extension = os.path.splitext(profile_pic.name)[1]
288 | profile_pic.name='{}{}'.format(uuid.UUID(int=rd.getrandbits(128)), extension)
289 | filename = default_storage.save(profile_pic.name, profile_pic)
290 | setattr(self.request.user.userprofile, 'profile_pic', filename)
291 | serializer=self.serializer_class(
292 | self.request.user.userprofile, data={}, partial=True)
293 | if serializer.is_valid():
294 | user=serializer.save().user
295 | response={'type': 'Success', 'message': 'successfully updated your info',
296 | 'user': UserSerializer(user).data}
297 | else:
298 | response=serializer.errors
299 | return Response(response)
300 |
301 | @api_view(['DELETE'])
302 | @permission_classes((IsAuthenticated,))
303 | def ProfilePictureDelete(request):
304 | user = request.user.userprofile
305 | user.profile_pic.url = 'default.png'
306 | return Response({'detail':'Profile picture deleted '})
307 |
308 |
309 |
310 | @api_view(['POST'])
311 | @permission_classes((IsAuthenticated,))
312 | def delete_user(request):
313 | user = request.user
314 | user.delete()
315 | return Response({'detail':'Account deleted successfully'},status=status.HTTP_200_OK)
316 |
317 | # THIS EMAIL VERIFICATION SYSTEM IS ONLY VALID FOR LOCAL TESTING
318 | # IN PRODUCTION WE NEED A REAL EMAIL , TILL NOW WE ARE USING DEFAULT EMAIL BACKEND
319 | # THIS DEFAULT BACKEND WILL PRINT THE VERIFICATION EMAIL IN THE CONSOLE
320 | # LATER WE CAN SETUP SMTP FOR REAL EMAIL SENDING TO USER
321 |
322 | @api_view(['POST'])
323 | @permission_classes((IsAuthenticated,))
324 | def send_activation_email(request):
325 | user = request.user
326 | user_profile = UserProfile.objects.get(user=user)
327 | try:
328 | mail_subject = 'Verify your Mumble account.'
329 | message = render_to_string('verify-email.html', {
330 | 'user': user_profile,
331 | 'uid': urlsafe_base64_encode(force_bytes(user.pk)),
332 | 'token': default_token_generator.make_token(user),
333 | })
334 | to_email = user.email
335 | email = EmailMessage(
336 | mail_subject, message, to=[to_email]
337 | )
338 | email.send()
339 | return Response('Mail sent Successfully',status=status.HTTP_200_OK)
340 | except Exception as e:
341 | return Response({'detail':f'{e}'},status=status.HTTP_403_FORBIDDEN)
342 |
343 |
344 | @api_view(['GET'])
345 | def activate(request, uidb64, token):
346 | try:
347 | uid = urlsafe_base64_decode(uidb64).decode()
348 | user = User._default_manager.get(pk=uid)
349 | except(TypeError, ValueError, OverflowError, User.DoesNotExist):
350 | user = None
351 | if user is not None and default_token_generator.check_token(user, token):
352 | user_profile = UserProfile.objects.get(user=user)
353 | user_profile.email_verified = True
354 | user_profile.save()
355 | return Response("Email Verified")
356 | else:
357 | return Response('Something went wrong , please try again',status=status.HTTP_406_NOT_ACCEPTABLE)
358 |
359 | @api_view(['POST'])
360 | @permission_classes((IsAuthenticated,))
361 | def password_change(request):
362 | user = request.user
363 | data = request.data
364 | new_password = data.get('new_password')
365 | new_password_confirm = data.get('new_password_confirm')
366 | if new_password_confirm and new_password is not None:
367 | if new_password == new_password_confirm:
368 | user.set_password(new_password)
369 | user.save()
370 | return Response({'detail':'Password changed successfully'},status=status.HTTP_200_OK)
371 | else:
372 | return Response({"detail":'Password doesn\'t match'})
373 | elif new_password is None:
374 | return Response({'detail':'New password field required'})
375 | elif new_password_confirm is None:
376 | return Response({'detail':'New password confirm field required'})
377 |
--------------------------------------------------------------------------------