├── src ├── courses │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0006_auto_20170206_2354.py │ │ ├── 0007_auto_20170206_2356.py │ │ ├── 0004_auto_20170206_1954.py │ │ ├── 0016_auto_20170207_1844.py │ │ ├── 0003_auto_20170206_1949.py │ │ ├── 0020_lecture_free.py │ │ ├── 0012_auto_20170207_0040.py │ │ ├── 0014_course_active.py │ │ ├── 0009_course_order.py │ │ ├── 0008_auto_20170207_0010.py │ │ ├── 0005_lecture_order.py │ │ ├── 0013_auto_20170207_0113.py │ │ ├── 0010_course_category.py │ │ ├── 0017_course_secondary.py │ │ ├── 0019_course_image.py │ │ ├── 0018_auto_20170207_2336.py │ │ ├── 0015_auto_20170207_1844.py │ │ ├── 0011_mycourses.py │ │ ├── 0001_initial.py │ │ └── 0002_lecture.py │ ├── tests.py │ ├── apps.py │ ├── templates │ │ ├── .DS_Store │ │ └── courses │ │ │ ├── course_form.html │ │ │ ├── course_confirm_delete.html │ │ │ ├── lecture_detail.html │ │ │ ├── snippets │ │ │ ├── course_featured_display.html │ │ │ └── course_thumbnail_display.html │ │ │ ├── must_purchase.html │ │ │ ├── course_detail.html │ │ │ └── course_list.html │ ├── urls.py │ ├── admin.py │ ├── utils.py │ ├── course_copy.py │ ├── forms.py │ ├── views.py │ └── models.py ├── search │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── templatetags │ │ ├── __init__.py │ │ └── search_form.py │ ├── models.py │ ├── tests.py │ ├── admin.py │ ├── apps.py │ ├── urls.py │ ├── templates │ │ └── search │ │ │ ├── snippets │ │ │ └── search_form.html │ │ │ └── default.html │ └── views.py ├── srvup │ ├── __init__.py │ ├── wsgi.py │ ├── urls.py │ ├── views.py │ └── settings.py ├── videos │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0004_video_slug.py │ │ ├── 0003_video_updated.py │ │ ├── 0001_initial.py │ │ ├── 0005_auto_20170203_0139.py │ │ └── 0002_auto_20170202_1736.py │ ├── templatetags │ │ ├── __init__.py │ │ └── render_video.py │ ├── tests.py │ ├── apps.py │ ├── templates │ │ ├── .DS_Store │ │ └── videos │ │ │ ├── snippets │ │ │ ├── render_video.html │ │ │ └── video-search-form.html │ │ │ ├── video_form.html │ │ │ ├── video_detail.html │ │ │ ├── video_confirm_delete.html │ │ │ └── video_list.html │ ├── forms.py │ ├── admin.py │ ├── urls.py │ ├── mixins.py │ ├── views.py │ └── models.py ├── analytics │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── tests.py │ ├── views.py │ ├── apps.py │ ├── admin.py │ └── models.py ├── categories │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0002_category_video.py │ │ └── 0001_initial.py │ ├── tests.py │ ├── apps.py │ ├── urls.py │ ├── admin.py │ ├── forms.py │ ├── views.py │ ├── templates │ │ └── categories │ │ │ ├── category_detail.html │ │ │ └── category_list.html │ └── models.py ├── .DS_Store ├── db.sqlite3 ├── templates │ ├── defaults │ │ ├── scripts.html │ │ └── head.html │ ├── base.html │ ├── home.html │ └── navbar.html └── manage.py ├── srvup.sublime-project ├── media_root ├── srvup │ └── images │ │ └── srvup_2_share.png ├── cfe │ └── images │ │ └── Screen_Shot_2017-02-07_at_3.37.03_PM.png └── new-course-m8sce │ └── images │ └── Screen_Shot_2017-02-07_at_3.39.14_PM.png ├── requirements.txt ├── static_root └── admin │ ├── fonts │ ├── Roboto-Bold-webfont.woff │ ├── Roboto-Light-webfont.woff │ ├── Roboto-Regular-webfont.woff │ └── README.txt │ ├── js │ ├── cancel.js │ ├── prepopulate.min.js │ ├── jquery.init.js │ ├── prepopulate_init.js │ ├── popup_response.js │ ├── collapse.min.js │ ├── change_form.js │ ├── vendor │ │ ├── xregexp │ │ │ └── LICENSE-XREGEXP.txt │ │ └── jquery │ │ │ └── LICENSE-JQUERY.txt │ ├── collapse.js │ ├── prepopulate.js │ ├── actions.min.js │ ├── timeparse.js │ ├── inlines.min.js │ ├── SelectBox.js │ ├── actions.js │ ├── admin │ │ └── RelatedObjectLookups.js │ ├── urlify.js │ ├── calendar.js │ └── core.js │ ├── img │ ├── tooltag-arrowright.svg │ ├── README.txt │ ├── icon-addlink.svg │ ├── tooltag-add.svg │ ├── icon-changelink.svg │ ├── icon-deletelink.svg │ ├── icon-yes.svg │ ├── search.svg │ ├── icon-alert.svg │ ├── icon-no.svg │ ├── inline-delete.svg │ ├── icon-unknown.svg │ ├── icon-unknown-alt.svg │ ├── icon-clock.svg │ ├── gis │ │ ├── move_vertex_on.svg │ │ └── move_vertex_off.svg │ ├── icon-calendar.svg │ ├── calendar-icons.svg │ ├── LICENSE │ ├── sorting-icons.svg │ └── selector-icons.svg │ └── css │ ├── fonts.css │ ├── dashboard.css │ ├── login.css │ ├── rtl.css │ ├── changelists.css │ └── forms.css ├── roadmap.md ├── LICENSE ├── .gitignore └── README.md /src/courses/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/search/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/srvup/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/videos/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/analytics/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/categories/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/search/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/videos/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/analytics/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/categories/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/courses/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/search/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/videos/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/HEAD/src/.DS_Store -------------------------------------------------------------------------------- /src/courses/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /src/search/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /src/search/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /src/videos/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /src/analytics/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /src/analytics/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /src/categories/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /src/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/HEAD/src/db.sqlite3 -------------------------------------------------------------------------------- /src/search/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /srvup.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "path": "." 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/courses/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoursesConfig(AppConfig): 5 | name = 'courses' 6 | -------------------------------------------------------------------------------- /src/courses/templates/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/HEAD/src/courses/templates/.DS_Store -------------------------------------------------------------------------------- /src/search/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SearchConfig(AppConfig): 5 | name = 'search' 6 | -------------------------------------------------------------------------------- /src/videos/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class VideosConfig(AppConfig): 5 | name = 'videos' 6 | -------------------------------------------------------------------------------- /src/videos/templates/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/HEAD/src/videos/templates/.DS_Store -------------------------------------------------------------------------------- /src/analytics/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AnalyticsConfig(AppConfig): 5 | name = 'analytics' 6 | -------------------------------------------------------------------------------- /src/categories/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CategoriesConfig(AppConfig): 5 | name = 'categories' 6 | -------------------------------------------------------------------------------- /media_root/srvup/images/srvup_2_share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/HEAD/media_root/srvup/images/srvup_2_share.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appdirs==1.4.0 2 | Django==1.10.5 3 | olefile==0.44 4 | packaging==16.8 5 | Pillow==4.0.0 6 | pyparsing==2.1.10 7 | six==1.10.0 8 | -------------------------------------------------------------------------------- /static_root/admin/fonts/Roboto-Bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/HEAD/static_root/admin/fonts/Roboto-Bold-webfont.woff -------------------------------------------------------------------------------- /static_root/admin/fonts/Roboto-Light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/HEAD/static_root/admin/fonts/Roboto-Light-webfont.woff -------------------------------------------------------------------------------- /static_root/admin/fonts/Roboto-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/HEAD/static_root/admin/fonts/Roboto-Regular-webfont.woff -------------------------------------------------------------------------------- /src/analytics/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | from .models import CourseViewEvent 5 | 6 | admin.site.register(CourseViewEvent) -------------------------------------------------------------------------------- /src/videos/templates/videos/snippets/render_video.html: -------------------------------------------------------------------------------- 1 | {% if video %} 2 |
3 | {{ video|safe }} 4 |
5 | {% endif %} -------------------------------------------------------------------------------- /static_root/admin/fonts/README.txt: -------------------------------------------------------------------------------- 1 | Roboto webfont source: https://www.google.com/fonts/specimen/Roboto 2 | Weights used in this project: Light (300), Regular (400), Bold (700) 3 | -------------------------------------------------------------------------------- /media_root/cfe/images/Screen_Shot_2017-02-07_at_3.37.03_PM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/HEAD/media_root/cfe/images/Screen_Shot_2017-02-07_at_3.37.03_PM.png -------------------------------------------------------------------------------- /media_root/new-course-m8sce/images/Screen_Shot_2017-02-07_at_3.39.14_PM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/HEAD/media_root/new-course-m8sce/images/Screen_Shot_2017-02-07_at_3.39.14_PM.png -------------------------------------------------------------------------------- /src/videos/templates/videos/snippets/video-search-form.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
-------------------------------------------------------------------------------- /src/search/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | 4 | from .views import ( 5 | SearchView 6 | ) 7 | 8 | urlpatterns = [ 9 | url(r'^$', SearchView.as_view(), name='default'), 10 | ] 11 | 12 | -------------------------------------------------------------------------------- /src/search/templatetags/search_form.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | register = template.Library() 3 | 4 | 5 | @register.inclusion_tag('search/snippets/search_form.html') 6 | def search_form(request, navbar=False): 7 | return {"request": request, 'navbar':navbar} -------------------------------------------------------------------------------- /static_root/admin/js/cancel.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 'use strict'; 3 | $(function() { 4 | $('.cancel-link').click(function(e) { 5 | e.preventDefault(); 6 | window.history.back(); 7 | }); 8 | }); 9 | })(django.jQuery); 10 | -------------------------------------------------------------------------------- /static_root/admin/img/tooltag-arrowright.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/categories/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | 4 | from .views import ( 5 | CategoryListView, 6 | CategoryDetailView, 7 | ) 8 | 9 | urlpatterns = [ 10 | url(r'^$', CategoryListView.as_view(), name='list'), 11 | url(r'^(?P[\w-]+)/$', CategoryDetailView.as_view(), name='detail'), 12 | ] 13 | -------------------------------------------------------------------------------- /src/videos/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | 4 | from .models import Video 5 | 6 | class VideoForm(forms.ModelForm): 7 | # order = forms.IntegerField(widget=forms.TextInput()) 8 | class Meta: 9 | model = Video 10 | fields = [ 11 | 'title', 12 | 'embed_code', 13 | ] 14 | -------------------------------------------------------------------------------- /static_root/admin/img/README.txt: -------------------------------------------------------------------------------- 1 | All icons are taken from Font Awesome (http://fontawesome.io/) project. 2 | The Font Awesome font is licensed under the SIL OFL 1.1: 3 | - http://scripts.sil.org/OFL 4 | 5 | SVG icons source: https://github.com/encharm/Font-Awesome-SVG-PNG 6 | Font-Awesome-SVG-PNG is licensed under the MIT license (see file license 7 | in current folder). 8 | -------------------------------------------------------------------------------- /static_root/admin/img/icon-addlink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static_root/admin/img/tooltag-add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/videos/templatetags/render_video.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | from videos.models import Video 6 | 7 | @register.inclusion_tag('videos/snippets/render_video.html') 8 | def render_video(video_obj): 9 | video = None 10 | if isinstance(video_obj, Video): 11 | video = video_obj.embed_code 12 | return {'video': video} -------------------------------------------------------------------------------- /static_root/admin/js/prepopulate.min.js: -------------------------------------------------------------------------------- 1 | (function(c){c.fn.prepopulate=function(e,f,g){return this.each(function(){var a=c(this),b=function(){if(!a.data("_changed")){var b=[];c.each(e,function(a,d){d=c(d);0 2 | 3 | 4 | -------------------------------------------------------------------------------- /static_root/admin/js/jquery.init.js: -------------------------------------------------------------------------------- 1 | /*global django:true, jQuery:false*/ 2 | /* Puts the included jQuery into our own namespace using noConflict and passing 3 | * it 'true'. This ensures that the included jQuery doesn't pollute the global 4 | * namespace (i.e. this preserves pre-existing values for both window.$ and 5 | * window.jQuery). 6 | */ 7 | var django = django || {}; 8 | django.jQuery = jQuery.noConflict(true); 9 | -------------------------------------------------------------------------------- /static_root/admin/img/icon-deletelink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static_root/admin/img/icon-yes.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/srvup/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for srvup 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/1.10/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", "srvup.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /src/analytics/models.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import models 3 | from courses.models import Course 4 | 5 | 6 | class CourseViewEvent(models.Model): 7 | user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True) 8 | course = models.ForeignKey(Course) 9 | views = models.IntegerField(default=0) 10 | updated = models.DateTimeField(auto_now=True) 11 | 12 | def __str__(self): 13 | return str(self.views) -------------------------------------------------------------------------------- /src/courses/templates/courses/course_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Videos | {{ block.super }}{% endblock title %} 4 | 5 | 6 | {% block content %} 7 |
8 |
9 |
{% csrf_token %} 10 | {{ form.as_p }} 11 | 12 |
13 |
14 |
15 | 16 | {% endblock content %} 17 | -------------------------------------------------------------------------------- /src/videos/templates/videos/video_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Videos | {{ block.super }}{% endblock title %} 4 | 5 | 6 | {% block content %} 7 |
8 |
9 |
{% csrf_token %} 10 | {{ form.as_p }} 11 | 12 |
13 |
14 |
15 | 16 | {% endblock content %} 17 | -------------------------------------------------------------------------------- /static_root/admin/img/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /roadmap.md: -------------------------------------------------------------------------------- 1 | Srvup 2 Roadmap 2 | 3 | ====== 4 | 5 | 6 | 1. Videos 7 | - List, View, Create, Delete, Search -- DONE 8 | - Viewer Permissions: 9 | - membership, owner, anyone 10 | 11 | 2. Courses / Series 12 | - price? 13 | - my courses (the things i own) -- one click 14 | 15 | 16 | 3. Categories / Tags 17 | - List, View, Create, Delete 18 | - Add videos 19 | - Featured 20 | 21 | 4. Search -- site wide search 22 | 23 | 5. Analytics 24 | - Recommendations based on habits 25 | 26 | 27 | -------------------------------------------------------------------------------- /static_root/admin/img/icon-alert.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static_root/admin/css/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Roboto'; 3 | src: url('../fonts/Roboto-Bold-webfont.woff'); 4 | font-weight: 700; 5 | font-style: normal; 6 | } 7 | 8 | @font-face { 9 | font-family: 'Roboto'; 10 | src: url('../fonts/Roboto-Regular-webfont.woff'); 11 | font-weight: 400; 12 | font-style: normal; 13 | } 14 | 15 | @font-face { 16 | font-family: 'Roboto'; 17 | src: url('../fonts/Roboto-Light-webfont.woff'); 18 | font-weight: 300; 19 | font-style: normal; 20 | } 21 | -------------------------------------------------------------------------------- /src/courses/migrations/0006_auto_20170206_2354.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-06 23:54 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('courses', '0005_lecture_order'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelOptions( 16 | name='lecture', 17 | options={'ordering': ['-order', '-title']}, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /src/videos/templates/videos/video_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Videos | {{ block.super }}{% endblock title %} 4 | 5 | 6 | {% block content %} 7 |
8 |
9 | {% include 'videos/snippets/video-search-form.html' with request=request %} 10 | 11 | 12 | 13 |

Hi

