├── 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 | 53 | 54 |
47 | 48 |   49 |

50 | MUMBLE 51 |

52 |
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 | 80 | 81 | 96 | 97 |
82 | 93 | Verify the Email 94 | 95 |
98 |
99 |
100 |
101 |

Thanks !

102 |

The Mumble Team

103 |
104 |
105 |
106 |

107 | If you didn't sign up to MUMBLE, please ignore this email or contact us at 108 | 110 | example@mumble.com 111 | 112 |

113 |
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 | 59 | 60 |
53 | 54 |   55 |

56 | MUMBLE 57 |

58 |
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 | 89 | 90 | 105 | 106 |
91 | 102 | Reset My Password 103 | 104 |
107 |
108 |
109 |
110 |

Thanks !

111 |

The Mumble Team

112 |
113 |
114 |
115 |

116 | If you didn't ask for changing the password, please ignore this email or contact us at 117 | 119 | example@mumble.com 120 | 121 |

122 |
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 |
35 | 36 | 37 | 38 |
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 | --------------------------------------------------------------------------------