diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | fossee_anime/settings.py | 12 | ||||
-rw-r--r-- | fossee_anime/urls.py | 6 | ||||
-rw-r--r-- | fossee_manim/admin.py | 29 | ||||
-rw-r--r-- | fossee_manim/forms.py | 24 | ||||
-rw-r--r-- | fossee_manim/models.py | 48 | ||||
-rw-r--r-- | fossee_manim/send_mails.py | 31 | ||||
-rw-r--r-- | fossee_manim/static/css/sticky-footer.css | 26 | ||||
-rw-r--r-- | fossee_manim/templates/fossee_manim/base.html | 15 | ||||
-rw-r--r-- | fossee_manim/templates/fossee_manim/categorical_list.html | 38 | ||||
-rw-r--r-- | fossee_manim/templates/fossee_manim/edit_proposal.html | 16 | ||||
-rw-r--r-- | fossee_manim/templates/fossee_manim/how_to.html | 21 | ||||
-rw-r--r-- | fossee_manim/templates/fossee_manim/index.html | 50 | ||||
-rw-r--r-- | fossee_manim/templates/fossee_manim/search_results.html | 31 | ||||
-rw-r--r-- | fossee_manim/templates/fossee_manim/upload_success.html | 29 | ||||
-rw-r--r-- | fossee_manim/templates/fossee_manim/video.html | 89 | ||||
-rw-r--r-- | fossee_manim/tests/test_models.py | 61 | ||||
-rw-r--r-- | fossee_manim/tests/test_views.py | 94 | ||||
-rw-r--r-- | fossee_manim/urls.py | 18 | ||||
-rw-r--r-- | fossee_manim/views.py | 222 | ||||
-rw-r--r-- | requirements.txt | 6 |
21 files changed, 734 insertions, 133 deletions
@@ -46,3 +46,4 @@ migrations/ index_clutter.html base_clutter.html +data/ diff --git a/fossee_anime/settings.py b/fossee_anime/settings.py index 53b200b..9b3d6ed 100644 --- a/fossee_anime/settings.py +++ b/fossee_anime/settings.py @@ -12,7 +12,7 @@ https://docs.djangoproject.com/en/1.9/ref/settings/ import os import sys -from local_settings import ( +from .local_settings import ( EMAIL_HOST, EMAIL_PORT, EMAIL_HOST_USER, @@ -77,6 +77,8 @@ TEMPLATES = [ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', + 'django.template.context_processors.media', + ], }, }, @@ -140,11 +142,11 @@ LOGIN_URL = '/login/' MEDIA_URL = '/data/' -MEDIA_ROOT = os.path.join(BASE_DIR, "workshop_app", "data") +MEDIA_ROOT = os.path.join(BASE_DIR, "fossee_manim", 'data') -LOG_FOLDER = os.path.join(BASE_DIR, "workshop_app", "logs") +LOG_FOLDER = os.path.join(BASE_DIR, "fossee_manim", "logs") -#Email Connection Settings +# Email Connection Settings EMAIL_HOST = EMAIL_HOST EMAIL_HOST_USER = EMAIL_HOST_USER EMAIL_HOST_PASSWORD = EMAIL_HOST_PASSWORD @@ -155,7 +157,7 @@ SENDER_EMAIL = SENDER_EMAIL EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' -#Change this to the production url +# Change this to the production url PRODUCTION_URL = 'your_production_url' ADMIN_EMAIL = 'your admin email'
\ No newline at end of file diff --git a/fossee_anime/urls.py b/fossee_anime/urls.py index dff7bac..9fb65d5 100644 --- a/fossee_anime/urls.py +++ b/fossee_anime/urls.py @@ -17,9 +17,13 @@ Including another URLconf from django.conf.urls import url, include from django.contrib import admin from fossee_manim import views +from django.conf import settings +from django.conf.urls.static import static urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^', include('fossee_manim.urls')), url(r'^', include('fossee_manim.urls_password_reset')) -]
\ No newline at end of file +] +urlpatterns += static(settings.MEDIA_URL, + document_root=settings.MEDIA_ROOT)
\ No newline at end of file diff --git a/fossee_manim/admin.py b/fossee_manim/admin.py index 35bf57b..121c487 100644 --- a/fossee_manim/admin.py +++ b/fossee_manim/admin.py @@ -46,5 +46,32 @@ class CategoryAdmin(admin.ModelAdmin): list_filter = ['name'] +class AnimationAdmin(admin.ModelAdmin): + list_display = ['title', 'status', 'contributor'] + list_filter = ['category'] + actions = ['download_data'] + + def download_data(self, request, queryset): + openfile = string_io() + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = 'attachment;\ + filename=animations_data.csv' + + writer = csv.writer(response) + writer.writerow(['title', 'contributor', 'reviewer', 'description', + 'status', 'github link', 'category']) + + for q in queryset: + writer.writerow([q.title, q.contributor.get_full_name(), + q.reviewer.get_full_name(), q.description, q.status, q.github, + q.category]) + + openfile.seek(0) + response.write(openfile.read()) + return response + + download_data.short_description = "Download data CSV file." + admin.site.register(Category, CategoryAdmin) -admin.site.register(Profile, ProfileAdmin)
\ No newline at end of file +admin.site.register(Profile, ProfileAdmin) +admin.site.register(Animation, AnimationAdmin)
\ No newline at end of file diff --git a/fossee_manim/forms.py b/fossee_manim/forms.py index ec7568d..7543935 100644 --- a/fossee_manim/forms.py +++ b/fossee_manim/forms.py @@ -2,7 +2,7 @@ from django import forms from django.utils import timezone from .models import ( Profile, User, Animation, - Comment + Comment, AnimationStats ) from string import punctuation, digits try: @@ -253,18 +253,30 @@ class AnimationProposal(forms.ModelForm): class CommentForm(forms.ModelForm): """ - Instructors will post comments on Coordinators profile """ def __init__(self, *args, **kwargs): super(CommentForm, self).__init__(*args, **kwargs) self.fields['comment'].label = "" - self.fields['comment'].widget.attrs['rows'] = 5 - self.fields['comment'].widget.attrs['cols'] = 95 class Meta: model = Comment - exclude = ['animation', 'created_date', 'commentor'] + exclude = ['animation', 'created_date', 'commentor', + 'animation_status'] widgets = { 'comments': forms.CharField(), - }
\ No newline at end of file + } + + +class UploadAnimationForm(forms.ModelForm): + + def __init__(self, *args, **kwargs): + super(UploadAnimationForm, self).__init__(*args, **kwargs) + self.fields['video_path'].label = "Animation" + + class Meta: + model = AnimationStats + exclude = ['animation', 'views', 'like', 'dislike', 'thumbnail'] + widgets = { + 'video_path': forms.FileInput(), + } diff --git a/fossee_manim/models.py b/fossee_manim/models.py index 77c7552..a3b3537 100644 --- a/fossee_manim/models.py +++ b/fossee_manim/models.py @@ -1,9 +1,14 @@ from django.db import models from django.contrib.auth.models import User from django.core.validators import RegexValidator +from django.conf import settings +from django.utils import timezone +from django.core.files import File from taggit.managers import TaggableManager from simple_history.models import HistoricalRecords -from django.utils import timezone +from os import path, sep +import tempfile +import subprocess position_choices = ( @@ -96,6 +101,12 @@ def has_profile(user): return True if hasattr(user, 'profile') else False +def attachments(instance, filename): + return path.join(instance.animation.category.name, + instance.animation.title, + str(instance.animation.id), filename) + + class Profile(models.Model): """Profile for users(instructors and coordinators)""" @@ -156,7 +167,6 @@ class Animation(models.Model): github = models.TextField() category = models.ForeignKey(Category, on_delete=models.CASCADE) created = models.DateTimeField(default=timezone.now) - updated = models.DateTimeField(default=timezone.now) tags = TaggableManager() history = HistoricalRecords() @@ -169,6 +179,7 @@ class Comment(models.Model): commentor = models.ForeignKey(User, on_delete=models.CASCADE) animation = models.ForeignKey(Animation, on_delete=models.CASCADE) created_date = models.DateTimeField(default=timezone.now) + animation_status = models.CharField(max_length=255) def __str__(self): return u"{1} | {0}".format( @@ -181,4 +192,35 @@ class AnimationStats(models.Model): animation = models.ForeignKey(Animation, on_delete=models.CASCADE) views = models.PositiveIntegerField(default=0) like = models.PositiveIntegerField(default=0) - dislike = models.PositiveIntegerField(default=0)
\ No newline at end of file + dislike = models.PositiveIntegerField(default=0) + thumbnail = models.ImageField(null=True, blank=True, upload_to=attachments) + video_path = models.FileField(null=True, blank=True, upload_to=attachments) + + def _create_thumbnail(self): + video_path = self.video_path.path + img_output = path.join( + tempfile.mkdtemp(), "{0}.jpg".format(self.animation.title) + ) + file_name = "{0}.jpg".format(self.animation.title) + subprocess.call(['ffmpeg', '-i', video_path, '-ss', '00:00:09.000', + '-vframes', '1', img_output]) + if path.exists(img_output): + que_file = open(img_output, 'rb') + # Converting to Python file object with + # some Django-specific additions + django_file = File(que_file) + self.thumbnail.save(file_name, django_file, save=True) + + def _create_ogv(self): + video_input = self.video_path.path + vid_output = path.join( + tempfile.mkdtemp(), "{0}.ogv".format(self.animation.title) + ) + file_name = "{0}.ogv".format(self.animation.title) + subprocess.call(['ffmpeg', '-i', video_input, '-r', '24', vid_output]) + if path.exists(vid_output): + que_file = open(vid_output, 'rb') + # Converting to Python file object with + # some Django-specific additions + django_file = File(que_file) + self.video_path.save(file_name, django_file, save=True)
\ No newline at end of file diff --git a/fossee_manim/send_mails.py b/fossee_manim/send_mails.py index f9f7a75..a329e8b 100644 --- a/fossee_manim/send_mails.py +++ b/fossee_manim/send_mails.py @@ -1,20 +1,19 @@ -from django.core.mail import EmailMultiAlternatives +from django.core.mail import EmailMultiAlternatives, send_mail from django.conf import settings +from django.utils.crypto import get_random_string from os import listdir, path from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.mime.base import MIMEBase from email import encoders from time import sleep -import hashlib -import logging.config -import re -from django.core.mail import send_mail from textwrap import dedent from random import randint from smtplib import SMTP -from django.utils.crypto import get_random_string from string import punctuation, digits +from hashlib import sha256 +import logging.config +import re try: from string import letters except ImportError: @@ -29,13 +28,15 @@ from fossee_anime.settings import ( SENDER_EMAIL, ADMIN_EMAIL ) + + __author__ = "Akshen Doke" def validateEmail(email): if len(email) > 7: if re.match("^.+\\@(\\[?)[a-zA-Z0-9\\-\\.]+\\.([a-zA-Z]{2,3}|[0-9]{1,3})(\\]?)$", - email) != None: + email) is not None: return 1 return 0 @@ -43,8 +44,8 @@ def validateEmail(email): def generate_activation_key(username): """Generates hashed secret key for email activation""" chars = letters + digits + punctuation - secret_key = get_random_string(randint(10,40), chars) - return hashlib.sha256((secret_key + username).encode('utf-8')).hexdigest() + secret_key = get_random_string(randint(10, 40), chars) + return sha256((secret_key + username).encode('utf-8')).hexdigest() def send_email(request, call_on, contributor=None, key=None, proposal=None): @@ -53,7 +54,7 @@ def send_email(request, call_on, contributor=None, key=None, proposal=None): try: with open(path.join(settings.LOG_FOLDER, - 'emailconfig.yaml'), 'r') as configfile: + 'emailconfig.yaml'), 'r') as configfile: config_dict = yaml.load(configfile) logging.config.dictConfig(config_dict) except: @@ -87,7 +88,8 @@ def send_email(request, call_on, contributor=None, key=None, proposal=None): Congratulations! your animations has been released on FOSSEE's website. - Please start with your honouriam process + Your animation will be live in 72 working hours. + Please start with your honorarium process In case of queries, please revert to this email.""".format(contributor.profile.user.username)) @@ -112,7 +114,7 @@ def send_email(request, call_on, contributor=None, key=None, proposal=None): logging.info("Animation Rejected: %s", request.user.email) send_mail( "FOSSEE Animation Status Update", message, SENDER_EMAIL, - [contributor.profile.user.email], fail_silently=True + [contributor.profile.user.email], fail_silently=True ) elif call_on == 'changes': message = dedent("""\ @@ -123,10 +125,9 @@ def send_email(request, call_on, contributor=None, key=None, proposal=None): In case of queries, please revert to this email.""".format(contributor.profile.user.username, - proposal.title)) + proposal.title)) logging.info("Changes Required: %s", request.user.email) send_mail( "FOSSEE Animation Changes required", message, SENDER_EMAIL, - [contributor.profile.user.email], fail_silently=True - )
\ No newline at end of file + [contributor.profile.user.email], fail_silently=True) diff --git a/fossee_manim/static/css/sticky-footer.css b/fossee_manim/static/css/sticky-footer.css index ea445db..4bcbcc5 100644 --- a/fossee_manim/static/css/sticky-footer.css +++ b/fossee_manim/static/css/sticky-footer.css @@ -72,15 +72,19 @@ } -/** - * Footer Styles - */ - -.footer { - position: fixed; - right: 0; - bottom: 0; - left: 0; - background-color: #efefef; - text-align: center; +/* video-thumbnail over image */ +#play-btn +{ + position: absolute; + top: 40%; + left: 50%; + transform: translate(-50%, -50%); + -ms-transform: translate(-50%, -50%); + background-color: #555; + color: white; + font-size: 16px; + padding: 10px 20px; + border: none; + cursor: pointer; + border-radius: 5px; }
\ No newline at end of file diff --git a/fossee_manim/templates/fossee_manim/base.html b/fossee_manim/templates/fossee_manim/base.html index c9d6242..1a4945d 100644 --- a/fossee_manim/templates/fossee_manim/base.html +++ b/fossee_manim/templates/fossee_manim/base.html @@ -43,10 +43,11 @@ <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> Categories </a> + <div class="dropdown-menu" aria-labelledby="navbarDropdown"> - <a class="dropdown-item" href="#">Math</a> - <a class="dropdown-item" href="#">Aerospace</a> - <a class="dropdown-item" href="#">Biology</a> + {% for c in categories %} + <a class="dropdown-item" href="{% url 'search_category' c.name %}">{{c.name}}</a> + {% endfor %} </div> </li> </ul> @@ -59,6 +60,7 @@ </a> {% if request.user.profile.position == 'contributor' %} <div class="dropdown-menu" aria-labelledby="navbarDropdown"> + <a class="dropdown-item" href="{% url 'how_to' %}">How To</a> <a class="dropdown-item" href="{% url 'send_proposal' %}">Send Proposal</a> <a class="dropdown-item" href="{% url 'proposal_status' %}">Proposal Status</a> <a class="dropdown-item" href="{% url 'view_profile' %}">View Profile</a> @@ -93,10 +95,5 @@ {% endblock %} </body> -<br><br> - <footer class="footer"> - <div class="container"> - <p align="center">Developed by FOSSEE group, IIT Bombay</p> - </div> - </footer> +<br> </html>
\ No newline at end of file diff --git a/fossee_manim/templates/fossee_manim/categorical_list.html b/fossee_manim/templates/fossee_manim/categorical_list.html new file mode 100644 index 0000000..0084b83 --- /dev/null +++ b/fossee_manim/templates/fossee_manim/categorical_list.html @@ -0,0 +1,38 @@ +{% extends 'fossee_manim/base.html' %} + + {% block title %} + FOSSEE Animation + {% endblock %} + +{% block content %} + <br> + <div class="container-fluid" > + <br> + <div class="row" align="center"> + <div class="col-md-12"> + <h1 style="float: left;">{{ categorial_list.0.animation.category }}</h1> + </div> + </div> + <hr> + <br> + + {% for video in categorial_list %} + <div class="row"> + <div class="col-md-4"> + <a href="{% url 'video' video.id %}" > + <span class="fa fa-play fa-4x" id="play-btn"></span> + <img height="90%" width="100%" src="{{ video.thumbnail.url }}"> + </a> + </div> + <div class="col-md-4"> + <p style="color:#26A669; font-size: 300%;"> {{ video.animation.title }} </p> + <p style="color:#26A669; font-size: 100%;"> {{ video.animation.description | truncatewords:5}} </p> + <a target="_blank" href="{{ video.animation.github }}"><p style="font-size: 70%;"> {{ video.animation.github }} </p></a> + </div> + </div> + <hr> + {% endfor %} + <br> + </div> + <br> +{% endblock %}
\ No newline at end of file diff --git a/fossee_manim/templates/fossee_manim/edit_proposal.html b/fossee_manim/templates/fossee_manim/edit_proposal.html index c73071b..e42a358 100644 --- a/fossee_manim/templates/fossee_manim/edit_proposal.html +++ b/fossee_manim/templates/fossee_manim/edit_proposal.html @@ -19,7 +19,23 @@ <br> <button class="btn btn-primary pull-right" type="submit">Save</button> + <br> + </form> + <br> + + {% if proposal_form.instance.status == 'changes' and request.user.profile.position == 'contributor' %} + <form method="POST" action="{% url 'upload_animation' proposal_form.instance.id %}" enctype="multipart/form-data"> + {% csrf_token %} + <label class="btn btn-info"> + {{ upload_form }} + <button class="btn btn-success" type="submit">Upload</button> + </label> </form> + {% else %} + <video width="100%" height="100%" controls> + <source src="{{video.0.video_path.url}}" type="video/mp4"> + </video> + {% endif %} <br><br> {% if request.user.profile.position == 'reviewer' %} diff --git a/fossee_manim/templates/fossee_manim/how_to.html b/fossee_manim/templates/fossee_manim/how_to.html new file mode 100644 index 0000000..830bdff --- /dev/null +++ b/fossee_manim/templates/fossee_manim/how_to.html @@ -0,0 +1,21 @@ +{% extends 'fossee_manim/base.html' %} + + {% block title %} + FOSSEE Animation + {% endblock %} + +{% block content %} + <div class="container-fluid"> + <br> + <h1>How to contribute</h1> + <ul> + <li><a class="nav-link" href="{% url 'register' %}">Register</a></li> + <li><a class="nav-link" href="https://purusharthsaxena.wordpress.com/getting-started-with-manim/" target="_blank">Visit here to know about manim(python based animation library)</a></li> + <li>Send proposal for a topic</li> + <li>Wait for the reviewer to respond</li> + <li>Upload animation video</li> + <li>Once approved, video will be released</li> + <li>Get Honorarium</li> + </ul> + </div> +{% endblock %}
\ No newline at end of file diff --git a/fossee_manim/templates/fossee_manim/index.html b/fossee_manim/templates/fossee_manim/index.html index 3d39b41..0753918 100644 --- a/fossee_manim/templates/fossee_manim/index.html +++ b/fossee_manim/templates/fossee_manim/index.html @@ -4,12 +4,11 @@ <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="Akshen Doke" content="PRE, FOSSEE-IITB"> - <meta name="title" content="Welcome to FOSSEE's Python Workshops"> + <meta name="title" content="Welcome to FOSSEE's Animation for Education"> <meta name="description" content="The FOSSEE team at IIT Bombay conducts remote-assisted training programs in Python. These are interactive workshop sessions with hands-on experience, live assistance, practice sessions and evaluation quizzes. - These programs can be conducted at your institution / organisation, free of charge. - Please note that you will require a coordinator and a minimum number of 25 participants to request for these workshops."> - <meta name="keywords" content="learn python for free, fossee, iit bombay, python workshops, fossee python, python workshops"> + These programs can be conducted at your institution / organisation, free of charge."> + <meta name="keywords" content="fossee, iit bombay, animation based learning math, manim, python based animations"> <!-- favicon --> <link rel="shortcut icon" type="image/png" href="{{ URL_ROOT}}/static/img/fevicon_python.png"/> @@ -37,22 +36,23 @@ <div class="container-fluid" style="height:100%; background-color: #3D5A50;"> <div class="row" id="r1" style="background-color:#ffffff"> <div class="col-md-2" id="brand"><strong>FOSSEE</strong> <br>Animation</div> - <div class="col-md-10" style="text-align:center;" id="tag"><strong>Python based animation for math</strong></div> + <div class="col-md-10" style="text-align:center;" id="tag"><strong>Animation based learning</strong></div> </div> <div class="row" id="r2" style="background-color:#3D5A50"> <div class="col-md-8"> - <form class="example" action="" id="search_bar"> - <input type="text" placeholder="Search." name="search" id="search_tab"> + <form class="example" id="search_bar" method="POST" action="/search/"> + {% csrf_token %} + <input type="text" id="sbox" name="sbox" type="search" placeholder="Search" id="search_tab"> <button type="submit"><i class="fa fa-search"></i></button> </form> </div> <div class="col-md-2" id="btns" style="text-align:center;"> - <a href="{{URL_ROOT}}/login"><strong>Login</strong></a> + <a href="{% url 'login' %}"><strong>Login</strong></a> </div> <div class="col-md-2" id="btns" style="text-align:center;"> - <a href="{{URL_ROOT}}/register"><strong>Register</strong></a> + <a href="{% url 'register' %}"><strong>Register</strong></a> </div> </div> @@ -71,28 +71,28 @@ <div class="row" id="r4" style="background-color:ghostwhite"> <hr style="height:3px;"> <div class="col-md-3" style="text-align:center;"> - <iframe width="240" height="125" - src="https://www.youtube.com/embed/JGLfa66Os0Q" frameborder="0" allow="accelerometer; - encrypted-media; gyroscope; picture-in-picture" allowfullscreen> - </iframe> + <a href="{% url 'video' anime.0.id %}" > + <span class="fa fa-play fa-4x" id="play-btn"></span> + <img height="25%" width="65%" src="{{ anime.0.thumbnail.url }}"> + </a> </div> <div class="col-md-3" style="text-align:center;"> - <iframe width="240" height="125" - src="https://www.youtube.com/embed/JGLfa66Os0Q" frameborder="0" allow="accelerometer; - encrypted-media; gyroscope; picture-in-picture" allowfullscreen> - </iframe> + <a href="{% url 'video' anime.1.id %}" > + <span class="fa fa-play fa-4x" id="play-btn"></span> + <img height="25%" width="65%" src="{{ anime.1.thumbnail.url }}"> + </a> </div> <div class="col-md-3" style="text-align:center;"> - <iframe width="240" height="125" - src="https://www.youtube.com/embed/JGLfa66Os0Q" frameborder="0" allow="accelerometer; - encrypted-media; gyroscope; picture-in-picture" allowfullscreen> - </iframe> + <a href="{% url 'video' anime.2.id %}" > + <span class="fa fa-play fa-4x" id="play-btn"></span> + <img height="25%" width="65%" src="{{ anime.2.thumbnail.url }}"> + </a> </div> <div class="col-md-3" style="text-align:center;"> - <iframe width="240" height="125" - src="https://www.youtube.com/embed/JGLfa66Os0Q" frameborder="0" allow="accelerometer; - encrypted-media; gyroscope; picture-in-picture" allowfullscreen> - </iframe> + <a href="{% url 'video' anime.3.id %}" > + <span class="fa fa-play fa-4x" id="play-btn"></span> + <img height="25%" width="65%" src="{{ anime.3.thumbnail.url }}"> + </a> </div> </div> diff --git a/fossee_manim/templates/fossee_manim/search_results.html b/fossee_manim/templates/fossee_manim/search_results.html index 2e70f78..744f953 100644 --- a/fossee_manim/templates/fossee_manim/search_results.html +++ b/fossee_manim/templates/fossee_manim/search_results.html @@ -1,13 +1,34 @@ {% extends 'fossee_manim/base.html' %} {% block title %} - Login + Search Result {% endblock %} {% block content %} - <br> - <div class="container" align="center"> - {{ args }} - </div> +<br> +<div class="container-fluid" > + <br> + + <hr> + <br> + {% for anime in s_result %} + <div class="row"> + <div class="col-md-4" > + <a href="{% url 'video' anime.id %}" > + <span class="fa fa-play fa-4x" id="play-btn"></span> + <img height="90%" width="100%" src="{{ anime.thumbnail.url }}"> + </a> + </div> + <div class="col-md-4"> + <p style="color:#26A669; font-size: 300%;"> {{ anime.animation.title }} </p> + <p style="color:#26A669; font-size: 100%;"> {{ anime.animation.description | truncatewords:5}} </p> + <a target="_blank" href="{{ anime.animation.github }}"><p style="font-size: 70%;"> {{ anime.animation.github }} </p></a> + </div> + </div> + <hr> + {% endfor %} + <br> +</div> +<br> {% endblock %}
\ No newline at end of file diff --git a/fossee_manim/templates/fossee_manim/upload_success.html b/fossee_manim/templates/fossee_manim/upload_success.html new file mode 100644 index 0000000..61a992a --- /dev/null +++ b/fossee_manim/templates/fossee_manim/upload_success.html @@ -0,0 +1,29 @@ + +{% extends 'fossee_manim/base.html' %} + +{% block title %} + Upload success +{% endblock %} + + + {% block extra %} + <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script> + <script src="{{URL_ROOT}}/static/fossee_manim/js/bootstrap-3.3.7.min.js"></script> + <script type="text/javascript"> + window.setTimeout(function() + { + location.href="{% url 'proposal_status' %}" + }, 9000); +</script> + + {% endblock %} +{% block content %} + <div class="container"> + <br><br> + <div class="jumbotron"> + <h3><strong>Congrats!</strong> video uploaded successfully!</h3> + <br> + <h5><strong>If you've previously uploaded any video, it would be overridden by this upload</h5> + </div> + </div> +{% endblock %}
\ No newline at end of file diff --git a/fossee_manim/templates/fossee_manim/video.html b/fossee_manim/templates/fossee_manim/video.html new file mode 100644 index 0000000..b94a34e --- /dev/null +++ b/fossee_manim/templates/fossee_manim/video.html @@ -0,0 +1,89 @@ +{% extends 'fossee_manim/base.html' %} + + {% block title %} + FOSSEE Animation + {% endblock %} + +{% block content %} + <div class="container" > + <div class="row" > + <div class="col-md-12" > + <br> + <video width="100%" height="90%" controls> + <source src="{{video.0.video_path.url}}" type="video/mp4"> + </video> + <div class="row"> + <div class="col-md-8"> + <p style="color:#26A669; font-size: 200%; float: left;"> {{ video.0.animation.title }} </p> + </div> + <div class="col-md-4"> + <a target="_blank" href="{{ video.animation.github }}"><p style="font-size: 70%; float: right;"> {{ video.0.animation.github }} </p></a> + </div> + </div> + <br> + </div> + + </div> + <div class="row"> + <div class="col-md-10"> + <hr> + <br> + <p style="color:#26A669; font-size: 100%; float: left;"> + {{ video.0.animation.description }} + </p> + <br> + </div> + </div> + <hr> + <div class="row"> + <div class="col-md-4"> + <a href="{% url 'video' reco.0.id %}" > + <span class="fa fa-play fa-4x" id="play-btn"></span> + <img height="95%" width="95%" src="{{ reco.0.thumbnail.url }}"> + </a> + </div> + <div class="col-md-4"> + <a href="{% url 'video' reco.1.id %}" > + <span class="fa fa-play fa-4x" id="play-btn"></span> + <img height="95%" width="95%" src="{{ reco.1.thumbnail.url }}"> + </a> + </div> + <div class="col-md-4"> + <a href="{% url 'video' reco.2.id %}" > + <span class="fa fa-play fa-4x" id="play-btn"></span> + <img height="95%" width="95%" src="{{ reco.2.thumbnail.url }}"> + </a> + </div> + </div> + <div> + <br> <br> + <h2>Comments</h2> + <div> + <form method="POST"> + <br> + {% csrf_token %} + {{ comment_form.as_p }} + <button type="submit" class="btn btn-default">Post</button> + </form> + + </div> + <hr style="background-color: #fff; + border-top: 5px double #8c8b8b;"> + <table> + {% for comment in comments %} + <tbody> + <tr> + <td> + <h5>{{ comment.commentor.profile.user.get_full_name }} | {{ comment.created_date | date }}</h5> + <h6 style="background-color: #ecf0f1; padding:10px;">{{ comment.comment }}</h6> + <hr style="border-top: 0.5px solid #8c8b8b;"> + </td> + </tr> + </tbody> + {% endfor %} + </table> + + <br> + </div> + </div> +{% endblock %}
\ No newline at end of file diff --git a/fossee_manim/tests/test_models.py b/fossee_manim/tests/test_models.py index d7ffade..8b2162c 100644 --- a/fossee_manim/tests/test_models.py +++ b/fossee_manim/tests/test_models.py @@ -1,6 +1,7 @@ from django.test import TestCase from fossee_manim.models import ( - Profile, User + Profile, User, Category, Animation, + Comment, AnimationStats ) from datetime import datetime @@ -22,6 +23,8 @@ def setUpModule(): testUser2 = User.objects.create(username='testuser2', email='test.user@gmail.com', password='pass@123') + category1 = Category.objects.create(name='Math', description='Mathematics') + reviewer_profile = Profile.objects.create(user=testUser2, position='reviewer', department='computer engineering', institute='ace', phone_number='1122334456', title='Doctor', how_did_you_hear_about_us='Google', location='powai', state='IN-MH', @@ -35,8 +38,10 @@ def setUpModule(): def tearDownModule(): - User.objects.all().delete() - Profile.objects.all().delete() + User.objects.all().delete() + Profile.objects.all().delete() + Category.objects.all().delete() + class ProfileModelTest(TestCase): ''' @@ -67,4 +72,52 @@ class ProfileModelTest(TestCase): self.assertEqual(self.contributor_profile1.position,'contributor') self.assertEqual(self.contributor_profile1.location,'powai') self.assertEqual(self.reviewer_profile1.location,'powai') - self.assertEqual(self.contributor_profile1.how_did_you_hear_about_us,'Google')
\ No newline at end of file + self.assertEqual(self.contributor_profile1.how_did_you_hear_about_us,'Google') + + +class CategoryModelTest(TestCase): + def setUp(self): + self.category1 = Category.objects.create(name='Biology', description='study of nature') + self.category2 = Category.objects.create(name='Aerospace', + description='Aerospace is the human effort in science, engineering, and business to fly in the atmosphere of Earth \ + (aeronautics) and surrounding space (astronautics). \ + Aerospace organizations research, design, manufacture, operate, or maintain aircraft or spacecraft. Aerospace activity \ + is very diverse, with a multitude of commercial, industrial and military applications.' + ) + + def test_category_model(self): + self.assertEqual(self.category1.description, 'study of nature') + self.assertEqual(self.category2.name, 'Aerospace') + + +class AnimationModelTest(TestCase): + def setUp(self): + self.demoUser2 = User.objects.create(username='demouser21', + email='test.user@gmail.com', password='pass@123') + self.testUser2 = User.objects.create(username='testuser21', + email='test.user@gmail.com', password='pass@123') + self.category1 = Category.objects.create(name='Biology', description='study of nature') + self.animation1 = Animation.objects.create(title='Testing Anime', contributor=self.demoUser2, + reviewer=self.testUser2, description='This is test animation upload', github='https://github.come/FOSSEE', + category=self.category1) + + def test_animation_model(self): + self.assertEqual(self.animation1.title, 'Testing Anime') + self.assertEqual(self.category1.name, 'Biology') + + +class CommentModelTest(TestCase): + def setUp(self): + self.demoUser2 = User.objects.create(username='demouser21', + email='test.user@gmail.com', password='pass@123') + self.testUser2 = User.objects.create(username='testuser21', + email='test.user@gmail.com', password='pass@123') + self.category1 = Category.objects.create(name='Biology', description='study of nature') + self.animation1 = Animation.objects.create(title='Testing Anime', contributor=self.demoUser2, + reviewer=self.testUser2, description='This is test animation upload', github='https://github.come/FOSSEE', + category=self.category1) + self.comment1 = Comment.objects.create(comment='This is a comment', commentor=self.testUser2, + animation=self.animation1, animation_status='changes') + + def test_comment_model(self): + self.assertEqual(self.comment1.comment, 'This is a comment') diff --git a/fossee_manim/tests/test_views.py b/fossee_manim/tests/test_views.py index e69de29..3209f42 100644 --- a/fossee_manim/tests/test_views.py +++ b/fossee_manim/tests/test_views.py @@ -0,0 +1,94 @@ +from django.test import TestCase +from fossee_manim.models import ( + Profile, User, Category, Animation, + Comment, AnimationStats, has_profile + ) +from datetime import datetime +from django.test import Client +from django.contrib.auth.models import Group, Permission +from django.contrib.auth import authenticate +from django.core.urlresolvers import reverse +from fossee_manim.views import view_profile, user_login, edit_profile + +class TestProfile(TestCase): + def setUp(self): + self.client = Client() + + self.user1 = User.objects.create( + username='demo_test_user1', + password='pass@123', + email='test.user@gmail.com') + + self.user2 = User.objects.create( + username='demo_test_user2', + email='test.user@gmail.com') + + self.user2.set_password('pass@123') + self.user2.save() + + self.user2_profile = Profile.objects.create( + user=self.user2, + department='Computer Engineering', + institute='ace', + title='Doctor', + position='instructor', + phone_number='1122993388', + location='mumbai', + how_did_you_hear_about_us='Google', + state='IN-MH', + is_email_verified=1 + ) + + def test_has_profile_for_user_without_profile(self): + """ + If no profile exists for user passed as argument return False + """ + has_profile_status = has_profile(self.user1) + self.assertFalse(has_profile_status) + + def test_has_profile_for_user_with_profile(self): + """ + If profile exists for user passed as argument return True + """ + has_profile_status = has_profile(self.user2) + self.assertTrue(has_profile_status) + + def test_view_profile_denies_anonymous(self): + """ + If not logged in redirect to login page + """ + response = self.client.get(reverse(view_profile), follow=True) + redirect_destination = '/login/?next=/view_profile/' + self.assertTrue(response.status_code,200) + self.assertRedirects(response, redirect_destination) + + def test_edit_profile_get(self): + """ + GET request to edit profile should display profile form + """ + + self.client.login(username=self.user2, password='pass@123') + response = self.client.get(reverse(edit_profile)) + user_profile = User.objects.get(id=self.user2.id) + profile = Profile.objects.get(user=user_profile) + self.assertEqual(response.status_code, 200) + self.assertEqual(profile.institute, 'ace') + self.client.logout() + + def test_edit_profile_post(self): + + self.client.login(username=self.user2, password='pass@123') + response = self.client.post('/edit_profile/', + { + 'first_name': 'demo_test', + 'last_name': 'user2', + 'institute': 'IIT', + 'department': 'aerospace engineering' + }) + + updated_profile_user = User.objects.get(id=self.user2.id) + updated_profile = Profile.objects.get(user=updated_profile_user) + self.assertEqual(updated_profile.institute, 'IIT') + self.assertEqual(updated_profile.department, 'aerospace engineering') + self.assertEqual(updated_profile.position, 'instructor') + self.assertEqual(response.status_code, 200) diff --git a/fossee_manim/urls.py b/fossee_manim/urls.py index 7bc7740..6181b67 100644 --- a/fossee_manim/urls.py +++ b/fossee_manim/urls.py @@ -1,5 +1,8 @@ from django.conf.urls import url from fossee_manim import views +from django.conf import settings +from django.conf.urls.static import static + urlpatterns = [ url(r'^$', views.index, name='index'), @@ -11,8 +14,19 @@ urlpatterns = [ url(r'^send_proposal/$', views.send_proposal, name='send_proposal'), url(r'^edit_proposal/([1-9][0-9]*)$', views.edit_proposal, name='edit_proposal'), + url(r'^upload_animation/([1-9][0-9]*)$', views.upload_animation, + name='upload_animation'), url(r'^proposal_status/$', views.proposal_status, name='proposal_status'), url(r'^search/$', views.search, name='search'), + url(r'^how_to/$', views.how_to, name='how_to'), url(r'^view_profile/$', views.view_profile, name='view_profile'), - url(r'^edit_profile/$', views.edit_profile, name='edit_profile') -]
\ No newline at end of file + url(r'^edit_profile/$', views.edit_profile, name='edit_profile'), + url(r'^video/([1-9][0-9]*)$', views.video, name='video'), + url(r'^search_category/(?P<cat>.+)$', views.search_category, + name='search_category') +] + +urlpatterns += static( + settings.MEDIA_URL, + document_root=settings.MEDIA_ROOT +) diff --git a/fossee_manim/views.py b/fossee_manim/views.py index d9b27b2..44f20c1 100644 --- a/fossee_manim/views.py +++ b/fossee_manim/views.py @@ -1,12 +1,13 @@ -from django.shortcuts import render +from os import listdir, path, sep, makedirs, remove from .forms import ( UserRegistrationForm, UserLoginForm, - ProfileForm, AnimationProposal, - CommentForm + ProfileForm, AnimationProposal, + CommentForm, UploadAnimationForm ) from .models import ( Profile, User, AnimationStats, - has_profile, Animation, Comment + has_profile, Animation, Comment, + Category ) from datetime import datetime, date from django.contrib.auth import login, logout, authenticate @@ -15,24 +16,42 @@ from django.contrib import messages from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.shortcuts import render, redirect from django.utils import timezone -from .send_mails import send_email from django.http import HttpResponse, HttpResponseRedirect -from textwrap import dedent from django.conf import settings -from os import listdir, path, sep +from django.core.files.uploadhandler import FileUploadHandler +from django.db.models import F, Subquery, OuterRef, Q from zipfile import ZipFile -from django.contrib import messages -from django.db.models import Q +from textwrap import dedent +from requests import get +from random import sample +from .send_mails import send_email import datetime as dt -import os +import shutil try: from StringIO import StringIO as string_io except ImportError: from io import BytesIO as string_io +def makepath(proposal_data, reject=None): + if not path.exists(path.join(settings.MEDIA_ROOT, + proposal_data.category.name)): + makedirs(path.join(settings.MEDIA_ROOT, + proposal_data.category.name)) + + if reject: + shutil.rmtree(path.join( + settings.MEDIA_ROOT, proposal_data.category.name, + proposal_data.title.replace(" ", "_") + + str(proposal_data.id))) + else: + makedirs(path.join(settings.MEDIA_ROOT, proposal_data.category.name, + proposal_data.title.replace(" ", "_") + + str(proposal_data.id))) + + def check_repo(link): - return True if 'github.com' in link else False + return (get(link).status_code == 200) def is_email_checked(user): @@ -43,7 +62,7 @@ def is_email_checked(user): def is_superuser(user): - return True if user.is_superuser else False + return user.is_superuser def index(request): @@ -52,8 +71,8 @@ def index(request): user = request.user form = UserLoginForm() if user.is_authenticated() and is_email_checked(user): - if user.groups.filter(name='reviewer').count() > 0: - return redirect('/view_profile/') + if user.groups.filter(name='reviewer').exists: + return redirect('/proposal_status/') return redirect('/view_profile/') elif request.method == "POST": form = UserLoginForm(request.POST) @@ -62,21 +81,24 @@ def index(request): login(request, user) if is_superuser(user): return redirect("/admin") - if user.groups.filter(name='reviewer').count() > 0: - return redirect('/view_profile/') + if user.groups.filter(name='reviewer').exists(): + return redirect('/proposal_status/') return redirect('/view_profile/') - - return render(request, "fossee_manim/index.html", {"form": form}) + anime = AnimationStats.objects.filter(animation__status='released').order_by('-id')[:5] + return render(request, "fossee_manim/index.html", {"form": form, + "anime" : anime + }) def is_reviewer(user): '''Check if the user is having reviewer rights''' - return True if user.groups.filter(name='reviewer').count() > 0 else False + return user.groups.filter(name='reviewer').exists() def user_login(request): '''User Login''' user = request.user + categories = Category.objects.all() if is_superuser(user): return redirect('/admin') if user.is_authenticated(): @@ -91,12 +113,13 @@ def user_login(request): login(request, user) if user.groups.filter(name='reviewer').count() > 0: return redirect('/view_profile/') - return redirect('/view_profile/') + return redirect('/how_to/') else: return render(request, 'fossee_manim/login.html', {"form": form}) else: form = UserLoginForm() - return render(request, 'fossee_manim/login.html', {"form": form}) + return render(request, 'fossee_manim/login.html', {"form": form, + 'categories': categories }) def user_logout(request): @@ -178,10 +201,12 @@ def user_register(request): def view_profile(request): """ view instructor and coordinator profile """ user = request.user + categories = Category.objects.all() if is_superuser(user): return redirect('/admin') if is_email_checked(user) and user.is_authenticated(): - return render(request, "fossee_manim/view_profile.html") + return render(request, "fossee_manim/view_profile.html", + {'categories': categories}) else: if user.is_authenticated(): return render(request, 'fossee_manim/activation.html') @@ -198,6 +223,7 @@ def edit_profile(request): """ edit profile details facility for reviewer and contributor """ user = request.user + categories = Category.objects.all() if is_superuser(user): return redirect('/admin') if is_email_checked(user): @@ -229,38 +255,42 @@ def edit_profile(request): form_data.save() return render( - request, 'fossee_manim/profile_updated.html' + request, 'fossee_manim/profile_updated.html', + {'categories': categories} ) else: context['form'] = form return render(request, 'fossee_manim/edit_profile.html', context) else: form = ProfileForm(user=user, instance=profile) - return render(request, 'fossee_manim/edit_profile.html', {'form': form} + return render(request, 'fossee_manim/edit_profile.html', {'form': form, + 'categories': categories} ) @login_required def send_proposal(request): user = request.user + categories = Category.objects.all() if request.method == 'POST': form = AnimationProposal(request.POST) if form.is_valid(): form_data = form.save(commit=False) form_data.contributor = user - form_data.status = "pending" + form_data.status = "pending" if check_repo(form_data.github): form_data.save() form.save_m2m() + # makepath(form_data) else: messages.warning(request, 'Please enter valid github details') return render(request, 'fossee_manim/send_proposal.html', - {'form': form}) + {'form': form, 'categories': categories}) return redirect('/proposal_status/') else: form = AnimationProposal() return render(request, 'fossee_manim/send_proposal.html', - {'form': form}) + {'form': form, 'categories': categories}) @login_required @@ -269,13 +299,14 @@ def proposal_status(request): profile = Profile.objects.get(user_id=user) anime = {} anime_list = {} + categories = Category.objects.all() if profile.position == 'contributor': anime = Animation.objects.filter(contributor_id=user) else: - anime_list = Animation.objects.filter(Q(status='pending') | - Q(status='changes')) + anime_list = Animation.objects.order_by('-created') return render(request, 'fossee_manim/proposal_status.html', - {'anime': anime, 'anime_list': anime_list}) + {'anime': anime, 'anime_list': anime_list, + 'categories': categories}) @login_required @@ -284,28 +315,32 @@ def edit_proposal(request, proposal_id=None): comment_form = CommentForm() proposal = Animation.objects.get(id=proposal_id) proposal_form = AnimationProposal(instance=proposal) + upload_form = UploadAnimationForm() + categories = Category.objects.all() + video = AnimationStats.objects.filter(animation=proposal_id) try: - comments = Comment.objects.all().order_by('-created_date') + comments = Comment.objects.filter(animation_id=proposal_id).order_by( + '-created_date' + ) except: comments = None if request.method == 'POST': text = request.POST.get('comment') - s1 = request.POST.get('release') - s2 = request.POST.get('rejected') + status1 = request.POST.get('release') + status2 = request.POST.get('rejected') - if s1 or s2 is not None: - if s1: + if status1 or status2 is not None: + if status1: proposal.status = 'released' - proposal.reviewer = user - proposal.save() send_email(request, call_on='released', - contributor=proposal.contributor) + contributor=proposal.contributor) else: proposal.status = 'rejected' - proposal.reviewer = user - proposal.save() + makepath(proposal, reject=1) send_email(request, call_on='rejected', - contributor=proposal.contributor) + contributor=proposal.contributor) + proposal.reviewer = user + proposal.save() return redirect('/proposal_status/') if text is not None: @@ -313,11 +348,13 @@ def edit_proposal(request, proposal_id=None): form_data = comment_form.save(commit=False) form_data.commentor = user form_data.animation = proposal + form_data.animation_status = proposal.status if user.profile.position == 'reviewer': proposal.status = 'changes' + proposal.save() send_email(request, call_on='changes', - contributor=proposal.contributor, - proposal=proposal) + contributor=proposal.contributor, + proposal=proposal) form_data.save() return redirect('/edit_proposal/{}'.format(proposal_id)) proposal_form = AnimationProposal(request.POST, instance=proposal) @@ -345,13 +382,106 @@ def edit_proposal(request, proposal_id=None): comments = paginator.page(paginator.num_pages) return render(request, 'fossee_manim/edit_proposal.html', {'proposal_form': proposal_form, - "comments": comments, - "comment_form": comment_form}) + "comments": comments, + "comment_form": comment_form, + "upload_form": upload_form, + 'video': video, + 'categories': categories}) def search(request): + categories = Category.objects.all() if request.method == 'POST': word = request.POST.get('sbox') + anime_list = AnimationStats.objects.filter( + Q(animation__title__contains=word) | Q(animation__description__contains=word) + | Q(animation__category__name__contains=word), animation__status='released') - return render(request, 'fossee_manim/search_results.html') + return render(request, 'fossee_manim/search_results.html', + {'s_result': anime_list, 'categories': categories}) + + +@login_required +def upload_animation(request, proposal_id=None): + if request.method == 'POST': + proposal = Animation.objects.get(id=proposal_id) + anim_stats = UploadAnimationForm(request.POST or None, + request.FILES or None) + + # return redirect('/edit_proposal/{}'.format(proposal_id)) + if anim_stats.is_valid(): + anim = AnimationStats.objects.filter( + animation=proposal) + if anim.exists(): + anobj = anim.first() + try: + remove(anobj.thumbnail.path) + except: + pass + remove(anobj.video_path.path) + anobj.delete() + anobj = AnimationStats.objects.create( + animation=proposal, video_path=request.FILES['video_path']) + else: + anobj = AnimationStats.objects.create( + animation=proposal, video_path=request.FILES['video_path']) + anobj._create_thumbnail() + + return render(request, 'fossee_manim/upload_success.html') + else: + return redirect('/view_profile/') + +def video(request, aid=None): + user = request.user + video = AnimationStats.objects.filter(id=aid, animation__status="released") + if len(video): + comment_form = CommentForm() + # if views crosses limit comment the line below + video.update(views=F('views')+1) + video.update(like=F('like')+1) + anim_list = AnimationStats.objects.filter(animation__status="released") + suggestion_list = [x for x in anim_list if ( + x.animation.category == video[0].animation.category)] + reviewer_id = video[0].animation.reviewer.id + comment_list = Comment.objects.filter(animation=video[0].animation) + comments = [x for x in comment_list if x.animation.status != + ('pending' or 'changes')] + if request.method == 'POST': + if is_email_checked(user): + comment_form = CommentForm(request.POST) + form_data = comment_form.save(commit=False) + form_data.commentor = user + form_data.animation = video[0].animation + form_data.animation_status = video[0].animation.status + form_data.save() + return redirect('/video/{}'.format(aid)) + else: + return redirect('/login/') + else: + return redirect('/view_profile/') + + if len(suggestion_list)>3: + suggestion_list = sample(suggestion_list, 3) + else: + suggestion_list = [x for x in anim_list if x.id != int(aid)][:3] + categories = Category.objects.all() + return render(request, 'fossee_manim/video.html', + {'video': video, 'categories': categories, + 'reco': suggestion_list, + "comment_form": comment_form, + 'comments': comments}) + + +def search_category(request, cat=None): + cat_id = Category.objects.get(name=cat) + anim_list = AnimationStats.objects.filter(animation__status="released") + cat_video_list = [x for x in anim_list if (x.animation.category == cat_id)] + categories = Category.objects.all() + return render(request, 'fossee_manim/categorical_list.html', + {'categorial_list': cat_video_list, 'categories': categories + }) + + +def how_to(request): + return render(request, 'fossee_manim/how_to.html')
\ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 85b47d2..27e309b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,10 @@ +certifi==2018.11.29 +chardet==3.0.4 Django==1.11 django-simple-history==2.7.0 django-taggit==0.23.0 +idna==2.8 +Pillow==5.4.1 pytz==2018.9 +requests==2.21.0 +urllib3==1.24.1 |