14 | 15 | {{ object.title }} 16 | {{ object.embed_code|safe }} 17 | {{ object.updated }} 18 | {{ object.id }} 19 |
20 |
21 | {% endblock content %} 22 | -------------------------------------------------------------------------------- /src/courses/migrations/0007_auto_20170206_2356.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-06 23:56 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('courses', '0006_auto_20170206_2354'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelOptions( 16 | name='lecture', 17 | options={'ordering': ['order', 'title']}, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /src/courses/migrations/0004_auto_20170206_1954.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-06 19:54 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('courses', '0003_auto_20170206_1949'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterUniqueTogether( 16 | name='lecture', 17 | unique_together=set([('slug', 'course')]), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /src/videos/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | from .models import Video 5 | 6 | class VideoAdmin(admin.ModelAdmin): 7 | list_filter = ['updated', 'timestamp'] 8 | list_display = ['title', 'updated', 'timestamp'] 9 | readonly_fields = ['updated', 'timestamp', 'short_title'] 10 | search_fields = ['title', 'embed_code'] 11 | 12 | class Meta: 13 | model = Video 14 | 15 | def short_title(self, obj): 16 | return obj.title[:3] 17 | 18 | admin.site.register(Video, VideoAdmin) -------------------------------------------------------------------------------- /src/videos/migrations/0004_video_slug.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-03 00:40 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('videos', '0003_video_updated'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='video', 17 | name='slug', 18 | field=models.SlugField(blank=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /src/courses/migrations/0016_auto_20170207_1844.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-07 18:44 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('courses', '0015_auto_20170207_1844'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RenameField( 16 | model_name='course', 17 | old_name='categor', 18 | new_name='category', 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /static_root/admin/js/prepopulate_init.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 'use strict'; 3 | var fields = $('#django-admin-prepopulated-fields-constants').data('prepopulatedFields'); 4 | $.each(fields, function(index, field) { 5 | $('.empty-form .form-row .field-' + field.name + ', .empty-form.form-row .field-' + field.name).addClass('prepopulated_field'); 6 | $(field.id).data('dependency_list', field.dependency_list).prepopulate( 7 | field.dependency_ids, field.maxLength, field.allowUnicode 8 | ); 9 | }); 10 | })(django.jQuery); 11 | -------------------------------------------------------------------------------- /src/courses/migrations/0003_auto_20170206_1949.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-06 19:49 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('courses', '0002_lecture'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='lecture', 17 | name='description', 18 | field=models.TextField(blank=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /src/courses/migrations/0020_lecture_free.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-09 02:03 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('courses', '0019_course_image'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='lecture', 17 | name='free', 18 | field=models.BooleanField(default=False), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /src/courses/migrations/0012_auto_20170207_0040.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-07 00:40 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('courses', '0011_mycourses'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelOptions( 16 | name='mycourses', 17 | options={'verbose_name': 'My courses', 'verbose_name_plural': 'My courses'}, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /src/courses/migrations/0014_course_active.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-07 01:22 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('courses', '0013_auto_20170207_0113'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='course', 17 | name='active', 18 | field=models.BooleanField(default=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /src/videos/migrations/0003_video_updated.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-02 17:40 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('videos', '0002_auto_20170202_1736'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='video', 17 | name='updated', 18 | field=models.DateTimeField(auto_now=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /static_root/admin/img/icon-no.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static_root/admin/img/inline-delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/courses/templates/courses/course_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Videos | {{ block.super }}{% endblock title %} 4 | 5 | 6 | {% block content %} 7 |
8 |
9 |
{% csrf_token %} 10 |

Are you sure you want to delete {{ object.title }}?

11 | 12 | Cancel 13 |
14 |
15 |
16 | 17 | {% endblock content %} 18 | -------------------------------------------------------------------------------- /src/courses/migrations/0009_course_order.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-07 00:15 3 | from __future__ import unicode_literals 4 | 5 | import courses.fields 6 | from django.db import migrations 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('courses', '0008_auto_20170207_0010'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='course', 18 | name='order', 19 | field=courses.fields.PositionField(default=-1), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /src/videos/templates/videos/video_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Videos | {{ block.super }}{% endblock title %} 4 | 5 | 6 | {% block content %} 7 |
8 |
9 |
{% csrf_token %} 10 |

Are you sure you want to delete {{ object.title }}?

11 | 12 | Cancel 13 |
14 |
15 |
16 | 17 | {% endblock content %} 18 | -------------------------------------------------------------------------------- /static_root/admin/css/dashboard.css: -------------------------------------------------------------------------------- 1 | /* DASHBOARD */ 2 | 3 | .dashboard .module table th { 4 | width: 100%; 5 | } 6 | 7 | .dashboard .module table td { 8 | white-space: nowrap; 9 | } 10 | 11 | .dashboard .module table td a { 12 | display: block; 13 | padding-right: .6em; 14 | } 15 | 16 | /* RECENT ACTIONS MODULE */ 17 | 18 | .module ul.actionlist { 19 | margin-left: 0; 20 | } 21 | 22 | ul.actionlist li { 23 | list-style-type: none; 24 | } 25 | 26 | ul.actionlist li { 27 | overflow: hidden; 28 | text-overflow: ellipsis; 29 | -o-text-overflow: ellipsis; 30 | } 31 | -------------------------------------------------------------------------------- /src/courses/migrations/0008_auto_20170207_0010.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-07 00:10 3 | from __future__ import unicode_literals 4 | 5 | import courses.fields 6 | from django.db import migrations 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('courses', '0007_auto_20170206_2356'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='lecture', 18 | name='order', 19 | field=courses.fields.PositionField(default=-1), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /src/courses/migrations/0005_lecture_order.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-06 23:53 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('courses', '0004_auto_20170206_1954'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='lecture', 17 | name='order', 18 | field=models.IntegerField(default=1), 19 | preserve_default=False, 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /src/courses/migrations/0013_auto_20170207_0113.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-07 01:13 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('courses', '0012_auto_20170207_0040'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='mycourses', 17 | name='courses', 18 | field=models.ManyToManyField(blank=True, related_name='owned', to='courses.Course'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /src/templates/defaults/scripts.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/courses/migrations/0010_course_category.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-07 00:19 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('courses', '0009_course_order'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='course', 17 | name='category', 18 | field=models.CharField(choices=[('main', 'Main'), ('sec', 'Secondary')], default='main', max_length=120), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /src/categories/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | 5 | from .forms import CategoryAdminForm 6 | from .models import Category 7 | 8 | class CategoryAdmin(admin.ModelAdmin): 9 | list_filter = ['updated', 'timestamp'] 10 | list_display = ['title', 'updated', 'timestamp'] 11 | readonly_fields = ['updated', 'timestamp', 'short_title'] 12 | search_fields = ['title'] 13 | form = CategoryAdminForm 14 | # class Meta: 15 | # model = Category 16 | 17 | def short_title(self, obj): 18 | return obj.title[:3] 19 | 20 | admin.site.register(Category, CategoryAdmin) 21 | -------------------------------------------------------------------------------- /static_root/admin/js/popup_response.js: -------------------------------------------------------------------------------- 1 | /*global opener */ 2 | (function() { 3 | 'use strict'; 4 | var initData = JSON.parse(document.getElementById('django-admin-popup-response-constants').dataset.popupResponse); 5 | switch(initData.action) { 6 | case 'change': 7 | opener.dismissChangeRelatedObjectPopup(window, initData.value, initData.obj, initData.new_value); 8 | break; 9 | case 'delete': 10 | opener.dismissDeleteRelatedObjectPopup(window, initData.value); 11 | break; 12 | default: 13 | opener.dismissAddRelatedObjectPopup(window, initData.value, initData.obj); 14 | break; 15 | } 16 | })(); 17 | -------------------------------------------------------------------------------- /static_root/admin/js/collapse.min.js: -------------------------------------------------------------------------------- 1 | (function(a){a(document).ready(function(){a("fieldset.collapse").each(function(b,c){0===a(c).find("div.errors").length&&a(c).addClass("collapsed").find("h2").first().append(' ('+gettext("Show")+")")});a("fieldset.collapse a.collapse-toggle").click(function(b){a(this).closest("fieldset").hasClass("collapsed")?a(this).text(gettext("Hide")).closest("fieldset").removeClass("collapsed").trigger("show.fieldset",[a(this).attr("id")]):a(this).text(gettext("Show")).closest("fieldset").addClass("collapsed").trigger("hide.fieldset", 2 | [a(this).attr("id")]);return!1})})})(django.jQuery); 3 | -------------------------------------------------------------------------------- /src/videos/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-02 17:24 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Video', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('embed_code', models.TextField()), 21 | ], 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /static_root/admin/img/icon-unknown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/courses/migrations/0017_course_secondary.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-07 18:58 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('categories', '0001_initial'), 12 | ('courses', '0016_auto_20170207_1844'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='course', 18 | name='secondary', 19 | field=models.ManyToManyField(blank=True, related_name='secondary_category', to='categories.Category'), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /static_root/admin/img/icon-unknown-alt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/courses/migrations/0019_course_image.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-07 23:36 3 | from __future__ import unicode_literals 4 | 5 | import courses.models 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('courses', '0018_auto_20170207_2336'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='course', 18 | name='image', 19 | field=models.ImageField(blank=True, height_field='image_height', null=True, upload_to=courses.models.handle_upload, width_field='image_width'), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /src/categories/migrations/0002_category_video.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-07 22:55 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('videos', '0005_auto_20170203_0139'), 13 | ('categories', '0001_initial'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AddField( 18 | model_name='category', 19 | name='video', 20 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='videos.Video'), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /static_root/admin/img/icon-clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/courses/templates/courses/lecture_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load render_video %} 3 | 4 | {% block title %}Videos | {{ block.super }}{% endblock title %} 5 | 6 | 7 | {% block content %} 8 | 13 |
14 |
15 |

{{ object.title }}

16 | 17 | {% render_video object.video %} 18 | 19 |

{{ object.updated }}

20 |

{{ object.description }}

21 | 22 |
23 |
24 | {% endblock content %} 25 | -------------------------------------------------------------------------------- /src/courses/templates/courses/snippets/course_featured_display.html: -------------------------------------------------------------------------------- 1 |
2 | {% if course.image %} 3 | 4 | {% endif %} 5 |
6 |

{{ course.title }}

7 | 8 |

9 | {% if not item.is_owner %} 10 | 11 | Purchase {{ item.display_price }} 12 | {% else %} 13 |

View

14 | {% endif %} 15 |

16 | 17 | 18 |
19 |
20 | 21 | -------------------------------------------------------------------------------- /src/videos/migrations/0005_auto_20170203_0139.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-03 01:39 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('videos', '0004_video_slug'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='video', 17 | name='free', 18 | field=models.BooleanField(default=True), 19 | ), 20 | migrations.AddField( 21 | model_name='video', 22 | name='member_required', 23 | field=models.BooleanField(default=False), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /src/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load search_form %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% block title %}Srvup{% endblock title %} 10 | 11 | {% include 'defaults/head.html' %} 12 | 13 | 14 | 15 |
16 | {% include 'navbar.html' %} 17 | {% block content %} 18 | {% endblock content %} 19 |
20 | 21 | {% include 'defaults/scripts.html' %} 22 | 23 | -------------------------------------------------------------------------------- /src/courses/migrations/0018_auto_20170207_2336.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-07 23:36 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('courses', '0017_course_secondary'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='course', 17 | name='image_height', 18 | field=models.IntegerField(blank=True, null=True), 19 | ), 20 | migrations.AddField( 21 | model_name='course', 22 | name='image_width', 23 | field=models.IntegerField(blank=True, null=True), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /src/templates/defaults/head.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/videos/templates/videos/video_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Videos | {{ block.super }}{% endblock title %} 4 | 5 | 6 | {% block content %} 7 |
8 |
9 |

Hi

10 | {% include 'videos/snippets/video-search-form.html' with request=request %} 11 | 12 | {% if request.GET.q %} 13 |

You searched for {{ request.GET.q }}

14 | {% endif %} 15 |
    16 | {% for item in object_list %} 17 |
  • {{ item.title }}
  • 18 | 19 | {% empty %} 20 |
  • No item found
  • 21 | {% endfor %} 22 |
23 |
24 |
25 | {% endblock content %} 26 | -------------------------------------------------------------------------------- /src/videos/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.contrib import admin 3 | 4 | 5 | from .views import ( 6 | VideoListView, 7 | VideoDetailView, 8 | VideoCreateView, 9 | VideoUpdateView, 10 | VideoDeleteView 11 | ) 12 | 13 | urlpatterns = [ 14 | url(r'^$', VideoListView.as_view(), name='list'), 15 | url(r'^create/$', VideoCreateView.as_view(), name='create'), 16 | # url(r'^videos/(?P\d+)/$', VideoDetailView.as_view(), name='video-detail'), 17 | url(r'^(?P[\w-]+)/$', VideoDetailView.as_view(), name='detail'), 18 | url(r'^(?P[\w-]+)/edit/$', VideoUpdateView.as_view(), name='update'), 19 | url(r'^(?P[\w-]+)/delete/$', VideoDeleteView.as_view(), name='delete'), 20 | 21 | ] 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /static_root/admin/js/change_form.js: -------------------------------------------------------------------------------- 1 | /*global showAddAnotherPopup, showRelatedObjectLookupPopup showRelatedObjectPopup updateRelatedObjectLinks*/ 2 | 3 | (function($) { 4 | 'use strict'; 5 | $(document).ready(function() { 6 | var modelName = $('#django-admin-form-add-constants').data('modelName'); 7 | $('.add-another').click(function(e) { 8 | e.preventDefault(); 9 | var event = $.Event('django:add-another-related'); 10 | $(this).trigger(event); 11 | if (!event.isDefaultPrevented()) { 12 | showAddAnotherPopup(this); 13 | } 14 | }); 15 | 16 | if (modelName) { 17 | $('form#' + modelName + '_form :input:visible:enabled:first').focus(); 18 | } 19 | }); 20 | })(django.jQuery); 21 | -------------------------------------------------------------------------------- /src/courses/templates/courses/must_purchase.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load humanize %} 3 | {% block title %}Videos | {{ block.super }}{% endblock title %} 4 | 5 | 6 | {% block content %} 7 |
8 |
9 | 10 | 11 | {% include 'videos/snippets/video-search-form.html' with request=request %} 12 |
13 |

Please purchase to continue

14 |
15 | 16 |

{{ object.title }}

17 | 18 | {{ object.description|linebreaks }} 19 | {% if not object.is_owner %} 20 | Purchase 21 | {{ object.display_price }} 22 | {% endif %} 23 |
24 | 25 |
26 | 27 | 28 | 29 |
30 |
31 | {% endblock content %} 32 | -------------------------------------------------------------------------------- /src/search/templates/search/snippets/search_form.html: -------------------------------------------------------------------------------- 1 | {% if not navbar %} 2 |
3 | 4 | 5 | 6 |
7 | 8 | {% else %} 9 | 15 | {% endif %} 16 | 17 | -------------------------------------------------------------------------------- /src/courses/templates/courses/snippets/course_thumbnail_display.html: -------------------------------------------------------------------------------- 1 |
2 | {% if course.image %} 3 | 4 | {% endif %} 5 |
6 |

{{ course.title }}

7 | {% if course.description %}

{{ course.description|truncatechars:100 }}

{% endif %} 8 | 9 |

10 | {% if item.is_owner %} 11 | Owned 12 | {% else %} 13 | Purchase {{ item.display_price }} 14 | 15 | {% endif %} 16 |

17 |

View

18 | 19 |
20 |
21 | 22 | -------------------------------------------------------------------------------- /src/srvup/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls.static import static 3 | from django.conf.urls import url, include 4 | from django.contrib import admin 5 | 6 | from .views import home, HomeView 7 | 8 | urlpatterns = [ 9 | url(r'^$', HomeView.as_view(), name='home'), 10 | url(r'^admin/', admin.site.urls), 11 | url(r'^search/', include('search.urls', namespace='search')), 12 | url(r'^categories/', include('categories.urls', namespace='categories')), 13 | url(r'^courses/', include('courses.urls', namespace='courses')), 14 | url(r'^videos/', include('videos.urls', namespace='videos')), 15 | ] 16 | 17 | if settings.DEBUG: 18 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 19 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/courses/migrations/0015_auto_20170207_1844.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-07 18:44 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('categories', '0001_initial'), 13 | ('courses', '0014_course_active'), 14 | ] 15 | 16 | operations = [ 17 | migrations.RemoveField( 18 | model_name='course', 19 | name='category', 20 | ), 21 | migrations.AddField( 22 | model_name='course', 23 | name='categor', 24 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='primary_category', to='categories.Category'), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /src/courses/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | 4 | from .views import ( 5 | CourseListView, 6 | CourseDetailView, 7 | CourseCreateView, 8 | CourseUpdateView, 9 | CourseDeleteView, 10 | LectureDetailView, 11 | CoursePurchaseView 12 | ) 13 | 14 | urlpatterns = [ 15 | url(r'^$', CourseListView.as_view(), name='list'), 16 | url(r'^create/$', CourseCreateView.as_view(), name='create'), 17 | url(r'^(?P[\w-]+)/$', CourseDetailView.as_view(), name='detail'), 18 | url(r'^(?P[\w-]+)/purchase/$', CoursePurchaseView.as_view(), name='purchase'), 19 | url(r'^(?P[\w-]+)/(?P[\w-]+)/$', LectureDetailView.as_view(), name='lecture-detail'), 20 | url(r'^(?P[\w-]+)/edit/$', CourseUpdateView.as_view(), name='update'), 21 | url(r'^(?P[\w-]+)/delete/$', CourseDeleteView.as_view(), name='delete'), 22 | ] 23 | 24 | -------------------------------------------------------------------------------- /src/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "srvup.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /src/search/templates/search/default.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load search_form %} 3 | 4 | {% block content %} 5 | 6 |
7 |
8 |

Search

9 | {% search_form request False %} 10 | 11 |
12 |
13 | {% if request.GET.q %} 14 |
15 |
16 | 17 |

Results for {{ request.GET.q }}

18 | {% for item in qs %} 19 | {{ item.id }}- {{ item.title }}
20 | {% endfor %} 21 | 22 | {% for item in l_qs %} 23 | {{ item.id }}- {{ item.title }}
24 | {% endfor %} 25 | 26 | {% for item in c_qs %} 27 | {{ item.id }}- {{ item.title }}
28 | {% endfor %} 29 | 30 |
31 | 32 |
33 | {% endif %} 34 | 35 | {% endblock %} -------------------------------------------------------------------------------- /src/categories/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.db.models import Q 3 | 4 | from videos.models import Video 5 | from .models import Category 6 | 7 | 8 | 9 | class CategoryAdminForm(forms.ModelForm): 10 | class Meta: 11 | model = Category 12 | fields = [ 13 | 'order', 14 | 'title', 15 | 'video', 16 | 'description', 17 | 'slug', 18 | ] 19 | def __init__(self, *args, **kwargs): 20 | super(CategoryAdminForm, self).__init__(*args, **kwargs) 21 | obj = kwargs.get("instance") 22 | qs = Video.objects.all().unused() 23 | if obj: 24 | if obj.video: 25 | this_ = Video.objects.filter(pk=obj.video.pk) 26 | qs = (qs | this_) 27 | self.fields['video'].queryset = qs 28 | else: 29 | self.fields['video'].queryset = qs -------------------------------------------------------------------------------- /src/videos/migrations/0002_auto_20170202_1736.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-02 17:36 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('videos', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='video', 18 | name='timestamp', 19 | field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), 20 | preserve_default=False, 21 | ), 22 | migrations.AddField( 23 | model_name='video', 24 | name='title', 25 | field=models.CharField(default='YouTube Video', max_length=120), 26 | preserve_default=False, 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /src/courses/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | from .forms import LectureAdminForm 5 | from .models import Course, Lecture, MyCourses 6 | 7 | 8 | admin.site.register(MyCourses) 9 | 10 | 11 | 12 | class LectureInline(admin.TabularInline): 13 | model = Lecture 14 | form = LectureAdminForm 15 | prepopulated_fields = {"slug": ("title",)} 16 | extra = 1 17 | 18 | class CourseAdmin(admin.ModelAdmin): 19 | inlines = [LectureInline] 20 | list_filter = ['updated', 'timestamp'] 21 | list_display = ['title', 'updated', 'timestamp', 'order'] 22 | readonly_fields = ['updated', 'timestamp', 'short_title'] 23 | search_fields = ['title', 'description'] 24 | list_editable = ['order'] 25 | 26 | class Meta: 27 | model = Course 28 | 29 | def short_title(self, obj): 30 | return obj.title[:3] 31 | 32 | admin.site.register(Course, CourseAdmin) 33 | 34 | -------------------------------------------------------------------------------- /src/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 | 5 | 6 |
7 |
8 |

Recommended

9 |
10 | {% for item in rec_courses %} 11 | 12 |
13 | {% include 'courses/snippets/course_featured_display.html' with course=item %} 14 |
15 | {% if forloop.counter|divisibleby:6 %} 16 |
17 | 18 | {% endif %} 19 | {% endfor %} 20 |

21 |
22 | 23 |
24 | 25 | 26 | 27 |
28 | {% for item in qs %} 29 |
30 | {% include 'courses/snippets/course_thumbnail_display.html' with course=item %} 31 |
32 | {% cycle '' '' '
' %} 33 | {% endfor %} 34 |
35 | 36 | 37 | {% endblock content %} 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/courses/utils.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | 4 | from django.contrib.humanize.templatetags.humanize import intcomma 5 | from django.utils.text import slugify 6 | 7 | def unique_string_generator(size=5, chars=string.ascii_lowercase + string.digits): 8 | return "".join(random.choice(chars) for _ in range(size)) 9 | 10 | 11 | def create_slug(instance, new_slug=None): 12 | if not new_slug: 13 | slug = slugify(instance.title) 14 | else: 15 | slug = new_slug 16 | Klass = instance.__class__ 17 | qs = Klass.objects.filter(slug=slug).order_by('-id') 18 | if qs.exists(): 19 | string_unique = unique_string_generator() 20 | newly_created_slug = slug + "-{id_}".format(id_=string_unique) 21 | return create_slug(instance, new_slug=newly_created_slug) 22 | return slug 23 | 24 | 25 | def make_display_price(price): 26 | dollars = round(price, 2) 27 | return "$%s%s" % (intcomma(int(dollars)), ("%0.2f" % dollars)[-3:]) -------------------------------------------------------------------------------- /src/videos/mixins.py: -------------------------------------------------------------------------------- 1 | from django.contrib.admin.views.decorators import staff_member_required 2 | from django.http import HttpResponse 3 | from django.utils.decorators import method_decorator 4 | 5 | class MemberRequiredMixin(object): 6 | def dispatch(self, request, *args, **kwargs): 7 | print("requiring..?") 8 | obj = self.get_object() 9 | user = request.user 10 | if request.user.is_staff: 11 | return super(MemberRequiredMixin, self).dispatch(request, *args, **kwargs) 12 | try: 13 | if obj.free: 14 | return super(MemberRequiredMixin, self).dispatch(request, *args, **kwargs) 15 | except: 16 | pass 17 | return HttpResponse("Oops not free") 18 | 19 | 20 | class StaffMemberRequiredMixin(object): 21 | @method_decorator(staff_member_required) 22 | def dispatch(self, request, *args, **kwargs): 23 | return super(StaffMemberRequiredMixin, self).dispatch(request, *args, **kwargs) 24 | -------------------------------------------------------------------------------- /src/categories/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.views.generic import ( 3 | CreateView, 4 | DetailView, 5 | ListView, 6 | UpdateView, 7 | DeleteView, 8 | RedirectView 9 | ) 10 | 11 | from .models import Category 12 | 13 | 14 | class CategoryListView(ListView): 15 | queryset = Category.objects.all().order_by('title') 16 | 17 | 18 | class CategoryDetailView(DetailView): 19 | queryset = Category.objects.all() 20 | 21 | def get_context_data(self,*args, **kwargs): 22 | context = super(CategoryDetailView, self).get_context_data(*args, **kwargs) 23 | obj = context.get("object") 24 | user = self.request.user 25 | qs1 = obj.primary_category.all().owned(self.request.user) 26 | context['featured_courses'] = qs1[:6] 27 | qs2 = obj.secondary_category.all().owned(self.request.user) 28 | qs = (qs1 | qs2).distinct() 29 | context['courses'] = qs 30 | return context -------------------------------------------------------------------------------- /src/categories/templates/categories/category_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load render_video %} 3 | 4 | {% block content %} 5 |
6 |
7 |

{{ object.title }} {% if courses.count > 1 %}{{ courses.count }} Courses{% endif %}

8 |
9 | {% render_video object.video %} 10 |
11 |
12 | 13 | 14 |
15 | {% for item in featured_courses %} 16 |
17 | {% include 'courses/snippets/course_featured_display.html' with course=item %} 18 |
19 | {% endfor %} 20 |
21 | 22 | 23 | 24 |
25 | {% for item in courses %} 26 |
27 | {% include 'courses/snippets/course_thumbnail_display.html' with course=item %} 28 |
29 | {% cycle '' '' '
' %} 30 | {% endfor %} 31 |
32 | 33 | 34 |
35 | {% endblock %} -------------------------------------------------------------------------------- /src/courses/course_copy.py: -------------------------------------------------------------------------------- 1 | from courses.models import Course 2 | 3 | 4 | def copy_courses(qs=Course.objects.all()): 5 | if qs.count() < 100: 6 | for obj in qs: 7 | user = obj.user 8 | title = obj.title 9 | image = obj.image 10 | category = obj.category 11 | secondary = obj.secondary.all() 12 | description = obj.description 13 | price = obj.price 14 | new_obj = Course.objects.create( 15 | user = user, 16 | title = title, 17 | image = image, 18 | category = category, 19 | 20 | description = description, 21 | price = price 22 | ) 23 | #new_obj.secondary = secondary 24 | for cat in secondary: 25 | new_obj.secondary.add(cat) 26 | new_obj.save() 27 | qs2 = Course.objects.all() 28 | if qs2.count() <= 100: 29 | return copy_courses(qs=qs2) 30 | return qs.count() 31 | 32 | 33 | copy_courses() -------------------------------------------------------------------------------- /src/categories/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-07 04:11 3 | from __future__ import unicode_literals 4 | 5 | import courses.fields 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Category', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('title', models.CharField(max_length=120)), 22 | ('slug', models.SlugField(blank=True)), 23 | ('order', courses.fields.PositionField(blank=True, default=-1)), 24 | ('description', models.TextField()), 25 | ('active', models.BooleanField(default=True)), 26 | ('updated', models.DateTimeField(auto_now=True)), 27 | ('timestamp', models.DateTimeField(auto_now_add=True)), 28 | ], 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /src/categories/templates/categories/category_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block content %} 5 |
6 |
7 |

Categories

8 |
9 |
26 | 27 | {% endif %} 28 | {% endfor %} 29 |
30 |
31 | 32 | 33 | 34 | 35 |
36 | 37 | 38 | {% endblock %} -------------------------------------------------------------------------------- /static_root/admin/img/gis/move_vertex_on.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static_root/admin/img/icon-calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /static_root/admin/img/gis/move_vertex_off.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/courses/migrations/0011_mycourses.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-07 00:38 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ('courses', '0010_course_category'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='MyCourses', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('updated', models.DateTimeField(auto_now=True)), 23 | ('timestamp', models.DateTimeField(auto_now_add=True)), 24 | ('courses', models.ManyToManyField(blank=True, to='courses.Course')), 25 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 26 | ], 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Coding For Entrepreneurs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /static_root/admin/img/calendar-icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /static_root/admin/img/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Code Charm Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /static_root/admin/img/sorting-icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/analytics/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-10 01:52 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | ('courses', '0020_lecture_free'), 16 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 17 | ] 18 | 19 | operations = [ 20 | migrations.CreateModel( 21 | name='CourseViewEvent', 22 | fields=[ 23 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 24 | ('views', models.IntegerField(default=0)), 25 | ('updated', models.DateTimeField(auto_now=True)), 26 | ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='courses.Course')), 27 | ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 28 | ], 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /static_root/admin/js/vendor/xregexp/LICENSE-XREGEXP.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2007-2012 Steven Levithan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /static_root/admin/js/collapse.js: -------------------------------------------------------------------------------- 1 | /*global gettext*/ 2 | (function($) { 3 | 'use strict'; 4 | $(document).ready(function() { 5 | // Add anchor tag for Show/Hide link 6 | $("fieldset.collapse").each(function(i, elem) { 7 | // Don't hide if fields in this fieldset have errors 8 | if ($(elem).find("div.errors").length === 0) { 9 | $(elem).addClass("collapsed").find("h2").first().append(' (' + gettext("Show") + 11 | ')'); 12 | } 13 | }); 14 | // Add toggle to anchor tag 15 | $("fieldset.collapse a.collapse-toggle").click(function(ev) { 16 | if ($(this).closest("fieldset").hasClass("collapsed")) { 17 | // Show 18 | $(this).text(gettext("Hide")).closest("fieldset").removeClass("collapsed").trigger("show.fieldset", [$(this).attr("id")]); 19 | } else { 20 | // Hide 21 | $(this).text(gettext("Show")).closest("fieldset").addClass("collapsed").trigger("hide.fieldset", [$(this).attr("id")]); 22 | } 23 | return false; 24 | }); 25 | }); 26 | })(django.jQuery); 27 | -------------------------------------------------------------------------------- /src/courses/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-03 20:57 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='Course', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('title', models.CharField(max_length=120)), 24 | ('slug', models.SlugField(blank=True)), 25 | ('description', models.TextField()), 26 | ('price', models.DecimalField(decimal_places=2, max_digits=100)), 27 | ('updated', models.DateTimeField(auto_now=True)), 28 | ('timestamp', models.DateTimeField(auto_now_add=True)), 29 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 30 | ], 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /src/courses/migrations/0002_lecture.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-06 19:45 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('videos', '0005_auto_20170203_0139'), 13 | ('courses', '0001_initial'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Lecture', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('title', models.CharField(max_length=120)), 22 | ('slug', models.SlugField(blank=True)), 23 | ('description', models.TextField()), 24 | ('updated', models.DateTimeField(auto_now=True)), 25 | ('timestamp', models.DateTimeField(auto_now_add=True)), 26 | ('course', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='courses.Course')), 27 | ('video', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='videos.Video')), 28 | ], 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /static_root/admin/js/vendor/jquery/LICENSE-JQUERY.txt: -------------------------------------------------------------------------------- 1 | Copyright jQuery Foundation and other contributors, https://jquery.org/ 2 | 3 | This software consists of voluntary contributions made by many 4 | individuals. For exact contribution history, see the revision history 5 | available at https://github.com/jquery/jquery 6 | 7 | ==== 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining 10 | a copy of this software and associated documentation files (the 11 | "Software"), to deal in the Software without restriction, including 12 | without limitation the rights to use, copy, modify, merge, publish, 13 | distribute, sublicense, and/or sell copies of the Software, and to 14 | permit persons to whom the Software is furnished to do so, subject to 15 | the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be 18 | included in all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 24 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 25 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 26 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 27 | -------------------------------------------------------------------------------- /src/courses/templates/courses/course_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load humanize %} 3 | {% block title %}Videos | {{ block.super }}{% endblock title %} 4 | 5 | 6 | {% block content %} 7 | 11 | 12 |
13 |
14 | 15 |
16 |
17 | {% if object.image %} 18 | 19 | {% endif %} 20 | 21 |

{{ object.title }}

22 | 23 | 24 | {{ object.description|linebreaks }} 25 | {% if not object.is_owner %} 26 | Purchase 27 | {{ object.display_price }} 28 | {% endif %} 29 |
30 | 31 |
32 | 33 |
34 |
35 |

Lectures

36 | {% for item in object.lecture_set.all %} 37 |
38 | {{ item.title }} {% if item.free %}Free Preview{% endif %} 39 | 40 | {% empty %} 41 |

Lectures coming soon

42 |
43 | {% endfor %} 44 |
45 |
46 | 47 |
48 |
49 | {% endblock content %} 50 | -------------------------------------------------------------------------------- /src/search/views.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Q 2 | from django.shortcuts import render 3 | from django.views.generic import View 4 | 5 | from categories.models import Category 6 | from courses.models import Course, Lecture 7 | 8 | 9 | class SearchView(View): 10 | def get(self, request, *args, **kwargs): 11 | query = request.GET.get('q') 12 | qs = None 13 | c_qs = None 14 | l_qs = None 15 | if query: 16 | lec_lookup = Q(title__icontains=query)\ 17 | | Q(description__icontains=query) 18 | # lookup2 = Q(title__icontains=query) & Q(description__icontains=query) 19 | query_lookup = lec_lookup | Q(category__title__icontains=query)\ 20 | | Q(category__description__icontains=query)\ 21 | | Q(lecture__title__icontains=query)\ 22 | 23 | qs = Course.objects.all().lectures().filter( 24 | query_lookup 25 | ).distinct() 26 | 27 | qs_ids = [x.id for x in qs] 28 | 29 | cat_lookup = Q(primary_category__in=qs_ids) | Q(secondary_category__in=qs_ids) 30 | c_qs = Category.objects.filter(lec_lookup | cat_lookup).distinct() 31 | l_qs = Lecture.objects.filter(lec_lookup).distinct() 32 | 33 | context = {"qs": qs, "c_qs": c_qs, "l_qs": l_qs} 34 | return render(request, "search/default.html", context) 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/courses/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.db.models import Q 3 | 4 | from videos.models import Video 5 | from .models import Course, Lecture 6 | 7 | 8 | class LectureAdminForm(forms.ModelForm): 9 | class Meta: 10 | model = Lecture 11 | fields = [ 12 | 'order', 13 | 'title', 14 | 'free', 15 | 'video', 16 | 'description', 17 | 'slug', 18 | ] 19 | def __init__(self, *args, **kwargs): 20 | super(LectureAdminForm, self).__init__(*args, **kwargs) 21 | obj = kwargs.get("instance") 22 | qs = Video.objects.all().unused() 23 | if obj: 24 | if obj.video: 25 | this_ = Video.objects.filter(pk=obj.video.pk) 26 | qs = (qs | this_) 27 | self.fields['video'].queryset = qs 28 | else: 29 | self.fields['video'].queryset = qs 30 | 31 | 32 | 33 | class CourseForm(forms.ModelForm): 34 | # number = forms.IntegerField() 35 | class Meta: 36 | model = Course 37 | fields = [ 38 | 'title', 39 | 'description', 40 | 'slug', 41 | 'price', 42 | 43 | ] 44 | 45 | def clean_slug(self): 46 | slug = self.cleaned_data.get("slug") 47 | qs = Course.objects.filter(slug=slug) 48 | if qs.count() > 1: 49 | raise forms.ValidationError("Slug must be unique") 50 | return slug -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | include/ 3 | pip-selfcheck.json 4 | .DS_STORE 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | env/ 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *,cover 51 | .hypothesis/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # IPython Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | 93 | # Rope project settings 94 | .ropeproject 95 | -------------------------------------------------------------------------------- /static_root/admin/css/login.css: -------------------------------------------------------------------------------- 1 | /* LOGIN FORM */ 2 | 3 | body.login { 4 | background: #f8f8f8; 5 | } 6 | 7 | .login #header { 8 | height: auto; 9 | padding: 5px 16px; 10 | } 11 | 12 | .login #header h1 { 13 | font-size: 18px; 14 | } 15 | 16 | .login #header h1 a { 17 | color: #fff; 18 | } 19 | 20 | .login #content { 21 | padding: 20px 20px 0; 22 | } 23 | 24 | .login #container { 25 | background: #fff; 26 | border: 1px solid #eaeaea; 27 | border-radius: 4px; 28 | overflow: hidden; 29 | width: 28em; 30 | min-width: 300px; 31 | margin: 100px auto; 32 | } 33 | 34 | .login #content-main { 35 | width: 100%; 36 | } 37 | 38 | .login .form-row { 39 | padding: 4px 0; 40 | float: left; 41 | width: 100%; 42 | border-bottom: none; 43 | } 44 | 45 | .login .form-row label { 46 | padding-right: 0.5em; 47 | line-height: 2em; 48 | font-size: 1em; 49 | clear: both; 50 | color: #333; 51 | } 52 | 53 | .login .form-row #id_username, .login .form-row #id_password { 54 | clear: both; 55 | padding: 8px; 56 | width: 100%; 57 | -webkit-box-sizing: border-box; 58 | -moz-box-sizing: border-box; 59 | box-sizing: border-box; 60 | } 61 | 62 | .login span.help { 63 | font-size: 10px; 64 | display: block; 65 | } 66 | 67 | .login .submit-row { 68 | clear: both; 69 | padding: 1em 0 0 9.4em; 70 | margin: 0; 71 | border: none; 72 | background: none; 73 | text-align: left; 74 | } 75 | 76 | .login .password-reset-link { 77 | text-align: center; 78 | } 79 | -------------------------------------------------------------------------------- /static_root/admin/js/prepopulate.js: -------------------------------------------------------------------------------- 1 | /*global URLify*/ 2 | (function($) { 3 | 'use strict'; 4 | $.fn.prepopulate = function(dependencies, maxLength, allowUnicode) { 5 | /* 6 | Depends on urlify.js 7 | Populates a selected field with the values of the dependent fields, 8 | URLifies and shortens the string. 9 | dependencies - array of dependent fields ids 10 | maxLength - maximum length of the URLify'd string 11 | allowUnicode - Unicode support of the URLify'd string 12 | */ 13 | return this.each(function() { 14 | var prepopulatedField = $(this); 15 | 16 | var populate = function() { 17 | // Bail if the field's value has been changed by the user 18 | if (prepopulatedField.data('_changed')) { 19 | return; 20 | } 21 | 22 | var values = []; 23 | $.each(dependencies, function(i, field) { 24 | field = $(field); 25 | if (field.val().length > 0) { 26 | values.push(field.val()); 27 | } 28 | }); 29 | prepopulatedField.val(URLify(values.join(' '), maxLength, allowUnicode)); 30 | }; 31 | 32 | prepopulatedField.data('_changed', false); 33 | prepopulatedField.change(function() { 34 | prepopulatedField.data('_changed', true); 35 | }); 36 | 37 | if (!prepopulatedField.val()) { 38 | $(dependencies.join(',')).keyup(populate).change(populate).focus(populate); 39 | } 40 | }); 41 | }; 42 | })(django.jQuery); 43 | -------------------------------------------------------------------------------- /src/videos/views.py: -------------------------------------------------------------------------------- 1 | import random 2 | from django.contrib.auth.mixins import LoginRequiredMixin 3 | from django.shortcuts import render, get_object_or_404 4 | from django.views.generic import ( 5 | CreateView, 6 | DetailView, 7 | ListView, 8 | UpdateView, 9 | DeleteView 10 | ) 11 | 12 | from .forms import VideoForm 13 | from .mixins import MemberRequiredMixin, StaffMemberRequiredMixin 14 | from .models import Video 15 | 16 | 17 | class VideoCreateView(StaffMemberRequiredMixin, CreateView): 18 | model = Video 19 | form_class = VideoForm 20 | #success_url = "/success/" 21 | 22 | 23 | class VideoDetailView(MemberRequiredMixin, DetailView): 24 | queryset = Video.objects.all() 25 | 26 | 27 | class VideoListView(ListView): 28 | def get_queryset(self): 29 | request = self.request 30 | qs = Video.objects.all() 31 | query = request.GET.get('q') 32 | if query: 33 | qs = qs.filter(title__icontains=query) 34 | return qs #.filter(title__icontains='vid') #.filter(user=self.request.user) 35 | 36 | # def get_context_data(self, *args, **kwargs): 37 | # context = super(VideoListView, self).get_context_data(*args, **kwargs) 38 | # context['random_number'] = random.randint(100, 10000) 39 | # print(context) 40 | # return context 41 | 42 | 43 | class VideoUpdateView(StaffMemberRequiredMixin, UpdateView): 44 | queryset = Video.objects.all() 45 | form_class = VideoForm 46 | 47 | 48 | class VideoDeleteView(StaffMemberRequiredMixin, DeleteView): 49 | queryset = Video.objects.all() 50 | success_url = '/videos/' 51 | 52 | 53 | 54 | 55 | # Create 56 | 57 | # Retreive 58 | 59 | # Update 60 | 61 | # Delete 62 | 63 | # List 64 | 65 | # Search -------------------------------------------------------------------------------- /src/srvup/views.py: -------------------------------------------------------------------------------- 1 | import random 2 | from django.db.models import Q 3 | from django.http import HttpResponse 4 | from django.shortcuts import render 5 | from django.views.generic import View 6 | 7 | from analytics.models import CourseViewEvent 8 | from courses.models import Course 9 | 10 | def home(request): 11 | print(request) 12 | print(request.user) 13 | print(request.path) 14 | #return HttpResponse("Hello") 15 | return render(request, "home.html", {}) 16 | 17 | 18 | class HomeView(View): 19 | def get(self, request, *args, **kwargs): # GET -- retrieve view / list view / search 20 | course_qs = Course.objects.all().lectures().owned(request.user) 21 | qs = course_qs.featured().distinct().order_by("?")[:6] 22 | event_qs = CourseViewEvent.objects.all().prefetch_related("course") 23 | if request.user.is_authenticated(): 24 | event_views = event_qs.filter(user=request.user) 25 | else: 26 | event_views = event_qs.filter(views__gte=10) 27 | 28 | event_views = event_views.order_by('views')[:20] 29 | ids_ = [x.course.id for x in event_views] 30 | print(ids_) 31 | rec_courses = course_qs.filter(id__in=ids_).order_by('?')[:6] 32 | context = { 33 | "qs": qs, 34 | "rec_courses": rec_courses, 35 | "name": "Justin", 36 | "random_number": random.randint(500, 1000) 37 | } 38 | return render(request, "home.html", context) 39 | 40 | # def post(self, request, *args, **kwargs): # POST -- create view 41 | # return HttpResponse("Hello") 42 | 43 | # def put(self, request, *args, **kwargs): # PUT -- update view 44 | # return HttpResponse("Hello") 45 | 46 | # def delete(self, request, *args, **kwargs): # DELETE -- delete view 47 | # return HttpResponse("Hello") -------------------------------------------------------------------------------- /src/videos/models.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | from django.db import models 3 | from django.db.models import Q 4 | from django.db.models.signals import pre_save, post_save 5 | from django.utils.text import slugify 6 | # Create your models here. 7 | from courses.utils import create_slug 8 | 9 | class VideoQuerySet(models.query.QuerySet): 10 | def active(self): 11 | return self.filter(active=True) 12 | 13 | def unused(self): 14 | return self.filter(Q(lecture__isnull=True)&Q(category__isnull=True)) 15 | 16 | 17 | class VideoManager(models.Manager): 18 | def get_queryset(self): 19 | return VideoQuerySet(self.model, using=self._db) 20 | 21 | def all(self): 22 | return self.get_queryset().all() 23 | 24 | 25 | 26 | class Video(models.Model): 27 | title = models.CharField(max_length=120) 28 | slug = models.SlugField(blank=True) 29 | embed_code = models.TextField() 30 | free = models.BooleanField(default=True) 31 | member_required = models.BooleanField(default=False) 32 | updated = models.DateTimeField(auto_now=True) 33 | timestamp = models.DateTimeField(auto_now_add=True) 34 | 35 | objects = VideoManager() 36 | 37 | def __str__(self): 38 | return self.title 39 | 40 | def get_absolute_url(self): 41 | #return "/videos/{slug_arg}/".format(slug_arg=self.slug) 42 | return reverse("videos:detail", kwargs={"slug": self.slug}) 43 | 44 | def pre_save_video_receiver(sender, instance, *args, **kwargs): 45 | if not instance.slug: 46 | instance.slug = create_slug(instance) 47 | 48 | pre_save.connect(pre_save_video_receiver, sender=Video) 49 | 50 | 51 | 52 | # def post_save_video_receiver(sender, instance, created, *args, **kwargs): 53 | # if not instance.slug: 54 | # instance.slug = slugify(instance.title) #.save() 55 | # instance.save() 56 | 57 | # post_save.connect(post_save_video_receiver, sender=Video) 58 | -------------------------------------------------------------------------------- /src/categories/models.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | from django.db import models 3 | from django.db.models import Count 4 | from django.db.models.signals import pre_save 5 | # Create your models here. 6 | 7 | from courses.fields import PositionField 8 | from courses.utils import create_slug 9 | 10 | from videos.models import Video 11 | 12 | class CategoryQuerySet(models.query.QuerySet): 13 | def active(self): 14 | return self.filter(active=True) 15 | 16 | 17 | class CategoryManager(models.Manager): 18 | def get_queryset(self): 19 | return CategoryQuerySet(self.model, using=self._db) 20 | 21 | def all(self): 22 | return self.get_queryset().all( 23 | ).active().annotate( 24 | courses_length= Count("secondary_category", distinct=True) 25 | ).prefetch_related('primary_category', 'secondary_category') 26 | 27 | # qs = Category.objects.all() 28 | # obj = qs.first() 29 | # courses = obj.course_set.all() # models unleashed 30 | 31 | class Category(models.Model): 32 | title = models.CharField(max_length=120) 33 | video = models.ForeignKey(Video, null=True, blank=True) 34 | slug = models.SlugField(blank=True) # unique = False 35 | order = PositionField(blank=True) 36 | description = models.TextField() 37 | active = models.BooleanField(default=True) 38 | updated = models.DateTimeField(auto_now=True) 39 | timestamp = models.DateTimeField(auto_now_add=True) 40 | 41 | objects = CategoryManager() 42 | 43 | def get_absolute_url(self): 44 | return reverse("categories:detail", kwargs={"slug": self.slug}) 45 | 46 | def __str__(self): 47 | return self.title 48 | 49 | 50 | def pre_save_category_receiver(sender, instance, *args, **kwargs): 51 | if not instance.slug: 52 | instance.slug = create_slug(instance) 53 | 54 | pre_save.connect(pre_save_category_receiver, sender=Category) -------------------------------------------------------------------------------- /src/templates/navbar.html: -------------------------------------------------------------------------------- 1 | {% load search_form %} 2 | -------------------------------------------------------------------------------- /src/courses/templates/courses/course_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Courses | {{ block.super }}{% endblock title %} 4 | 5 | 6 | {% block content %} 7 | 8 | 9 |
10 |
11 |

Courses

12 | 13 | 14 |
15 | {% for item in object_list %} 16 |
17 | {% include 'courses/snippets/course_thumbnail_display.html' with course=item %} 18 | 19 |
20 | 21 | 22 | {% if forloop.counter|divisibleby:3 %} 23 |
24 | {% endif %} 25 | 26 | 27 | {% empty %} 28 |

No item found

29 | {% endfor %} 30 |
31 | 32 | 33 | 62 | 63 | 64 | 65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | 73 | 74 |
75 |
76 | {% endblock content %} 77 | -------------------------------------------------------------------------------- /static_root/admin/js/actions.min.js: -------------------------------------------------------------------------------- 1 | (function(a){var f;a.fn.actions=function(e){var b=a.extend({},a.fn.actions.defaults,e),g=a(this),k=!1,l=function(){a(b.acrossClears).hide();a(b.acrossQuestions).show();a(b.allContainer).hide()},m=function(){a(b.acrossClears).show();a(b.acrossQuestions).hide();a(b.actionContainer).toggleClass(b.selectedClass);a(b.allContainer).show();a(b.counterContainer).hide()},n=function(){a(b.acrossClears).hide();a(b.acrossQuestions).hide();a(b.allContainer).hide();a(b.counterContainer).show()},p=function(){n(); 2 | a(b.acrossInput).val(0);a(b.actionContainer).removeClass(b.selectedClass)},q=function(c){c?l():n();a(g).prop("checked",c).parent().parent().toggleClass(b.selectedClass,c)},h=function(){var c=a(g).filter(":checked").length,d=a(".action-counter").data("actionsIcnt");a(b.counterContainer).html(interpolate(ngettext("%(sel)s of %(cnt)s selected","%(sel)s of %(cnt)s selected",c),{sel:c,cnt:d},!0));a(b.allToggle).prop("checked",function(){var a;c===g.length?(a=!0,l()):(a=!1,p());return a})};a(b.counterContainer).show(); 3 | a(this).filter(":checked").each(function(c){a(this).parent().parent().toggleClass(b.selectedClass);h();1===a(b.acrossInput).val()&&m()});a(b.allToggle).show().click(function(){q(a(this).prop("checked"));h()});a("a",b.acrossQuestions).click(function(c){c.preventDefault();a(b.acrossInput).val(1);m()});a("a",b.acrossClears).click(function(c){c.preventDefault();a(b.allToggle).prop("checked",!1);p();q(0);h()});f=null;a(g).click(function(c){c||(c=window.event);var d=c.target?c.target:c.srcElement;if(f&& 4 | a.data(f)!==a.data(d)&&!0===c.shiftKey){var e=!1;a(f).prop("checked",d.checked).parent().parent().toggleClass(b.selectedClass,d.checked);a(g).each(function(){if(a.data(this)===a.data(f)||a.data(this)===a.data(d))e=e?!1:!0;e&&a(this).prop("checked",d.checked).parent().parent().toggleClass(b.selectedClass,d.checked)})}a(d).parent().parent().toggleClass(b.selectedClass,d.checked);f=d;h()});a("form#changelist-form table#result_list tr").find("td:gt(0) :input").change(function(){k=!0});a('form#changelist-form button[name="index"]').click(function(a){if(k)return confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost."))}); 5 | a('form#changelist-form input[name="_save"]').click(function(c){var d=!1;a("select option:selected",b.actionContainer).each(function(){a(this).val()&&(d=!0)});if(d)return k?confirm(gettext("You have selected an action, but you haven't saved your changes to individual fields yet. Please click OK to save. You'll need to re-run the action.")):confirm(gettext("You have selected an action, and you haven't made any changes on individual fields. You're probably looking for the Go button rather than the Save button."))})}; 6 | a.fn.actions.defaults={actionContainer:"div.actions",counterContainer:"span.action-counter",allContainer:"div.actions span.all",acrossInput:"div.actions input.select-across",acrossQuestions:"div.actions span.question",acrossClears:"div.actions span.clear",allToggle:"#action-toggle",selectedClass:"selected"};a(document).ready(function(){var e=a("tr input.action-select");0 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /static_root/admin/js/timeparse.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | var timeParsePatterns = [ 4 | // 9 5 | { 6 | re: /^\d{1,2}$/i, 7 | handler: function(bits) { 8 | if (bits[0].length === 1) { 9 | return '0' + bits[0] + ':00'; 10 | } else { 11 | return bits[0] + ':00'; 12 | } 13 | } 14 | }, 15 | // 13:00 16 | { 17 | re: /^\d{2}[:.]\d{2}$/i, 18 | handler: function(bits) { 19 | return bits[0].replace('.', ':'); 20 | } 21 | }, 22 | // 9:00 23 | { 24 | re: /^\d[:.]\d{2}$/i, 25 | handler: function(bits) { 26 | return '0' + bits[0].replace('.', ':'); 27 | } 28 | }, 29 | // 3 am / 3 a.m. / 3am 30 | { 31 | re: /^(\d+)\s*([ap])(?:.?m.?)?$/i, 32 | handler: function(bits) { 33 | var hour = parseInt(bits[1]); 34 | if (hour === 12) { 35 | hour = 0; 36 | } 37 | if (bits[2].toLowerCase() === 'p') { 38 | if (hour === 12) { 39 | hour = 0; 40 | } 41 | return (hour + 12) + ':00'; 42 | } else { 43 | if (hour < 10) { 44 | return '0' + hour + ':00'; 45 | } else { 46 | return hour + ':00'; 47 | } 48 | } 49 | } 50 | }, 51 | // 3.30 am / 3:15 a.m. / 3.00am 52 | { 53 | re: /^(\d+)[.:](\d{2})\s*([ap]).?m.?$/i, 54 | handler: function(bits) { 55 | var hour = parseInt(bits[1]); 56 | var mins = parseInt(bits[2]); 57 | if (mins < 10) { 58 | mins = '0' + mins; 59 | } 60 | if (hour === 12) { 61 | hour = 0; 62 | } 63 | if (bits[3].toLowerCase() === 'p') { 64 | if (hour === 12) { 65 | hour = 0; 66 | } 67 | return (hour + 12) + ':' + mins; 68 | } else { 69 | if (hour < 10) { 70 | return '0' + hour + ':' + mins; 71 | } else { 72 | return hour + ':' + mins; 73 | } 74 | } 75 | } 76 | }, 77 | // noon 78 | { 79 | re: /^no/i, 80 | handler: function(bits) { 81 | return '12:00'; 82 | } 83 | }, 84 | // midnight 85 | { 86 | re: /^mid/i, 87 | handler: function(bits) { 88 | return '00:00'; 89 | } 90 | } 91 | ]; 92 | 93 | function parseTimeString(s) { 94 | for (var i = 0; i < timeParsePatterns.length; i++) { 95 | var re = timeParsePatterns[i].re; 96 | var handler = timeParsePatterns[i].handler; 97 | var bits = re.exec(s); 98 | if (bits) { 99 | return handler(bits); 100 | } 101 | } 102 | return s; 103 | } 104 | 105 | window.parseTimeString = parseTimeString; 106 | })(); 107 | -------------------------------------------------------------------------------- /src/srvup/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for srvup project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.10.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.10/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '4irb76iz^5uww2nuvqm3*1qt#!*f%*n21d)ewc59e5ge7*8@-e' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', # a place to manage data 35 | 'django.contrib.auth', # users 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.humanize', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 42 | 'analytics', 43 | 'categories', 44 | 'courses', 45 | 'search', 46 | 'videos', 47 | ] 48 | 49 | MIDDLEWARE = [ 50 | 'django.middleware.security.SecurityMiddleware', 51 | 'django.contrib.sessions.middleware.SessionMiddleware', 52 | 'django.middleware.common.CommonMiddleware', 53 | 'django.middleware.csrf.CsrfViewMiddleware', 54 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 55 | 'django.contrib.messages.middleware.MessageMiddleware', 56 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 57 | ] 58 | 59 | ROOT_URLCONF = 'srvup.urls' 60 | 61 | TEMPLATES = [ 62 | { 63 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 64 | 'DIRS': [os.path.join(BASE_DIR, 'templates')], 65 | 'APP_DIRS': True, 66 | 'OPTIONS': { 67 | 'context_processors': [ 68 | 'django.template.context_processors.debug', 69 | 'django.template.context_processors.request', 70 | 'django.contrib.auth.context_processors.auth', 71 | 'django.contrib.messages.context_processors.messages', 72 | ], 73 | }, 74 | }, 75 | ] 76 | 77 | WSGI_APPLICATION = 'srvup.wsgi.application' 78 | 79 | 80 | # Database 81 | # https://docs.djangoproject.com/en/1.10/ref/settings/#databases 82 | 83 | DATABASES = { 84 | 'default': { 85 | 'ENGINE': 'django.db.backends.sqlite3', 86 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 87 | } 88 | } 89 | 90 | 91 | # Password validation 92 | # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators 93 | 94 | AUTH_PASSWORD_VALIDATORS = [ 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 100 | }, 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 103 | }, 104 | { 105 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 106 | }, 107 | ] 108 | 109 | 110 | # Internationalization 111 | # https://docs.djangoproject.com/en/1.10/topics/i18n/ 112 | 113 | LANGUAGE_CODE = 'en-us' 114 | 115 | TIME_ZONE = 'UTC' 116 | 117 | USE_I18N = True 118 | 119 | USE_L10N = True 120 | 121 | USE_TZ = True 122 | 123 | 124 | # Static files (CSS, JavaScript, Images) 125 | # https://docs.djangoproject.com/en/1.10/howto/static-files/ 126 | 127 | STATIC_URL = '/static/' 128 | 129 | STATICFILES_DIRS = [ 130 | os.path.join(BASE_DIR, 'static_in_env'), 131 | ] 132 | 133 | VENV_PATH = os.path.dirname(BASE_DIR) 134 | # print(VENV_PATH) 135 | STATIC_ROOT = os.path.join(VENV_PATH, 'static_root') # AWS S3 136 | 137 | MEDIA_URL = '/media/' 138 | MEDIA_ROOT = os.path.join(VENV_PATH, 'media_root') 139 | 140 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /static_root/admin/js/inlines.min.js: -------------------------------------------------------------------------------- 1 | (function(c){c.fn.formset=function(b){var a=c.extend({},c.fn.formset.defaults,b),d=c(this);b=d.parent();var k=function(a,g,l){var b=new RegExp("("+g+"-(\\d+|__prefix__))");g=g+"-"+l;c(a).prop("for")&&c(a).prop("for",c(a).prop("for").replace(b,g));a.id&&(a.id=a.id.replace(b,g));a.name&&(a.name=a.name.replace(b,g))},e=c("#id_"+a.prefix+"-TOTAL_FORMS").prop("autocomplete","off"),l=parseInt(e.val(),10),g=c("#id_"+a.prefix+"-MAX_NUM_FORMS").prop("autocomplete","off"),h=""===g.val()||0'+a.addText+""),m=b.find("tr:last a")):(d.filter(":last").after('"),m=d.filter(":last").next().find("a"));m.click(function(b){b.preventDefault();b=c("#"+a.prefix+"-empty");var f=b.clone(!0);f.removeClass(a.emptyCssClass).addClass(a.formCssClass).attr("id", 3 | a.prefix+"-"+l);f.is("tr")?f.children(":last").append('"):f.is("ul")||f.is("ol")?f.append('
  • '+a.deleteText+"
  • "):f.children(":first").append(''+a.deleteText+"");f.find("*").each(function(){k(this,a.prefix,e.val())});f.insertBefore(c(b));c(e).val(parseInt(e.val(),10)+1);l+=1;""!==g.val()&&0>=g.val()-e.val()&&m.parent().hide(); 4 | f.find("a."+a.deleteCssClass).click(function(b){b.preventDefault();f.remove();--l;a.removed&&a.removed(f);c(document).trigger("formset:removed",[f,a.prefix]);b=c("."+a.formCssClass);c("#id_"+a.prefix+"-TOTAL_FORMS").val(b.length);(""===g.val()||0 b) { 122 | return 1; 123 | } 124 | if (a < b) { 125 | return -1; 126 | } 127 | } 128 | catch (e) { 129 | // silently fail on IE 'unknown' exception 130 | } 131 | return 0; 132 | } ); 133 | }, 134 | select_all: function(id) { 135 | var box = document.getElementById(id); 136 | var boxOptions = box.options; 137 | var boxOptionsLength = boxOptions.length; 138 | for (var i = 0; i < boxOptionsLength; i++) { 139 | boxOptions[i].selected = 'selected'; 140 | } 141 | } 142 | }; 143 | window.SelectBox = SelectBox; 144 | })(django.jQuery); 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Srvup 2 logo](https://cfe2-static.s3-us-west-2.amazonaws.com/media/projects/srvup-2/images/share/srvup_2_share.png) 2 | 3 | # Srvup 2 | Video Membership 4 | Learn step-by-step to create a video membership site like [CodingForEntrepreneurs.com](http://codingforentrepreneurs.com) using Django 1.10 and Bootstrap. 5 | 6 | Video content is taking over all content on the web so it's time to build a system that allows you share videos with the world. Let's do this! 7 | 8 | 9 | Watch it [here](https://www.codingforentrepreneurs.com/projects/srvup-2/) 10 | 11 | ==== 12 | 13 | ### Lecture Code 14 | 15 | 1. Introduction 16 | 17 | 2. Walkthrough 18 | 19 | 3. Setup & Requirements 20 | 21 | 4. On Versions 22 | 23 | 5. Github 24 | 25 | [6 - Virtualenv & Django Setup](../../tree/552d2b476a6e82a2106b4b80ce71111220038ee1) 26 | 27 | [7 - First App & Model](../../tree/e28137ffc0b941649bdc369f83c7ad9f3b4196de) 28 | 29 | [8 - Admin & Create Super User](../../tree/b516953207a9af1cada6ecf16c1619ea500d691d) 30 | 31 | [9 - Making Model Changes](../../tree/5734a79a542efbc860ab84f2ab176331712592be) 32 | 33 | [10 - Customize the Admin](../../tree/bce03cffdb8d638ef5d4251404f376e870abe29c) 34 | 35 | [11 - Setup Views](../../tree/72d40c910e1ed8aa4608105bd3a62fcf14d69145) 36 | 37 | [12 - Views & Urls](../../tree/7b1fad85df52e665a46a5037b40a31dfadee9475) 38 | 39 | [13 - Loading Templates](../../tree/2b5e57426e380647d36926644cfbf60135549aa1) 40 | 41 | [14 - Context Basics](../../tree/e8f115dd0fe5c46591912a6adca3f4dc24ddeb7b) 42 | 43 | [15 - Context in Generic Views](../../tree/1f20689ac8fe53284931ae81f46fc03d812c8e37) 44 | 45 | [16 - Template Inheritance](../../tree/7dedca15455dadd1ae922d910aee51f7af19792e) 46 | 47 | [17 - Video Detail View](../../tree/67d9c1c819e6cf233c30a5df906465be844c5295) 48 | 49 | [18 - SlugField & Slug Lookups](../../tree/a203bbbc458c93ba89617467304566920f363ffe) 50 | 51 | [19 - Auto Generate Slugs with Signals](../../tree/35d753ab054c7e2aa6a378383a2eda6991f781b4) 52 | 53 | [20 - Override Get Object or Get Queryset](../../tree/6cbb0665aa0f8fc82e7b3c21024f1b996f6ff48c) 54 | 55 | [21 - Django Model Form](../../tree/c82bee8e06e155f6b966f758df944b935d39c234) 56 | 57 | [22 - Get Absolute URL](../../tree/0e57431993e09f4550e7350fcb44c3182d36106e) 58 | 59 | [23 - Delete & Update View](../../tree/62300c42ea666c26c03e36bdde6e187b75dfb4c2) 60 | 61 | [24 - Video Search View](../../tree/aba2c1a98dad63687ae55657de07f5deab51809d) 62 | 63 | [25 - Member Required Mixin](../../tree/fe50072c0551abd8a34937206e6d6e688794e406) 64 | 65 | [26 - Serving Static Files](../../tree/f270011761027f4d885005a3bd8656bffac03f61) 66 | 67 | [27 - Course Setup Part 1](../../tree/2b53d4bd7b65dc160d4b4e1bcc3074261be02d60) 68 | 69 | [28 - Urls inside Apps](../../tree/03796b218d8c13db97e50a0d2ab436c197e768c8) 70 | 71 | [29 - Course Setup Part 2](../../tree/4d331f8cee91b619ea386d2e561d3d87f2bbbbe6) 72 | 73 | [30 - Migrate Courses App](../../tree/1baae3e348d01005a5ae42e43ab447c847e5d226) 74 | 75 | [31 - MultipleObjectsReturned](../../tree/515d39bc5b1d085ce5575efe0225b74a11f86f5a) 76 | 77 | [32 - Create Unique Slug](../../tree/5ee92b739b87d8c70e5123c2988935eff4385b8f) 78 | 79 | [33 - Form Validation](../../tree/9bf8804746b47787f4a89a40cf3afe5a02dfb2d3) 80 | 81 | [34 - Minor Bootstrap Additions](../../tree/8c08e49c556a0180d837685a116cfae069cfe924) 82 | 83 | [35 - Lecture Model](../../tree/e2702d37dfc7ab1a4b17e5e676275bc115b69d1f) 84 | 85 | [36 - Add Lecture to Admin](../../tree/730a678b88ce22bfe21fbe0eb81df8a9bb2fd05d) 86 | 87 | [37 - Unique Lecture Slugs](../../tree/519788c16ee7edf864b2dcd73a38f852f5661c0c) 88 | 89 | [38 - Lecture Detail View](../../tree/48cf1dd654acef45087f1d2f2f780d43d2a36636) 90 | 91 | [39 - Dynamic Values for Foreign Key Fields](../../tree/debfa81b77b74f86a422243a633c6b4f3d527b8a) 92 | 93 | [40 - Django PositionField](../../tree/d51a2bf379fbf5a1cffd4819dab22586377afa2a) 94 | 95 | [41 - Django PositionField Part 2](../../tree/66da55f412f8ee3daef58e160880a1c41d632c8b) 96 | 97 | [42 - Display Price](../../tree/bc94ab4573a7d278ae5862bca72bb22e4046380c) 98 | 99 | [43 - My Courses](../../tree/750bf6a9790c7395e9cfe238d543ae00aa585625) 100 | 101 | [44 - Prefetch Related](../../tree/b146e402b85b6e4c62524c4ddda7b074441631a5) 102 | 103 | [45 - Manager & QuerySet method for Prefetch Related](../../tree/58c10aa14ffb3aee7688886cc42fc04a8a796491) 104 | 105 | [46 - One Click Purchase](../../tree/5a70447b6843b451305b7448513e81b8ce32c0a3) 106 | 107 | [47 - Categories App](../../tree/cb0d2ad2b39625270fa9131692511716c26b905e) 108 | 109 | [48 - Category List Groups](../../tree/0d6d94d287da86262ebed710962ce19eb8697f84) 110 | 111 | [49 - Prefetch Related Course](../../tree/d311e2cb11e273129a6165e31cc9de7e7e57bcab) 112 | 113 | [50 - Annotate Secondary Categories](../../tree/3b311ac672812e4e2dc92746cc892530ed4902cd) 114 | 115 | [51 - Category Video](../../tree/4d4ea8d20e391695403c1b6e62633a30e16e95e0) 116 | 117 | [52 - Render Video Template Tag](../../tree/5730155ae547aaa16aedde36191c60f8323b6928) 118 | 119 | [53 - Course Image Field](../../tree/bfcaf4311c91887e0fd6ec31e1e0079639104806) 120 | 121 | [54 - List Display Courses](../../tree/35718d8973edc907c0c0f1d4a1cb5a68f217a32d) 122 | 123 | [55 - Override Category Context Data](../../tree/f86b3179b2254d00be9a22b729fc03065b1edb0f) 124 | 125 | [56 - Update Course Detail View Style](../../tree/24bf712e40d660f9170a4b974f21f64f55b3fe95) 126 | 127 | [57 - Lecture View Ownership](../../tree/024232f39a9d3531a82ee07bee52f0f8b0c3f5ce) 128 | 129 | [58 - Lecture Free Preview](../../tree/999981e105284103cccdcb52f14428f37d139820) 130 | 131 | [59 - Copy QuerySet Values](../../tree/df2726d040f0d9901e600238dc79d864715dc5b9) 132 | 133 | [60 - Update Course List Display](../../tree/02ba79007eb1e7a29f02e362444e7f83fa396f0a) 134 | 135 | [61 - Pagination](../../tree/bfa8e7ceca9dfaee8e0215ca3571485fcf9724b3) 136 | 137 | [62 - Category Display Counts](../../tree/6931e31bc22a839b8968d6174ec52bbeed81bf8f) 138 | 139 | [63 - Search App](../../tree/4cfabb4048b4b8c513d977365061ac9b8775cf8a) 140 | 141 | [64 - Basic Search Results](../../tree/c6a42b0b97b26d7af5b968e07cf4a764965a2bda) 142 | 143 | [65 - Complex Lookups with Q](../../tree/cd8c7bcc58c0832d7a49b9bcee9108e9b3a3270f) 144 | 145 | [66 - Search Template Tag](../../tree/aaf65138e36c8d3f87b5d8de337866c67b5ce299) 146 | 147 | [67 - Improve Navigation](../../tree/3bf170bfe7e7a1ea4772452b26c9ecb4fd43f4eb) 148 | 149 | [68 - Style Paginatinor](../../tree/78b404b137f1e08d63dbd494b2c1d185241e8528) 150 | 151 | [69 - Featured Category on Homepage](../../tree/8be0428d0c10cca2e4d942475b0f0e7da726ce37) 152 | 153 | [70 Analytics App](../../tree/f213112e554bc865afb4721636d0856b0a3d0765) 154 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /static_root/admin/js/actions.js: -------------------------------------------------------------------------------- 1 | /*global gettext, interpolate, ngettext*/ 2 | (function($) { 3 | 'use strict'; 4 | var lastChecked; 5 | 6 | $.fn.actions = function(opts) { 7 | var options = $.extend({}, $.fn.actions.defaults, opts); 8 | var actionCheckboxes = $(this); 9 | var list_editable_changed = false; 10 | var showQuestion = function() { 11 | $(options.acrossClears).hide(); 12 | $(options.acrossQuestions).show(); 13 | $(options.allContainer).hide(); 14 | }, 15 | showClear = function() { 16 | $(options.acrossClears).show(); 17 | $(options.acrossQuestions).hide(); 18 | $(options.actionContainer).toggleClass(options.selectedClass); 19 | $(options.allContainer).show(); 20 | $(options.counterContainer).hide(); 21 | }, 22 | reset = function() { 23 | $(options.acrossClears).hide(); 24 | $(options.acrossQuestions).hide(); 25 | $(options.allContainer).hide(); 26 | $(options.counterContainer).show(); 27 | }, 28 | clearAcross = function() { 29 | reset(); 30 | $(options.acrossInput).val(0); 31 | $(options.actionContainer).removeClass(options.selectedClass); 32 | }, 33 | checker = function(checked) { 34 | if (checked) { 35 | showQuestion(); 36 | } else { 37 | reset(); 38 | } 39 | $(actionCheckboxes).prop("checked", checked) 40 | .parent().parent().toggleClass(options.selectedClass, checked); 41 | }, 42 | updateCounter = function() { 43 | var sel = $(actionCheckboxes).filter(":checked").length; 44 | // data-actions-icnt is defined in the generated HTML 45 | // and contains the total amount of objects in the queryset 46 | var actions_icnt = $('.action-counter').data('actionsIcnt'); 47 | $(options.counterContainer).html(interpolate( 48 | ngettext('%(sel)s of %(cnt)s selected', '%(sel)s of %(cnt)s selected', sel), { 49 | sel: sel, 50 | cnt: actions_icnt 51 | }, true)); 52 | $(options.allToggle).prop("checked", function() { 53 | var value; 54 | if (sel === actionCheckboxes.length) { 55 | value = true; 56 | showQuestion(); 57 | } else { 58 | value = false; 59 | clearAcross(); 60 | } 61 | return value; 62 | }); 63 | }; 64 | // Show counter by default 65 | $(options.counterContainer).show(); 66 | // Check state of checkboxes and reinit state if needed 67 | $(this).filter(":checked").each(function(i) { 68 | $(this).parent().parent().toggleClass(options.selectedClass); 69 | updateCounter(); 70 | if ($(options.acrossInput).val() === 1) { 71 | showClear(); 72 | } 73 | }); 74 | $(options.allToggle).show().click(function() { 75 | checker($(this).prop("checked")); 76 | updateCounter(); 77 | }); 78 | $("a", options.acrossQuestions).click(function(event) { 79 | event.preventDefault(); 80 | $(options.acrossInput).val(1); 81 | showClear(); 82 | }); 83 | $("a", options.acrossClears).click(function(event) { 84 | event.preventDefault(); 85 | $(options.allToggle).prop("checked", false); 86 | clearAcross(); 87 | checker(0); 88 | updateCounter(); 89 | }); 90 | lastChecked = null; 91 | $(actionCheckboxes).click(function(event) { 92 | if (!event) { event = window.event; } 93 | var target = event.target ? event.target : event.srcElement; 94 | if (lastChecked && $.data(lastChecked) !== $.data(target) && event.shiftKey === true) { 95 | var inrange = false; 96 | $(lastChecked).prop("checked", target.checked) 97 | .parent().parent().toggleClass(options.selectedClass, target.checked); 98 | $(actionCheckboxes).each(function() { 99 | if ($.data(this) === $.data(lastChecked) || $.data(this) === $.data(target)) { 100 | inrange = (inrange) ? false : true; 101 | } 102 | if (inrange) { 103 | $(this).prop("checked", target.checked) 104 | .parent().parent().toggleClass(options.selectedClass, target.checked); 105 | } 106 | }); 107 | } 108 | $(target).parent().parent().toggleClass(options.selectedClass, target.checked); 109 | lastChecked = target; 110 | updateCounter(); 111 | }); 112 | $('form#changelist-form table#result_list tr').find('td:gt(0) :input').change(function() { 113 | list_editable_changed = true; 114 | }); 115 | $('form#changelist-form button[name="index"]').click(function(event) { 116 | if (list_editable_changed) { 117 | return confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost.")); 118 | } 119 | }); 120 | $('form#changelist-form input[name="_save"]').click(function(event) { 121 | var action_changed = false; 122 | $('select option:selected', options.actionContainer).each(function() { 123 | if ($(this).val()) { 124 | action_changed = true; 125 | } 126 | }); 127 | if (action_changed) { 128 | if (list_editable_changed) { 129 | return confirm(gettext("You have selected an action, but you haven't saved your changes to individual fields yet. Please click OK to save. You'll need to re-run the action.")); 130 | } else { 131 | return confirm(gettext("You have selected an action, and you haven't made any changes on individual fields. You're probably looking for the Go button rather than the Save button.")); 132 | } 133 | } 134 | }); 135 | }; 136 | /* Setup plugin defaults */ 137 | $.fn.actions.defaults = { 138 | actionContainer: "div.actions", 139 | counterContainer: "span.action-counter", 140 | allContainer: "div.actions span.all", 141 | acrossInput: "div.actions input.select-across", 142 | acrossQuestions: "div.actions span.question", 143 | acrossClears: "div.actions span.clear", 144 | allToggle: "#action-toggle", 145 | selectedClass: "selected" 146 | }; 147 | $(document).ready(function() { 148 | var $actionsEls = $('tr input.action-select'); 149 | if ($actionsEls.length > 0) { 150 | $actionsEls.actions(); 151 | } 152 | }); 153 | })(django.jQuery); 154 | -------------------------------------------------------------------------------- /static_root/admin/js/admin/RelatedObjectLookups.js: -------------------------------------------------------------------------------- 1 | /*global SelectBox, interpolate*/ 2 | // Handles related-objects functionality: lookup link for raw_id_fields 3 | // and Add Another links. 4 | 5 | (function($) { 6 | 'use strict'; 7 | 8 | // IE doesn't accept periods or dashes in the window name, but the element IDs 9 | // we use to generate popup window names may contain them, therefore we map them 10 | // to allowed characters in a reversible way so that we can locate the correct 11 | // element when the popup window is dismissed. 12 | function id_to_windowname(text) { 13 | text = text.replace(/\./g, '__dot__'); 14 | text = text.replace(/\-/g, '__dash__'); 15 | return text; 16 | } 17 | 18 | function windowname_to_id(text) { 19 | text = text.replace(/__dot__/g, '.'); 20 | text = text.replace(/__dash__/g, '-'); 21 | return text; 22 | } 23 | 24 | function showAdminPopup(triggeringLink, name_regexp, add_popup) { 25 | var name = triggeringLink.id.replace(name_regexp, ''); 26 | name = id_to_windowname(name); 27 | var href = triggeringLink.href; 28 | if (add_popup) { 29 | if (href.indexOf('?') === -1) { 30 | href += '?_popup=1'; 31 | } else { 32 | href += '&_popup=1'; 33 | } 34 | } 35 | var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes'); 36 | win.focus(); 37 | return false; 38 | } 39 | 40 | function showRelatedObjectLookupPopup(triggeringLink) { 41 | return showAdminPopup(triggeringLink, /^lookup_/, true); 42 | } 43 | 44 | function dismissRelatedLookupPopup(win, chosenId) { 45 | var name = windowname_to_id(win.name); 46 | var elem = document.getElementById(name); 47 | if (elem.className.indexOf('vManyToManyRawIdAdminField') !== -1 && elem.value) { 48 | elem.value += ',' + chosenId; 49 | } else { 50 | document.getElementById(name).value = chosenId; 51 | } 52 | win.close(); 53 | } 54 | 55 | function showRelatedObjectPopup(triggeringLink) { 56 | return showAdminPopup(triggeringLink, /^(change|add|delete)_/, false); 57 | } 58 | 59 | function updateRelatedObjectLinks(triggeringLink) { 60 | var $this = $(triggeringLink); 61 | var siblings = $this.nextAll('.change-related, .delete-related'); 62 | if (!siblings.length) { 63 | return; 64 | } 65 | var value = $this.val(); 66 | if (value) { 67 | siblings.each(function() { 68 | var elm = $(this); 69 | elm.attr('href', elm.attr('data-href-template').replace('__fk__', value)); 70 | }); 71 | } else { 72 | siblings.removeAttr('href'); 73 | } 74 | } 75 | 76 | function dismissAddRelatedObjectPopup(win, newId, newRepr) { 77 | var name = windowname_to_id(win.name); 78 | var elem = document.getElementById(name); 79 | if (elem) { 80 | var elemName = elem.nodeName.toUpperCase(); 81 | if (elemName === 'SELECT') { 82 | elem.options[elem.options.length] = new Option(newRepr, newId, true, true); 83 | } else if (elemName === 'INPUT') { 84 | if (elem.className.indexOf('vManyToManyRawIdAdminField') !== -1 && elem.value) { 85 | elem.value += ',' + newId; 86 | } else { 87 | elem.value = newId; 88 | } 89 | } 90 | // Trigger a change event to update related links if required. 91 | $(elem).trigger('change'); 92 | } else { 93 | var toId = name + "_to"; 94 | var o = new Option(newRepr, newId); 95 | SelectBox.add_to_cache(toId, o); 96 | SelectBox.redisplay(toId); 97 | } 98 | win.close(); 99 | } 100 | 101 | function dismissChangeRelatedObjectPopup(win, objId, newRepr, newId) { 102 | var id = windowname_to_id(win.name).replace(/^edit_/, ''); 103 | var selectsSelector = interpolate('#%s, #%s_from, #%s_to', [id, id, id]); 104 | var selects = $(selectsSelector); 105 | selects.find('option').each(function() { 106 | if (this.value === objId) { 107 | this.textContent = newRepr; 108 | this.value = newId; 109 | } 110 | }); 111 | win.close(); 112 | } 113 | 114 | function dismissDeleteRelatedObjectPopup(win, objId) { 115 | var id = windowname_to_id(win.name).replace(/^delete_/, ''); 116 | var selectsSelector = interpolate('#%s, #%s_from, #%s_to', [id, id, id]); 117 | var selects = $(selectsSelector); 118 | selects.find('option').each(function() { 119 | if (this.value === objId) { 120 | $(this).remove(); 121 | } 122 | }).trigger('change'); 123 | win.close(); 124 | } 125 | 126 | // Global for testing purposes 127 | window.id_to_windowname = id_to_windowname; 128 | window.windowname_to_id = windowname_to_id; 129 | 130 | window.showRelatedObjectLookupPopup = showRelatedObjectLookupPopup; 131 | window.dismissRelatedLookupPopup = dismissRelatedLookupPopup; 132 | window.showRelatedObjectPopup = showRelatedObjectPopup; 133 | window.updateRelatedObjectLinks = updateRelatedObjectLinks; 134 | window.dismissAddRelatedObjectPopup = dismissAddRelatedObjectPopup; 135 | window.dismissChangeRelatedObjectPopup = dismissChangeRelatedObjectPopup; 136 | window.dismissDeleteRelatedObjectPopup = dismissDeleteRelatedObjectPopup; 137 | 138 | // Kept for backward compatibility 139 | window.showAddAnotherPopup = showRelatedObjectPopup; 140 | window.dismissAddAnotherPopup = dismissAddRelatedObjectPopup; 141 | 142 | $(document).ready(function() { 143 | $("a[data-popup-opener]").click(function(event) { 144 | event.preventDefault(); 145 | opener.dismissRelatedLookupPopup(window, $(this).data("popup-opener")); 146 | }); 147 | $('body').on('click', '.related-widget-wrapper-link', function(e) { 148 | e.preventDefault(); 149 | if (this.href) { 150 | var event = $.Event('django:show-related', {href: this.href}); 151 | $(this).trigger(event); 152 | if (!event.isDefaultPrevented()) { 153 | showRelatedObjectPopup(this); 154 | } 155 | } 156 | }); 157 | $('body').on('change', '.related-widget-wrapper select', function(e) { 158 | var event = $.Event('django:update-related'); 159 | $(this).trigger(event); 160 | if (!event.isDefaultPrevented()) { 161 | updateRelatedObjectLinks(this); 162 | } 163 | }); 164 | $('.related-widget-wrapper select').trigger('change'); 165 | $('.related-lookup').click(function(e) { 166 | e.preventDefault(); 167 | var event = $.Event('django:lookup-related'); 168 | $(this).trigger(event); 169 | if (!event.isDefaultPrevented()) { 170 | showRelatedObjectLookupPopup(this); 171 | } 172 | }); 173 | }); 174 | 175 | })(django.jQuery); 176 | -------------------------------------------------------------------------------- /static_root/admin/css/changelists.css: -------------------------------------------------------------------------------- 1 | /* CHANGELISTS */ 2 | 3 | #changelist { 4 | position: relative; 5 | width: 100%; 6 | } 7 | 8 | #changelist table { 9 | width: 100%; 10 | } 11 | 12 | .change-list .hiddenfields { display:none; } 13 | 14 | .change-list .filtered table { 15 | border-right: none; 16 | } 17 | 18 | .change-list .filtered { 19 | min-height: 400px; 20 | } 21 | 22 | .change-list .filtered .results, .change-list .filtered .paginator, 23 | .filtered #toolbar, .filtered div.xfull { 24 | margin-right: 280px; 25 | width: auto; 26 | } 27 | 28 | .change-list .filtered table tbody th { 29 | padding-right: 1em; 30 | } 31 | 32 | #changelist-form .results { 33 | overflow-x: auto; 34 | } 35 | 36 | #changelist .toplinks { 37 | border-bottom: 1px solid #ddd; 38 | } 39 | 40 | #changelist .paginator { 41 | color: #666; 42 | border-bottom: 1px solid #eee; 43 | background: #fff; 44 | overflow: hidden; 45 | } 46 | 47 | /* CHANGELIST TABLES */ 48 | 49 | #changelist table thead th { 50 | padding: 0; 51 | white-space: nowrap; 52 | vertical-align: middle; 53 | } 54 | 55 | #changelist table thead th.action-checkbox-column { 56 | width: 1.5em; 57 | text-align: center; 58 | } 59 | 60 | #changelist table tbody td.action-checkbox { 61 | text-align: center; 62 | } 63 | 64 | #changelist table tfoot { 65 | color: #666; 66 | } 67 | 68 | /* TOOLBAR */ 69 | 70 | #changelist #toolbar { 71 | padding: 8px 10px; 72 | margin-bottom: 15px; 73 | border-top: 1px solid #eee; 74 | border-bottom: 1px solid #eee; 75 | background: #f8f8f8; 76 | color: #666; 77 | } 78 | 79 | #changelist #toolbar form input { 80 | border-radius: 4px; 81 | font-size: 14px; 82 | padding: 5px; 83 | color: #333; 84 | } 85 | 86 | #changelist #toolbar form #searchbar { 87 | height: 19px; 88 | border: 1px solid #ccc; 89 | padding: 2px 5px; 90 | margin: 0; 91 | vertical-align: top; 92 | font-size: 13px; 93 | } 94 | 95 | #changelist #toolbar form #searchbar:focus { 96 | border-color: #999; 97 | } 98 | 99 | #changelist #toolbar form input[type="submit"] { 100 | border: 1px solid #ccc; 101 | padding: 2px 10px; 102 | margin: 0; 103 | vertical-align: middle; 104 | background: #fff; 105 | box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset; 106 | cursor: pointer; 107 | color: #333; 108 | } 109 | 110 | #changelist #toolbar form input[type="submit"]:focus, 111 | #changelist #toolbar form input[type="submit"]:hover { 112 | border-color: #999; 113 | } 114 | 115 | #changelist #changelist-search img { 116 | vertical-align: middle; 117 | margin-right: 4px; 118 | } 119 | 120 | /* FILTER COLUMN */ 121 | 122 | #changelist-filter { 123 | position: absolute; 124 | top: 0; 125 | right: 0; 126 | z-index: 1000; 127 | width: 240px; 128 | background: #f8f8f8; 129 | border-left: none; 130 | margin: 0; 131 | } 132 | 133 | #changelist-filter h2 { 134 | font-size: 14px; 135 | text-transform: uppercase; 136 | letter-spacing: 0.5px; 137 | padding: 5px 15px; 138 | margin-bottom: 12px; 139 | border-bottom: none; 140 | } 141 | 142 | #changelist-filter h3 { 143 | font-weight: 400; 144 | font-size: 14px; 145 | padding: 0 15px; 146 | margin-bottom: 10px; 147 | } 148 | 149 | #changelist-filter ul { 150 | margin: 5px 0; 151 | padding: 0 15px 15px; 152 | border-bottom: 1px solid #eaeaea; 153 | } 154 | 155 | #changelist-filter ul:last-child { 156 | border-bottom: none; 157 | padding-bottom: none; 158 | } 159 | 160 | #changelist-filter li { 161 | list-style-type: none; 162 | margin-left: 0; 163 | padding-left: 0; 164 | } 165 | 166 | #changelist-filter a { 167 | display: block; 168 | color: #999; 169 | } 170 | 171 | #changelist-filter li.selected { 172 | border-left: 5px solid #eaeaea; 173 | padding-left: 10px; 174 | margin-left: -15px; 175 | } 176 | 177 | #changelist-filter li.selected a { 178 | color: #5b80b2; 179 | } 180 | 181 | #changelist-filter a:focus, #changelist-filter a:hover, 182 | #changelist-filter li.selected a:focus, 183 | #changelist-filter li.selected a:hover { 184 | color: #036; 185 | } 186 | 187 | /* DATE DRILLDOWN */ 188 | 189 | .change-list ul.toplinks { 190 | display: block; 191 | float: left; 192 | padding: 0; 193 | margin: 0; 194 | width: 100%; 195 | } 196 | 197 | .change-list ul.toplinks li { 198 | padding: 3px 6px; 199 | font-weight: bold; 200 | list-style-type: none; 201 | display: inline-block; 202 | } 203 | 204 | .change-list ul.toplinks .date-back a { 205 | color: #999; 206 | } 207 | 208 | .change-list ul.toplinks .date-back a:focus, 209 | .change-list ul.toplinks .date-back a:hover { 210 | color: #036; 211 | } 212 | 213 | /* PAGINATOR */ 214 | 215 | .paginator { 216 | font-size: 13px; 217 | padding-top: 10px; 218 | padding-bottom: 10px; 219 | line-height: 22px; 220 | margin: 0; 221 | border-top: 1px solid #ddd; 222 | } 223 | 224 | .paginator a:link, .paginator a:visited { 225 | padding: 2px 6px; 226 | background: #79aec8; 227 | text-decoration: none; 228 | color: #fff; 229 | } 230 | 231 | .paginator a.showall { 232 | padding: 0; 233 | border: none; 234 | background: none; 235 | color: #5b80b2; 236 | } 237 | 238 | .paginator a.showall:focus, .paginator a.showall:hover { 239 | background: none; 240 | color: #036; 241 | } 242 | 243 | .paginator .end { 244 | margin-right: 6px; 245 | } 246 | 247 | .paginator .this-page { 248 | padding: 2px 6px; 249 | font-weight: bold; 250 | font-size: 13px; 251 | vertical-align: top; 252 | } 253 | 254 | .paginator a:focus, .paginator a:hover { 255 | color: white; 256 | background: #036; 257 | } 258 | 259 | /* ACTIONS */ 260 | 261 | .filtered .actions { 262 | margin-right: 280px; 263 | border-right: none; 264 | } 265 | 266 | #changelist table input { 267 | margin: 0; 268 | vertical-align: baseline; 269 | } 270 | 271 | #changelist table tbody tr.selected { 272 | background-color: #FFFFCC; 273 | } 274 | 275 | #changelist .actions { 276 | padding: 10px; 277 | background: #fff; 278 | border-top: none; 279 | border-bottom: none; 280 | line-height: 24px; 281 | color: #999; 282 | } 283 | 284 | #changelist .actions.selected { 285 | background: #fffccf; 286 | border-top: 1px solid #fffee8; 287 | border-bottom: 1px solid #edecd6; 288 | } 289 | 290 | #changelist .actions span.all, 291 | #changelist .actions span.action-counter, 292 | #changelist .actions span.clear, 293 | #changelist .actions span.question { 294 | font-size: 13px; 295 | margin: 0 0.5em; 296 | display: none; 297 | } 298 | 299 | #changelist .actions:last-child { 300 | border-bottom: none; 301 | } 302 | 303 | #changelist .actions select { 304 | vertical-align: top; 305 | height: 24px; 306 | background: none; 307 | color: #000; 308 | border: 1px solid #ccc; 309 | border-radius: 4px; 310 | font-size: 14px; 311 | padding: 0 0 0 4px; 312 | margin: 0; 313 | margin-left: 10px; 314 | } 315 | 316 | #changelist .actions select:focus { 317 | border-color: #999; 318 | } 319 | 320 | #changelist .actions label { 321 | display: inline-block; 322 | vertical-align: middle; 323 | font-size: 13px; 324 | } 325 | 326 | #changelist .actions .button { 327 | font-size: 13px; 328 | border: 1px solid #ccc; 329 | border-radius: 4px; 330 | background: #fff; 331 | box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset; 332 | cursor: pointer; 333 | height: 24px; 334 | line-height: 1; 335 | padding: 4px 8px; 336 | margin: 0; 337 | color: #333; 338 | } 339 | 340 | #changelist .actions .button:focus, #changelist .actions .button:hover { 341 | border-color: #999; 342 | } 343 | -------------------------------------------------------------------------------- /static_root/admin/js/urlify.js: -------------------------------------------------------------------------------- 1 | /*global XRegExp*/ 2 | (function() { 3 | 'use strict'; 4 | 5 | var LATIN_MAP = { 6 | 'À': 'A', 'Á': 'A', 'Â': 'A', 'Ã': 'A', 'Ä': 'A', 'Å': 'A', 'Æ': 'AE', 7 | 'Ç': 'C', 'È': 'E', 'É': 'E', 'Ê': 'E', 'Ë': 'E', 'Ì': 'I', 'Í': 'I', 8 | 'Î': 'I', 'Ï': 'I', 'Ð': 'D', 'Ñ': 'N', 'Ò': 'O', 'Ó': 'O', 'Ô': 'O', 9 | 'Õ': 'O', 'Ö': 'O', 'Ő': 'O', 'Ø': 'O', 'Ù': 'U', 'Ú': 'U', 'Û': 'U', 10 | 'Ü': 'U', 'Ű': 'U', 'Ý': 'Y', 'Þ': 'TH', 'Ÿ': 'Y', 'ß': 'ss', 'à': 'a', 11 | 'á': 'a', 'â': 'a', 'ã': 'a', 'ä': 'a', 'å': 'a', 'æ': 'ae', 'ç': 'c', 12 | 'è': 'e', 'é': 'e', 'ê': 'e', 'ë': 'e', 'ì': 'i', 'í': 'i', 'î': 'i', 13 | 'ï': 'i', 'ð': 'd', 'ñ': 'n', 'ò': 'o', 'ó': 'o', 'ô': 'o', 'õ': 'o', 14 | 'ö': 'o', 'ő': 'o', 'ø': 'o', 'ù': 'u', 'ú': 'u', 'û': 'u', 'ü': 'u', 15 | 'ű': 'u', 'ý': 'y', 'þ': 'th', 'ÿ': 'y' 16 | }; 17 | var LATIN_SYMBOLS_MAP = { 18 | '©': '(c)' 19 | }; 20 | var GREEK_MAP = { 21 | 'α': 'a', 'β': 'b', 'γ': 'g', 'δ': 'd', 'ε': 'e', 'ζ': 'z', 'η': 'h', 22 | 'θ': '8', 'ι': 'i', 'κ': 'k', 'λ': 'l', 'μ': 'm', 'ν': 'n', 'ξ': '3', 23 | 'ο': 'o', 'π': 'p', 'ρ': 'r', 'σ': 's', 'τ': 't', 'υ': 'y', 'φ': 'f', 24 | 'χ': 'x', 'ψ': 'ps', 'ω': 'w', 'ά': 'a', 'έ': 'e', 'ί': 'i', 'ό': 'o', 25 | 'ύ': 'y', 'ή': 'h', 'ώ': 'w', 'ς': 's', 'ϊ': 'i', 'ΰ': 'y', 'ϋ': 'y', 26 | 'ΐ': 'i', 'Α': 'A', 'Β': 'B', 'Γ': 'G', 'Δ': 'D', 'Ε': 'E', 'Ζ': 'Z', 27 | 'Η': 'H', 'Θ': '8', 'Ι': 'I', 'Κ': 'K', 'Λ': 'L', 'Μ': 'M', 'Ν': 'N', 28 | 'Ξ': '3', 'Ο': 'O', 'Π': 'P', 'Ρ': 'R', 'Σ': 'S', 'Τ': 'T', 'Υ': 'Y', 29 | 'Φ': 'F', 'Χ': 'X', 'Ψ': 'PS', 'Ω': 'W', 'Ά': 'A', 'Έ': 'E', 'Ί': 'I', 30 | 'Ό': 'O', 'Ύ': 'Y', 'Ή': 'H', 'Ώ': 'W', 'Ϊ': 'I', 'Ϋ': 'Y' 31 | }; 32 | var TURKISH_MAP = { 33 | 'ş': 's', 'Ş': 'S', 'ı': 'i', 'İ': 'I', 'ç': 'c', 'Ç': 'C', 'ü': 'u', 34 | 'Ü': 'U', 'ö': 'o', 'Ö': 'O', 'ğ': 'g', 'Ğ': 'G' 35 | }; 36 | var ROMANIAN_MAP = { 37 | 'ă': 'a', 'î': 'i', 'ș': 's', 'ț': 't', 'â': 'a', 38 | 'Ă': 'A', 'Î': 'I', 'Ș': 'S', 'Ț': 'T', 'Â': 'A' 39 | }; 40 | var RUSSIAN_MAP = { 41 | 'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo', 42 | 'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'j', 'к': 'k', 'л': 'l', 'м': 'm', 43 | 'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u', 44 | 'ф': 'f', 'х': 'h', 'ц': 'c', 'ч': 'ch', 'ш': 'sh', 'щ': 'sh', 'ъ': '', 45 | 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya', 46 | 'А': 'A', 'Б': 'B', 'В': 'V', 'Г': 'G', 'Д': 'D', 'Е': 'E', 'Ё': 'Yo', 47 | 'Ж': 'Zh', 'З': 'Z', 'И': 'I', 'Й': 'J', 'К': 'K', 'Л': 'L', 'М': 'M', 48 | 'Н': 'N', 'О': 'O', 'П': 'P', 'Р': 'R', 'С': 'S', 'Т': 'T', 'У': 'U', 49 | 'Ф': 'F', 'Х': 'H', 'Ц': 'C', 'Ч': 'Ch', 'Ш': 'Sh', 'Щ': 'Sh', 'Ъ': '', 50 | 'Ы': 'Y', 'Ь': '', 'Э': 'E', 'Ю': 'Yu', 'Я': 'Ya' 51 | }; 52 | var UKRAINIAN_MAP = { 53 | 'Є': 'Ye', 'І': 'I', 'Ї': 'Yi', 'Ґ': 'G', 'є': 'ye', 'і': 'i', 54 | 'ї': 'yi', 'ґ': 'g' 55 | }; 56 | var CZECH_MAP = { 57 | 'č': 'c', 'ď': 'd', 'ě': 'e', 'ň': 'n', 'ř': 'r', 'š': 's', 'ť': 't', 58 | 'ů': 'u', 'ž': 'z', 'Č': 'C', 'Ď': 'D', 'Ě': 'E', 'Ň': 'N', 'Ř': 'R', 59 | 'Š': 'S', 'Ť': 'T', 'Ů': 'U', 'Ž': 'Z' 60 | }; 61 | var POLISH_MAP = { 62 | 'ą': 'a', 'ć': 'c', 'ę': 'e', 'ł': 'l', 'ń': 'n', 'ó': 'o', 'ś': 's', 63 | 'ź': 'z', 'ż': 'z', 64 | 'Ą': 'A', 'Ć': 'C', 'Ę': 'E', 'Ł': 'L', 'Ń': 'N', 'Ó': 'O', 'Ś': 'S', 65 | 'Ź': 'Z', 'Ż': 'Z' 66 | }; 67 | var LATVIAN_MAP = { 68 | 'ā': 'a', 'č': 'c', 'ē': 'e', 'ģ': 'g', 'ī': 'i', 'ķ': 'k', 'ļ': 'l', 69 | 'ņ': 'n', 'š': 's', 'ū': 'u', 'ž': 'z', 70 | 'Ā': 'A', 'Č': 'C', 'Ē': 'E', 'Ģ': 'G', 'Ī': 'I', 'Ķ': 'K', 'Ļ': 'L', 71 | 'Ņ': 'N', 'Š': 'S', 'Ū': 'U', 'Ž': 'Z' 72 | }; 73 | var ARABIC_MAP = { 74 | 'أ': 'a', 'ب': 'b', 'ت': 't', 'ث': 'th', 'ج': 'g', 'ح': 'h', 'خ': 'kh', 'د': 'd', 75 | 'ذ': 'th', 'ر': 'r', 'ز': 'z', 'س': 's', 'ش': 'sh', 'ص': 's', 'ض': 'd', 'ط': 't', 76 | 'ظ': 'th', 'ع': 'aa', 'غ': 'gh', 'ف': 'f', 'ق': 'k', 'ك': 'k', 'ل': 'l', 'م': 'm', 77 | 'ن': 'n', 'ه': 'h', 'و': 'o', 'ي': 'y' 78 | }; 79 | var LITHUANIAN_MAP = { 80 | 'ą': 'a', 'č': 'c', 'ę': 'e', 'ė': 'e', 'į': 'i', 'š': 's', 'ų': 'u', 81 | 'ū': 'u', 'ž': 'z', 82 | 'Ą': 'A', 'Č': 'C', 'Ę': 'E', 'Ė': 'E', 'Į': 'I', 'Š': 'S', 'Ų': 'U', 83 | 'Ū': 'U', 'Ž': 'Z' 84 | }; 85 | var SERBIAN_MAP = { 86 | 'ђ': 'dj', 'ј': 'j', 'љ': 'lj', 'њ': 'nj', 'ћ': 'c', 'џ': 'dz', 87 | 'đ': 'dj', 'Ђ': 'Dj', 'Ј': 'j', 'Љ': 'Lj', 'Њ': 'Nj', 'Ћ': 'C', 88 | 'Џ': 'Dz', 'Đ': 'Dj' 89 | }; 90 | var AZERBAIJANI_MAP = { 91 | 'ç': 'c', 'ə': 'e', 'ğ': 'g', 'ı': 'i', 'ö': 'o', 'ş': 's', 'ü': 'u', 92 | 'Ç': 'C', 'Ə': 'E', 'Ğ': 'G', 'İ': 'I', 'Ö': 'O', 'Ş': 'S', 'Ü': 'U' 93 | }; 94 | var GEORGIAN_MAP = { 95 | 'ა': 'a', 'ბ': 'b', 'გ': 'g', 'დ': 'd', 'ე': 'e', 'ვ': 'v', 'ზ': 'z', 96 | 'თ': 't', 'ი': 'i', 'კ': 'k', 'ლ': 'l', 'მ': 'm', 'ნ': 'n', 'ო': 'o', 97 | 'პ': 'p', 'ჟ': 'j', 'რ': 'r', 'ს': 's', 'ტ': 't', 'უ': 'u', 'ფ': 'f', 98 | 'ქ': 'q', 'ღ': 'g', 'ყ': 'y', 'შ': 'sh', 'ჩ': 'ch', 'ც': 'c', 'ძ': 'dz', 99 | 'წ': 'w', 'ჭ': 'ch', 'ხ': 'x', 'ჯ': 'j', 'ჰ': 'h' 100 | }; 101 | 102 | var ALL_DOWNCODE_MAPS = [ 103 | LATIN_MAP, 104 | LATIN_SYMBOLS_MAP, 105 | GREEK_MAP, 106 | TURKISH_MAP, 107 | ROMANIAN_MAP, 108 | RUSSIAN_MAP, 109 | UKRAINIAN_MAP, 110 | CZECH_MAP, 111 | POLISH_MAP, 112 | LATVIAN_MAP, 113 | ARABIC_MAP, 114 | LITHUANIAN_MAP, 115 | SERBIAN_MAP, 116 | AZERBAIJANI_MAP, 117 | GEORGIAN_MAP 118 | ]; 119 | 120 | var Downcoder = { 121 | 'Initialize': function() { 122 | if (Downcoder.map) { // already made 123 | return; 124 | } 125 | Downcoder.map = {}; 126 | Downcoder.chars = []; 127 | for (var i = 0; i < ALL_DOWNCODE_MAPS.length; i++) { 128 | var lookup = ALL_DOWNCODE_MAPS[i]; 129 | for (var c in lookup) { 130 | if (lookup.hasOwnProperty(c)) { 131 | Downcoder.map[c] = lookup[c]; 132 | } 133 | } 134 | } 135 | for (var k in Downcoder.map) { 136 | if (Downcoder.map.hasOwnProperty(k)) { 137 | Downcoder.chars.push(k); 138 | } 139 | } 140 | Downcoder.regex = new RegExp(Downcoder.chars.join('|'), 'g'); 141 | } 142 | }; 143 | 144 | function downcode(slug) { 145 | Downcoder.Initialize(); 146 | return slug.replace(Downcoder.regex, function(m) { 147 | return Downcoder.map[m]; 148 | }); 149 | } 150 | 151 | 152 | function URLify(s, num_chars, allowUnicode) { 153 | // changes, e.g., "Petty theft" to "petty-theft" 154 | // remove all these words from the string before urlifying 155 | if (!allowUnicode) { 156 | s = downcode(s); 157 | } 158 | var removelist = [ 159 | "a", "an", "as", "at", "before", "but", "by", "for", "from", "is", 160 | "in", "into", "like", "of", "off", "on", "onto", "per", "since", 161 | "than", "the", "this", "that", "to", "up", "via", "with" 162 | ]; 163 | var r = new RegExp('\\b(' + removelist.join('|') + ')\\b', 'gi'); 164 | s = s.replace(r, ''); 165 | // if downcode doesn't hit, the char will be stripped here 166 | if (allowUnicode) { 167 | // Keep Unicode letters including both lowercase and uppercase 168 | // characters, whitespace, and dash; remove other characters. 169 | s = XRegExp.replace(s, XRegExp('[^-_\\p{L}\\p{N}\\s]', 'g'), ''); 170 | } else { 171 | s = s.replace(/[^-\w\s]/g, ''); // remove unneeded chars 172 | } 173 | s = s.replace(/^\s+|\s+$/g, ''); // trim leading/trailing spaces 174 | s = s.replace(/[-\s]+/g, '-'); // convert spaces to hyphens 175 | s = s.toLowerCase(); // convert to lowercase 176 | return s.substring(0, num_chars); // trim to first num_chars chars 177 | } 178 | window.URLify = URLify; 179 | })(); 180 | -------------------------------------------------------------------------------- /static_root/admin/js/calendar.js: -------------------------------------------------------------------------------- 1 | /*global gettext, pgettext, get_format, quickElement, removeChildren, addEvent*/ 2 | /* 3 | calendar.js - Calendar functions by Adrian Holovaty 4 | depends on core.js for utility functions like removeChildren or quickElement 5 | */ 6 | 7 | (function() { 8 | 'use strict'; 9 | // CalendarNamespace -- Provides a collection of HTML calendar-related helper functions 10 | var CalendarNamespace = { 11 | monthsOfYear: [ 12 | gettext('January'), 13 | gettext('February'), 14 | gettext('March'), 15 | gettext('April'), 16 | gettext('May'), 17 | gettext('June'), 18 | gettext('July'), 19 | gettext('August'), 20 | gettext('September'), 21 | gettext('October'), 22 | gettext('November'), 23 | gettext('December') 24 | ], 25 | daysOfWeek: [ 26 | pgettext('one letter Sunday', 'S'), 27 | pgettext('one letter Monday', 'M'), 28 | pgettext('one letter Tuesday', 'T'), 29 | pgettext('one letter Wednesday', 'W'), 30 | pgettext('one letter Thursday', 'T'), 31 | pgettext('one letter Friday', 'F'), 32 | pgettext('one letter Saturday', 'S') 33 | ], 34 | firstDayOfWeek: parseInt(get_format('FIRST_DAY_OF_WEEK')), 35 | isLeapYear: function(year) { 36 | return (((year % 4) === 0) && ((year % 100) !== 0 ) || ((year % 400) === 0)); 37 | }, 38 | getDaysInMonth: function(month, year) { 39 | var days; 40 | if (month === 1 || month === 3 || month === 5 || month === 7 || month === 8 || month === 10 || month === 12) { 41 | days = 31; 42 | } 43 | else if (month === 4 || month === 6 || month === 9 || month === 11) { 44 | days = 30; 45 | } 46 | else if (month === 2 && CalendarNamespace.isLeapYear(year)) { 47 | days = 29; 48 | } 49 | else { 50 | days = 28; 51 | } 52 | return days; 53 | }, 54 | draw: function(month, year, div_id, callback, selected) { // month = 1-12, year = 1-9999 55 | var today = new Date(); 56 | var todayDay = today.getDate(); 57 | var todayMonth = today.getMonth() + 1; 58 | var todayYear = today.getFullYear(); 59 | var todayClass = ''; 60 | 61 | // Use UTC functions here because the date field does not contain time 62 | // and using the UTC function variants prevent the local time offset 63 | // from altering the date, specifically the day field. For example: 64 | // 65 | // ``` 66 | // var x = new Date('2013-10-02'); 67 | // var day = x.getDate(); 68 | // ``` 69 | // 70 | // The day variable above will be 1 instead of 2 in, say, US Pacific time 71 | // zone. 72 | var isSelectedMonth = false; 73 | if (typeof selected !== 'undefined') { 74 | isSelectedMonth = (selected.getUTCFullYear() === year && (selected.getUTCMonth() + 1) === month); 75 | } 76 | 77 | month = parseInt(month); 78 | year = parseInt(year); 79 | var calDiv = document.getElementById(div_id); 80 | removeChildren(calDiv); 81 | var calTable = document.createElement('table'); 82 | quickElement('caption', calTable, CalendarNamespace.monthsOfYear[month - 1] + ' ' + year); 83 | var tableBody = quickElement('tbody', calTable); 84 | 85 | // Draw days-of-week header 86 | var tableRow = quickElement('tr', tableBody); 87 | for (var i = 0; i < 7; i++) { 88 | quickElement('th', tableRow, CalendarNamespace.daysOfWeek[(i + CalendarNamespace.firstDayOfWeek) % 7]); 89 | } 90 | 91 | var startingPos = new Date(year, month - 1, 1 - CalendarNamespace.firstDayOfWeek).getDay(); 92 | var days = CalendarNamespace.getDaysInMonth(month, year); 93 | 94 | var nonDayCell; 95 | 96 | // Draw blanks before first of month 97 | tableRow = quickElement('tr', tableBody); 98 | for (i = 0; i < startingPos; i++) { 99 | nonDayCell = quickElement('td', tableRow, ' '); 100 | nonDayCell.className = "nonday"; 101 | } 102 | 103 | function calendarMonth(y, m) { 104 | function onClick(e) { 105 | e.preventDefault(); 106 | callback(y, m, django.jQuery(this).text()); 107 | } 108 | return onClick; 109 | } 110 | 111 | // Draw days of month 112 | var currentDay = 1; 113 | for (i = startingPos; currentDay <= days; i++) { 114 | if (i % 7 === 0 && currentDay !== 1) { 115 | tableRow = quickElement('tr', tableBody); 116 | } 117 | if ((currentDay === todayDay) && (month === todayMonth) && (year === todayYear)) { 118 | todayClass = 'today'; 119 | } else { 120 | todayClass = ''; 121 | } 122 | 123 | // use UTC function; see above for explanation. 124 | if (isSelectedMonth && currentDay === selected.getUTCDate()) { 125 | if (todayClass !== '') { 126 | todayClass += " "; 127 | } 128 | todayClass += "selected"; 129 | } 130 | 131 | var cell = quickElement('td', tableRow, '', 'class', todayClass); 132 | var link = quickElement('a', cell, currentDay, 'href', '#'); 133 | addEvent(link, 'click', calendarMonth(year, month)); 134 | currentDay++; 135 | } 136 | 137 | // Draw blanks after end of month (optional, but makes for valid code) 138 | while (tableRow.childNodes.length < 7) { 139 | nonDayCell = quickElement('td', tableRow, ' '); 140 | nonDayCell.className = "nonday"; 141 | } 142 | 143 | calDiv.appendChild(calTable); 144 | } 145 | }; 146 | 147 | // Calendar -- A calendar instance 148 | function Calendar(div_id, callback, selected) { 149 | // div_id (string) is the ID of the element in which the calendar will 150 | // be displayed 151 | // callback (string) is the name of a JavaScript function that will be 152 | // called with the parameters (year, month, day) when a day in the 153 | // calendar is clicked 154 | this.div_id = div_id; 155 | this.callback = callback; 156 | this.today = new Date(); 157 | this.currentMonth = this.today.getMonth() + 1; 158 | this.currentYear = this.today.getFullYear(); 159 | if (typeof selected !== 'undefined') { 160 | this.selected = selected; 161 | } 162 | } 163 | Calendar.prototype = { 164 | drawCurrent: function() { 165 | CalendarNamespace.draw(this.currentMonth, this.currentYear, this.div_id, this.callback, this.selected); 166 | }, 167 | drawDate: function(month, year, selected) { 168 | this.currentMonth = month; 169 | this.currentYear = year; 170 | 171 | if(selected) { 172 | this.selected = selected; 173 | } 174 | 175 | this.drawCurrent(); 176 | }, 177 | drawPreviousMonth: function() { 178 | if (this.currentMonth === 1) { 179 | this.currentMonth = 12; 180 | this.currentYear--; 181 | } 182 | else { 183 | this.currentMonth--; 184 | } 185 | this.drawCurrent(); 186 | }, 187 | drawNextMonth: function() { 188 | if (this.currentMonth === 12) { 189 | this.currentMonth = 1; 190 | this.currentYear++; 191 | } 192 | else { 193 | this.currentMonth++; 194 | } 195 | this.drawCurrent(); 196 | }, 197 | drawPreviousYear: function() { 198 | this.currentYear--; 199 | this.drawCurrent(); 200 | }, 201 | drawNextYear: function() { 202 | this.currentYear++; 203 | this.drawCurrent(); 204 | } 205 | }; 206 | window.Calendar = Calendar; 207 | window.CalendarNamespace = CalendarNamespace; 208 | })(); 209 | -------------------------------------------------------------------------------- /static_root/admin/js/core.js: -------------------------------------------------------------------------------- 1 | // Core javascript helper functions 2 | 3 | // basic browser identification & version 4 | var isOpera = (navigator.userAgent.indexOf("Opera") >= 0) && parseFloat(navigator.appVersion); 5 | var isIE = ((document.all) && (!isOpera)) && parseFloat(navigator.appVersion.split("MSIE ")[1].split(";")[0]); 6 | 7 | // Cross-browser event handlers. 8 | function addEvent(obj, evType, fn) { 9 | 'use strict'; 10 | if (obj.addEventListener) { 11 | obj.addEventListener(evType, fn, false); 12 | return true; 13 | } else if (obj.attachEvent) { 14 | var r = obj.attachEvent("on" + evType, fn); 15 | return r; 16 | } else { 17 | return false; 18 | } 19 | } 20 | 21 | function removeEvent(obj, evType, fn) { 22 | 'use strict'; 23 | if (obj.removeEventListener) { 24 | obj.removeEventListener(evType, fn, false); 25 | return true; 26 | } else if (obj.detachEvent) { 27 | obj.detachEvent("on" + evType, fn); 28 | return true; 29 | } else { 30 | return false; 31 | } 32 | } 33 | 34 | function cancelEventPropagation(e) { 35 | 'use strict'; 36 | if (!e) { 37 | e = window.event; 38 | } 39 | e.cancelBubble = true; 40 | if (e.stopPropagation) { 41 | e.stopPropagation(); 42 | } 43 | } 44 | 45 | // quickElement(tagType, parentReference [, textInChildNode, attribute, attributeValue ...]); 46 | function quickElement() { 47 | 'use strict'; 48 | var obj = document.createElement(arguments[0]); 49 | if (arguments[2]) { 50 | var textNode = document.createTextNode(arguments[2]); 51 | obj.appendChild(textNode); 52 | } 53 | var len = arguments.length; 54 | for (var i = 3; i < len; i += 2) { 55 | obj.setAttribute(arguments[i], arguments[i + 1]); 56 | } 57 | arguments[1].appendChild(obj); 58 | return obj; 59 | } 60 | 61 | // "a" is reference to an object 62 | function removeChildren(a) { 63 | 'use strict'; 64 | while (a.hasChildNodes()) { 65 | a.removeChild(a.lastChild); 66 | } 67 | } 68 | 69 | // ---------------------------------------------------------------------------- 70 | // Find-position functions by PPK 71 | // See http://www.quirksmode.org/js/findpos.html 72 | // ---------------------------------------------------------------------------- 73 | function findPosX(obj) { 74 | 'use strict'; 75 | var curleft = 0; 76 | if (obj.offsetParent) { 77 | while (obj.offsetParent) { 78 | curleft += obj.offsetLeft - ((isOpera) ? 0 : obj.scrollLeft); 79 | obj = obj.offsetParent; 80 | } 81 | // IE offsetParent does not include the top-level 82 | if (isIE && obj.parentElement) { 83 | curleft += obj.offsetLeft - obj.scrollLeft; 84 | } 85 | } else if (obj.x) { 86 | curleft += obj.x; 87 | } 88 | return curleft; 89 | } 90 | 91 | function findPosY(obj) { 92 | 'use strict'; 93 | var curtop = 0; 94 | if (obj.offsetParent) { 95 | while (obj.offsetParent) { 96 | curtop += obj.offsetTop - ((isOpera) ? 0 : obj.scrollTop); 97 | obj = obj.offsetParent; 98 | } 99 | // IE offsetParent does not include the top-level 100 | if (isIE && obj.parentElement) { 101 | curtop += obj.offsetTop - obj.scrollTop; 102 | } 103 | } else if (obj.y) { 104 | curtop += obj.y; 105 | } 106 | return curtop; 107 | } 108 | 109 | //----------------------------------------------------------------------------- 110 | // Date object extensions 111 | // ---------------------------------------------------------------------------- 112 | (function() { 113 | 'use strict'; 114 | Date.prototype.getTwelveHours = function() { 115 | var hours = this.getHours(); 116 | if (hours === 0) { 117 | return 12; 118 | } 119 | else { 120 | return hours <= 12 ? hours : hours - 12; 121 | } 122 | }; 123 | 124 | Date.prototype.getTwoDigitMonth = function() { 125 | return (this.getMonth() < 9) ? '0' + (this.getMonth() + 1) : (this.getMonth() + 1); 126 | }; 127 | 128 | Date.prototype.getTwoDigitDate = function() { 129 | return (this.getDate() < 10) ? '0' + this.getDate() : this.getDate(); 130 | }; 131 | 132 | Date.prototype.getTwoDigitTwelveHour = function() { 133 | return (this.getTwelveHours() < 10) ? '0' + this.getTwelveHours() : this.getTwelveHours(); 134 | }; 135 | 136 | Date.prototype.getTwoDigitHour = function() { 137 | return (this.getHours() < 10) ? '0' + this.getHours() : this.getHours(); 138 | }; 139 | 140 | Date.prototype.getTwoDigitMinute = function() { 141 | return (this.getMinutes() < 10) ? '0' + this.getMinutes() : this.getMinutes(); 142 | }; 143 | 144 | Date.prototype.getTwoDigitSecond = function() { 145 | return (this.getSeconds() < 10) ? '0' + this.getSeconds() : this.getSeconds(); 146 | }; 147 | 148 | Date.prototype.getHourMinute = function() { 149 | return this.getTwoDigitHour() + ':' + this.getTwoDigitMinute(); 150 | }; 151 | 152 | Date.prototype.getHourMinuteSecond = function() { 153 | return this.getTwoDigitHour() + ':' + this.getTwoDigitMinute() + ':' + this.getTwoDigitSecond(); 154 | }; 155 | 156 | Date.prototype.getFullMonthName = function() { 157 | return typeof window.CalendarNamespace === "undefined" 158 | ? this.getTwoDigitMonth() 159 | : window.CalendarNamespace.monthsOfYear[this.getMonth()]; 160 | }; 161 | 162 | Date.prototype.strftime = function(format) { 163 | var fields = { 164 | B: this.getFullMonthName(), 165 | c: this.toString(), 166 | d: this.getTwoDigitDate(), 167 | H: this.getTwoDigitHour(), 168 | I: this.getTwoDigitTwelveHour(), 169 | m: this.getTwoDigitMonth(), 170 | M: this.getTwoDigitMinute(), 171 | p: (this.getHours() >= 12) ? 'PM' : 'AM', 172 | S: this.getTwoDigitSecond(), 173 | w: '0' + this.getDay(), 174 | x: this.toLocaleDateString(), 175 | X: this.toLocaleTimeString(), 176 | y: ('' + this.getFullYear()).substr(2, 4), 177 | Y: '' + this.getFullYear(), 178 | '%': '%' 179 | }; 180 | var result = '', i = 0; 181 | while (i < format.length) { 182 | if (format.charAt(i) === '%') { 183 | result = result + fields[format.charAt(i + 1)]; 184 | ++i; 185 | } 186 | else { 187 | result = result + format.charAt(i); 188 | } 189 | ++i; 190 | } 191 | return result; 192 | }; 193 | 194 | // ---------------------------------------------------------------------------- 195 | // String object extensions 196 | // ---------------------------------------------------------------------------- 197 | String.prototype.pad_left = function(pad_length, pad_string) { 198 | var new_string = this; 199 | for (var i = 0; new_string.length < pad_length; i++) { 200 | new_string = pad_string + new_string; 201 | } 202 | return new_string; 203 | }; 204 | 205 | String.prototype.strptime = function(format) { 206 | var split_format = format.split(/[.\-/]/); 207 | var date = this.split(/[.\-/]/); 208 | var i = 0; 209 | var day, month, year; 210 | while (i < split_format.length) { 211 | switch (split_format[i]) { 212 | case "%d": 213 | day = date[i]; 214 | break; 215 | case "%m": 216 | month = date[i] - 1; 217 | break; 218 | case "%Y": 219 | year = date[i]; 220 | break; 221 | case "%y": 222 | year = date[i]; 223 | break; 224 | } 225 | ++i; 226 | } 227 | // Create Date object from UTC since the parsed value is supposed to be 228 | // in UTC, not local time. Also, the calendar uses UTC functions for 229 | // date extraction. 230 | return new Date(Date.UTC(year, month, day)); 231 | }; 232 | 233 | })(); 234 | // ---------------------------------------------------------------------------- 235 | // Get the computed style for and element 236 | // ---------------------------------------------------------------------------- 237 | function getStyle(oElm, strCssRule) { 238 | 'use strict'; 239 | var strValue = ""; 240 | if(document.defaultView && document.defaultView.getComputedStyle) { 241 | strValue = document.defaultView.getComputedStyle(oElm, "").getPropertyValue(strCssRule); 242 | } 243 | else if(oElm.currentStyle) { 244 | strCssRule = strCssRule.replace(/\-(\w)/g, function(strMatch, p1) { 245 | return p1.toUpperCase(); 246 | }); 247 | strValue = oElm.currentStyle[strCssRule]; 248 | } 249 | return strValue; 250 | } 251 | -------------------------------------------------------------------------------- /static_root/admin/css/forms.css: -------------------------------------------------------------------------------- 1 | @import url('widgets.css'); 2 | 3 | /* FORM ROWS */ 4 | 5 | .form-row { 6 | overflow: hidden; 7 | padding: 10px; 8 | font-size: 13px; 9 | border-bottom: 1px solid #eee; 10 | } 11 | 12 | .form-row img, .form-row input { 13 | vertical-align: middle; 14 | } 15 | 16 | .form-row label input[type="checkbox"] { 17 | margin-top: 0; 18 | vertical-align: 0; 19 | } 20 | 21 | form .form-row p { 22 | padding-left: 0; 23 | } 24 | 25 | .hidden { 26 | display: none; 27 | } 28 | 29 | /* FORM LABELS */ 30 | 31 | label { 32 | font-weight: normal; 33 | color: #666; 34 | font-size: 13px; 35 | } 36 | 37 | .required label, label.required { 38 | font-weight: bold; 39 | color: #333; 40 | } 41 | 42 | /* RADIO BUTTONS */ 43 | 44 | form ul.radiolist li { 45 | list-style-type: none; 46 | } 47 | 48 | form ul.radiolist label { 49 | float: none; 50 | display: inline; 51 | } 52 | 53 | form ul.radiolist input[type="radio"] { 54 | margin: -2px 4px 0 0; 55 | padding: 0; 56 | } 57 | 58 | form ul.inline { 59 | margin-left: 0; 60 | padding: 0; 61 | } 62 | 63 | form ul.inline li { 64 | float: left; 65 | padding-right: 7px; 66 | } 67 | 68 | /* ALIGNED FIELDSETS */ 69 | 70 | .aligned label { 71 | display: block; 72 | padding: 4px 10px 0 0; 73 | float: left; 74 | width: 160px; 75 | word-wrap: break-word; 76 | line-height: 1; 77 | } 78 | 79 | .aligned label:not(.vCheckboxLabel):after { 80 | content: ''; 81 | display: inline-block; 82 | vertical-align: middle; 83 | height: 26px; 84 | } 85 | 86 | .aligned label + p { 87 | padding: 6px 0; 88 | margin-top: 0; 89 | margin-bottom: 0; 90 | margin-left: 170px; 91 | } 92 | 93 | .aligned ul label { 94 | display: inline; 95 | float: none; 96 | width: auto; 97 | } 98 | 99 | .aligned .form-row input { 100 | margin-bottom: 0; 101 | } 102 | 103 | .colMS .aligned .vLargeTextField, .colMS .aligned .vXMLLargeTextField { 104 | width: 350px; 105 | } 106 | 107 | form .aligned ul { 108 | margin-left: 160px; 109 | padding-left: 10px; 110 | } 111 | 112 | form .aligned ul.radiolist { 113 | display: inline-block; 114 | margin: 0; 115 | padding: 0; 116 | } 117 | 118 | form .aligned p.help { 119 | clear: left; 120 | margin-top: 0; 121 | margin-left: 160px; 122 | padding-left: 10px; 123 | } 124 | 125 | form .aligned label + p.help { 126 | margin-left: 0; 127 | padding-left: 0; 128 | } 129 | 130 | form .aligned p.help:last-child { 131 | margin-bottom: 0; 132 | padding-bottom: 0; 133 | } 134 | 135 | form .aligned input + p.help, 136 | form .aligned textarea + p.help, 137 | form .aligned select + p.help { 138 | margin-left: 160px; 139 | padding-left: 10px; 140 | } 141 | 142 | form .aligned ul li { 143 | list-style: none; 144 | } 145 | 146 | form .aligned table p { 147 | margin-left: 0; 148 | padding-left: 0; 149 | } 150 | 151 | .aligned .vCheckboxLabel { 152 | float: none; 153 | width: auto; 154 | display: inline-block; 155 | vertical-align: -3px; 156 | padding: 0 0 5px 5px; 157 | } 158 | 159 | .aligned .vCheckboxLabel + p.help { 160 | margin-top: -4px; 161 | } 162 | 163 | .colM .aligned .vLargeTextField, .colM .aligned .vXMLLargeTextField { 164 | width: 610px; 165 | } 166 | 167 | .checkbox-row p.help { 168 | margin-left: 0; 169 | padding-left: 0; 170 | } 171 | 172 | fieldset .field-box { 173 | float: left; 174 | margin-right: 20px; 175 | } 176 | 177 | /* WIDE FIELDSETS */ 178 | 179 | .wide label { 180 | width: 200px; 181 | } 182 | 183 | form .wide p, form .wide input + p.help { 184 | margin-left: 200px; 185 | } 186 | 187 | form .wide p.help { 188 | padding-left: 38px; 189 | } 190 | 191 | .colM fieldset.wide .vLargeTextField, .colM fieldset.wide .vXMLLargeTextField { 192 | width: 450px; 193 | } 194 | 195 | /* COLLAPSED FIELDSETS */ 196 | 197 | fieldset.collapsed * { 198 | display: none; 199 | } 200 | 201 | fieldset.collapsed h2, fieldset.collapsed { 202 | display: block; 203 | } 204 | 205 | fieldset.collapsed { 206 | border: 1px solid #eee; 207 | border-radius: 4px; 208 | overflow: hidden; 209 | } 210 | 211 | fieldset.collapsed h2 { 212 | background: #f8f8f8; 213 | color: #666; 214 | } 215 | 216 | fieldset .collapse-toggle { 217 | color: #fff; 218 | } 219 | 220 | fieldset.collapsed .collapse-toggle { 221 | background: transparent; 222 | display: inline; 223 | color: #447e9b; 224 | } 225 | 226 | /* MONOSPACE TEXTAREAS */ 227 | 228 | fieldset.monospace textarea { 229 | font-family: "Bitstream Vera Sans Mono", Monaco, "Courier New", Courier, monospace; 230 | } 231 | 232 | /* SUBMIT ROW */ 233 | 234 | .submit-row { 235 | padding: 12px 14px; 236 | margin: 0 0 20px; 237 | background: #f8f8f8; 238 | border: 1px solid #eee; 239 | border-radius: 4px; 240 | text-align: right; 241 | overflow: hidden; 242 | } 243 | 244 | body.popup .submit-row { 245 | overflow: auto; 246 | } 247 | 248 | .submit-row input { 249 | height: 35px; 250 | line-height: 15px; 251 | margin: 0 0 0 5px; 252 | } 253 | 254 | .submit-row input.default { 255 | margin: 0 0 0 8px; 256 | text-transform: uppercase; 257 | } 258 | 259 | .submit-row p { 260 | margin: 0.3em; 261 | } 262 | 263 | .submit-row p.deletelink-box { 264 | float: left; 265 | margin: 0; 266 | } 267 | 268 | .submit-row a.deletelink { 269 | display: block; 270 | background: #ba2121; 271 | border-radius: 4px; 272 | padding: 10px 15px; 273 | height: 15px; 274 | line-height: 15px; 275 | color: #fff; 276 | } 277 | 278 | .submit-row a.deletelink:focus, 279 | .submit-row a.deletelink:hover, 280 | .submit-row a.deletelink:active { 281 | background: #a41515; 282 | } 283 | 284 | /* CUSTOM FORM FIELDS */ 285 | 286 | .vSelectMultipleField { 287 | vertical-align: top; 288 | } 289 | 290 | .vCheckboxField { 291 | border: none; 292 | } 293 | 294 | .vDateField, .vTimeField { 295 | margin-right: 2px; 296 | margin-bottom: 4px; 297 | } 298 | 299 | .vDateField { 300 | min-width: 6.85em; 301 | } 302 | 303 | .vTimeField { 304 | min-width: 4.7em; 305 | } 306 | 307 | .vURLField { 308 | width: 30em; 309 | } 310 | 311 | .vLargeTextField, .vXMLLargeTextField { 312 | width: 48em; 313 | } 314 | 315 | .flatpages-flatpage #id_content { 316 | height: 40.2em; 317 | } 318 | 319 | .module table .vPositiveSmallIntegerField { 320 | width: 2.2em; 321 | } 322 | 323 | .vTextField { 324 | width: 20em; 325 | } 326 | 327 | .vIntegerField { 328 | width: 5em; 329 | } 330 | 331 | .vBigIntegerField { 332 | width: 10em; 333 | } 334 | 335 | .vForeignKeyRawIdAdminField { 336 | width: 5em; 337 | } 338 | 339 | /* INLINES */ 340 | 341 | .inline-group { 342 | padding: 0; 343 | margin: 0 0 30px; 344 | } 345 | 346 | .inline-group thead th { 347 | padding: 8px 10px; 348 | } 349 | 350 | .inline-group .aligned label { 351 | width: 160px; 352 | } 353 | 354 | .inline-related { 355 | position: relative; 356 | } 357 | 358 | .inline-related h3 { 359 | margin: 0; 360 | color: #666; 361 | padding: 5px; 362 | font-size: 13px; 363 | background: #f8f8f8; 364 | border-top: 1px solid #eee; 365 | border-bottom: 1px solid #eee; 366 | } 367 | 368 | .inline-related h3 span.delete { 369 | float: right; 370 | } 371 | 372 | .inline-related h3 span.delete label { 373 | margin-left: 2px; 374 | font-size: 11px; 375 | } 376 | 377 | .inline-related fieldset { 378 | margin: 0; 379 | background: #fff; 380 | border: none; 381 | width: 100%; 382 | } 383 | 384 | .inline-related fieldset.module h3 { 385 | margin: 0; 386 | padding: 2px 5px 3px 5px; 387 | font-size: 11px; 388 | text-align: left; 389 | font-weight: bold; 390 | background: #bcd; 391 | color: #fff; 392 | } 393 | 394 | .inline-group .tabular fieldset.module { 395 | border: none; 396 | } 397 | 398 | .inline-related.tabular fieldset.module table { 399 | width: 100%; 400 | } 401 | 402 | .last-related fieldset { 403 | border: none; 404 | } 405 | 406 | .inline-group .tabular tr.has_original td { 407 | padding-top: 2em; 408 | } 409 | 410 | .inline-group .tabular tr td.original { 411 | padding: 2px 0 0 0; 412 | width: 0; 413 | _position: relative; 414 | } 415 | 416 | .inline-group .tabular th.original { 417 | width: 0px; 418 | padding: 0; 419 | } 420 | 421 | .inline-group .tabular td.original p { 422 | position: absolute; 423 | left: 0; 424 | height: 1.1em; 425 | padding: 2px 9px; 426 | overflow: hidden; 427 | font-size: 9px; 428 | font-weight: bold; 429 | color: #666; 430 | _width: 700px; 431 | } 432 | 433 | .inline-group ul.tools { 434 | padding: 0; 435 | margin: 0; 436 | list-style: none; 437 | } 438 | 439 | .inline-group ul.tools li { 440 | display: inline; 441 | padding: 0 5px; 442 | } 443 | 444 | .inline-group div.add-row, 445 | .inline-group .tabular tr.add-row td { 446 | color: #666; 447 | background: #f8f8f8; 448 | padding: 8px 10px; 449 | border-bottom: 1px solid #eee; 450 | } 451 | 452 | .inline-group .tabular tr.add-row td { 453 | padding: 8px 10px; 454 | border-bottom: 1px solid #eee; 455 | } 456 | 457 | .inline-group ul.tools a.add, 458 | .inline-group div.add-row a, 459 | .inline-group .tabular tr.add-row td a { 460 | background: url(../img/icon-addlink.svg) 0 1px no-repeat; 461 | padding-left: 16px; 462 | font-size: 12px; 463 | } 464 | 465 | .empty-form { 466 | display: none; 467 | } 468 | 469 | /* RELATED FIELD ADD ONE / LOOKUP */ 470 | 471 | .add-another, .related-lookup { 472 | margin-left: 5px; 473 | display: inline-block; 474 | vertical-align: middle; 475 | background-repeat: no-repeat; 476 | background-size: 14px; 477 | } 478 | 479 | .add-another { 480 | width: 16px; 481 | height: 16px; 482 | background-image: url(../img/icon-addlink.svg); 483 | } 484 | 485 | .related-lookup { 486 | width: 16px; 487 | height: 16px; 488 | background-image: url(../img/search.svg); 489 | } 490 | 491 | form .related-widget-wrapper ul { 492 | display: inline-block; 493 | margin-left: 0; 494 | padding-left: 0; 495 | } 496 | 497 | .clearable-file-input input { 498 | margin-top: 0; 499 | } 500 | --------------------------------------------------------------------------------