├── .gitignore ├── courses ├── __init__.py ├── admin.py ├── apps.py ├── helpers.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20210626_1616.py │ ├── 0003_auto_20210626_1730.py │ ├── 0004_auto_20210627_2131.py │ ├── 0005_auto_20210627_2132.py │ ├── 0006_auto_20210703_0954.py │ ├── 0007_auto_20210708_1429.py │ ├── 0008_auto_20210709_0905.py │ ├── 0009_auto_20210711_1301.py │ ├── 0010_auto_20210711_1305.py │ ├── 0011_auto_20210711_1312.py │ ├── 0012_auto_20210711_1313.py │ ├── 0013_auto_20210711_1355.py │ ├── 0014_auto_20210711_1910.py │ ├── 0015_auto_20210711_1918.py │ ├── 0016_auto_20210711_2218.py │ ├── 0017_auto_20210712_0120.py │ ├── 0018_auto_20210712_0130.py │ ├── 0019_auto_20210712_1012.py │ ├── 0020_auto_20210712_1334.py │ └── __init__.py ├── models.py ├── permissions.py ├── serializers.py ├── tests.py ├── urls.py └── views.py ├── manage.py ├── payments ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20210708_1052.py │ └── __init__.py ├── models.py ├── tests.py ├── urls.py └── views.py ├── requirements.txt ├── udemy_clone ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py └── users ├── __init__.py ├── admin.py ├── apps.py ├── migrations ├── 0001_initial.py └── __init__.py ├── models.py ├── serializers.py ├── tests.py └── views.py /.gitignore: -------------------------------------------------------------------------------- 1 | db.sqlite3 2 | media 3 | __pycache__/ 4 | .env 5 | -------------------------------------------------------------------------------- /courses/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OtchereDev/Udemy_clone/3fc36ffaefddff2b3bea018d3c000a42b5d1ef73/courses/__init__.py -------------------------------------------------------------------------------- /courses/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Course,CourseSection,Rate,Sector,Comment,Episode 3 | 4 | 5 | admin.site.register(Course) 6 | admin.site.register(CourseSection) 7 | admin.site.register(Rate) 8 | admin.site.register(Sector) 9 | admin.site.register(Comment) 10 | admin.site.register(Episode) 11 | -------------------------------------------------------------------------------- /courses/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoursesConfig(AppConfig): 5 | name = 'courses' 6 | -------------------------------------------------------------------------------- /courses/helpers.py: -------------------------------------------------------------------------------- 1 | 2 | def get_timer(length:float,type:str='long'): 3 | h = length // 3600 4 | m = length % 3600 // 60 5 | s = length % 3600 % 60 6 | if type=='short': 7 | return f"{h}h {f'0{m}' if m < 10 else m}m" 8 | 9 | if type=='min': 10 | return f"{f'0{m}' if m < 10 else m}min" 11 | 12 | else: 13 | if h>=1: 14 | return f"{h}:{f'0{m}' if m < 10 else m}:{f'0{round(s)}' if s < 10 else round(s)}" 15 | else: 16 | return f"{f'0{m}' if m < 10 else m}:{f'0{round(s)}' if s < 10 else round(s)}" -------------------------------------------------------------------------------- /courses/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.5 on 2021-06-26 16:16 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Comment', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('message', models.TextField()), 20 | ('created', models.DateTimeField(auto_now=True)), 21 | ], 22 | ), 23 | migrations.CreateModel( 24 | name='Course', 25 | fields=[ 26 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 27 | ('title', models.CharField(max_length=225)), 28 | ('description', models.TextField()), 29 | ('created', models.DateTimeField(auto_now_add=True)), 30 | ('updated', models.DateTimeField(auto_now=True)), 31 | ('student_engaged_rating', models.IntegerField(default=0)), 32 | ('language', models.CharField(max_length=225)), 33 | ('course_length', models.CharField(default=0, max_length=20)), 34 | ], 35 | ), 36 | migrations.CreateModel( 37 | name='Episode', 38 | fields=[ 39 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 40 | ('title', models.CharField(max_length=225)), 41 | ('file', models.FileField(upload_to='courses')), 42 | ('length', models.FloatField(default=0)), 43 | ], 44 | ), 45 | migrations.CreateModel( 46 | name='Rate', 47 | fields=[ 48 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 49 | ('rate_number', models.IntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(5)])), 50 | ], 51 | ), 52 | migrations.CreateModel( 53 | name='Sector', 54 | fields=[ 55 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 56 | ('name', models.CharField(max_length=225)), 57 | ], 58 | ), 59 | migrations.CreateModel( 60 | name='CourseSection', 61 | fields=[ 62 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 63 | ('section_title', models.CharField(blank=True, max_length=225, null=True)), 64 | ('section_number', models.IntegerField(blank=True, null=True)), 65 | ('episodes', models.ManyToManyField(blank=True, to='courses.Episode')), 66 | ], 67 | ), 68 | ] 69 | -------------------------------------------------------------------------------- /courses/migrations/0002_auto_20210626_1616.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.5 on 2021-06-26 16:16 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ('courses', '0001_initial'), 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.AddField( 19 | model_name='course', 20 | name='author', 21 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), 22 | ), 23 | migrations.AddField( 24 | model_name='course', 25 | name='comment', 26 | field=models.ManyToManyField(blank=True, to='courses.Comment'), 27 | ), 28 | migrations.AddField( 29 | model_name='course', 30 | name='course_sections', 31 | field=models.ManyToManyField(blank=True, to='courses.CourseSection'), 32 | ), 33 | migrations.AddField( 34 | model_name='course', 35 | name='rating', 36 | field=models.ManyToManyField(blank=True, to='courses.Rate'), 37 | ), 38 | migrations.AddField( 39 | model_name='course', 40 | name='sector', 41 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='courses.sector'), 42 | ), 43 | migrations.AddField( 44 | model_name='comment', 45 | name='user', 46 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), 47 | ), 48 | ] 49 | -------------------------------------------------------------------------------- /courses/migrations/0003_auto_20210626_1730.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.5 on 2021-06-26 17:30 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('courses', '0002_auto_20210626_1616'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='course', 15 | old_name='student_engaged_rating', 16 | new_name='student_rating', 17 | ), 18 | migrations.RemoveField( 19 | model_name='course', 20 | name='sector', 21 | ), 22 | migrations.AddField( 23 | model_name='sector', 24 | name='related_courses', 25 | field=models.ManyToManyField(blank=True, to='courses.Course'), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /courses/migrations/0004_auto_20210627_2131.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.5 on 2021-06-27 21:31 2 | 3 | from django.db import migrations, models 4 | import uuid 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('courses', '0003_auto_20210626_1730'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='course', 16 | name='course_uuid', 17 | field=models.UUIDField(default=uuid.uuid4), 18 | ), 19 | migrations.AddField( 20 | model_name='sector', 21 | name='sector_uuid', 22 | field=models.UUIDField(default=uuid.uuid4), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /courses/migrations/0005_auto_20210627_2132.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.5 on 2021-06-27 21:32 2 | 3 | from django.db import migrations, models 4 | import uuid 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('courses', '0004_auto_20210627_2131'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='course', 16 | name='course_uuid', 17 | field=models.UUIDField(default=uuid.uuid4, unique=True), 18 | ), 19 | migrations.AlterField( 20 | model_name='sector', 21 | name='sector_uuid', 22 | field=models.UUIDField(default=uuid.uuid4, unique=True), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /courses/migrations/0006_auto_20210703_0954.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.5 on 2021-07-03 09:54 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('courses', '0005_auto_20210627_2132'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='course', 15 | name='image_url', 16 | field=models.ImageField(default='', upload_to='course_images'), 17 | preserve_default=False, 18 | ), 19 | migrations.AddField( 20 | model_name='course', 21 | name='price', 22 | field=models.DecimalField(decimal_places=2, default=10, max_digits=5), 23 | preserve_default=False, 24 | ), 25 | migrations.AddField( 26 | model_name='sector', 27 | name='sector_image', 28 | field=models.ImageField(default='', upload_to='sector_images'), 29 | preserve_default=False, 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /courses/migrations/0007_auto_20210708_1429.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.5 on 2021-07-08 14:29 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('courses', '0006_auto_20210703_0954'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='episode', 15 | name='length', 16 | field=models.DecimalField(decimal_places=2, max_digits=100), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /courses/migrations/0008_auto_20210709_0905.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.5 on 2021-07-09 09:05 2 | 3 | import cloudinary_storage.storage 4 | import cloudinary_storage.validators 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('courses', '0007_auto_20210708_1429'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='course', 17 | name='image_url', 18 | field=models.ImageField(storage=cloudinary_storage.storage.MediaCloudinaryStorage(), upload_to='course_images'), 19 | ), 20 | migrations.AlterField( 21 | model_name='episode', 22 | name='file', 23 | field=models.FileField(upload_to='courses', validators=[cloudinary_storage.validators.validate_video]), 24 | ), 25 | migrations.AlterField( 26 | model_name='sector', 27 | name='sector_image', 28 | field=models.ImageField(storage=cloudinary_storage.storage.MediaCloudinaryStorage(), upload_to='sector_images'), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /courses/migrations/0009_auto_20210711_1301.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.5 on 2021-07-11 13:01 2 | 3 | import cloudinary_storage.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('courses', '0008_auto_20210709_0905'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='episode', 16 | name='file', 17 | field=models.ImageField(upload_to='courses', validators=[cloudinary_storage.validators.validate_video]), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /courses/migrations/0010_auto_20210711_1305.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.5 on 2021-07-11 13:05 2 | 3 | import cloudinary_storage.storage 4 | import cloudinary_storage.validators 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('courses', '0009_auto_20210711_1301'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='episode', 17 | name='file', 18 | field=models.ImageField(storage=cloudinary_storage.storage.VideoMediaCloudinaryStorage(), upload_to='courses', validators=[cloudinary_storage.validators.validate_video]), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /courses/migrations/0011_auto_20210711_1312.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.5 on 2021-07-11 13:12 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('courses', '0010_auto_20210711_1305'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='episode', 15 | name='file', 16 | field=models.ImageField(upload_to='courses'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /courses/migrations/0012_auto_20210711_1313.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.5 on 2021-07-11 13:13 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('courses', '0011_auto_20210711_1312'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='episode', 15 | name='file', 16 | field=models.FileField(upload_to='courses'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /courses/migrations/0013_auto_20210711_1355.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.5 on 2021-07-11 13:55 2 | 3 | import cloudinary.models 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('courses', '0012_auto_20210711_1313'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='episode', 16 | name='file', 17 | field=cloudinary.models.CloudinaryField(max_length=255, verbose_name='video'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /courses/migrations/0014_auto_20210711_1910.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.5 on 2021-07-11 19:10 2 | 3 | import cloudinary_storage.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('courses', '0013_auto_20210711_1355'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='episode', 16 | name='file', 17 | field=models.FileField(upload_to='courses', validators=[cloudinary_storage.validators.validate_video]), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /courses/migrations/0015_auto_20210711_1918.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.5 on 2021-07-11 19:18 2 | 3 | import cloudinary_storage.storage 4 | import cloudinary_storage.validators 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('courses', '0014_auto_20210711_1910'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='episode', 17 | name='file', 18 | field=models.FileField(storage=cloudinary_storage.storage.VideoMediaCloudinaryStorage(), upload_to='courses', validators=[cloudinary_storage.validators.validate_video]), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /courses/migrations/0016_auto_20210711_2218.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.5 on 2021-07-11 22:18 2 | 3 | import cloudinary_storage.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('courses', '0015_auto_20210711_1918'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='episode', 16 | name='file', 17 | field=models.FileField(upload_to='courses', validators=[cloudinary_storage.validators.validate_video]), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /courses/migrations/0017_auto_20210712_0120.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.5 on 2021-07-12 01:20 2 | 3 | import cloudinary.models 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('courses', '0016_auto_20210711_2218'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='episode', 16 | name='file', 17 | field=cloudinary.models.CloudinaryField(max_length=255), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /courses/migrations/0018_auto_20210712_0130.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.5 on 2021-07-12 01:30 2 | 3 | import cloudinary.models 4 | import cloudinary_storage.validators 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('courses', '0017_auto_20210712_0120'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='episode', 17 | name='file', 18 | field=cloudinary.models.CloudinaryField(max_length=255, validators=[cloudinary_storage.validators.validate_video]), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /courses/migrations/0019_auto_20210712_1012.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.5 on 2021-07-12 10:12 2 | 3 | import cloudinary.models 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('courses', '0018_auto_20210712_0130'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='course', 16 | name='image_url', 17 | field=cloudinary.models.CloudinaryField(max_length=255), 18 | ), 19 | migrations.AlterField( 20 | model_name='sector', 21 | name='sector_image', 22 | field=cloudinary.models.CloudinaryField(max_length=255), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /courses/migrations/0020_auto_20210712_1334.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.5 on 2021-07-12 13:34 2 | 3 | import cloudinary_storage.storage 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('courses', '0019_auto_20210712_1012'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='course', 16 | name='image_url', 17 | field=models.ImageField(storage=cloudinary_storage.storage.MediaCloudinaryStorage(), upload_to='course_images'), 18 | ), 19 | migrations.AlterField( 20 | model_name='sector', 21 | name='sector_image', 22 | field=models.ImageField(storage=cloudinary_storage.storage.MediaCloudinaryStorage(), upload_to='sector_images'), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /courses/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OtchereDev/Udemy_clone/3fc36ffaefddff2b3bea018d3c000a42b5d1ef73/courses/migrations/__init__.py -------------------------------------------------------------------------------- /courses/models.py: -------------------------------------------------------------------------------- 1 | # from users.models import User 2 | from django.db import models 3 | from django.core.validators import MaxValueValidator,MinValueValidator 4 | from django.conf import settings 5 | from django.contrib.auth import get_user_model 6 | import uuid 7 | from mutagen.mp4 import MP4,MP4StreamInfoError 8 | from .helpers import get_timer 9 | from decimal import Decimal 10 | 11 | from cloudinary_storage.storage import MediaCloudinaryStorage 12 | from cloudinary_storage.validators import validate_video 13 | 14 | from cloudinary.models import CloudinaryField 15 | 16 | 17 | 18 | 19 | class Course(models.Model): 20 | title=models.CharField(max_length=225) 21 | description=models.TextField() 22 | created=models.DateTimeField(auto_now_add=True) 23 | updated=models.DateTimeField(auto_now=True) 24 | rating=models.ManyToManyField('Rate',blank=True) 25 | # sector=models.ForeignKey('Sector',on_delete=models.CASCADE) 26 | author=models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.CASCADE) 27 | student_rating=models.IntegerField(default=0) 28 | language=models.CharField(max_length=225) 29 | course_length=models.CharField(default=0,max_length=20) 30 | course_sections=models.ManyToManyField('CourseSection',blank=True) 31 | comment=models.ManyToManyField('Comment',blank=True) 32 | course_uuid=models.UUIDField(default=uuid.uuid4,unique=True) 33 | image_url=models.ImageField(upload_to='course_images',storage=MediaCloudinaryStorage()) 34 | price=models.DecimalField(max_digits=5 ,decimal_places=2) 35 | 36 | def get_rating(self): 37 | ratings=self.rating.all() 38 | rate=0 39 | for rating in ratings: 40 | rate+=rating.rate_number 41 | try: 42 | rate/=len(ratings) 43 | except ZeroDivisionError: 44 | rate=0 45 | 46 | 47 | return rate 48 | 49 | def get_no_rating(self): 50 | return len(self.rating.all()) 51 | 52 | def get_brief_description(self): 53 | return self.description[:100] 54 | 55 | def get_enrolled_students(self): 56 | students=get_user_model().objects.filter(paid_course=self) 57 | return len(students) 58 | 59 | def get_total_lectures(self): 60 | lectures=0 61 | for section in self.course_sections.all(): 62 | lectures+=len(section.episodes.all()) 63 | return lectures 64 | 65 | def total_course_length(self): 66 | length=Decimal(0.00) 67 | 68 | for section in self.course_sections.all(): 69 | for episode in section.episodes.all(): 70 | length+=episode.length 71 | 72 | 73 | 74 | return get_timer(length,type="short") 75 | 76 | 77 | class Rate(models.Model): 78 | rate_number=models.IntegerField(validators=[MinValueValidator(0),MaxValueValidator(5)]) 79 | 80 | 81 | class CourseSection(models.Model): 82 | section_title=models.CharField(max_length=225,blank=True,null=True) 83 | section_number=models.IntegerField(blank=True,null=True) 84 | episodes=models.ManyToManyField('Episode',blank=True) 85 | 86 | def total_length(self): 87 | total=Decimal(0.00) 88 | for episode in self.episodes.all(): 89 | total+=episode.length 90 | return get_timer(total,type='min') 91 | 92 | 93 | 94 | class Episode(models.Model): 95 | title=models.CharField(max_length=225) 96 | file=CloudinaryField(resource_type='video',validators=[validate_video],folder='media') 97 | # file=models.FileField(upload_to='courses',validators=[validate_video],) 98 | length=models.DecimalField(max_digits=100,decimal_places=2) 99 | 100 | def get_video_length(self): 101 | try: 102 | video=MP4(self.file) 103 | return video.info.length 104 | 105 | except MP4StreamInfoError: 106 | return 0.0 107 | 108 | def get_video_length_time(self): 109 | return get_timer(self.length) 110 | 111 | def get_absolute_url(self): 112 | return self.file.url 113 | 114 | 115 | 116 | def save(self,*args, **kwargs): 117 | self.length=self.get_video_length() 118 | # print(self.length) 119 | # print(self.file.path) 120 | 121 | 122 | return super().save(*args, **kwargs) 123 | 124 | 125 | class Comment(models.Model): 126 | user=models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.CASCADE) 127 | message=models.TextField() 128 | created=models.DateTimeField(auto_now=True) 129 | 130 | 131 | class Sector(models.Model): 132 | name=models.CharField(max_length=225) 133 | sector_uuid=models.UUIDField(default=uuid.uuid4,unique=True) 134 | related_courses=models.ManyToManyField(Course,blank=True) 135 | sector_image=models.ImageField(upload_to='sector_images',storage=MediaCloudinaryStorage()) 136 | 137 | -------------------------------------------------------------------------------- /courses/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework.permissions import BasePermission 2 | 3 | 4 | class AuthorPermission(BasePermission): 5 | def has_permission(self, request, view): 6 | return request.user.author -------------------------------------------------------------------------------- /courses/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from users.serializers import UserSerializer 3 | from courses.models import Comment, Course, CourseSection, Episode, Sector 4 | from rest_framework.serializers import ModelSerializer 5 | 6 | class EpisodePaidSerializer(ModelSerializer): 7 | length=serializers.CharField(source='get_video_length_time') 8 | file=serializers.CharField(source='get_absolute_url') 9 | class Meta: 10 | model=Episode 11 | fields=[ 12 | "title", 13 | "file", 14 | "length", 15 | ] 16 | 17 | class CourseSectionPaidSerializer(ModelSerializer): 18 | episodes=EpisodePaidSerializer(many=True) 19 | total_duduration=serializers.CharField(source='total_length') 20 | class Meta: 21 | model=CourseSection 22 | fields=[ 23 | "section_title", 24 | "section_number", 25 | "episodes", 26 | "total_duduration" 27 | ] 28 | 29 | class EpisodeUnPaidSerializer(ModelSerializer): 30 | length=serializers.CharField(source='get_video_length_time') 31 | 32 | 33 | class Meta: 34 | model=Episode 35 | fields=[ 36 | "title", 37 | "length", 38 | 'id', 39 | 40 | ] 41 | 42 | class CourseSectionUnPaidSerializer(ModelSerializer): 43 | episodes=EpisodeUnPaidSerializer(many=True) 44 | total_duration=serializers.CharField(source='total_length') 45 | 46 | class Meta: 47 | model=CourseSection 48 | fields=[ 49 | "section_title", 50 | "section_number", 51 | "episodes", 52 | "total_duration" 53 | ] 54 | 55 | class CommentSerializer(ModelSerializer): 56 | user=UserSerializer(read_only=True) 57 | class Meta: 58 | model=Comment 59 | fields=[ 60 | 'user', 61 | 'message', 62 | 'created', 63 | 'id' 64 | ] 65 | 66 | class CourseUnPaidSerializer(ModelSerializer): 67 | 68 | comment=CommentSerializer(many=True) 69 | author=UserSerializer() 70 | course_sections=CourseSectionUnPaidSerializer(many=True) 71 | student_rating=serializers.IntegerField(source='get_rating') 72 | student_rating_no=serializers.IntegerField(source='get_no_rating') 73 | student_no=serializers.IntegerField(source='get_enrolled_students') 74 | total_lectures=serializers.IntegerField(source="get_total_lectures") 75 | total_duration=serializers.CharField(source='total_course_length') 76 | class Meta: 77 | model=Course 78 | exclude=[ 79 | 'rating', 80 | 81 | ] 82 | 83 | class CoursePaidSerializer(ModelSerializer): 84 | 85 | comment=CommentSerializer(many=True) 86 | author=UserSerializer() 87 | course_sections=CourseSectionPaidSerializer(many=True) 88 | student_rating=serializers.IntegerField(source='get_rating') 89 | student_rating_no=serializers.IntegerField(source='get_no_rating') 90 | student_no=serializers.IntegerField(source='get_enrolled_students') 91 | total_lectures=serializers.IntegerField(source="get_total_lectures") 92 | total_duration=serializers.CharField(source='total_course_length') 93 | class Meta: 94 | model=Course 95 | exclude=[ 96 | 'rating', 97 | 98 | ] 99 | 100 | class SectorSerializer(ModelSerializer): 101 | class Meta: 102 | model=Sector 103 | exclude=[ 104 | 'related_courses', 105 | ] 106 | 107 | class CourseDisplaySerializer(ModelSerializer): 108 | rating=serializers.IntegerField(source='get_rating') 109 | student_no=serializers.IntegerField(source='get_enrolled_students') 110 | author=UserSerializer() 111 | class Meta: 112 | model=Course 113 | fields=['course_uuid',"title",'rating','student_no',"author","price","image_url"] 114 | 115 | 116 | class CartItemSerializer(ModelSerializer): 117 | author=UserSerializer() 118 | class Meta: 119 | model=Course 120 | # add price later,image url 121 | fields=['course_uuid','title',"author","price","image_url"] 122 | 123 | class CourseListSerializer(ModelSerializer): 124 | rating=serializers.IntegerField(source='get_rating') 125 | student_no=serializers.IntegerField(source='get_enrolled_students') 126 | author=UserSerializer() 127 | description=serializers.CharField(source='get_brief_description') 128 | total_lectures=serializers.IntegerField(source="get_total_lectures") 129 | class Meta: 130 | model=Course 131 | fields=['course_uuid',"title",'rating','student_no',"author","price","image_url",'description','total_lectures'] 132 | 133 | -------------------------------------------------------------------------------- /courses/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /courses/urls.py: -------------------------------------------------------------------------------- 1 | from courses.views import AddComment, CourseDetail, CourseManage, CourseSearch, CourseStudy, CoursesHomeViews,CourseManageCourseList, GetCartDetail, SearchCourse 2 | from django.urls import path 3 | 4 | 5 | app_name='courses' 6 | 7 | urlpatterns = [ 8 | 9 | path('cart/',GetCartDetail.as_view()), 10 | path('detail//',CourseDetail.as_view()), 11 | path("search//",SearchCourse.as_view()), 12 | path("study//",CourseStudy.as_view()), 13 | path('comment//',AddComment.as_view()), 14 | path('course_management/',CourseManageCourseList.as_view()), 15 | path('course_management//',CourseManage.as_view()), 16 | path('',CoursesHomeViews.as_view(),), 17 | path('/',CourseSearch.as_view()), 18 | ] 19 | -------------------------------------------------------------------------------- /courses/views.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ValidationError 2 | from django.db.models.query_utils import Q 3 | from django.http.response import HttpResponseBadRequest, HttpResponseNotAllowed 4 | from courses.serializers import CartItemSerializer, CommentSerializer, CourseDisplaySerializer, CourseListSerializer, CoursePaidSerializer, CourseUnPaidSerializer, SectorSerializer 5 | from courses.models import Course, Sector 6 | 7 | from rest_framework.views import APIView 8 | from rest_framework.response import Response 9 | from rest_framework import status 10 | from rest_framework.permissions import IsAuthenticated 11 | 12 | 13 | from decimal import Decimal 14 | import json 15 | 16 | 17 | class CoursesHomeViews(APIView): 18 | def get(self,request,*args, **kwargs): 19 | sectors=Sector.objects.order_by('?')[:6] 20 | 21 | sector_response=[] 22 | 23 | for sector in sectors: 24 | sector_courses=sector.related_courses.order_by('?')[:4] 25 | courses_serializer=CourseDisplaySerializer(sector_courses,many=True) 26 | sector_obj={ 27 | "sector_name": sector.name, 28 | "sector_uuid": sector.sector_uuid, 29 | "featured_courses": courses_serializer.data, 30 | "sector_image":sector.sector_image.url 31 | } 32 | sector_response.append(sector_obj) 33 | 34 | # serializer=SectorSerializer(sectors=sector_response,many=True) 35 | 36 | return Response(data=sector_response,status=status.HTTP_200_OK) 37 | 38 | 39 | # not used yet 40 | class CourseCreateView(APIView): 41 | permission_classes=[IsAuthenticated] 42 | def post(self,request,*args, **kwargs): 43 | serializer=CoursePaidSerializer(request.data) 44 | 45 | if serializer.is_valid(): 46 | print(serializer.validated_data) 47 | obj=serializer.save(author=request.user) 48 | print(obj) 49 | 50 | return Response(obj,status=status.HTTP_201_CREATED) 51 | else: 52 | 53 | return Response(data=serializer.errors,status=status.HTTP_400_BAD_REQUEST) 54 | 55 | 56 | class CourseSearch(APIView): 57 | def get(self,request,sector_uuid,*args, **kwargs): 58 | sector=Sector.objects.filter(sector_uuid=sector_uuid) 59 | 60 | if not sector: 61 | return HttpResponseBadRequest("Course sector does not exist") 62 | 63 | sector_courses=sector[0].related_courses.all() 64 | 65 | serializer=CourseListSerializer(sector_courses,many=True) 66 | 67 | total_students=0 68 | 69 | for course in sector_courses: 70 | total_students+=course.get_enrolled_students() 71 | 72 | 73 | 74 | return Response({'data':serializer.data, 75 | 'sector_name':sector[0].name, 76 | 'total_students':total_students, 77 | 'image':sector[0].sector_image.url}, 78 | status=status.HTTP_200_OK) 79 | 80 | 81 | class CourseDetail(APIView): 82 | def get(self,request,course_uuid,*args, **kwargs): 83 | try: 84 | 85 | course=Course.objects.filter(course_uuid=course_uuid) 86 | except ValidationError: 87 | return HttpResponseBadRequest('Invalid Course uuid') 88 | 89 | if not course: 90 | return HttpResponseBadRequest('Course does not exist') 91 | 92 | serializer=CourseUnPaidSerializer(course[0]) 93 | 94 | 95 | return Response(serializer.data,status=status.HTTP_200_OK) 96 | 97 | 98 | 99 | 100 | class CourseStudy(APIView): 101 | permission_classes=[IsAuthenticated] 102 | def get(self,request,course_uuid): 103 | check_course=Course.objects.filter(course_uuid=course_uuid) 104 | 105 | if not check_course: 106 | 107 | return HttpResponseBadRequest('Course does not exist') 108 | 109 | user_course=request.user.paid_course.filter(course_uuid=course_uuid) 110 | 111 | 112 | if not user_course: 113 | 114 | return HttpResponseNotAllowed("User has not purchased this course") 115 | 116 | course=Course.objects.filter(course_uuid=course_uuid)[0] 117 | 118 | serializer=CoursePaidSerializer(course) 119 | 120 | 121 | 122 | return Response(serializer.data,status=status.HTTP_200_OK) 123 | 124 | 125 | # not used yet 126 | class CourseManageCourseList(APIView): 127 | permission_classes=[IsAuthenticated] 128 | def get(self,request,*args, **kwargs): 129 | courses=Course.objects.filter(author=request.user) 130 | serializer=CoursePaidSerializer(courses,many=True) 131 | 132 | return Response(data=serializer.data,status=status.HTTP_200_OK) 133 | 134 | 135 | # not used yet 136 | class CourseManage(APIView): 137 | permission_classes=[IsAuthenticated] 138 | 139 | def patch(self,request,course_uuid,*args, **kwargs): 140 | course=Course.objects.filter(course_uuid=course_uuid,author=request.user) 141 | 142 | if not course: 143 | return HttpResponseBadRequest() 144 | 145 | serializer=CoursePaidSerializer(request.data,instance=course[0]) 146 | 147 | if serializer.is_valid(): 148 | data=serializer.save() 149 | 150 | return Response(data=data,status=status.HTTP_201_CREATED) 151 | 152 | 153 | def delete(self,request,course_uuid,*args, **kwargs): 154 | 155 | course=Course.objects.filter(course_uuid=course_uuid,author=request.user) 156 | 157 | if not course: 158 | return HttpResponseBadRequest() 159 | 160 | course[0].delete() 161 | 162 | return Response(data={ 163 | "status":"successfull", 164 | "course":course[0] 165 | },status=status.HTTP_204_NO_CONTENT) 166 | 167 | 168 | class GetCartDetail(APIView): 169 | def post(self,request,*args, **kwargs): 170 | 171 | try: 172 | body = json.loads(request.body) 173 | 174 | except json.decoder.JSONDecodeError: 175 | return HttpResponseBadRequest() 176 | 177 | if type(body.get('cart')) != list: 178 | return HttpResponseBadRequest() 179 | 180 | 181 | if len(body.get("cart")) ==0: 182 | return Response(data=[]) 183 | 184 | courses=[] 185 | 186 | for uuid in body.get("cart"): 187 | item = Course.objects.filter(course_uuid=uuid) 188 | 189 | if not item: 190 | return HttpResponseBadRequest() 191 | 192 | courses.append(item[0]) 193 | 194 | # serializer for cart 195 | serializer =CartItemSerializer(courses,many=True) 196 | 197 | # TODO : After you have added the price field 198 | cart_cost=Decimal(0.00) 199 | 200 | for item in serializer.data: 201 | 202 | cart_cost+=Decimal(item.get("price")) 203 | 204 | 205 | return Response(data={"cart_detail":serializer.data,"cart_total":str(cart_cost)}) 206 | 207 | 208 | 209 | class SearchCourse(APIView): 210 | 211 | def get(self,request,search_term): 212 | matches= Course.objects.filter(Q(title__icontains=search_term)| 213 | Q(description__icontains=search_term)) 214 | 215 | serializer=CourseListSerializer(matches,many=True) 216 | 217 | return Response(data=serializer.data) 218 | 219 | 220 | 221 | class AddComment(APIView): 222 | permission_classes=[IsAuthenticated] 223 | def post(self,request,course_uuid,*args, **kwargs): 224 | try: 225 | course=Course.objects.get(course_uuid=course_uuid) 226 | except Course.DoesNotExist: 227 | return Response(status=status.HTTP_400_BAD_REQUEST) 228 | 229 | content=json.loads(request.body) 230 | 231 | if not content.get('message'): 232 | return Response(status=status.HTTP_400_BAD_REQUEST) 233 | 234 | serializer = CommentSerializer(data=content) 235 | 236 | if serializer.is_valid(): 237 | comment=serializer.save(user=request.user) 238 | 239 | course.comment.add(comment) 240 | 241 | return Response(status=status.HTTP_200_OK) 242 | 243 | else: 244 | return Response(data=serializer.errors,status=status.HTTP_400_BAD_REQUEST) 245 | 246 | -------------------------------------------------------------------------------- /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', 'udemy_clone.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /payments/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OtchereDev/Udemy_clone/3fc36ffaefddff2b3bea018d3c000a42b5d1ef73/payments/__init__.py -------------------------------------------------------------------------------- /payments/admin.py: -------------------------------------------------------------------------------- 1 | from payments.models import Payment, PaymentIntent 2 | from django.contrib import admin 3 | 4 | admin.site.register(Payment) 5 | admin.site.register(PaymentIntent) 6 | -------------------------------------------------------------------------------- /payments/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PaymentsConfig(AppConfig): 5 | name = 'payments' 6 | -------------------------------------------------------------------------------- /payments/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.5 on 2021-07-07 00:35 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ('courses', '0006_auto_20210703_0954'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='PaymentIntent', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('payment_intent_id', models.CharField(max_length=225)), 23 | ('checkout_id', models.CharField(max_length=225)), 24 | ('created', models.DateTimeField(auto_now=True)), 25 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 26 | ], 27 | ), 28 | migrations.CreateModel( 29 | name='Payment', 30 | fields=[ 31 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 32 | ('total_amount', models.DecimalField(decimal_places=2, max_digits=7)), 33 | ('created', models.DateTimeField(auto_now=True)), 34 | ('courses', models.ManyToManyField(to='courses.Course')), 35 | ('payment_intent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='payments.paymentintent')), 36 | ], 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /payments/migrations/0002_auto_20210708_1052.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.5 on 2021-07-08 10:52 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('courses', '0006_auto_20210703_0954'), 10 | ('payments', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='payment', 16 | name='courses', 17 | ), 18 | migrations.AddField( 19 | model_name='paymentintent', 20 | name='courses', 21 | field=models.ManyToManyField(to='courses.Course'), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /payments/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OtchereDev/Udemy_clone/3fc36ffaefddff2b3bea018d3c000a42b5d1ef73/payments/migrations/__init__.py -------------------------------------------------------------------------------- /payments/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from users.models import User 3 | from courses.models import Course 4 | 5 | class PaymentIntent(models.Model): 6 | payment_intent_id=models.CharField(max_length=225) 7 | checkout_id=models.CharField(max_length=225) 8 | user=models.ForeignKey(User,on_delete=models.CASCADE) 9 | courses=models.ManyToManyField(Course) 10 | created=models.DateTimeField(auto_now=True) 11 | 12 | class Payment(models.Model): 13 | payment_intent=models.ForeignKey(PaymentIntent,on_delete=models.CASCADE) 14 | total_amount=models.DecimalField(max_digits=7,decimal_places=2) 15 | created=models.DateTimeField(auto_now=True) 16 | 17 | 18 | -------------------------------------------------------------------------------- /payments/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /payments/urls.py: -------------------------------------------------------------------------------- 1 | 2 | from payments.views import PaymentHandler, Webhook 3 | from django.urls import path 4 | 5 | app_name='payments' 6 | 7 | urlpatterns = [ 8 | path('webhook/',Webhook.as_view()), 9 | path('',PaymentHandler.as_view()), 10 | ] 11 | -------------------------------------------------------------------------------- /payments/views.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ValidationError 2 | from payments.models import Payment, PaymentIntent 3 | import stripe 4 | from rest_framework.views import APIView 5 | from rest_framework.response import Response 6 | from django.conf import settings 7 | from courses.models import Course 8 | import json 9 | from decimal import Decimal 10 | 11 | 12 | stripe.api_key=settings.STRIPE_SECRET_KEY 13 | endpoint_secret=settings.WEBHOOK_SECRET 14 | 15 | class PaymentHandler(APIView): 16 | 17 | def post(self,request): 18 | 19 | if request.body: 20 | body=json.loads(request.body) 21 | if body and len(body): 22 | # fetch course detail as line_items 23 | courses_line_items=[] 24 | cart_course=[] 25 | for item in body: 26 | try: 27 | course=Course.objects.get(course_uuid=item) 28 | 29 | line_item={ 30 | 'price_data': { 31 | 'currency': 'usd', 32 | 'unit_amount': int(course.price*100), 33 | 'product_data': { 34 | 'name': course.title, 35 | 36 | }, 37 | }, 38 | 'quantity': 1, 39 | } 40 | 41 | courses_line_items.append(line_item) 42 | cart_course.append(course) 43 | 44 | except Course.DoesNotExist: 45 | pass 46 | except ValidationError: 47 | pass 48 | 49 | else: 50 | return Response(status=400) 51 | else: 52 | return Response(status=400) 53 | 54 | 55 | checkout_session = stripe.checkout.Session.create( 56 | payment_method_types=['card'], 57 | line_items=courses_line_items, 58 | mode='payment', 59 | success_url='http://localhost:3000/', 60 | cancel_url="http://localhost:3000/", 61 | 62 | ) 63 | 64 | 65 | intent=PaymentIntent.objects.create( 66 | payment_intent_id=checkout_session.payment_intent, 67 | checkout_id=checkout_session.id, 68 | user=request.user, 69 | ) 70 | 71 | for course in cart_course: 72 | intent.courses.add(course) 73 | 74 | 75 | return Response({"url":checkout_session.url}) 76 | 77 | 78 | 79 | class Webhook(APIView): 80 | 81 | def post(self,request,*args, **kwargs): 82 | payload = request.body 83 | sig_header = request.META['HTTP_STRIPE_SIGNATURE'] 84 | event = None 85 | 86 | try: 87 | event = stripe.Webhook.construct_event( 88 | payload, sig_header, endpoint_secret 89 | ) 90 | except ValueError as e: 91 | # Invalid payload 92 | return Response(status=400) 93 | except stripe.error.SignatureVerificationError as e: 94 | # Invalid signature 95 | return Response(status=400) 96 | 97 | 98 | if event['type'] == 'checkout.session.completed': 99 | session = event['data']['object'] 100 | 101 | try: 102 | # fetch user intent 103 | intent=PaymentIntent.objects.get( 104 | payment_intent_id=session.payment_intent, 105 | checkout_id=session.id 106 | 107 | ) 108 | except PaymentIntent.DoesNotExist: 109 | return Response(status=400) 110 | 111 | # create payment reciept 112 | Payment.objects.create( 113 | payment_intent=intent, 114 | total_amount= Decimal(session.amount_total)/100, 115 | ) 116 | 117 | for course in intent.courses.all(): 118 | # TODO add course to user profile 119 | intent.user.paid_course.add(course) 120 | 121 | # Fulfill the purchase... 122 | # fulfill_order(session) 123 | 124 | # Passed signature verification 125 | return Response(status=200) 126 | 127 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==3.1.5 2 | gunicorn==20.0.4 3 | djangorestframework==3.12.2 4 | djoser==2.1.0 5 | django-cors-headers==3.5.0 6 | django-cloudinary-storage==0.3.0 7 | mutagen==1.45.1 8 | stripe==2.58.0 9 | cloudinary -------------------------------------------------------------------------------- /udemy_clone/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OtchereDev/Udemy_clone/3fc36ffaefddff2b3bea018d3c000a42b5d1ef73/udemy_clone/__init__.py -------------------------------------------------------------------------------- /udemy_clone/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for udemy_clone 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', 'udemy_clone.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /udemy_clone/settings.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from pathlib import Path 4 | import os 5 | 6 | import cloudinary 7 | 8 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 9 | BASE_DIR = Path(__file__).resolve().parent.parent 10 | 11 | 12 | # Quick-start development settings - unsuitable for production 13 | # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ 14 | 15 | # SECURITY WARNING: keep the secret key used in production secret! 16 | SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY') 17 | 18 | # SECURITY WARNING: don't run with debug turned on in production! 19 | DEBUG = True 20 | 21 | ALLOWED_HOSTS = [] 22 | 23 | 24 | # Application definition 25 | 26 | INSTALLED_APPS = [ 27 | 'django.contrib.admin', 28 | 'django.contrib.auth', 29 | 'django.contrib.contenttypes', 30 | 'django.contrib.sessions', 31 | 'django.contrib.messages', 32 | 'django.contrib.staticfiles', 33 | 34 | #third party apps 35 | "rest_framework", 36 | "djoser" , 37 | 'corsheaders', 38 | 'cloudinary_storage', 39 | 'cloudinary', 40 | 41 | # own apps 42 | 'courses', 43 | 'payments', 44 | 'users', 45 | ] 46 | 47 | MIDDLEWARE = [ 48 | 'django.middleware.security.SecurityMiddleware', 49 | 'django.contrib.sessions.middleware.SessionMiddleware', 50 | 'corsheaders.middleware.CorsMiddleware', 51 | 'django.middleware.common.CommonMiddleware', 52 | 'django.middleware.csrf.CsrfViewMiddleware', 53 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 54 | 'django.contrib.messages.middleware.MessageMiddleware', 55 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 56 | ] 57 | 58 | ROOT_URLCONF = 'udemy_clone.urls' 59 | 60 | TEMPLATES = [ 61 | { 62 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 63 | 'DIRS': [BASE_DIR/'templates',], 64 | 'APP_DIRS': True, 65 | 'OPTIONS': { 66 | 'context_processors': [ 67 | 'django.template.context_processors.debug', 68 | 'django.template.context_processors.request', 69 | 'django.contrib.auth.context_processors.auth', 70 | 'django.contrib.messages.context_processors.messages', 71 | ], 72 | }, 73 | }, 74 | ] 75 | 76 | WSGI_APPLICATION = 'udemy_clone.wsgi.application' 77 | 78 | 79 | # Database 80 | # https://docs.djangoproject.com/en/3.1/ref/settings/#databases 81 | 82 | DATABASES = { 83 | 'default': { 84 | 'ENGINE': 'django.db.backends.sqlite3', 85 | 'NAME': BASE_DIR / 'db.sqlite3', 86 | } 87 | } 88 | 89 | 90 | # Password validation 91 | # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators 92 | 93 | AUTH_PASSWORD_VALIDATORS = [ 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 102 | }, 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 105 | }, 106 | ] 107 | 108 | 109 | # Internationalization 110 | # https://docs.djangoproject.com/en/3.1/topics/i18n/ 111 | 112 | LANGUAGE_CODE = 'en-us' 113 | 114 | TIME_ZONE = 'UTC' 115 | 116 | USE_I18N = True 117 | 118 | USE_L10N = True 119 | 120 | USE_TZ = True 121 | 122 | 123 | # Static files (CSS, JavaScript, Images) 124 | # https://docs.djangoproject.com/en/3.1/howto/static-files/ 125 | 126 | STATIC_URL = '/static/' 127 | MEDIA_URL='/media/' 128 | 129 | 130 | MEDIA_ROOT=BASE_DIR/'media' 131 | STATIC_ROOT=BASE_DIR/'static_root' 132 | 133 | STATICFILES_DIRS=[ 134 | BASE_DIR/'static', 135 | ] 136 | 137 | # Auth settings 138 | AUTH_USER_MODEL='users.User' 139 | 140 | DJOSER={ 141 | 'SERIALIZERS':{ 142 | 'user': 'users.serializers.UserAuthSerializer', 143 | 'current_user': 'users.serializers.UserAuthSerializer' 144 | } 145 | } 146 | 147 | REST_FRAMEWORK = { 148 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 149 | 'rest_framework_simplejwt.authentication.JWTAuthentication', 150 | ), 151 | } 152 | 153 | SIMPLE_JWT = { 154 | 'AUTH_HEADER_TYPES': ('Token',), 155 | } 156 | 157 | 158 | CORS_ALLOWED_ORIGINS = [ 159 | 160 | 'http://localhost:3000', 161 | 'http://127.0.0.1:3000', 162 | 163 | ] 164 | 165 | CSRF_TRUSTED_ORIGINS = [ 166 | 'localhost:3000', 167 | '127.0.0.1:3000', 168 | ] 169 | 170 | 171 | # PAYMENT 172 | STRIPE_SECRET_KEY=os.environ.get('STRIPE_SECRET_KEY') 173 | STRIPE_PUBLIC_KEY=os.environ.get('STRIPE_PUBLIC_KEY') 174 | WEBHOOK_SECRET=os.environ.get('WEBHOOK_SECRET') 175 | 176 | 177 | APPEND_SLASH=False 178 | 179 | # cloudinary settings 180 | CLOUDINARY_STORAGE = { 181 | 'CLOUD_NAME': os.environ.get('CLOUDINARY_CLOUD_NAME'), 182 | 'API_KEY': os.environ.get('CLOUDINARY_API_KEY'), 183 | 'API_SECRET': os.environ.get('CLOUDINARY_API_SECRET') 184 | } 185 | 186 | cloudinary.config( 187 | cloud_name =os.environ.get('CLOUDINARY_CLOUD_NAME'), 188 | api_key = os.environ.get('CLOUDINARY_API_KEY'), 189 | api_secret = os.environ.get('CLOUDINARY_API_SECRET'), 190 | secure = False 191 | ) -------------------------------------------------------------------------------- /udemy_clone/urls.py: -------------------------------------------------------------------------------- 1 | 2 | from django.contrib import admin 3 | from django.urls import path 4 | from django.conf.urls.static import static 5 | from django.conf import settings 6 | from django.urls.conf import include 7 | 8 | urlpatterns = [ 9 | path('admin/', admin.site.urls), 10 | path('courses/',include('courses.urls',namespace='courses')), 11 | path('payments/',include('payments.urls',namespace='payments')), 12 | path('auth/', include('djoser.urls')), 13 | path('auth/', include('djoser.urls.jwt')), 14 | ] 15 | 16 | if settings.DEBUG: 17 | urlpatterns+=static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT) 18 | urlpatterns+=static(settings.STATIC_URL,document_root=settings.STATIC_ROOT) 19 | 20 | 21 | -------------------------------------------------------------------------------- /udemy_clone/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for udemy_clone 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', 'udemy_clone.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OtchereDev/Udemy_clone/3fc36ffaefddff2b3bea018d3c000a42b5d1ef73/users/__init__.py -------------------------------------------------------------------------------- /users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import User 3 | 4 | admin.site.register(User) 5 | -------------------------------------------------------------------------------- /users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersConfig(AppConfig): 5 | name = 'users' 6 | -------------------------------------------------------------------------------- /users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.5 on 2021-06-26 16:16 2 | 3 | from django.db import migrations, models 4 | import users.models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('auth', '0012_alter_user_first_name_max_length'), 13 | ('courses', '0001_initial'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='User', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('password', models.CharField(max_length=128, verbose_name='password')), 22 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 23 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 24 | ('name', models.CharField(blank=True, max_length=225, null=True)), 25 | ('email', models.EmailField(max_length=225, unique=True)), 26 | ('created', models.DateTimeField(auto_now_add=True)), 27 | ('is_active', models.BooleanField(default=True)), 28 | ('is_staff', models.BooleanField(default=False)), 29 | ('is_author', models.BooleanField(default=False)), 30 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), 31 | ('paid_course', models.ManyToManyField(blank=True, to='courses.Course')), 32 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), 33 | ], 34 | options={ 35 | 'abstract': False, 36 | }, 37 | managers=[ 38 | ('objects', users.models.UserManager()), 39 | ], 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /users/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OtchereDev/Udemy_clone/3fc36ffaefddff2b3bea018d3c000a42b5d1ef73/users/migrations/__init__.py -------------------------------------------------------------------------------- /users/models.py: -------------------------------------------------------------------------------- 1 | from courses.models import Course 2 | from django.db import models 3 | from django.contrib.auth.models import AbstractBaseUser,BaseUserManager, PermissionsMixin 4 | 5 | class UserManager(BaseUserManager): 6 | use_in_migrations=True 7 | def create_superuser(self,email,password,name,**other_fields): 8 | other_fields.setdefault('is_staff',True) 9 | other_fields.setdefault('is_superuser',True) 10 | other_fields.setdefault('is_author',True) 11 | 12 | 13 | if other_fields.get('is_staff') is not True: 14 | return ValueError('Superuser must be assign is_staff=True') 15 | 16 | if other_fields.get('is_superuser') is not True: 17 | return ValueError('Superuser must be assign is_superuser=True') 18 | 19 | if other_fields.get('is_author') is not True: 20 | return ValueError('Superuser must be assign is_author=True') 21 | 22 | return self.create_user(email,password,name,**other_fields) 23 | 24 | 25 | def create_author(self,email,password,name,**other_fields): 26 | other_fields.setdefault('is_staff',False) 27 | other_fields.setdefault('is_superuser',False) 28 | other_fields.setdefault('is_author',True) 29 | 30 | if other_fields.get('is_author') is not True: 31 | return ValueError('Author must be assign is_author=True') 32 | 33 | return self.create_user(email,password,name,**other_fields) 34 | 35 | 36 | def create_user(self,email,password,name,**other_fields): 37 | 38 | if not email: 39 | raise ValueError('You must provide a valid email') 40 | 41 | email=self.normalize_email(email) 42 | 43 | user=self.model(email=email,name=name,**other_fields) 44 | 45 | user.set_password(password) 46 | 47 | user.save() 48 | 49 | return user 50 | 51 | 52 | class User(AbstractBaseUser,PermissionsMixin): 53 | name=models.CharField(max_length=225,null=True,blank=True) 54 | email=models.EmailField(max_length=225,unique=True) 55 | paid_course=models.ManyToManyField(Course,blank=True) 56 | created=models.DateTimeField(auto_now_add=True) 57 | 58 | is_active=models.BooleanField(default=True) 59 | is_staff=models.BooleanField(default=False) 60 | is_author=models.BooleanField(default=False) 61 | # is_superuser=models.BooleanField(default=False) 62 | 63 | USERNAME_FIELD='email' 64 | REQUIRED_FIELDS=['name'] 65 | 66 | objects=UserManager() 67 | 68 | def __str__(self): 69 | return self.name 70 | 71 | def get_all_courses(self): 72 | courses=[] 73 | for course in self.paid_course.all(): 74 | courses.append(course.course_uuid) 75 | 76 | return courses 77 | 78 | -------------------------------------------------------------------------------- /users/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework.serializers import ModelSerializer 2 | from rest_framework import serializers 3 | from .models import User 4 | 5 | class UserSerializer(ModelSerializer): 6 | class Meta: 7 | model=User 8 | fields=[ 9 | "name" 10 | ] 11 | 12 | 13 | class UserAuthSerializer(ModelSerializer): 14 | courses=serializers.ListField(source='get_all_courses') 15 | class Meta: 16 | model=User 17 | fields=[ 18 | 'name', 19 | 'id', 20 | 'courses', 21 | 'email' 22 | ] -------------------------------------------------------------------------------- /users/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /users/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | --------------------------------------------------------------------------------