├── .gitignore
├── LICENSE
├── README.md
├── media_root
├── 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
└── srvup
│ └── images
│ └── srvup_2_share.png
├── requirements.txt
├── roadmap.md
├── src
├── .DS_Store
├── analytics
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
├── categories
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── forms.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ ├── 0002_category_video.py
│ │ └── __init__.py
│ ├── models.py
│ ├── templates
│ │ └── categories
│ │ │ ├── category_detail.html
│ │ │ └── category_list.html
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── courses
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── course_copy.py
│ ├── fields.py
│ ├── forms.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ ├── 0002_lecture.py
│ │ ├── 0003_auto_20170206_1949.py
│ │ ├── 0004_auto_20170206_1954.py
│ │ ├── 0005_lecture_order.py
│ │ ├── 0006_auto_20170206_2354.py
│ │ ├── 0007_auto_20170206_2356.py
│ │ ├── 0008_auto_20170207_0010.py
│ │ ├── 0009_course_order.py
│ │ ├── 0010_course_category.py
│ │ ├── 0011_mycourses.py
│ │ ├── 0012_auto_20170207_0040.py
│ │ ├── 0013_auto_20170207_0113.py
│ │ ├── 0014_course_active.py
│ │ ├── 0015_auto_20170207_1844.py
│ │ ├── 0016_auto_20170207_1844.py
│ │ ├── 0017_course_secondary.py
│ │ ├── 0018_auto_20170207_2336.py
│ │ ├── 0019_course_image.py
│ │ ├── 0020_lecture_free.py
│ │ └── __init__.py
│ ├── models.py
│ ├── templates
│ │ ├── .DS_Store
│ │ └── courses
│ │ │ ├── course_confirm_delete.html
│ │ │ ├── course_detail.html
│ │ │ ├── course_form.html
│ │ │ ├── course_list.html
│ │ │ ├── lecture_detail.html
│ │ │ ├── must_purchase.html
│ │ │ └── snippets
│ │ │ ├── course_featured_display.html
│ │ │ └── course_thumbnail_display.html
│ ├── tests.py
│ ├── urls.py
│ ├── utils.py
│ └── views.py
├── db.sqlite3
├── manage.py
├── search
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── templates
│ │ └── search
│ │ │ ├── default.html
│ │ │ └── snippets
│ │ │ └── search_form.html
│ ├── templatetags
│ │ ├── __init__.py
│ │ └── search_form.py
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── srvup
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ ├── views.py
│ └── wsgi.py
├── static_in_env
│ ├── css
│ │ └── bootstrap.min.css
│ └── js
│ │ └── bootstrap.min.js
├── templates
│ ├── base.html
│ ├── defaults
│ │ ├── head.html
│ │ └── scripts.html
│ ├── home.html
│ └── navbar.html
└── videos
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── forms.py
│ ├── migrations
│ ├── 0001_initial.py
│ ├── 0002_auto_20170202_1736.py
│ ├── 0003_video_updated.py
│ ├── 0004_video_slug.py
│ ├── 0005_auto_20170203_0139.py
│ └── __init__.py
│ ├── mixins.py
│ ├── models.py
│ ├── templates
│ ├── .DS_Store
│ └── videos
│ │ ├── snippets
│ │ ├── render_video.html
│ │ └── video-search-form.html
│ │ ├── video_confirm_delete.html
│ │ ├── video_detail.html
│ │ ├── video_form.html
│ │ └── video_list.html
│ ├── templatetags
│ ├── __init__.py
│ └── render_video.py
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── srvup.sublime-project
├── srvup.sublime-workspace
└── static_root
├── admin
├── css
│ ├── base.css
│ ├── changelists.css
│ ├── dashboard.css
│ ├── fonts.css
│ ├── forms.css
│ ├── login.css
│ ├── rtl.css
│ └── widgets.css
├── fonts
│ ├── LICENSE.txt
│ ├── README.txt
│ ├── Roboto-Bold-webfont.woff
│ ├── Roboto-Light-webfont.woff
│ └── Roboto-Regular-webfont.woff
├── img
│ ├── LICENSE
│ ├── README.txt
│ ├── calendar-icons.svg
│ ├── gis
│ │ ├── move_vertex_off.svg
│ │ └── move_vertex_on.svg
│ ├── icon-addlink.svg
│ ├── icon-alert.svg
│ ├── icon-calendar.svg
│ ├── icon-changelink.svg
│ ├── icon-clock.svg
│ ├── icon-deletelink.svg
│ ├── icon-no.svg
│ ├── icon-unknown-alt.svg
│ ├── icon-unknown.svg
│ ├── icon-yes.svg
│ ├── inline-delete.svg
│ ├── search.svg
│ ├── selector-icons.svg
│ ├── sorting-icons.svg
│ ├── tooltag-add.svg
│ └── tooltag-arrowright.svg
└── js
│ ├── SelectBox.js
│ ├── SelectFilter2.js
│ ├── actions.js
│ ├── actions.min.js
│ ├── admin
│ ├── DateTimeShortcuts.js
│ └── RelatedObjectLookups.js
│ ├── calendar.js
│ ├── cancel.js
│ ├── change_form.js
│ ├── collapse.js
│ ├── collapse.min.js
│ ├── core.js
│ ├── inlines.js
│ ├── inlines.min.js
│ ├── jquery.init.js
│ ├── popup_response.js
│ ├── prepopulate.js
│ ├── prepopulate.min.js
│ ├── prepopulate_init.js
│ ├── timeparse.js
│ ├── urlify.js
│ └── vendor
│ ├── jquery
│ ├── LICENSE-JQUERY.txt
│ ├── jquery.js
│ └── jquery.min.js
│ └── xregexp
│ ├── LICENSE-XREGEXP.txt
│ ├── xregexp.js
│ └── xregexp.min.js
├── css
└── bootstrap.min.css
└── js
└── bootstrap.min.js
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
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 |
--------------------------------------------------------------------------------
/media_root/cfe/images/Screen_Shot_2017-02-07_at_3.37.03_PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/9ddf3f6f32fff64c66cc30a44a8ca1d46f4b9c88/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/9ddf3f6f32fff64c66cc30a44a8ca1d46f4b9c88/media_root/new-course-m8sce/images/Screen_Shot_2017-02-07_at_3.39.14_PM.png
--------------------------------------------------------------------------------
/media_root/srvup/images/srvup_2_share.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/9ddf3f6f32fff64c66cc30a44a8ca1d46f4b9c88/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/9ddf3f6f32fff64c66cc30a44a8ca1d46f4b9c88/src/.DS_Store
--------------------------------------------------------------------------------
/src/analytics/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/9ddf3f6f32fff64c66cc30a44a8ca1d46f4b9c88/src/analytics/__init__.py
--------------------------------------------------------------------------------
/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/analytics/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class AnalyticsConfig(AppConfig):
5 | name = 'analytics'
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/analytics/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/9ddf3f6f32fff64c66cc30a44a8ca1d46f4b9c88/src/analytics/migrations/__init__.py
--------------------------------------------------------------------------------
/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/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/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/9ddf3f6f32fff64c66cc30a44a8ca1d46f4b9c88/src/categories/__init__.py
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/categories/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class CategoriesConfig(AppConfig):
5 | name = 'categories'
6 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/src/categories/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/9ddf3f6f32fff64c66cc30a44a8ca1d46f4b9c88/src/categories/migrations/__init__.py
--------------------------------------------------------------------------------
/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/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/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 %}
--------------------------------------------------------------------------------
/src/categories/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
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/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/courses/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/9ddf3f6f32fff64c66cc30a44a8ca1d46f4b9c88/src/courses/__init__.py
--------------------------------------------------------------------------------
/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/courses/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class CoursesConfig(AppConfig):
5 | name = 'courses'
6 |
--------------------------------------------------------------------------------
/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/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/9ddf3f6f32fff64c66cc30a44a8ca1d46f4b9c88/src/courses/migrations/__init__.py
--------------------------------------------------------------------------------
/src/courses/models.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.db import models
3 | from django.db.models import Prefetch, Q
4 | from django.db.models.signals import pre_save, post_save
5 | from django.core.urlresolvers import reverse
6 | from django.utils.text import slugify
7 |
8 | # Create your models here.
9 | from categories.models import Category
10 | from videos.models import Video
11 |
12 | from .fields import PositionField
13 | from .utils import create_slug, make_display_price
14 |
15 |
16 | class MyCourses(models.Model):
17 | user = models.OneToOneField(settings.AUTH_USER_MODEL)
18 | courses = models.ManyToManyField('Course', related_name='owned', blank=True)
19 | updated = models.DateTimeField(auto_now=True)
20 | timestamp = models.DateTimeField(auto_now_add=True)
21 |
22 | def __str__(self):
23 | return str(self.courses.all().count())
24 |
25 | class Meta:
26 | verbose_name = 'My courses'
27 | verbose_name_plural = 'My courses'
28 |
29 |
30 | def post_save_user_create(sender, instance, created, *args, **kwargs):
31 | if created:
32 | MyCourses.objects.get_or_create(user=instance)
33 |
34 | post_save.connect(post_save_user_create, sender=settings.AUTH_USER_MODEL)
35 |
36 |
37 |
38 |
39 | POS_CHOICES = (
40 | ('main', 'Main'),
41 | ('sec', 'Secondary'),
42 | )
43 |
44 |
45 | class CourseQuerySet(models.query.QuerySet):
46 | def active(self):
47 | return self.filter(active=True)
48 |
49 | def lectures(self):
50 | return self.prefetch_related('lecture_set')
51 |
52 | def featured(self):
53 | return self.filter(Q(category__slug__iexact='featured')) #| Q(secondary__slug__iexact='featured'))
54 |
55 | def owned(self, user):
56 | if user.is_authenticated():
57 | qs = MyCourses.objects.filter(user=user)
58 | else:
59 | qs = MyCourses.objects.none()
60 | return self.prefetch_related(
61 | Prefetch('owned',
62 | queryset=qs,
63 | to_attr='is_owner'
64 | )
65 | )
66 |
67 |
68 | class CourseManager(models.Manager):
69 | def get_queryset(self):
70 | return CourseQuerySet(self.model, using=self._db)
71 |
72 | def all(self):
73 | return self.get_queryset().all().active()
74 | #return super(CourseManager, self).all()
75 |
76 |
77 | def handle_upload(instance, filename):
78 | if instance.slug:
79 | return "%s/images/%s" %(instance.slug, filename)
80 | return "unknown/images/%s" %(filename)
81 |
82 | class Course(models.Model):
83 | user = models.ForeignKey(settings.AUTH_USER_MODEL)
84 | title = models.CharField(max_length=120)
85 | slug = models.SlugField(blank=True) # unique = False
86 | image = models.ImageField(upload_to=handle_upload,
87 | height_field='image_height',
88 | width_field='image_width',
89 | blank=True, null=True)
90 | image_height = models.IntegerField(blank=True, null=True)
91 | image_width = models.IntegerField(blank=True, null=True)
92 | # category = models.CharField(max_length=120, choices=POS_CHOICES, default='main')
93 | category = models.ForeignKey(Category, related_name='primary_category', null=True, blank=True)
94 | secondary = models.ManyToManyField(Category, related_name='secondary_category', blank=True)
95 | order = PositionField(collection='category')
96 | description = models.TextField()
97 | price = models.DecimalField(decimal_places=2, max_digits=100)
98 | active = models.BooleanField(default=True)
99 | updated = models.DateTimeField(auto_now=True)
100 | timestamp = models.DateTimeField(auto_now_add=True)
101 |
102 | objects = CourseManager() # Course.objects.all()
103 |
104 | # newsomething = CourseManager() # Course.newsomething.all()
105 |
106 | def __str__(self):
107 | return self.title
108 |
109 | def get_absolute_url(self):
110 | return reverse("courses:detail", kwargs={"slug": self.slug})
111 |
112 | def get_purchase_url(self):
113 | return reverse("courses:purchase", kwargs={"slug": self.slug})
114 |
115 | def display_price(self):
116 | return make_display_price(self.price)
117 |
118 |
119 | def post_save_course_receiver(sender, instance, created, *args, **kwargs):
120 | if not instance.category in instance.secondary.all():
121 | instance.secondary.add(instance.category)
122 |
123 | post_save.connect(post_save_course_receiver, sender=Course)
124 |
125 |
126 | # limit_choices_to={'lecture__isnull': True, 'title__icontains': "Something"}
127 |
128 | class Lecture(models.Model):
129 | course = models.ForeignKey(Course, on_delete=models.SET_NULL, null=True)
130 | video = models.ForeignKey(Video, on_delete=models.SET_NULL, null=True)
131 | title = models.CharField(max_length=120)
132 | order = PositionField(collection='course')
133 | slug = models.SlugField(blank=True) # unique = False
134 | free = models.BooleanField(default=False)
135 | description = models.TextField(blank=True)
136 | updated = models.DateTimeField(auto_now=True)
137 | timestamp = models.DateTimeField(auto_now_add=True)
138 |
139 | def __str__(self):
140 | return self.title
141 |
142 | class Meta:
143 | unique_together = (('slug', 'course'),)
144 | ordering = ['order', 'title'] # 0 - 100 , a-z
145 |
146 | def get_absolute_url(self):
147 | return reverse("courses:lecture-detail",
148 | kwargs={
149 | "cslug": self.course.slug,
150 | "lslug": self.slug,
151 | }
152 | )
153 |
154 |
155 |
156 |
157 |
158 |
159 | def pre_save_video_receiver(sender, instance, *args, **kwargs):
160 | if not instance.slug:
161 | instance.slug = create_slug(instance)
162 |
163 | pre_save.connect(pre_save_video_receiver, sender=Course)
164 | # pre_save.connect(pre_save_video_receiver, sender=Lecture)
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
--------------------------------------------------------------------------------
/src/courses/templates/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/9ddf3f6f32fff64c66cc30a44a8ca1d46f4b9c88/src/courses/templates/.DS_Store
--------------------------------------------------------------------------------
/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 |
16 |
17 | {% endblock content %}
18 |
--------------------------------------------------------------------------------
/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 |
8 | Courses
9 | {{ object.title }}
10 |
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 |
43 | {% endfor %}
44 |
45 |
46 |
47 |
48 |
49 | {% endblock content %}
50 |
--------------------------------------------------------------------------------
/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 |
15 |
16 | {% endblock content %}
17 |
--------------------------------------------------------------------------------
/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 |
34 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | {% endblock content %}
77 |
--------------------------------------------------------------------------------
/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 |
9 | Courses
10 | {{ object.course.title }}
11 | {{ object.title }}
12 |
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/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/courses/templates/courses/snippets/course_featured_display.html:
--------------------------------------------------------------------------------
1 |
2 | {% if course.image %}
3 |
4 | {% endif %}
5 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/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/courses/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/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/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/courses/views.py:
--------------------------------------------------------------------------------
1 | import random
2 | from django.contrib.auth.mixins import LoginRequiredMixin
3 | from django.db.models import Prefetch
4 | from django.shortcuts import render, get_object_or_404, redirect
5 | from django.http import Http404
6 | from django.views.generic import (
7 | CreateView,
8 | DetailView,
9 | ListView,
10 | UpdateView,
11 | DeleteView,
12 | RedirectView,
13 | View
14 | )
15 |
16 | #from .forms import VideoForm
17 | from analytics.models import CourseViewEvent
18 |
19 |
20 | from videos.mixins import MemberRequiredMixin, StaffMemberRequiredMixin
21 | from .forms import CourseForm
22 | from .models import Course, Lecture, MyCourses
23 |
24 |
25 | class CourseCreateView(StaffMemberRequiredMixin, CreateView):
26 | model = Course
27 | form_class = CourseForm
28 |
29 | def form_valid(self, form):
30 | obj = form.save(commit=False)
31 | obj.user = self.request.user
32 | obj.save()
33 | return super(CourseCreateView, self).form_valid(form)
34 |
35 |
36 |
37 | class LectureDetailView(View):
38 | def get(self, request, cslug=None, lslug=None, *args, **kwargs):
39 | obj = None
40 | qs = Course.objects.filter(slug=cslug).lectures().owned(request.user)
41 | if not qs.exists():
42 | raise Http404
43 | course_ = qs.first()
44 | if request.user.is_authenticated():
45 | view_event, created = CourseViewEvent.objects.get_or_create(user=request.user, course=course_)
46 | if view_event:
47 | view_event.views += 1
48 | view_event.save()
49 |
50 | lectures_qs = course_.lecture_set.filter(slug=lslug)
51 | if not lectures_qs.exists():
52 | raise Http404
53 |
54 | obj = lectures_qs.first()
55 | context = {
56 | "object": obj,
57 | "course": course_,
58 | }
59 |
60 | if not course_.is_owner and not obj.free: #and not user.is_member:
61 | return render(request, "courses/must_purchase.html", {"object": course_})
62 |
63 | return render(request, "courses/lecture_detail.html", context)
64 |
65 |
66 |
67 | class CourseDetailView(DetailView):
68 | #queryset = Course.objects.all()
69 | def get_object(self):
70 | slug = self.kwargs.get("slug")
71 | qs = Course.objects.filter(slug=slug).lectures().owned(self.request.user)
72 | if qs.exists():
73 | obj = qs.first()
74 | if self.request.user.is_authenticated():
75 | view_event, created = CourseViewEvent.objects.get_or_create(user=self.request.user, course=obj)
76 | if view_event:
77 | view_event.views += 1
78 | view_event.save()
79 | return obj
80 | raise Http404
81 |
82 |
83 | class CoursePurchaseView(LoginRequiredMixin, RedirectView):
84 | permanent = False
85 |
86 | def get_redirect_url(self, slug=None):
87 | qs = Course.objects.filter(slug=slug).owned(self.request.user)
88 | if qs.exists():
89 | user = self.request.user
90 | if user.is_authenticated():
91 | my_courses = user.mycourses
92 | # run transaction
93 | # if transaction successful:
94 | my_courses.courses.add(qs.first())
95 | return qs.first().get_absolute_url()
96 | return qs.first().get_absolute_url()
97 | return "/courses/"
98 |
99 |
100 |
101 | class CourseListView(ListView):
102 | paginate_by = 12
103 |
104 | def get_context_data(self, *args, **kwargs):
105 | context = super(CourseListView, self).get_context_data(*args, **kwargs)
106 | print(dir(context.get('page_obj')))
107 | return context
108 |
109 | def get_queryset(self):
110 | request = self.request
111 | qs = Course.objects.all()
112 | query = request.GET.get('q')
113 | user = self.request.user
114 | if query:
115 | qs = qs.filter(title__icontains=query)
116 | if user.is_authenticated():
117 | qs = qs.owned(user)
118 | return qs
119 |
120 |
121 | class CourseUpdateView(StaffMemberRequiredMixin, UpdateView):
122 | queryset = Course.objects.all()
123 | form_class = CourseForm
124 |
125 | def form_valid(self, form):
126 | obj = form.save(commit=False)
127 | if not self.request.user.is_staff:
128 | obj.user = self.request.user
129 | obj.save()
130 | return super(CourseUpdateView, self).form_valid(form)
131 |
132 | def get_object(self):
133 | slug = self.kwargs.get("slug")
134 | obj = Course.objects.filter(slug=slug)
135 | if obj.exists():
136 | return obj.first()
137 | raise Http404
138 |
139 |
140 |
141 | class CourseDeleteView(StaffMemberRequiredMixin, DeleteView):
142 | queryset = Course.objects.all()
143 | success_url = '/videos/'
144 |
145 | def get_object(self):
146 | slug = self.kwargs.get("slug")
147 | obj = Course.objects.filter(slug=slug)
148 | if obj.exists():
149 | return obj.first()
150 | raise Http404
--------------------------------------------------------------------------------
/src/db.sqlite3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/9ddf3f6f32fff64c66cc30a44a8ca1d46f4b9c88/src/db.sqlite3
--------------------------------------------------------------------------------
/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/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/9ddf3f6f32fff64c66cc30a44a8ca1d46f4b9c88/src/search/__init__.py
--------------------------------------------------------------------------------
/src/search/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/src/search/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class SearchConfig(AppConfig):
5 | name = 'search'
6 |
--------------------------------------------------------------------------------
/src/search/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/9ddf3f6f32fff64c66cc30a44a8ca1d46f4b9c88/src/search/migrations/__init__.py
--------------------------------------------------------------------------------
/src/search/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | # Create your models here.
4 |
--------------------------------------------------------------------------------
/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 |
33 | {% endif %}
34 |
35 | {% endblock %}
--------------------------------------------------------------------------------
/src/search/templates/search/snippets/search_form.html:
--------------------------------------------------------------------------------
1 | {% if not navbar %}
2 |
7 |
8 | {% else %}
9 |
15 | {% endif %}
16 |
17 |
--------------------------------------------------------------------------------
/src/search/templatetags/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/9ddf3f6f32fff64c66cc30a44a8ca1d46f4b9c88/src/search/templatetags/__init__.py
--------------------------------------------------------------------------------
/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}
--------------------------------------------------------------------------------
/src/search/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/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/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/srvup/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/9ddf3f6f32fff64c66cc30a44a8ca1d46f4b9c88/src/srvup/__init__.py
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/templates/defaults/head.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/templates/defaults/scripts.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/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/templates/navbar.html:
--------------------------------------------------------------------------------
1 | {% load search_form %}
2 |
3 |
4 |
5 |
14 |
15 |
16 |
17 |
33 | {% search_form request True %}
34 |
35 |
36 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/videos/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/9ddf3f6f32fff64c66cc30a44a8ca1d46f4b9c88/src/videos/__init__.py
--------------------------------------------------------------------------------
/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/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class VideosConfig(AppConfig):
5 | name = 'videos'
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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/videos/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/9ddf3f6f32fff64c66cc30a44a8ca1d46f4b9c88/src/videos/migrations/__init__.py
--------------------------------------------------------------------------------
/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/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/videos/templates/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/9ddf3f6f32fff64c66cc30a44a8ca1d46f4b9c88/src/videos/templates/.DS_Store
--------------------------------------------------------------------------------
/src/videos/templates/videos/snippets/render_video.html:
--------------------------------------------------------------------------------
1 | {% if video %}
2 |
3 | {{ video|safe }}
4 |
5 | {% endif %}
--------------------------------------------------------------------------------
/src/videos/templates/videos/snippets/video-search-form.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
16 |
17 | {% endblock content %}
18 |
--------------------------------------------------------------------------------
/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/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 |
15 |
16 | {% endblock content %}
17 |
--------------------------------------------------------------------------------
/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/templatetags/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/9ddf3f6f32fff64c66cc30a44a8ca1d46f4b9c88/src/videos/templatetags/__init__.py
--------------------------------------------------------------------------------
/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}
--------------------------------------------------------------------------------
/src/videos/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/srvup.sublime-project:
--------------------------------------------------------------------------------
1 | {
2 | "folders":
3 | [
4 | {
5 | "path": "."
6 | }
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/css/rtl.css:
--------------------------------------------------------------------------------
1 | body {
2 | direction: rtl;
3 | }
4 |
5 | /* LOGIN */
6 |
7 | .login .form-row {
8 | float: right;
9 | }
10 |
11 | .login .form-row label {
12 | float: right;
13 | padding-left: 0.5em;
14 | padding-right: 0;
15 | text-align: left;
16 | }
17 |
18 | .login .submit-row {
19 | clear: both;
20 | padding: 1em 9.4em 0 0;
21 | }
22 |
23 | /* GLOBAL */
24 |
25 | th {
26 | text-align: right;
27 | }
28 |
29 | .module h2, .module caption {
30 | text-align: right;
31 | }
32 |
33 | .module ul, .module ol {
34 | margin-left: 0;
35 | margin-right: 1.5em;
36 | }
37 |
38 | .addlink, .changelink {
39 | padding-left: 0;
40 | padding-right: 16px;
41 | background-position: 100% 1px;
42 | }
43 |
44 | .deletelink {
45 | padding-left: 0;
46 | padding-right: 16px;
47 | background-position: 100% 1px;
48 | }
49 |
50 | .object-tools {
51 | float: left;
52 | }
53 |
54 | thead th:first-child,
55 | tfoot td:first-child {
56 | border-left: none;
57 | }
58 |
59 | /* LAYOUT */
60 |
61 | #user-tools {
62 | right: auto;
63 | left: 0;
64 | text-align: left;
65 | }
66 |
67 | div.breadcrumbs {
68 | text-align: right;
69 | }
70 |
71 | #content-main {
72 | float: right;
73 | }
74 |
75 | #content-related {
76 | float: left;
77 | margin-left: -300px;
78 | margin-right: auto;
79 | }
80 |
81 | .colMS {
82 | margin-left: 300px;
83 | margin-right: 0;
84 | }
85 |
86 | /* SORTABLE TABLES */
87 |
88 | table thead th.sorted .sortoptions {
89 | float: left;
90 | }
91 |
92 | thead th.sorted .text {
93 | padding-right: 0;
94 | padding-left: 42px;
95 | }
96 |
97 | /* dashboard styles */
98 |
99 | .dashboard .module table td a {
100 | padding-left: .6em;
101 | padding-right: 16px;
102 | }
103 |
104 | /* changelists styles */
105 |
106 | .change-list .filtered table {
107 | border-left: none;
108 | border-right: 0px none;
109 | }
110 |
111 | #changelist-filter {
112 | right: auto;
113 | left: 0;
114 | border-left: none;
115 | border-right: none;
116 | }
117 |
118 | .change-list .filtered .results, .change-list .filtered .paginator, .filtered #toolbar, .filtered div.xfull {
119 | margin-right: 0;
120 | margin-left: 280px;
121 | }
122 |
123 | #changelist-filter li.selected {
124 | border-left: none;
125 | padding-left: 10px;
126 | margin-left: 0;
127 | border-right: 5px solid #eaeaea;
128 | padding-right: 10px;
129 | margin-right: -15px;
130 | }
131 |
132 | .filtered .actions {
133 | margin-left: 280px;
134 | margin-right: 0;
135 | }
136 |
137 | #changelist table tbody td:first-child, #changelist table tbody th:first-child {
138 | border-right: none;
139 | border-left: none;
140 | }
141 |
142 | /* FORMS */
143 |
144 | .aligned label {
145 | padding: 0 0 3px 1em;
146 | float: right;
147 | }
148 |
149 | .submit-row {
150 | text-align: left
151 | }
152 |
153 | .submit-row p.deletelink-box {
154 | float: right;
155 | }
156 |
157 | .submit-row input.default {
158 | margin-left: 0;
159 | }
160 |
161 | .vDateField, .vTimeField {
162 | margin-left: 2px;
163 | }
164 |
165 | .aligned .form-row input {
166 | margin-left: 5px;
167 | }
168 |
169 | form ul.inline li {
170 | float: right;
171 | padding-right: 0;
172 | padding-left: 7px;
173 | }
174 |
175 | input[type=submit].default, .submit-row input.default {
176 | float: left;
177 | }
178 |
179 | fieldset .field-box {
180 | float: right;
181 | margin-left: 20px;
182 | margin-right: 0;
183 | }
184 |
185 | .errorlist li {
186 | background-position: 100% 12px;
187 | padding: 0;
188 | }
189 |
190 | .errornote {
191 | background-position: 100% 12px;
192 | padding: 10px 12px;
193 | }
194 |
195 | /* WIDGETS */
196 |
197 | .calendarnav-previous {
198 | top: 0;
199 | left: auto;
200 | right: 10px;
201 | }
202 |
203 | .calendarnav-next {
204 | top: 0;
205 | right: auto;
206 | left: 10px;
207 | }
208 |
209 | .calendar caption, .calendarbox h2 {
210 | text-align: center;
211 | }
212 |
213 | .selector {
214 | float: right;
215 | }
216 |
217 | .selector .selector-filter {
218 | text-align: right;
219 | }
220 |
221 | .inline-deletelink {
222 | float: left;
223 | }
224 |
225 | form .form-row p.datetime {
226 | overflow: hidden;
227 | }
228 |
229 | /* MISC */
230 |
231 | .inline-related h2, .inline-group h2 {
232 | text-align: right
233 | }
234 |
235 | .inline-related h3 span.delete {
236 | padding-right: 20px;
237 | padding-left: inherit;
238 | left: 10px;
239 | right: inherit;
240 | float:left;
241 | }
242 |
243 | .inline-related h3 span.delete label {
244 | margin-left: inherit;
245 | margin-right: 2px;
246 | }
247 |
248 | /* IE7 specific bug fixes */
249 |
250 | div.colM {
251 | position: relative;
252 | }
253 |
254 | .submit-row input {
255 | float: left;
256 | }
257 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/static_root/admin/fonts/Roboto-Bold-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/9ddf3f6f32fff64c66cc30a44a8ca1d46f4b9c88/static_root/admin/fonts/Roboto-Bold-webfont.woff
--------------------------------------------------------------------------------
/static_root/admin/fonts/Roboto-Light-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/9ddf3f6f32fff64c66cc30a44a8ca1d46f4b9c88/static_root/admin/fonts/Roboto-Light-webfont.woff
--------------------------------------------------------------------------------
/static_root/admin/fonts/Roboto-Regular-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingforentrepreneurs/Srvup-2/9ddf3f6f32fff64c66cc30a44a8ca1d46f4b9c88/static_root/admin/fonts/Roboto-Regular-webfont.woff
--------------------------------------------------------------------------------
/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/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/calendar-icons.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/static_root/admin/img/gis/move_vertex_off.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static_root/admin/img/gis/move_vertex_on.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static_root/admin/img/icon-addlink.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/static_root/admin/img/icon-alert.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/static_root/admin/img/icon-calendar.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/static_root/admin/img/icon-changelink.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/static_root/admin/img/icon-clock.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/static_root/admin/img/icon-deletelink.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/static_root/admin/img/icon-no.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/static_root/admin/img/icon-unknown-alt.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/static_root/admin/img/icon-unknown.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/static_root/admin/img/icon-yes.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/static_root/admin/img/inline-delete.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/static_root/admin/img/search.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/static_root/admin/img/selector-icons.svg:
--------------------------------------------------------------------------------
1 |
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/img/sorting-icons.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/static_root/admin/img/tooltag-add.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/static_root/admin/img/tooltag-arrowright.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/static_root/admin/js/SelectBox.js:
--------------------------------------------------------------------------------
1 | (function($) {
2 | 'use strict';
3 | var SelectBox = {
4 | cache: {},
5 | init: function(id) {
6 | var box = document.getElementById(id);
7 | var node;
8 | SelectBox.cache[id] = [];
9 | var cache = SelectBox.cache[id];
10 | var boxOptions = box.options;
11 | var boxOptionsLength = boxOptions.length;
12 | for (var i = 0, j = boxOptionsLength; i < j; i++) {
13 | node = boxOptions[i];
14 | cache.push({value: node.value, text: node.text, displayed: 1});
15 | }
16 | },
17 | redisplay: function(id) {
18 | // Repopulate HTML select box from cache
19 | var box = document.getElementById(id);
20 | var node;
21 | $(box).empty(); // clear all options
22 | var new_options = box.outerHTML.slice(0, -9); // grab just the opening tag
23 | var cache = SelectBox.cache[id];
24 | for (var i = 0, j = cache.length; i < j; i++) {
25 | node = cache[i];
26 | if (node.displayed) {
27 | var new_option = new Option(node.text, node.value, false, false);
28 | // Shows a tooltip when hovering over the option
29 | new_option.setAttribute("title", node.text);
30 | new_options += new_option.outerHTML;
31 | }
32 | }
33 | new_options += '';
34 | box.outerHTML = new_options;
35 | },
36 | filter: function(id, text) {
37 | // Redisplay the HTML select box, displaying only the choices containing ALL
38 | // the words in text. (It's an AND search.)
39 | var tokens = text.toLowerCase().split(/\s+/);
40 | var node, token;
41 | var cache = SelectBox.cache[id];
42 | for (var i = 0, j = cache.length; i < j; i++) {
43 | node = cache[i];
44 | node.displayed = 1;
45 | var node_text = node.text.toLowerCase();
46 | var numTokens = tokens.length;
47 | for (var k = 0; k < numTokens; k++) {
48 | token = tokens[k];
49 | if (node_text.indexOf(token) === -1) {
50 | node.displayed = 0;
51 | break; // Once the first token isn't found we're done
52 | }
53 | }
54 | }
55 | SelectBox.redisplay(id);
56 | },
57 | delete_from_cache: function(id, value) {
58 | var node, delete_index = null;
59 | var cache = SelectBox.cache[id];
60 | for (var i = 0, j = cache.length; i < j; i++) {
61 | node = cache[i];
62 | if (node.value === value) {
63 | delete_index = i;
64 | break;
65 | }
66 | }
67 | cache.splice(delete_index, 1);
68 | },
69 | add_to_cache: function(id, option) {
70 | SelectBox.cache[id].push({value: option.value, text: option.text, displayed: 1});
71 | },
72 | cache_contains: function(id, value) {
73 | // Check if an item is contained in the cache
74 | var node;
75 | var cache = SelectBox.cache[id];
76 | for (var i = 0, j = cache.length; i < j; i++) {
77 | node = cache[i];
78 | if (node.value === value) {
79 | return true;
80 | }
81 | }
82 | return false;
83 | },
84 | move: function(from, to) {
85 | var from_box = document.getElementById(from);
86 | var option;
87 | var boxOptions = from_box.options;
88 | var boxOptionsLength = boxOptions.length;
89 | for (var i = 0, j = boxOptionsLength; i < j; i++) {
90 | option = boxOptions[i];
91 | var option_value = option.value;
92 | if (option.selected && SelectBox.cache_contains(from, option_value)) {
93 | SelectBox.add_to_cache(to, {value: option_value, text: option.text, displayed: 1});
94 | SelectBox.delete_from_cache(from, option_value);
95 | }
96 | }
97 | SelectBox.redisplay(from);
98 | SelectBox.redisplay(to);
99 | },
100 | move_all: function(from, to) {
101 | var from_box = document.getElementById(from);
102 | var option;
103 | var boxOptions = from_box.options;
104 | var boxOptionsLength = boxOptions.length;
105 | for (var i = 0, j = boxOptionsLength; i < j; i++) {
106 | option = boxOptions[i];
107 | var option_value = option.value;
108 | if (SelectBox.cache_contains(from, option_value)) {
109 | SelectBox.add_to_cache(to, {value: option_value, text: option.text, displayed: 1});
110 | SelectBox.delete_from_cache(from, option_value);
111 | }
112 | }
113 | SelectBox.redisplay(from);
114 | SelectBox.redisplay(to);
115 | },
116 | sort: function(id) {
117 | SelectBox.cache[id].sort(function(a, b) {
118 | a = a.text.toLowerCase();
119 | b = b.text.toLowerCase();
120 | try {
121 | if (a > 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 |
--------------------------------------------------------------------------------
/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/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' + 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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 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 |
--------------------------------------------------------------------------------
/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
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 |
--------------------------------------------------------------------------------