From 1b1315388ca5c8727c7053ce95122b65e79e83f5 Mon Sep 17 00:00:00 2001 From: Moeen Zamani Date: Fri, 19 Feb 2016 15:39:27 +0330 Subject: [PATCH] public release v0.1.0 --- .gitignore | 9 + AUTHORS | 3 + CHANGELOG.md | 6 + INSTALL.md | 114 +++ LICENSE | 20 + README.md | 120 ++++ SIGN | 10 + VERSION | 1 + core/README.md | 50 ++ core/backend/djacket/__init__.py | 0 core/backend/djacket/settings.py | 171 +++++ core/backend/djacket/storage.py | 20 + core/backend/djacket/urls.py | 24 + core/backend/djacket/views.py | 20 + core/backend/djacket/wsgi.py | 16 + core/backend/filter/__init__.py | 0 core/backend/filter/admin.py | 1 + core/backend/filter/models.py | 3 + core/backend/filter/templatetags/__init__.py | 0 .../filter/templatetags/djacket_filters.py | 45 ++ core/backend/filter/tests.py | 1 + core/backend/filter/views.py | 3 + core/backend/git/__init__.py | 0 core/backend/git/action.py | 11 + core/backend/git/decorators.py | 68 ++ core/backend/git/http.py | 162 +++++ core/backend/git/object.py | 366 ++++++++++ core/backend/git/repo.py | 164 +++++ core/backend/git/service.py | 11 + core/backend/git/statistics.py | 123 ++++ core/backend/git/tests.py | 8 + core/backend/manage.py | 10 + core/backend/repository/__init__.py | 0 core/backend/repository/admin.py | 8 + core/backend/repository/api/__init__.py | 0 core/backend/repository/api/urls.py | 9 + core/backend/repository/api/views.py | 48 ++ core/backend/repository/decorators.py | 97 +++ core/backend/repository/forms.py | 156 +++++ core/backend/repository/models.py | 151 ++++ core/backend/repository/signals.py | 42 ++ core/backend/repository/tests.py | 1 + core/backend/repository/urls.py | 22 + core/backend/repository/views.py | 204 ++++++ core/backend/user/__init__.py | 0 core/backend/user/admin.py | 6 + core/backend/user/auth.py | 19 + core/backend/user/decorators.py | 19 + core/backend/user/forms.py | 246 +++++++ core/backend/user/models.py | 43 ++ core/backend/user/signals.py | 54 ++ core/backend/user/tests.py | 1 + core/backend/user/urls.py | 12 + core/backend/user/views.py | 122 ++++ core/backend/utils/__init__.py | 0 core/backend/utils/date.py | 47 ++ core/backend/utils/decorators.py | 17 + core/backend/utils/system.py | 45 ++ core/backend/utils/urlparser.py | 6 + core/frontend/.bowerrc | 5 + core/frontend/bower.json | 40 ++ core/frontend/gulpfile.js | 59 ++ core/frontend/package.json | 31 + .../build/static/android-icon-36x36.png | Bin 0 -> 1438 bytes .../public/build/static/apple-icon-60x60.png | Bin 0 -> 1912 bytes .../public/build/static/apple-icon.png | Bin 0 -> 7292 bytes .../public/build/static/browserconfig.xml | 2 + core/frontend/public/build/static/favicon.ico | Bin 0 -> 1150 bytes .../public/build/static/libs/README.md | 3 + .../public/build/static/manifest.json | 11 + .../public/build/static/ms-icon-70x70.png | Bin 0 -> 2021 bytes .../build/static/scripts/djacket-session.js | 1 + .../public/build/static/scripts/djacket.js | 1 + .../build/static/scripts/icon-selection.js | 1 + .../build/static/styles/djacket-session.css | 1 + .../public/build/static/styles/djacket.css | 1 + core/frontend/public/build/views/404.html | 37 + core/frontend/public/build/views/500.html | 37 + .../public/build/views/base/deposit-item.html | 1 + .../public/build/views/base/empty-repo.html | 15 + .../public/build/views/base/loading.html | 1 + .../build/views/base/repo-area51-form.html | 1 + .../build/views/base/repo-branches.html | 1 + .../public/build/views/base/repo-browse.html | 3 + .../public/build/views/base/repo-commits.html | 1 + .../build/views/base/repo-creation-form.html | 1 + .../public/build/views/base/repo-graphs.html | 1 + .../build/views/base/repo-no-commits.html | 1 + .../public/build/views/base/session-base.html | 1 + .../public/build/views/base/sidebar.html | 1 + .../build/views/base/user-area51-form.html | 1 + .../build/views/base/user-info-form.html | 1 + .../build/views/base/user-login-form.html | 1 + .../build/views/base/user-profile-form.html | 1 + .../build/views/base/user-register-form.html | 1 + core/frontend/public/build/views/index.html | 69 ++ .../public/build/views/repository/new.html | 1 + .../build/views/repository/repo-main.html | 1 + .../build/views/repository/repo-pjax.html | 1 + .../build/views/repository/repo-sections.html | 1 + .../build/views/repository/repo-settings.html | 1 + .../public/build/views/user/deposit.html | 1 + .../build/views/user/user-settings.html | 1 + .../dev/static/scripts/djacket-session.js | 212 ++++++ .../public/dev/static/scripts/djacket.js | 36 + .../dev/static/scripts/icon-selection.js | 106 +++ .../public/dev/static/styles/_base.scss | 121 ++++ .../public/dev/static/styles/_branches.scss | 3 + .../public/dev/static/styles/_commits.scss | 5 + .../public/dev/static/styles/_container.scss | 46 ++ .../public/dev/static/styles/_deposit.scss | 55 ++ .../public/dev/static/styles/_empty-repo.scss | 25 + .../public/dev/static/styles/_forms.scss | 136 ++++ .../public/dev/static/styles/_graphs.scss | 19 + .../public/dev/static/styles/_loading.scss | 62 ++ .../public/dev/static/styles/_markdown.scss | 651 ++++++++++++++++++ .../public/dev/static/styles/_mixins.scss | 27 + .../public/dev/static/styles/_new.scss | 18 + .../dev/static/styles/_repo-settings.scss | 6 + .../public/dev/static/styles/_repo.scss | 85 +++ .../public/dev/static/styles/_reset.scss | 49 ++ .../public/dev/static/styles/_sidebar.scss | 67 ++ .../public/dev/static/styles/_title.scss | 9 + .../dev/static/styles/_user-settings.scss | 6 + .../public/dev/static/styles/_vars.scss | 21 + .../public/dev/static/styles/_view-tabs.scss | 48 ++ .../dev/static/styles/djacket-session.scss | 27 + .../public/dev/static/styles/djacket.scss | 118 ++++ .../public/dev/views/base/deposit-item.html | 30 + .../public/dev/views/base/empty-repo.html | 31 + .../public/dev/views/base/loading.html | 11 + .../dev/views/base/repo-area51-form.html | 28 + .../public/dev/views/base/repo-branches.html | 20 + .../public/dev/views/base/repo-browse.html | 49 ++ .../public/dev/views/base/repo-commits.html | 28 + .../dev/views/base/repo-creation-form.html | 52 ++ .../public/dev/views/base/repo-graphs.html | 14 + .../dev/views/base/repo-no-commits.html | 3 + .../public/dev/views/base/session-base.html | 43 ++ .../public/dev/views/base/sidebar.html | 35 + .../dev/views/base/user-area51-form.html | 28 + .../public/dev/views/base/user-info-form.html | 47 ++ .../dev/views/base/user-login-form.html | 33 + .../dev/views/base/user-profile-form.html | 32 + .../dev/views/base/user-register-form.html | 38 + .../public/dev/views/repository/new.html | 17 + .../dev/views/repository/repo-main.html | 60 ++ .../dev/views/repository/repo-pjax.html | 2 + .../dev/views/repository/repo-sections.html | 13 + .../dev/views/repository/repo-settings.html | 30 + .../public/dev/views/user/deposit.html | 23 + .../public/dev/views/user/user-settings.html | 40 ++ core/media/avatars/README.md | 3 + core/media/stock/1.png | Bin 0 -> 14000 bytes core/media/stock/10.png | Bin 0 -> 11921 bytes core/media/stock/11.png | Bin 0 -> 9106 bytes core/media/stock/2.png | Bin 0 -> 9467 bytes core/media/stock/3.png | Bin 0 -> 12874 bytes core/media/stock/4.png | Bin 0 -> 14831 bytes core/media/stock/5.png | Bin 0 -> 15375 bytes core/media/stock/6.png | Bin 0 -> 14852 bytes core/media/stock/7.png | Bin 0 -> 11640 bytes core/media/stock/8.png | Bin 0 -> 12987 bytes core/media/stock/9.png | Bin 0 -> 11619 bytes core/static/README.md | 3 + djacket.sh | 136 ++++ index.png | Bin 0 -> 14858 bytes libs/README.md | 21 + requirements.txt | 4 + run/README.md | 5 + run/djacket.gunicorn.conf | 15 + setup-djacket.sh | 195 ++++++ 172 files changed, 6321 insertions(+) create mode 100644 .gitignore create mode 100644 AUTHORS create mode 100644 CHANGELOG.md create mode 100644 INSTALL.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 SIGN create mode 100644 VERSION create mode 100644 core/README.md create mode 100644 core/backend/djacket/__init__.py create mode 100644 core/backend/djacket/settings.py create mode 100644 core/backend/djacket/storage.py create mode 100644 core/backend/djacket/urls.py create mode 100644 core/backend/djacket/views.py create mode 100644 core/backend/djacket/wsgi.py create mode 100644 core/backend/filter/__init__.py create mode 100644 core/backend/filter/admin.py create mode 100644 core/backend/filter/models.py create mode 100644 core/backend/filter/templatetags/__init__.py create mode 100644 core/backend/filter/templatetags/djacket_filters.py create mode 100644 core/backend/filter/tests.py create mode 100644 core/backend/filter/views.py create mode 100644 core/backend/git/__init__.py create mode 100644 core/backend/git/action.py create mode 100644 core/backend/git/decorators.py create mode 100644 core/backend/git/http.py create mode 100644 core/backend/git/object.py create mode 100644 core/backend/git/repo.py create mode 100644 core/backend/git/service.py create mode 100644 core/backend/git/statistics.py create mode 100644 core/backend/git/tests.py create mode 100755 core/backend/manage.py create mode 100644 core/backend/repository/__init__.py create mode 100644 core/backend/repository/admin.py create mode 100644 core/backend/repository/api/__init__.py create mode 100644 core/backend/repository/api/urls.py create mode 100644 core/backend/repository/api/views.py create mode 100644 core/backend/repository/decorators.py create mode 100644 core/backend/repository/forms.py create mode 100644 core/backend/repository/models.py create mode 100644 core/backend/repository/signals.py create mode 100644 core/backend/repository/tests.py create mode 100644 core/backend/repository/urls.py create mode 100644 core/backend/repository/views.py create mode 100644 core/backend/user/__init__.py create mode 100644 core/backend/user/admin.py create mode 100644 core/backend/user/auth.py create mode 100644 core/backend/user/decorators.py create mode 100644 core/backend/user/forms.py create mode 100644 core/backend/user/models.py create mode 100644 core/backend/user/signals.py create mode 100644 core/backend/user/tests.py create mode 100644 core/backend/user/urls.py create mode 100644 core/backend/user/views.py create mode 100644 core/backend/utils/__init__.py create mode 100644 core/backend/utils/date.py create mode 100644 core/backend/utils/decorators.py create mode 100644 core/backend/utils/system.py create mode 100644 core/backend/utils/urlparser.py create mode 100644 core/frontend/.bowerrc create mode 100644 core/frontend/bower.json create mode 100644 core/frontend/gulpfile.js create mode 100644 core/frontend/package.json create mode 100644 core/frontend/public/build/static/android-icon-36x36.png create mode 100644 core/frontend/public/build/static/apple-icon-60x60.png create mode 100644 core/frontend/public/build/static/apple-icon.png create mode 100644 core/frontend/public/build/static/browserconfig.xml create mode 100644 core/frontend/public/build/static/favicon.ico create mode 100644 core/frontend/public/build/static/libs/README.md create mode 100644 core/frontend/public/build/static/manifest.json create mode 100644 core/frontend/public/build/static/ms-icon-70x70.png create mode 100644 core/frontend/public/build/static/scripts/djacket-session.js create mode 100644 core/frontend/public/build/static/scripts/djacket.js create mode 100644 core/frontend/public/build/static/scripts/icon-selection.js create mode 100644 core/frontend/public/build/static/styles/djacket-session.css create mode 100644 core/frontend/public/build/static/styles/djacket.css create mode 100644 core/frontend/public/build/views/404.html create mode 100644 core/frontend/public/build/views/500.html create mode 100644 core/frontend/public/build/views/base/deposit-item.html create mode 100644 core/frontend/public/build/views/base/empty-repo.html create mode 100644 core/frontend/public/build/views/base/loading.html create mode 100644 core/frontend/public/build/views/base/repo-area51-form.html create mode 100644 core/frontend/public/build/views/base/repo-branches.html create mode 100644 core/frontend/public/build/views/base/repo-browse.html create mode 100644 core/frontend/public/build/views/base/repo-commits.html create mode 100644 core/frontend/public/build/views/base/repo-creation-form.html create mode 100644 core/frontend/public/build/views/base/repo-graphs.html create mode 100644 core/frontend/public/build/views/base/repo-no-commits.html create mode 100644 core/frontend/public/build/views/base/session-base.html create mode 100644 core/frontend/public/build/views/base/sidebar.html create mode 100644 core/frontend/public/build/views/base/user-area51-form.html create mode 100644 core/frontend/public/build/views/base/user-info-form.html create mode 100644 core/frontend/public/build/views/base/user-login-form.html create mode 100644 core/frontend/public/build/views/base/user-profile-form.html create mode 100644 core/frontend/public/build/views/base/user-register-form.html create mode 100644 core/frontend/public/build/views/index.html create mode 100644 core/frontend/public/build/views/repository/new.html create mode 100644 core/frontend/public/build/views/repository/repo-main.html create mode 100644 core/frontend/public/build/views/repository/repo-pjax.html create mode 100644 core/frontend/public/build/views/repository/repo-sections.html create mode 100644 core/frontend/public/build/views/repository/repo-settings.html create mode 100644 core/frontend/public/build/views/user/deposit.html create mode 100644 core/frontend/public/build/views/user/user-settings.html create mode 100644 core/frontend/public/dev/static/scripts/djacket-session.js create mode 100644 core/frontend/public/dev/static/scripts/djacket.js create mode 100644 core/frontend/public/dev/static/scripts/icon-selection.js create mode 100644 core/frontend/public/dev/static/styles/_base.scss create mode 100644 core/frontend/public/dev/static/styles/_branches.scss create mode 100644 core/frontend/public/dev/static/styles/_commits.scss create mode 100644 core/frontend/public/dev/static/styles/_container.scss create mode 100644 core/frontend/public/dev/static/styles/_deposit.scss create mode 100644 core/frontend/public/dev/static/styles/_empty-repo.scss create mode 100644 core/frontend/public/dev/static/styles/_forms.scss create mode 100644 core/frontend/public/dev/static/styles/_graphs.scss create mode 100644 core/frontend/public/dev/static/styles/_loading.scss create mode 100644 core/frontend/public/dev/static/styles/_markdown.scss create mode 100644 core/frontend/public/dev/static/styles/_mixins.scss create mode 100644 core/frontend/public/dev/static/styles/_new.scss create mode 100644 core/frontend/public/dev/static/styles/_repo-settings.scss create mode 100644 core/frontend/public/dev/static/styles/_repo.scss create mode 100644 core/frontend/public/dev/static/styles/_reset.scss create mode 100644 core/frontend/public/dev/static/styles/_sidebar.scss create mode 100644 core/frontend/public/dev/static/styles/_title.scss create mode 100644 core/frontend/public/dev/static/styles/_user-settings.scss create mode 100644 core/frontend/public/dev/static/styles/_vars.scss create mode 100644 core/frontend/public/dev/static/styles/_view-tabs.scss create mode 100644 core/frontend/public/dev/static/styles/djacket-session.scss create mode 100644 core/frontend/public/dev/static/styles/djacket.scss create mode 100644 core/frontend/public/dev/views/base/deposit-item.html create mode 100644 core/frontend/public/dev/views/base/empty-repo.html create mode 100644 core/frontend/public/dev/views/base/loading.html create mode 100644 core/frontend/public/dev/views/base/repo-area51-form.html create mode 100644 core/frontend/public/dev/views/base/repo-branches.html create mode 100644 core/frontend/public/dev/views/base/repo-browse.html create mode 100644 core/frontend/public/dev/views/base/repo-commits.html create mode 100644 core/frontend/public/dev/views/base/repo-creation-form.html create mode 100644 core/frontend/public/dev/views/base/repo-graphs.html create mode 100644 core/frontend/public/dev/views/base/repo-no-commits.html create mode 100644 core/frontend/public/dev/views/base/session-base.html create mode 100644 core/frontend/public/dev/views/base/sidebar.html create mode 100644 core/frontend/public/dev/views/base/user-area51-form.html create mode 100644 core/frontend/public/dev/views/base/user-info-form.html create mode 100644 core/frontend/public/dev/views/base/user-login-form.html create mode 100644 core/frontend/public/dev/views/base/user-profile-form.html create mode 100644 core/frontend/public/dev/views/base/user-register-form.html create mode 100644 core/frontend/public/dev/views/repository/new.html create mode 100644 core/frontend/public/dev/views/repository/repo-main.html create mode 100644 core/frontend/public/dev/views/repository/repo-pjax.html create mode 100644 core/frontend/public/dev/views/repository/repo-sections.html create mode 100644 core/frontend/public/dev/views/repository/repo-settings.html create mode 100644 core/frontend/public/dev/views/user/deposit.html create mode 100644 core/frontend/public/dev/views/user/user-settings.html create mode 100644 core/media/avatars/README.md create mode 100644 core/media/stock/1.png create mode 100644 core/media/stock/10.png create mode 100644 core/media/stock/11.png create mode 100644 core/media/stock/2.png create mode 100644 core/media/stock/3.png create mode 100644 core/media/stock/4.png create mode 100644 core/media/stock/5.png create mode 100644 core/media/stock/6.png create mode 100644 core/media/stock/7.png create mode 100644 core/media/stock/8.png create mode 100644 core/media/stock/9.png create mode 100644 core/static/README.md create mode 100644 djacket.sh create mode 100644 index.png create mode 100644 libs/README.md create mode 100644 requirements.txt create mode 100644 run/README.md create mode 100644 run/djacket.gunicorn.conf create mode 100644 setup-djacket.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..91b798a --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +node_modules +bower_components + + +__pycache__ + + +libs/* +!libs/README.md diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..f37e711 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,3 @@ +Djacket project initiated in Dec, 2015. Below is the list of contributers. + +Moeen Zamani diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..aaead41 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# 0.1.0 (February 19, 2016) +* Djacket is publicly released with these features: + - User creation/deletion/settings + - Repo creation/deletion/settings + - Repo browsing/branches/commits/graphs + - See other users public repos diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..10823ce --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,114 @@ +# Installation / Troubleshooting + +Here the whole process of Djacket installation is explained so in case you feel to do a manual + installation or want to solve problems occurred along the way, the path would be clear. +Process is as explained below in following sub-sections. + + +## Requirements +Djacket requires +``` +- Git (version 1.8 or above) +- Python3 and pip3 (any version of Python version 3.x) +- Pillow imaging library (https://pypi.python.org/pypi/Pillow) +``` +to be installed and working. + +Other libraries will be exported during installation and start. +These requirements will be checked at the beginning of installation. + + +## Deposit +There should be an absolute path for a folder to keep Git repositories on server. Path for this folder + is set to variable GIT_DEPOSIT_ROOT in 'core/backend/djacket/settings.py' for Django runtime like this: + +``` + GIT_DEPOSIT_ROOT = '/path/to/your/deposit/folder' +``` + +This path will be set during running of installation script. + +Each user will have a folder inside GIT_DEPOSIT_ROOT with his/her username + as the folder name and inside this folder is owner's repositories. + +So for example if user's username is "thomas", his repositories will be kept under +``` + GIT_DEPOSIT_ROOT/thomas/ +``` + +If you decided to change path of this folder make sure to move all users repositories. + + +## Django Server Application +Next, python environment variable PYTHONPATH will be set to "/libs" folder so Djacket can + run successfully with it's runtime libraries paths provided. +After that Django "manage.py" actions should run. These actions are committed using these commands: +``` +./manage.py makemigrations +./manage.py migrate +./manage.py collectstatic +./manage.py createsuperuser +``` +During these, database migrations, collection of static files and creation of super user are done. + + +## Security +There are some security considerations that should be done. +First user should enter their server IP address or domain name in one of formats accepted by Django framework +to be provided in ALLOWED_HOSTS variable in "core/backend/djacket/settings.py" like this: +``` +ALLOWED_HOSTS = ['.example.com'] +or +ALLOWED_HOSTS = ['www.example.com'] +or +ALLOWED_HOSTS = ['123.123.123.123'] +``` +Then Django secret key is generated and put to "core/backend/djacket/settings.py": +``` + SECRET_KEY = 'some random generated string' +``` +During installation a random secret key will be generated automatically and installed. + + +## Ready +Now your Djacket application is completely setup. +The only thing here is configuring the web server to host application, static and media files. +Nginx is the recommended web server. If you already have Apache installed on your server there's no problem for these two to work along since your Nginx configuration will not conflict with Apache if you do it right. + +An example of Nginx configuration is shown below. Assuming that you started Djacket on port 8080 with command: + +``` +./djacket.sh start 8080 +``` +then you can configure Nginx to have a server as: + +``` +server { + listen 8585; + server_name example.com; + client_max_body_size 32M; # Maximum acceptable payload for an upload. If more than 32MB is needed, change it to higher values. + + location / { + proxy_set_header Host $http_host; + proxy_pass http://0.0.0.0:8080; # Port number Djacket is started on. + } + + location /media { + alias /path/to/djacket-v0.1.0/core/media/; + } + + location /static { + alias /path/to/djacket-v0.1.0/core/static/; + } +} +``` + +Now if you visit http://example.com:8585, you have a fully functioning Djacket. + +# Other configurations +You can also deploy Djacket using other technologies such as Apache/mod_wsgi or Nginx/uwsgi, although installation script + prefers Nginx/gunicorn. + +Nevertheless if you prefer other stacks, everything is the same up until "Ready" section. +When you reach "Ready" section your Djacket Django application is fully functional and is kept under "core" folder. +Now you can deploy it as a Django application using Apache/mod_wsgi or Nginx/uswgi, etc. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6a8aaa8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2016 Djacket Project. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..79b56c7 --- /dev/null +++ b/README.md @@ -0,0 +1,120 @@ +# Djacket +A Git management system written in [Python/Django](https://www.djangoproject.com/). It's meant to be for personal +or small business usages. Installation is available and tested for Linux servers. + +

+ Index +

+ + +# Requirements +Djacket requires these packages: +``` +- Git (version 1.8 or above) +- Python3 and pip3 (any version of Python version 3.x) +- Pillow imaging library (https://pypi.python.org/pypi/Pillow) +``` +to be installed and working. + + +# Installation +Download latest release from https://github.com/Djacket/djacket/releases/latest, then extract and install: +``` +tar xzvf djacket-v0.1.0.tar.gz +cd djacket-v0.1.0 +bash setup-djacket.sh +``` +after installation you can start Djacket by running +``` +./djacket.sh start 8080 +``` +Your web server should be configured to serve Djacket and it's static and media files. After installation, folder path for +static and media files is printed out for server settings. [Nginx](http://nginx.org/en/) is the recommended web server (and it can work along with your existing installation of Apache too). +In your Nginx configuration put (assuming your domain is example.com): +``` +server { + listen 8585; + server_name example.com; + client_max_body_size 32M; + + location / { + proxy_set_header Host $http_host; + proxy_pass http://0.0.0.0:8080; # Port number Djacket is started on. + } + + location /media { + alias /path/to/djacket-v0.1.0/core/media/; + } + + location /static { + alias /path/to/djacket-v0.1.0/core/static/; + } +} +``` +Now if you visit http://example.com:8585, you have a fully functioning Djacket. + + +For more details, troubleshooting or manual installation see [INSTALL](./INSTALL.md) file. + + +# Contribution +Djacket's backend technology is [Python/Django](https://www.djangoproject.com/) v1.8. aside from this, libraries: +``` +python-dateutil +django-easy-pjax +``` +are used, so if you want to make changes to backend you need these installed. These libraries will be provided inside libs +folder for each release. + +Frontend is maintained using [gulpjs](http://gulpjs.com/) and [bower](http://bower.io/), so first make sure you have +[nodejs](https://nodejs.org/en/) and gulp installed globally then inside core/frontend, run +``` +npm install +``` +Now you can make changes to frontend views, styles and scripts by modifying files in core/frontend/public/dev. +Any changes you make will be compiled by gulp and pushed to build folder. So if you made any changes make sure to do +``` +gulp compile +``` +which runs the compilation task or if you are on constant development run +``` +gulp +``` +so default task will run (which is watching for file changes and pushing them to build folder) + + +# Issues +If you encountered any bugs or issues, Please report it either in Github issues section or send it as +an email to [projectdjacket@gmail.com](mailto:projectdjacket@gmail.com) + + +# Documentation +Project is fully documented and it can be reached by going to: +``` +/admin/doc/ +``` +"docutils" package is required for browsing through. + + +# License + +The MIT License (MIT) + +Copyright (c) 2016 Djacket Project. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/SIGN b/SIGN new file mode 100644 index 0000000..a3de97f --- /dev/null +++ b/SIGN @@ -0,0 +1,10 @@ + + o 88 .o +8888ooooo. 88 ooo .88 +8d88' `888b .o .oooo88 .ooooooo 88 8. .oooooo o8888oo +888 888 .88 d88` '88b d88` 88o. doo ""888"" +888 888 8 88 888 888 888 88o. 88oooo 888 +888 888 88 888 .888 b88 88 8. boo 888 . +888 . .888 88 '88bod`888 .ooooooo 88 ooo .oooooo "888Y +88888`bod8P' 88 + Y88" diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..b82608c --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +v0.1.0 diff --git a/core/README.md b/core/README.md new file mode 100644 index 0000000..27f4617 --- /dev/null +++ b/core/README.md @@ -0,0 +1,50 @@ +# Djacket /core + +This folder contains core application. + +## /backend +Django project is inside this folder with all the applications. +Apps inside are: + +- djacket: main Django project folder. +- filter: creates custom Djacket template filters. +- git: Djacket's Git library for interactions + with repositories. +- repository: creates/modifies repositories. +- user: creates/modifies users and their profiles. +- utils: has extra tools useful across the whole project. + +If using default settings from installation script [SQLite](https://www.sqlite.org/) database file will be kept here. PostgreSQL database settings are available in settings.py for a more advanced database approach. + +## /frontend +Contains view and static files for development and production. All styles, scripts and views templates are developed under /public/dev folder then using ["gulp"](http://gulpjs.com/) tasks they are compiled and pushed to /public/build path for production. +For any customization, first make sure you have +[nodejs](https://nodejs.org/en/) and gulp installed globally then inside /frontend folder, run + +``` + npm install +``` + +Now you can make changes to gulpfile and commit them. +[Bower](http://bower.io/) configurations are available as well in ".bowerrc" file. +For any customization, inside /frontend folder first run + +``` + bower install +``` + +then you can make changes to frontend libraries such as [jQuery](https://jquery.com/), [Chart.js](www.chartjs.org/), [Font-Awesome](https://fortawesome.github.io/Font-Awesome/), etc. + +## /media +As of right now only user avatar images are kept are. + +## /static +Contains static files. All static files will be piled up in here after installation script executes 'collectstatic'. +Webserver's (Apache, Nginx, etc) "/static" location should be pointed to this folder. +If there's a need for changing static folder, you can change variable 'STATIC_ROOT' in backend/djacket/settings.py and run + +``` + ./manage.py collectstatic +``` + +Note that your Webserver's settings should be changed then too. diff --git a/core/backend/djacket/__init__.py b/core/backend/djacket/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/backend/djacket/settings.py b/core/backend/djacket/settings.py new file mode 100644 index 0000000..1e5f02f --- /dev/null +++ b/core/backend/djacket/settings.py @@ -0,0 +1,171 @@ +""" +Django settings for djacket project. + +Generated by 'django-admin startproject' using Django 1.8. + +For more information on this file, see +https://docs.djangoproject.com/en/1.8/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.8/ref/settings/ +""" + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +import os + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Front-end folder to keep views, styles and scripts. +# Default is set to a BASE_DIR/../'frontend' +FRONTEND_DIR = os.path.join(BASE_DIR, '..', 'frontend') + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +# SECRET_KEY = 'a randomly generated string will be installed' + + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = False + + +# For security reasons, set domain or host of your site in ALLOWED_HOSTS +# e.g. +# ALLOWED_HOSTS = ['.exampledomain.com'] +# You can find more details in https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts. + + +# Application definition + +INSTALLED_APPS = ( + 'django.contrib.admin', + 'django.contrib.admindocs', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'easy_pjax', + 'user', + 'repository', + 'filter' +) + +MIDDLEWARE_CLASSES = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django.middleware.security.SecurityMiddleware', +) + +ROOT_URLCONF = 'djacket.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(FRONTEND_DIR, 'public', 'build', 'views'),], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + 'django.core.context_processors.request', + ], + }, + }, +] + +WSGI_APPLICATION = 'djacket.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.8/ref/settings/#databases + +# Currently there's not much of a database transaction (only on user/repository creation) +# so SQLite database would suffice. But in case you feel it's not enough for you +# comment sqlite3 database settings below and uncomment PostgreSQL settings. +# Make sure your PostgreSQL database is setup first and your database is created. + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, '..', 'djacketdb.sqlite3'), + } +} + +# PostgreSQL database settings. +# DATABASES = { +# 'default': { +# 'ENGINE': 'django.db.backends.postgresql_psycopg2', +# 'NAME': 'djacket', +# 'USER': 'postgres', +# 'PASSWORD': '', +# 'HOST': 'localhost', +# 'PORT': '5432', +# } +# } + + + +# Internationalization +# https://docs.djangoproject.com/en/1.8/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.8/howto/static-files/ +# Static files will be collected to be served in 'BASE_DIR/../static/', outside of server code. + +STATIC_URL = '/static/' +STATICFILES_DIRS = [os.path.join(FRONTEND_DIR, 'public', 'build', 'static'),] +STATIC_ROOT = os.path.join(BASE_DIR, '..', 'static') + +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +) + + +# Media files (User avatar images) +# Default is set to ''BASE_DIR/../media', outside of server code. + +MEDIA_ROOT = os.path.join(BASE_DIR, '..', 'media') +MEDIA_URL = '/media/' + + +# User authentication urls configuration. + +LOGIN_URL = 'index' +LOGOUT_URL = 'user_logout' +LOGIN_REDIRECT_URL = 'index' + + +# Djacket version number and site name. + +DJACKET_VERSION = '0.1.0' +SITE_NAME = 'Djacket' + + +# Git repositories deposit folder on server. +# This folder can be set by initializing a variable called 'GIT_DEPOSIT_ROOT'. +# e.g. +# GIT_DEPOSIT_ROOT = '/path/to/your/deposit/folder' +# Other variables from installation such as ALLOWED_HOSTS and SECRET_KEY will be set here. diff --git a/core/backend/djacket/storage.py b/core/backend/djacket/storage.py new file mode 100644 index 0000000..982c292 --- /dev/null +++ b/core/backend/djacket/storage.py @@ -0,0 +1,20 @@ +import os + +from django.core.files.storage import FileSystemStorage +from django.conf import settings + + +class OverwriteStorage(FileSystemStorage): + """ + Overwrites to a file if it exists on file system. + """ + + def get_available_name(self, name): + """ + If a file with the given name exists in MEDIA_ROOT, + it will be removed otherwise it's name is returned. + """ + + if self.exists(name): + os.remove(os.path.join(settings.MEDIA_ROOT, name)) + return name diff --git a/core/backend/djacket/urls.py b/core/backend/djacket/urls.py new file mode 100644 index 0000000..cf5805b --- /dev/null +++ b/core/backend/djacket/urls.py @@ -0,0 +1,24 @@ +from django.conf import settings +from django.contrib import admin +from django.conf.urls import include, url + +from user.views import user_deposit +from djacket.views import index + + +# Djacket main urls will be addressed here. + +urlpatterns = [ + #url(r'^media/(?P.*)$', 'django.views.static.serve', {'document_root': settings.MEDIA_ROOT}), # serve static medias on /media/* + url(r'^admin/doc/', include('django.contrib.admindocs.urls')), # project docs view. + url(r'^admin/', include(admin.site.urls)), # admin interface. + url(r'^', include('repository.urls')), # import repository app urls with no prefix. + url(r'^account/', include('user.urls')), # import user app urls with 'account' prefix. + url(r'^(?P\w+)$', user_deposit, name='user_deposit'), # show user deposit for url '/username'. + url(r'^$', index, name='index'), # index changes to logged in user if he/she is authenticated or to djacket intro if not. +] + +# serve static medias on /media/* while DEBUG settings are on +if settings.DEBUG: + urlpatterns = urlpatterns + \ + [url(r'^media/(?P.*)$', 'django.views.static.serve', {'document_root': settings.MEDIA_ROOT}),] diff --git a/core/backend/djacket/views.py b/core/backend/djacket/views.py new file mode 100644 index 0000000..b1821d4 --- /dev/null +++ b/core/backend/djacket/views.py @@ -0,0 +1,20 @@ +from django.shortcuts import render +from django.contrib.auth import REDIRECT_FIELD_NAME +from django.contrib.auth.models import User, AnonymousUser +from django.views.decorators.http import require_http_methods + +from user.forms import UserRegistrationForm +from user.views import user_deposit + + +@require_http_methods(['GET']) +def index(request): + """ + View for index page. If request user is not authenticated then Djacket intro + page is shown, else if user is authenticated his/her deposit is shown. + """ + + if request.user.is_authenticated(): + return user_deposit(request, request.user.username) + else: + return render(request, 'index.html', {'active': 'login', REDIRECT_FIELD_NAME: request.GET.get(REDIRECT_FIELD_NAME, '')}) diff --git a/core/backend/djacket/wsgi.py b/core/backend/djacket/wsgi.py new file mode 100644 index 0000000..65745c5 --- /dev/null +++ b/core/backend/djacket/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for djacket project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djacket.settings") + +application = get_wsgi_application() diff --git a/core/backend/filter/__init__.py b/core/backend/filter/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/backend/filter/admin.py b/core/backend/filter/admin.py new file mode 100644 index 0000000..694323f --- /dev/null +++ b/core/backend/filter/admin.py @@ -0,0 +1 @@ +from django.contrib import admin diff --git a/core/backend/filter/models.py b/core/backend/filter/models.py new file mode 100644 index 0000000..77f5b30 --- /dev/null +++ b/core/backend/filter/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# No models in this module. diff --git a/core/backend/filter/templatetags/__init__.py b/core/backend/filter/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/backend/filter/templatetags/djacket_filters.py b/core/backend/filter/templatetags/djacket_filters.py new file mode 100644 index 0000000..c2cfacc --- /dev/null +++ b/core/backend/filter/templatetags/djacket_filters.py @@ -0,0 +1,45 @@ +from django import template + +register = template.Library() + + +@register.filter(name='obj_name') +def obj_name(value, arg): + """ + Returns object name from an absolute path seperated by provided 'arg'. + + e.g. + For a path seperated by '/' like '/pa/th/to/file.ext' it returns 'file.ext'. + """ + + return value.split(arg)[-1] + + +@register.filter(name='obj_ext') +def obj_ext(value): + """ + Returns extention of an object. + + e.g. + For an object with name 'somecode.py' it returns 'py'. + """ + + return value.split('.')[-1] + + +@register.filter(name='ellipsize') +def ellipsize(value, limit=32): + """ + Truncates a string after a given number of chars keeping whole words. + + Usage: + {{ string|ellipsize }} + {{ string|ellipsize:50 }} + """ + + if len(value) <= limit: + return value + else: + value = value[:limit] + words = value.split(' ')[:-1] + return ' '.join(words) + '...' diff --git a/core/backend/filter/tests.py b/core/backend/filter/tests.py new file mode 100644 index 0000000..2e9cb5f --- /dev/null +++ b/core/backend/filter/tests.py @@ -0,0 +1 @@ +from django.test import TestCase diff --git a/core/backend/filter/views.py b/core/backend/filter/views.py new file mode 100644 index 0000000..91588ec --- /dev/null +++ b/core/backend/filter/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# No views in this module. diff --git a/core/backend/git/__init__.py b/core/backend/git/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/backend/git/action.py b/core/backend/git/action.py new file mode 100644 index 0000000..dc9b575 --- /dev/null +++ b/core/backend/git/action.py @@ -0,0 +1,11 @@ +GIT_ACTION_RESULT = 'result' +GIT_ACTION_ADVERTISEMENT = 'advertisement' +GIT_ACTIONS = [GIT_ACTION_ADVERTISEMENT, GIT_ACTION_RESULT] + + +def is_valid_git_action(action): + """ + Returns true if the given action is one of git valid actions. + """ + + return action in GIT_ACTIONS diff --git a/core/backend/git/decorators.py b/core/backend/git/decorators.py new file mode 100644 index 0000000..bb73ab0 --- /dev/null +++ b/core/backend/git/decorators.py @@ -0,0 +1,68 @@ +from functools import wraps + +from django.contrib.auth.models import User +from django.http import HttpResponse, HttpResponseForbidden + +from git.service import GIT_SERVICE_RECEIVE_PACK, GIT_SERVICE_UPLOAD_PACK +from repository.models import Repository, RepositoryAccess +from user.auth import base_auth + + +def git_access_required(func): + """ + Checks to see if a repository is accessible to user. There are 3 scenarios: + 1) If repository is private and user is doing a 'git-clone' or 'git-pull': In this + case only users who have access to repository are admitted. + 2) If repository is public and user is doing a 'git-clone' or 'git-pull': This operation + is ok since repository is available for everyone. + 3) If user is committing something to repository then regardless of being a public/private + repository, user should be authenticated and has access to repository. + """ + + @wraps(func) + def _decorator(request, *args, **kwargs): + service = _parse_git_service(request.build_absolute_uri()) + user = User.objects.get(username=kwargs['username']) + repo = Repository.objects.get(owner=user, name=kwargs['repository']) + if service == GIT_SERVICE_UPLOAD_PACK and repo.private: # private repo and doing a 'git-clone' or 'git-pull' + return _check_access(request, func, *args, **kwargs) + elif service == GIT_SERVICE_UPLOAD_PACK and not repo.private: # public repo and doing a 'git-clone' or 'git-pull' + return func(request, *args, **kwargs) + elif service == GIT_SERVICE_RECEIVE_PACK: # doing a 'git-commit' + return _check_access(request, func, *args, **kwargs) + return _decorator + + +def _check_access(request, func, *args, **kwargs): + """ + Checks user's authentication and access to repository. + """ + + if request.META.get('HTTP_AUTHORIZATION'): + user = base_auth(request.META['HTTP_AUTHORIZATION']) + if user: + repo = Repository.objects.get(owner=user, name=kwargs['repository']) + access = RepositoryAccess.objects.get(user=user, repository=repo) + if access: + return func(request, *args, **kwargs) + else: # User has no access to repository. + return HttpResponseForbidden('Access forbidden.') + else: # User is not registered on Djacket. + return HttpResponseForbidden('Access forbidden.') + res = HttpResponse() + res.status_code = 401 # Basic authentication is needed. + res['WWW-Authenticate'] = 'Basic' + return res + + +def _parse_git_service(request_path): + """ + Parses request url and specifies which git service is requested. + """ + + if request_path.endswith('git-upload-pack'): + return GIT_SERVICE_UPLOAD_PACK + elif request_path.endswith('git-receive-pack'): + return GIT_SERVICE_RECEIVE_PACK + else: + return None diff --git a/core/backend/git/http.py b/core/backend/git/http.py new file mode 100644 index 0000000..17cbbf2 --- /dev/null +++ b/core/backend/git/http.py @@ -0,0 +1,162 @@ +from django.http import HttpResponse, HttpResponseNotFound + +from git.repo import Repo +from git.service import GIT_SERVICE_UPLOAD_PACK, GIT_SERVICE_RECEIVE_PACK + +GIT_HTTP_INFO_REFS = 1 +GIT_HTTP_SERVICE_UPLOAD_PACK = 2 +GIT_HTTP_SERVICE_RECEIVE_PACK = 3 + + +def get_http_error(exception): + """ + Returns proper http failure status code according to the provided exception. + """ + + if 'Not a git repository' in exception.args[0]: + return HttpResponseNotFound() + + +class GitResponse(HttpResponse): + """ + Git http response. + """ + + def __init__(self, *args, **kwargs): + self.service = kwargs.pop('service', None) + self.action = kwargs.pop('action', None) + self.repository = kwargs.pop('repository', None) + self.data = kwargs.pop('data', None) + super(GitResponse, self).__init__(*args, **kwargs) + + + def get_packet_length(self, packet): + """ + Returns length of the given packet in a 4 byte hex string. + This string is placed at the beginning of the response body. + + e.g. + 001e + """ + + hex = '0123456789abcdef' + length = len(packet) + 4 + prefix = hex[int((length >> 12) & 0xf)] + prefix = prefix + hex[int((length >> 8) & 0xf)] + prefix = prefix + hex[int((length >> 4) & 0xf)] + prefix = prefix + hex[int((length) & 0xf)] + + return prefix + + + def get_header_expires(self): + """ + Returns 'Expires' header value. + + e.g. + 'Fri, 01 Jan 1980 00:00:00 GMT' + """ + + return 'Fri, 01 Jan 1980 00:00:00 GMT' + + + def get_header_pragma(self): + """ + Returns 'Pragma' header value. + + e.g. + 'no-cache' + """ + + return 'no-cache' + + + def get_header_cache_control(self): + """ + Returns 'Cache-Control' header value. + + e.g. + 'no-cache, max-age=0, must-revalidate' + """ + + return 'no-cache, max-age=0, must-revalidate' + + + def get_header_content_type(self): + """ + Creates the 'Content-Type' header according to the requested service and action. + Structure of a 'Content-Type' header is 'application/x-\{service\}-\{action\}' + + e.g. + 'application-git-receive-pack-advertisement' + """ + + return 'application/x-{0}-{1}'.format(self.service, self.action) + + + def set_response_header(self): + """ + Sets response headers, according to the requested service and action. + Header contains values such as 'Expires', 'Pragma', 'Cache-Control' and 'Content-Type'. + """ + + self.__setitem__('Expires', self.get_header_expires()) + self.__setitem__('Pragma', self.get_header_pragma()) + self.__setitem__('Cache-Control', self.get_header_cache_control()) + self.__setitem__('Content-Type', self.get_header_content_type()) + + + def set_response_first_line(self): + """ + Sets first line of git response that includes length and requested service. + + e.g. + 001f# service=git-receive-pack + """ + + first_line = '# service={0}\n'.format(self.service) + prefix = self.get_packet_length(first_line) + self.write('{0}{1}0000'.format(prefix, first_line)) + + + def set_response_payload(self, payload_type): + """ + Writes 'refs' object information of the given repository to http response. + """ + + if payload_type == GIT_HTTP_INFO_REFS: + self.write(self.repository.get_info_refs(self.service)) + elif payload_type == GIT_HTTP_SERVICE_RECEIVE_PACK: + self.write(self.repository.commit(self.data)) + elif payload_type == GIT_HTTP_SERVICE_UPLOAD_PACK: + self.write(self.repository.pull(self.data)) + + + def get_http_info_refs(self): + """ + Returns a HttpResponse for info/refs requests. + """ + + try: + self.set_response_header() + self.set_response_first_line() + self.set_response_payload(GIT_HTTP_INFO_REFS) + return self + except BaseException as e: + return get_http_error(e) + + + def get_http_service_rpc(self): + """ + Returns a HttpResponse to 'git-upload-pack', 'git-receive-pack' requests. + """ + + try: + self.set_response_header() + if self.service == GIT_SERVICE_RECEIVE_PACK: + self.set_response_payload(GIT_HTTP_SERVICE_RECEIVE_PACK) + elif self.service == GIT_SERVICE_UPLOAD_PACK: + self.set_response_payload(GIT_HTTP_SERVICE_UPLOAD_PACK) + return self + except BaseException as e: + return get_http_error(e) diff --git a/core/backend/git/object.py b/core/backend/git/object.py new file mode 100644 index 0000000..5f430d3 --- /dev/null +++ b/core/backend/git/object.py @@ -0,0 +1,366 @@ +from abc import ABCMeta, abstractmethod + +from utils.system import run_command +from utils.date import time_to_utc + +GIT_BLOB_OBJECT = 'blob' +GIT_TREE_OBJECT = 'tree' +GIT_COMMIT_OBJECT = 'commit' +GIT_VALID_OBJECT_KINDS = [GIT_BLOB_OBJECT, GIT_TREE_OBJECT, GIT_COMMIT_OBJECT] + + +class GitObject: + """ + Represents a git object. Each GitObject is either a blob, tree or a commit which + is represented in object's "kind" attribute. + GitObject needs a Repository object as it's container and a revision. + Then it will be located using it's repository, revision and path or sha1_hash + depending on being a blob/tree or a commit object. + + + * tag object will be implemented in future versions. + """ + + __metaclass__ = ABCMeta + + + @abstractmethod + def __init__(self, repo, kind, rev): + assert repo is not None, 'Repository reference is not passed' + assert repo.is_valid(), 'Entered repository is not valid' + + self.repo = repo + self.kind = kind + self.rev = rev + + + def is_blob(self): + """ + Returns true if this object is a blob. + """ + + return self.kind == GIT_BLOB_OBJECT + + + def is_tree(self): + """ + Returns true if this object is a tree. + """ + + return self.kind == GIT_TREE_OBJECT + + + def is_commit(self): + """ + Returns true if this object is a commit. + """ + + return self.kind == GIT_COMMIT_OBJECT + + + def get_repo(self): + """ + Returns repository of object. + """ + + return self.repo + + + def get_kind(self): + """ + Returns kind of object. + """ + + return self.kind + + + def get_revision(self): + """ + Returns revision of object. + """ + + return self.rev + + + @abstractmethod + def get_subject(self): + pass + + + @abstractmethod + def get_committer_date(self): + pass + + + @abstractmethod + def get_committer_email(self): + pass + + + @abstractmethod + def get_committer_name(self): + pass + + + @abstractmethod + def show(self): + pass + + +class GitBlob(GitObject): + """ + Represents a Git blob object. + Path of object is needed for locating and getting blob's information. + """ + + def __init__(self, repo, path, rev='HEAD'): + assert path is not None, 'Path of blob should not be None' + + super(GitBlob, self).__init__(repo, GIT_BLOB_OBJECT, rev) + self.path = path + + + def get_path(self): + """ + Returns path of this blob in repository. + """ + + return self.path + + + def get_subject(self): + """ + Returns latest commit message given to this blob. + """ + + cmd = 'git log -1 --format="%s" {0} -- {1}'.format(self.rev, self.path) + + git_output = run_command(cmd=cmd, data=None, location=self.repo.location, chw=True).strip() + return git_output + + + def get_committer_date(self): + """ + Returns latest commiter date for this blob in "ISO 8601-like" format and UTC timezone. + """ + + cmd = 'git log -1 --format="%ci" {0} -- {1}'.format(self.rev, self.path) + + git_output = run_command(cmd=cmd, data=None, location=self.repo.location, chw=True) + return time_to_utc(git_output.strip()) + + + def get_committer_email(self): + """ + Returns committer email. + """ + + cmd = 'git log -1 --format="%ce" {0} -- {1}'.format(self.rev, self.path) + + git_output = run_command(cmd=cmd, data=None, location=self.repo.location, chw=True).strip() + return git_output + + + def get_committer_name(self): + """ + Returns committer name. + """ + + cmd = 'git log -1 --format="%cn" {0} -- {1}'.format(self.rev, self.path) + + git_output = run_command(cmd=cmd, data=None, location=self.repo.location, chw=True).strip() + return git_output + + + def show(self): + """ + Reads content of blob and returns it as a pretty formated output. + """ + + level = '{0}:{1}'.format(self.rev, self.path) + file_content = run_command(cmd='git cat-file -p {0}'.format(level), data=None, location=self.repo.location, chw=True) + return file_content + + + def __str__(self): + """ + Returns a string representation of blob's object. + """ + + return 'GitBlob {0}, revision {1} for {2}'.format(self.path, self.rev, self.repo) + + +class GitTree(GitObject): + """ + Represents a Git blob tree. + Path of object is needed for locating and getting tree's information. + """ + + def __init__(self, repo, path, rev='HEAD'): + assert path is not None, 'Path of tree should not be None' + + super(GitTree, self).__init__(repo, GIT_TREE_OBJECT, rev) + self.path = path + + + def get_path(self): + """ + Returns path of this tree in repository. + """ + + return self.path + + + def get_subject(self): + """ + Returns latest commit message given to this tree. + """ + + cmd = 'git log -1 --format="%s" {0} -- {1}'.format(self.rev, self.path) + + git_output = run_command(cmd=cmd, data=None, location=self.repo.location, chw=True).strip() + return git_output + + + def get_committer_date(self): + """ + Returns latest commiter date for this tree in "ISO 8601-like" format and UTC timezone. + """ + + cmd = 'git log -1 --format="%ci" {0} -- {1}'.format(self.rev, self.path) + + git_output = run_command(cmd=cmd, data=None, location=self.repo.location, chw=True) + return time_to_utc(git_output.strip()) + + + def get_committer_email(self): + """ + Returns committer email. + """ + + cmd = 'git log -1 --format="%ce" {0} -- {1}'.format(self.rev, self.path) + + git_output = run_command(cmd=cmd, data=None, location=self.repo.location, chw=True).strip() + return git_output + + + def get_committer_name(self): + """ + Returns committer name. + """ + + cmd = 'git log -1 --format="%cn" {0} -- {1}'.format(self.rev, self.path) + + git_output = run_command(cmd=cmd, data=None, location=self.repo.location, chw=True).strip() + return git_output + + + def show(self): + """ + Returns content of tree object which may be other trees or blobs. + """ + + level = '{0}:{1}'.format(self.rev, self.path) + cmd = 'git ls-tree --full-tree {0}'.format(level) + git_output = run_command(cmd=cmd, data=None, location=self.repo.location, chw=True).split('\n')[:-1] + + tree_contents = [] + for item in git_output: + kind, path = item.split()[1], '{0}/{1}'.format(self.path, item.split()[3]) + if kind == GIT_BLOB_OBJECT: + tree_contents.append(GitBlob(repo=self.repo, path=path, rev=self.rev)) + elif kind == GIT_TREE_OBJECT: + tree_contents.append(GitTree(repo=self.repo, path=path, rev=self.rev)) + return tree_contents + + + def __str__(self): + """ + Returns a string representation of tree's object. + """ + + return 'GitTree {0}, revision {1} for {2}'.format(self.path, self.rev, self.repo) + + +class GitCommit(GitObject): + """ + Represents a Git commit object. + SHA1 hash of object is needed for locating and getting object's information. + """ + + def __init__(self, repo, sha1_hash, rev='HEAD'): + assert sha1_hash is not None, 'SHA-1 hash of commit should not be None' + + super(GitCommit, self).__init__(repo, GIT_COMMIT_OBJECT, rev) + self.sha1_hash = sha1_hash + + + def get_sha1_hash(self): + """ + Returns SHA-1 hash of commit. + """ + + return self.sha1_hash + + + def get_subject(self): + """ + Returns latest commit message given to this commit. + """ + + cmd='git log -1 --format="%s" {0}'.format(self.sha1_hash) + + git_output = run_command(cmd=cmd, data=None, location=self.repo.location, chw=True).strip() + return git_output + + + def get_committer_date(self): + """ + Returns latest commiter date for this commit in "ISO 8601-like" format and UTC timezone. + """ + + cmd='git log -1 --format="%ci" {0}'.format(self.sha1_hash) + + git_output = run_command(cmd=cmd, data=None, location=self.repo.location, chw=True) + return time_to_utc(git_output.strip()) + + + def get_committer_email(self): + """ + Returns committer email. + """ + + cmd='git log {0} -1 --format="%ce" {1}'.format(self.rev, self.sha1_hash) + + git_output = run_command(cmd=cmd, data=None, location=self.repo.location, chw=True).strip() + return git_output + + + def get_committer_name(self): + """ + Returns committer name. + """ + + cmd='git log {0} -1 --format="%cn" {1}'.format(self.rev, self.sha1_hash) + + git_output = run_command(cmd=cmd, data=None, location=self.repo.location, chw=True).strip() + return git_output + + + def show(self): + """ + Returns information about commit. + """ + + cmd = 'git show {0}'.format(self.sha1_hash) + + git_output = run_command(cmd=cmd, data=None, location=self.repo.location, chw=True) + return git_output + + + def __str__(self): + """ + Returns a string representation of commit's object. + """ + + return 'Commit {0}, revision {1} for {1}'.format(self.sha1_hash, self.rev, self.repo) diff --git a/core/backend/git/repo.py b/core/backend/git/repo.py new file mode 100644 index 0000000..46d5b00 --- /dev/null +++ b/core/backend/git/repo.py @@ -0,0 +1,164 @@ +import os + +from django.conf import settings + +from git.object import GIT_BLOB_OBJECT, GIT_TREE_OBJECT, GitTree, GitBlob, GitCommit +from utils.system import run_command +from utils.date import time_to_utc + + +class Repo: + """ + Repository class for creating and working with a git repository. Repository object + is created with an absolute path for a git repository. This absolute path can be + specified in server deposit by using 'get_repository_location' static method in this class. + """ + + + def __init__(self, location): + self.location = location + + + @staticmethod + def get_repository_location(username, repository): + """ + Returns absolute path of the given repository and username in server storage. + """ + + return os.path.join(settings.GIT_DEPOSIT_ROOT, username, '{0}.git'.format(repository)) + + + def is_valid(self): + """ + Validity property to see if a git repository is available in the given location for object. + """ + + git_rev_parse = run_command(cmd='git rev-parse', data=None, location=self.location, chw=True) + return len(git_rev_parse) == 0 + + + def init_bare_repo(self): + """ + Initializes a bare git repository in the object location. + """ + + expected_location = os.path.join(settings.GIT_DEPOSIT_ROOT, self.location) + if not os.path.exists(expected_location): + os.makedirs(expected_location) + run_command(cmd='git init --bare', data=None, location=expected_location, chw=False) + + + def commit(self, payload): + """ + Commits the given payload to this repository. + """ + + return run_command(cmd='git receive-pack --stateless-rpc', data=payload, location=self.location, chw=False) + + + def pull(self, payload): + """ + Pulls changes of this repository for pull or clone actions. + """ + + return run_command(cmd='git upload-pack --stateless-rpc', data=payload, location=self.location, chw=False) + + + def get_last_update(self): + """ + Returns latest update date of repository in "ISO 8601-like" format and 'UTC' timezone. + """ + + if not os.path.exists(self.location): + return None + + git_output = run_command(cmd='git log -1 --format="%ai"', data=None, location=self.location, chw=True) + if git_output is None or len(git_output) == 0: + return None + return time_to_utc(git_output.strip()) + + + def get_info_refs(self, service): + """ + Returns 'refs' object information of the repository according to the given service. + """ + + return run_command(cmd='{0} --stateless-rpc --advertise-refs'.format(service), + data=None, location=self.location, chw=False) + + + def get_latest_status(self): + """ + Returns a status message of repository latest commit. + + e.g. + 'John Smith committed 9ece390, 2 hours ago' + """ + + return run_command(cmd='git log -1 --format="%cn committed %h, %cr"', data=None, location=self.location, chw=True) + + + def get_commits(self, rev): + """ + Returns all commits in all branches for this repository. + """ + + git_output = run_command(cmd='git log {0} --format="%H"'.format(rev), + data=None, location=self.location, chw=True).split('\n')[:-1] + + return [GitCommit(repo=self, sha1_hash=commit, rev=rev) for commit in git_output] + + + def get_contributers(self): + """ + Returns all contributers in all branches for this repository. + """ + + return None + + + def get_branches(self): + """ + Returns all branches for this repository. + """ + + result = run_command(cmd='git branch', data=None, location=self.location, chw=True).split('\n')[:-1] + branches = [item[2:] for item in result] + return branches + + + def get_head(self): + """ + Returns HEAD revision for this repository. + """ + + return run_command(cmd='git rev-parse --abbrev-ref HEAD', data=None, + location=self.location, chw=True).split('\n')[:-1][0] + + + def ls_tree(self, recursive, rev='HEAD'): + """ + Lists contents of repository either in a recursive or non-recursive mode. + Returns a list of GitTree/GitBlob objects representing contents. + """ + + r = '-r' if recursive else '' # if recursive is set to 'True' then a '-r' option is added to command. + cmd = 'git ls-tree --full-tree {0} {1}'.format(r, rev) + git_output = run_command(cmd=cmd, data=None, location=self.location, chw=True).split('\n')[:-1] + + tree_contents = [] + for item in git_output: + kind, path = item.split()[1], '{0}'.format(item.split()[3]) + if kind == GIT_BLOB_OBJECT: + tree_contents.append(GitBlob(repo=self, path=path, rev=rev)) + elif kind == GIT_TREE_OBJECT: + tree_contents.append(GitTree(repo=self, path=path, rev=rev)) + return tree_contents + + + def __str__(self): + """ + Returns a string representation of Repostiory's object. + """ + + return 'Git repository in {0}'.format(self.location) diff --git a/core/backend/git/service.py b/core/backend/git/service.py new file mode 100644 index 0000000..db19a55 --- /dev/null +++ b/core/backend/git/service.py @@ -0,0 +1,11 @@ +GIT_SERVICE_UPLOAD_PACK = 'git-upload-pack' +GIT_SERVICE_RECEIVE_PACK = 'git-receive-pack' +GIT_SERVICES = [GIT_SERVICE_UPLOAD_PACK, GIT_SERVICE_RECEIVE_PACK] + + +def is_valid_git_service(service): + """ + Returns true if the given service is one of git valid service packs. + """ + + return service in GIT_SERVICES diff --git a/core/backend/git/statistics.py b/core/backend/git/statistics.py new file mode 100644 index 0000000..db4c0d6 --- /dev/null +++ b/core/backend/git/statistics.py @@ -0,0 +1,123 @@ +import json +from dateutil import parser +from datetime import datetime + +from utils.date import get_year, get_month, get_weeknumber, get_weekday + + +def extract_month(utc_timestamp): + """ + Extracts month from utc timestamp string. + """ + + datetime = parser.parse(utc_timestamp) + return '{0}-{1}'.format(datetime.year, datetime.month) + + +def extract_day(utc_timestamp): + """ + Extracts day from utc timestamp string. + """ + + datetime = parser.parse(utc_timestamp) + return '{0}-{1}-{2}'.format(datetime.year, datetime.month, datetime.day) + + +class DataPresentation: + """ + Represents a presentation format for a given dataset (Datasets are python 'dict's basically). + If data format is 'py' then it's left alone. + But in case of 'js', a json compatible presentation is returned. + """ + + JS_FORMAT, PY_FORMAT = 'js', 'py' + VALID_DATA_FORMATS = [JS_FORMAT, PY_FORMAT] # Available data presentation output formats. + + + def __init__(self, data_format): + assert data_format in self.VALID_DATA_FORMATS, 'Input data format is not a valid one.' + self.data_format = data_format + + + def present(self, data): + """ + Returns presentation of the given dataset. + """ + + if self.data_format == 'js': + return json.dumps(data) + elif self.data_format == 'py': + return data + + +class GitStatistics: + """ + Generates data analysis for a git repository. This data will be available + in python 'dict' or javascript 'json' formats. One can use this statistics + to plot graphs or analyze repository activities. + """ + + # Available time intervals for generating datasets. + DAILY_INTERVALS = 'daily' + WEEKLY_INTERVALS = 'weekly' + MONTHLY_INTERVALS = 'monthly' + VALID_DATA_GENERATION_INTERVALS = [DAILY_INTERVALS, WEEKLY_INTERVALS, MONTHLY_INTERVALS] + + + def __init__(self, repo, rev): + self.repo = repo + self.rev = rev + + self.current_year = datetime.utcnow().isocalendar()[0] + self.current_week = datetime.utcnow().isocalendar()[1] + + + def _for_commits_daily(self, commits): + """ + Returns number of commits per day for the given commits. + """ + + # get dates only in the current year. + dates = [extract_day(commit.get_committer_date()) for commit in commits \ + if get_year(commit.get_committer_date()) == self.current_year] + return {date: dates.count(date) for date in dates} + + + def _for_commits_weekly(self, commits): + """ + Returns number of commits per day for the given commits. + """ + + # get dates only in the current year and current week. + dates = [get_weekday(extract_day(commit.get_committer_date())) for commit in commits \ + if get_year(commit.get_committer_date()) == self.current_year and \ + get_weeknumber(commit.get_committer_date()) == self.current_week] + + return {wd: dates.count(wd) if wd in dates else 0 for wd in range(1, 8)} + + + def _for_commits_monthly(self, commits): + """ + Returns number of commits per month for the given commits. + """ + + dates = [get_month(extract_month(commit.get_committer_date())) for commit in commits + if get_year(commit.get_committer_date()) == self.current_year] + return {mn: dates.count(mn) if mn in dates else 0 for mn in range(1, 13)} + + + def for_commits(self, by, data_format): + """ + Returns dataset for number of commits per given time interval. + """ + + assert by in self.VALID_DATA_GENERATION_INTERVALS, 'Input interval is not a valid one.' + + commits = self.repo.get_commits(self.rev) + + if by == self.DAILY_INTERVALS: + return DataPresentation(data_format).present(self._for_commits_daily(commits)) + elif by == self.WEEKLY_INTERVALS: + return DataPresentation(data_format).present(self._for_commits_weekly(commits)) + elif by == self.MONTHLY_INTERVALS: + return DataPresentation(data_format).present(self._for_commits_monthly(commits)) diff --git a/core/backend/git/tests.py b/core/backend/git/tests.py new file mode 100644 index 0000000..b55a85a --- /dev/null +++ b/core/backend/git/tests.py @@ -0,0 +1,8 @@ +from git.repo import Repo +from git.object import GitObject +from git.statistics import GitStatistics +from git.statistics import DataPresentation + + +def run_tests(): + pass diff --git a/core/backend/manage.py b/core/backend/manage.py new file mode 100755 index 0000000..597cefc --- /dev/null +++ b/core/backend/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djacket.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/core/backend/repository/__init__.py b/core/backend/repository/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/backend/repository/admin.py b/core/backend/repository/admin.py new file mode 100644 index 0000000..a9c1498 --- /dev/null +++ b/core/backend/repository/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from repository.models import Repository, RepositoryStar, RepositoryFork + + +admin.site.register(Repository) +admin.site.register(RepositoryStar) +admin.site.register(RepositoryFork) diff --git a/core/backend/repository/api/__init__.py b/core/backend/repository/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/backend/repository/api/urls.py b/core/backend/repository/api/urls.py new file mode 100644 index 0000000..ecd4307 --- /dev/null +++ b/core/backend/repository/api/urls.py @@ -0,0 +1,9 @@ +from django.conf.urls import url + +from repository.api.views import commits_stats, readme + + +urlpatterns = [ + url(r'(?P\w+)/(?P[-\w]+).git/commits_stats$', commits_stats, name='commits_stats'), + url(r'(?P\w+)/(?P[-\w]+).git/readme/(?P[-\w]+)/*', readme, name='readme'), +] diff --git a/core/backend/repository/api/views.py b/core/backend/repository/api/views.py new file mode 100644 index 0000000..05c50c9 --- /dev/null +++ b/core/backend/repository/api/views.py @@ -0,0 +1,48 @@ +import json + +from django.http import Http404, HttpResponse +from django.views.decorators.http import require_http_methods + +from repository.decorators import require_existing_repo, require_existing_rev, require_access, require_valid_readme +from git.statistics import DataPresentation +from utils.urlparser import partition_url +from utils.decorators import require_ajax +from git.statistics import GitStatistics +from git.object import GitBlob +from git.repo import Repo + +weekly, monthly = GitStatistics.WEEKLY_INTERVALS, GitStatistics.MONTHLY_INTERVALS +py, js = DataPresentation.PY_FORMAT, DataPresentation.JS_FORMAT + + +@require_http_methods(['GET']) +@require_access +@require_existing_repo +@require_ajax +def commits_stats(request, username, repository): + """ + Returns number of commits for the given repository. + """ + + repo = Repo(Repo.get_repository_location(username, repository)) + stats = GitStatistics(repo, repo.get_head()) + + return HttpResponse(json.dumps({'weekly': stats.for_commits(weekly, js), 'monthly': stats.for_commits(monthly, js)})) + + +@require_http_methods(['GET']) +@require_access +@require_existing_repo +@require_existing_rev +@require_valid_readme +@require_ajax +def readme(request, username, repository, rev): + """ + Returns README content of a folder inside the given repository. + """ + + repo = Repo(Repo.get_repository_location(username, repository)) + request_sections = partition_url(request.path_info) + rdm = GitBlob(repo=repo, path='/'.join(request_sections[5:]), rev=rev).show() + + return HttpResponse(rdm) diff --git a/core/backend/repository/decorators.py b/core/backend/repository/decorators.py new file mode 100644 index 0000000..2d35875 --- /dev/null +++ b/core/backend/repository/decorators.py @@ -0,0 +1,97 @@ +from functools import wraps + +from django.http import Http404 +from django.contrib.auth.models import User + +from repository.models import Repository +from git.repo import Repo + + +def require_existing_repo(func): + """ + Checks to see if requested repository is available on user's deposit. + Raises 404 if not. + """ + + @wraps(func) + def _decorator(request, *args, **kwargs): + try: + user = User.objects.get(username=kwargs['username']) + repo = Repository.objects.get(owner=user, name=kwargs['repository']) + return func(request, *args, **kwargs) + except: + raise Http404() + return _decorator + + +def require_existing_rev(func): + """ + Checks to see if requested revision exists in repository. + Raises 404 if not. + """ + + @wraps(func) + def _decorator(request, *args, **kwargs): + try: + repo = Repo(Repo.get_repository_location(kwargs['username'], kwargs['repository'])) + branches = repo.get_branches() + if 'rev' not in kwargs or kwargs['rev'] in branches + ['HEAD']: + return func(request, *args, **kwargs) + else: + raise Http404() + except: + raise Http404() + return _decorator + + +def require_access(func): + """ + Checks to see if requesting user has access to repository. If repository + is private then only it's owner has access to it. If not, then everyone + can see or clone this repository. + Raises 404 if not. + """ + + @wraps(func) + def _decorator(request, *args, **kwargs): + try: + user = User.objects.get(username=kwargs['username']) + repo = Repository.objects.get(owner=user, name=kwargs['repository']) + if repo.private and request.user.id != user.id: + raise Http404() + else: + return func(request, *args, **kwargs) + except: + raise Http404() + return _decorator + + +def require_owner_access(func): + """ + Only allows actions on repository if requesting user is repository owner. + For example for getting access to repository settings. + Raises 404 if not. + """ + + @wraps(func) + def _decorator(request, *args, **kwargs): + if request.user.username != kwargs['username']: + raise Http404() + else: + return func(request, *args, **kwargs) + return _decorator + + +def require_valid_readme(func): + """ + Checks to see if requested README is a valid text README file. + Raises 404 if not. + """ + + @wraps(func) + def _decorator(request, *args, **kwargs): + if request.path_info.endswith('README.md') or request.path_info.endswith('README.rst'): + return func(request, *args, **kwargs) + else: + raise Http404() + return _decorator diff --git a/core/backend/repository/forms.py b/core/backend/repository/forms.py new file mode 100644 index 0000000..4a2d838 --- /dev/null +++ b/core/backend/repository/forms.py @@ -0,0 +1,156 @@ +from django import forms + +from repository.models import Repository, REPOSITORY_NAME_MAX_LENGTH, REPOSITORY_NAME_MIN_LENGTH, REPOSITORY_DESCRIPTION_MAX_LENGTH +from utils.system import rename_tree +from git.repo import Repo + + +def name_length_validator(value): + """ + Validates given name in terms of length and uniqeness among user's repositories. + """ + + if len(value) < REPOSITORY_NAME_MIN_LENGTH or len(value) > REPOSITORY_NAME_MAX_LENGTH: + raise forms.ValidationError(u'Repository name length should be greater than {0} and less than {1}.' + .format(REPOSITORY_NAME_MIN_LENGTH, REPOSITORY_NAME_MAX_LENGTH)) + + +def ascii_validator(value): + """ + Checks to see if the given name is in "ASCII" format. + """ + + try: + value.encode('ascii') + except UnicodeEncodeError: + raise forms.ValidationError(u'Repository name should be in English characters.') + + +def special_characters_validator(value): + """ + Checks to if the given name has any special characters: + !, @, #, $, %, ^, &, *, (, ), +, =, {, }, [, ], :, ;, ", \, ', \, /, <, >, ',', ? + """ + + if set('!@#$%^&*()+={}[]:;"\'\/<>,?').intersection(value): + raise forms.ValidationError(u'Repository name should only contain letters, numbers, hyphen and underscore.') + + +class RepositoryCreationForm(forms.Form): + """ + Form for creating a new repository. + """ + + name = forms.CharField(required=True, validators=[name_length_validator, ascii_validator, special_characters_validator]) + description = forms.CharField(required=False) + private = forms.BooleanField(required=False) + + + def __init__(self, *args, **kwargs): + self.user = kwargs.pop('user', None) + self._initialize_form(kwargs.pop('instance', None)) + self.edit = kwargs.pop('edit', False) # boolean flag to indicate we're not creating a new object. + super(RepositoryCreationForm, self).__init__(*args, **kwargs) + + + def _initialize_form(self, instance): + """ + If instance object is provided then we initialize form with + the given instance. + """ + + if instance is not None: + assert isinstance(instance, Repository), "Instance should be of Repository type." + self.instance = instance + self.repo_entering_name = instance.name # store current repository name in case user changed it's name. + + + def _moderate_name(self, name): + """ + Returns name string without whitespaces and concatenates name parts with '-' character. + """ + + return '-'.join(name.split()) + + + def _repo_exists(self, repository_name): + """ + Checks to see if user has an exact repository with the given name. + """ + + user_repos = Repository.objects.all_repositores(user=self.user) + if not self.edit: # if a new object is being created we check for it's name uniqeness. + if len(user_repos) > 0: + return len(user_repos.filter(name=repository_name)) > 0 + elif self.edit and self.instance is not None: # if object is being edited we check for a name conflict with other repos. + if set(repo.name for repo in user_repos.filter(name=repository_name) + if repo.id != self.instance.id).intersection({repository_name}): + return True + return False + + + def _update_instance(self, data): + """ + Updates given instance with provided data kwargs. + """ + + assert self.instance is not None, "Instance object shoud not be None." + self.instance.name = data['name'] + self.instance.description = data['description'] + self.instance.private = data['private'] + self.instance.save() + + + def clean(self): + """ + Clean method for moderating repository name. + """ + + self.cleaned_data['name'] = self._moderate_name(self.data['name']) + name = self.cleaned_data['name'] + + if self._repo_exists(name): + raise forms.ValidationError(u'You have another repository with this name.') + return self.cleaned_data + + + def save(self): + """ + Save method to create a new repository with provided information from registration form. + """ + + data = self.cleaned_data + + if self.edit: + self._update_instance(data) + if self.instance.name != self.repo_entering_name: # rename repository folder if it's name has changed. + rename_tree(Repo.get_repository_location( + self.instance.owner.username, self.repo_entering_name), '{0}.git'.format(self.instance.name)) + elif not self.edit: + repository = Repository.objects.create(name=data['name'], + description=data['description'], owner=self.user, private=data['private']) + repository.save() + + +class RepositoryArea51Form(forms.Form): + """ + Form for deleting repository (and probably other dangerous stuffs in future). + """ + + confirmed_deletion = forms.BooleanField(required=False) + + + def __init__(self, *args, **kwargs): + self.repository = kwargs.pop('repository') + super(RepositoryArea51Form, self).__init__(*args, **kwargs) + + + def save(self): + """ + Save method to commit dangerous operations to repository. + """ + + data = self.cleaned_data + + if data['confirmed_deletion']: + self.repository.delete() diff --git a/core/backend/repository/models.py b/core/backend/repository/models.py new file mode 100644 index 0000000..2f20df5 --- /dev/null +++ b/core/backend/repository/models.py @@ -0,0 +1,151 @@ +from django.db import models +from django.db.models import Q +from django.contrib.auth.models import User + +from utils.date import time_to_utc +from git.repo import Repo + +REPOSITORY_NAME_MAX_LENGTH = 64 +REPOSITORY_NAME_MIN_LENGTH = 3 +REPOSITORY_DESCRIPTION_MAX_LENGTH = 256 + + +class RepositoryManager(models.Manager): + """ + Manager class for running queries on repositories. + """ + + + def get_queryset(self): + """ + Returns default queryset for DjacketUser objects. + """ + + return super(RepositoryManager, self).get_queryset() + + + def all_repositores(self, user): + """ + Returns all repositories a user owns. + """ + + return super(RepositoryManager, self).get_queryset().filter(Q(owner_id=user.id)) + + + def public_repositores(self, user): + """ + Returns all public repositories a user owns. + """ + + return super(RepositoryManager, self).get_queryset().filter(Q(owner_id=user.id) & Q(private=False)) + + +class Repository(models.Model): + """ + A one-to-many model for keeping users repositries and it's data. + """ + + name = models.CharField(max_length=REPOSITORY_NAME_MAX_LENGTH) + description = models.CharField(max_length=REPOSITORY_DESCRIPTION_MAX_LENGTH) + creation_date = models.DateTimeField(auto_now=True) + owner = models.ForeignKey(User) + private = models.BooleanField(default=False) + + objects = RepositoryManager() + + + def __str__(self): + return '{0}\'s {1}'.format(self.owner.username, self.name) + + + @property + def stars(self): + """ + Propery for how many stars this repository has. + """ + + return RepositoryStar.objects.filter(repository_id=self.id).count() + + + @property + def forks(self): + """ + Property for how many times this repository has been forked. + """ + + return RepositoryFork.objects.filter(repository_id=self.id).count() + + + @property + def accesses(self): + """ + Property for returning users having access to this repository. + """ + + return RepositoryAccess.objects.filter(repository_id=self.id) + + + @property + def last_update(self): + """ + Property for viewing latest activty date on repository in "ISO 8601-like" format and 'UTC' timezone. + """ + + last_update = Repo(Repo.get_repository_location(self.owner.username, self.name)).get_last_update() + if last_update is None: + return time_to_utc(str(self.creation_date)) + else: + return last_update + + + @property + def get_latest_status(self): + """ + Property for viewing repository latest commit status message. + """ + + repo = Repo(Repo.get_repository_location(self.owner.username, self.name)) + return repo.get_latest_status() + + +class RepositoryAccess(models.Model): + """ + A one-to-many model for keeping users that have access to a repository. + """ + + user = models.ForeignKey(User) + repository = models.ForeignKey(Repository) + + + def __str__(self): + return '{0} has access to {1}'.format(self.user.username, self.repository.name) + + +class RepositoryStar(models.Model): + """ + A one-to-many model for keeping stars of a repository. + """ + + user = models.ForeignKey(User) + repository = models.ForeignKey(Repository) + + + def __str__(self): + return '{0}\'s star for {1}'.format(self.user.username, self.repository.name) + + +class RepositoryFork(models.Model): + """ + A one-to-many model for keeping track of forks for a repository. + """ + + user = models.ForeignKey(User) + repository = models.ForeignKey(Repository) + + + def __str__(self): + return '{0} forked {1}'.format(self.user.username, self.repository.name) + + +# Send signals for operations needed after repository object is saved. +from repository.signals import * diff --git a/core/backend/repository/signals.py b/core/backend/repository/signals.py new file mode 100644 index 0000000..e9354a2 --- /dev/null +++ b/core/backend/repository/signals.py @@ -0,0 +1,42 @@ +from django.conf import settings +from django.db.models import signals + +from repository.models import Repository, RepositoryAccess +from utils.system import remove_tree +from git.repo import Repo + + +def init_bare_repo_signal(sender, instance, created, **kwargs): + """ + Initialize a bare git repository on server's deposit after it's creation. + """ + + if created: + # Create repository folder under GIT_DEPOSIT_ROOT/username/repository.git and initialize it as bare. + repository_location = Repo.get_repository_location(instance.owner.username, instance.name) + repo = Repo(repository_location) + repo.init_bare_repo() + + +def add_repo_access_signal(sender, instance, created, **kwargs): + """ + Add creating user to the list of accessed users to this repository. + """ + + if created: + repository_access = RepositoryAccess(user=instance.owner, repository=instance) + repository_access.save() + + +def remove_repo_signal(sender, instance, using, **kwargs): + """ + Remove repository folder from server deposit after it's deletion. + """ + + # Remove repository folder under GIT_DEPOSIT_ROOT/username/repository.git + remove_tree(Repo.get_repository_location(instance.owner.username, instance.name)) + + +signals.post_save.connect(init_bare_repo_signal, sender=Repository, weak=False) +signals.post_save.connect(add_repo_access_signal, sender=Repository, weak=False) +signals.post_delete.connect(remove_repo_signal, sender=Repository, weak=False) diff --git a/core/backend/repository/tests.py b/core/backend/repository/tests.py new file mode 100644 index 0000000..2e9cb5f --- /dev/null +++ b/core/backend/repository/tests.py @@ -0,0 +1 @@ +from django.test import TestCase diff --git a/core/backend/repository/urls.py b/core/backend/repository/urls.py new file mode 100644 index 0000000..a0c5969 --- /dev/null +++ b/core/backend/repository/urls.py @@ -0,0 +1,22 @@ +from django.conf.urls import include, url + +from repository.views import get_info_refs, service_rpc, new_repository, \ + view_repository, repository_settings, repository_area51, \ + repository_branches, repository_commits, repository_graphs + + +urlpatterns = [ + url(r'new$', new_repository, name='new_repository'), + url(r'^api/', include('repository.api.urls')), + url(r'(?P\w+)/(?P[-\w]+).git/info/refs', get_info_refs, name='get_info_refs'), + url(r'(?P\w+)/(?P[-\w]+).git/git-upload-pack$', service_rpc, name='service_rpc'), + url(r'(?P\w+)/(?P[-\w]+).git/git-receive-pack$', service_rpc, name='service_rpc'), + url(r'(?P\w+)/(?P[-\w]+).git/settings$', repository_settings, name='repository_settings'), + url(r'(?P\w+)/(?P[-\w]+).git/area51$', repository_area51, name='repository_area51'), + url(r'(?P\w+)/(?P[-\w]+).git/branches$', repository_branches, name='repository_branches'), + url(r'(?P\w+)/(?P[-\w]+).git/commits$', repository_commits, name='repository_commits'), + url(r'(?P\w+)/(?P[-\w]+).git/graphs$', repository_graphs, name='repository_graphs'), + url(r'(?P\w+)/(?P[-\w]+).git/tree/(?P[-\w]+)/*', view_repository, name='view_repository'), + url(r'(?P\w+)/(?P[-\w]+).git/blob/(?P[-\w]+)/*', view_repository, name='view_repository'), + url(r'(?P\w+)/(?P[-\w]+).git$', view_repository, name='view_repository'), +] diff --git a/core/backend/repository/views.py b/core/backend/repository/views.py new file mode 100644 index 0000000..fcea528 --- /dev/null +++ b/core/backend/repository/views.py @@ -0,0 +1,204 @@ +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse +from django.http import Http404, HttpResponse +from django.shortcuts import render, redirect +from django.views.decorators.csrf import csrf_exempt +from django.contrib.auth.decorators import login_required +from django.views.decorators.http import require_http_methods + +from repository.decorators import require_existing_repo, require_existing_rev, require_access, require_owner_access +from git.object import GitTree, GitBlob, GIT_BLOB_OBJECT, GIT_TREE_OBJECT, GIT_VALID_OBJECT_KINDS +from repository.forms import RepositoryCreationForm, RepositoryArea51Form +from git.action import GIT_ACTION_ADVERTISEMENT, GIT_ACTION_RESULT +from git.decorators import git_access_required +from utils.urlparser import partition_url +from git.statistics import GitStatistics +from repository.models import Repository +from git.http import GitResponse +from git.repo import Repo + + +@require_http_methods(['GET']) +@require_existing_repo +@git_access_required +def get_info_refs(request, username, repository): + """ + Responds to '/info/refs' requests for the given service, username and repository. + """ + + requested_repo = Repo(Repo.get_repository_location(username, repository)) + response = GitResponse(service=request.GET['service'], action=GIT_ACTION_ADVERTISEMENT, + repository=requested_repo, data=None) + + return response.get_http_info_refs() + + +@require_http_methods(['POST']) +@require_existing_repo +@git_access_required +@csrf_exempt +def service_rpc(request, username, repository): + """ + Responds to 'git-receive-pack' or 'git-upload-pack' requests + for the given username and repository. + Decorator 'csrf_exempt' is used because git POST requests does not provide csrf cookies and + therefore validation cannot be done. + """ + + requested_repo = Repo(Repo.get_repository_location(username, repository)) + response = GitResponse(service=request.path_info.split('/')[-1], action=GIT_ACTION_RESULT, + repository=requested_repo, data=request.body) + + return response.get_http_service_rpc() + + +@require_http_methods(['GET', 'POST']) +@login_required +def new_repository(request): + """ + Responds to user's request to create a new repository. + """ + + if request.method == 'POST': + form = RepositoryCreationForm(data=request.POST, user=request.user) + if form.is_valid(): + form.save() + return redirect('view_repository', username=request.user.username, repository=form.cleaned_data['name']) + elif request.method == 'GET': + form = RepositoryCreationForm() + + return render(request, 'repository/new.html', {'form': form}) + + +@require_http_methods(['POST']) +@login_required +@require_existing_repo +@require_owner_access +def repository_area51(request, username, repository): + """ + View for deleting a repository of user's deposit. + """ + + repository = Repository.objects.get(owner=request.user, name=repository) + form = RepositoryArea51Form(data=request.POST, repository=repository) + if form.is_valid(): + form.save() + return redirect('index') + else: + raise Http404() + + +@require_http_methods(['GET']) +@require_existing_repo +@require_access +@require_existing_rev +def view_repository(request, username, repository, rev='HEAD'): + """ + View for showing repository objects inside the given revision (either trees or blobs). + """ + + requested_repo = Repo(Repo.get_repository_location(username, repository)) + objects = _parse_repo_url(request.path_info, requested_repo, rev) + if objects is None: + raise Http404() + else: + return render(request, 'repository/repo-pjax.html', + {'template': 'browse', 'repo_owner': username, 'repo_name': repository, + 'repo_lsmsg': requested_repo.get_latest_status, 'rev': rev, 'objects': objects}) + + +@require_http_methods(['GET']) +@require_existing_repo +@require_access +@require_existing_rev +def repository_branches(request, username, repository): + """ + View for viewing branches for repository. + """ + + requested_repo = Repo(Repo.get_repository_location(username, repository)) + branches = requested_repo.get_branches() + return render(request, 'repository/repo-pjax.html', + {'template': 'branches', 'repo_owner': username, 'repo_name': repository, + 'num_branches':len(branches), 'HEAD': requested_repo.get_head(), 'branches': branches}) + + +@require_http_methods(['GET']) +@require_existing_repo +@require_access +@require_existing_rev +def repository_commits(request, username, repository): + """ + View for viewing commits for repository. + """ + + requested_repo = Repo(Repo.get_repository_location(username, repository)) + commits = {branch:requested_repo.get_commits(branch) for branch in requested_repo.get_branches()} + return render(request, 'repository/repo-pjax.html', + {'template': 'commits', 'repo_owner': username, 'repo_name': repository, 'commits': commits}) + + +@require_http_methods(['GET']) +@require_existing_repo +@require_access +@require_existing_rev +def repository_graphs(request, username, repository): + """ + View for viewing commits for repository. + """ + + requested_repo = Repo(Repo.get_repository_location(username, repository)) + branches = requested_repo.get_branches() + + return render(request, 'repository/repo-pjax.html', + {'template': 'graphs', 'repo_owner': username, 'repo_name': repository, + 'repo_lsmsg': requested_repo.get_latest_status, 'num_branches': len(branches)}) + + +@require_http_methods(['GET', 'POST']) +@require_existing_repo +@login_required +@require_owner_access +def repository_settings(request, username, repository): + """ + View for changing/viewing repository settings. + """ + + repo = Repository.objects.get(owner=request.user, name=repository) + if request.method == 'POST': + form = RepositoryCreationForm(data=request.POST, user=request.user, instance=repo, edit=True) + if form.is_valid(): + form.save() + return redirect('view_repository', username=form.instance.owner.username, repository=form.instance.name) + elif request.method == 'GET': + form = RepositoryCreationForm(instance=repo, edit=True) + + return render(request, 'repository/repo-settings.html', + {'repo': repo, 'repo_owner': username, 'repo_name': repository, 'form': form}) + + +def _parse_repo_url(request_path, repository, rev): + """ + Parses url for viewing repository. Splits request url on '/' and checks parts + to specify which revision and object should be shown. + + e.g. + Input like "/username/repository.git/tree/master/some/folder/object.c" will return + branch "master" and blob object "some/folder/object.c" to show. + + """ + + request_sections = partition_url(request_path) + num_sections = len(request_sections) + + if num_sections == 2: + return repository.ls_tree(recursive=False, rev=rev) + elif num_sections == 4 and request_sections[2] == GIT_TREE_OBJECT: + return repository.ls_tree(recursive=False, rev=request_sections[3]) + elif num_sections > 4: + if request_sections[2] == GIT_TREE_OBJECT: + return GitTree(repo=repository, path='/'.join(request_sections[4:]), rev=rev).show() + elif request_sections[2] == GIT_BLOB_OBJECT: + return GitBlob(repo=repository, path='/'.join(request_sections[4:]), rev=rev) + else: + return None diff --git a/core/backend/user/__init__.py b/core/backend/user/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/backend/user/admin.py b/core/backend/user/admin.py new file mode 100644 index 0000000..306d21d --- /dev/null +++ b/core/backend/user/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin + +from user.models import UserProfile + + +admin.site.register(UserProfile) diff --git a/core/backend/user/auth.py b/core/backend/user/auth.py new file mode 100644 index 0000000..f5c1d7a --- /dev/null +++ b/core/backend/user/auth.py @@ -0,0 +1,19 @@ +import base64 + +from django.contrib.auth import authenticate + + +def base_auth(authorization_header): + """ + Authenticates user based on "HTTP_AUTHORIZATION" header values for + HTTP basic authorization purposes. + """ + + authmeth, auth = authorization_header.split(' ', 1) + if authmeth.lower() == 'basic': + auth = base64.b64decode(auth.strip()).decode('utf8') + username, password = auth.split(':', 1) + user = authenticate(username=username, password=password) + return user + else: + return None diff --git a/core/backend/user/decorators.py b/core/backend/user/decorators.py new file mode 100644 index 0000000..b61e082 --- /dev/null +++ b/core/backend/user/decorators.py @@ -0,0 +1,19 @@ +from functools import wraps + +from django.shortcuts import redirect +from django.conf import settings + + +def redirect_if_authorized(func): + """ + Redirects user to the given view (identified by 'view_name' parameter) + if user is authenticated. + """ + + @wraps(func) + def _decorator(request, *args, **kwargs): + if request.user.is_authenticated(): + return redirect(settings.LOGIN_REDIRECT_URL) + else: + return func(request, *args, **kwargs) + return _decorator diff --git a/core/backend/user/forms.py b/core/backend/user/forms.py new file mode 100644 index 0000000..9d909bd --- /dev/null +++ b/core/backend/user/forms.py @@ -0,0 +1,246 @@ +import string + +from django import forms +from django.contrib.auth.models import User +from django.core.validators import validate_email +from django.core.exceptions import ValidationError +from django.core.files.images import get_image_dimensions + +from user.models import UserProfile, FULLNAME_MAX_LENGTH + +RESTRICTED_USERNAMES = ['media', 'static', 'license', 'terms', 'privacy', 'djacket', \ + 'admin', 'help', 'account', 'login', 'logout', 'register', 'settings', \ + 'profile', 'repo', 'about', 'deposit', 'configure', 'configuration', 'configurations', \ + 'browse', 'history', 'hint', 'hints', 'commit', 'commits', 'graph', 'graphs', 'api', \ + 'branch', 'branches', 'checkout', 'push', 'pull', 'clone', 'star', 'start', 'fork', \ + 'user', 'accounts'] +USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH = 3, 20 +PASSWORD_MIN_LENGTH = 5 + +AVATAR_IMAGE_MAX_SIZE = 640*1024 +AVATAR_IMAGE_MAX_DIMENSION = 640 +AVATAR_IMAGE_MIN_DIMENSION = 96 +VALID_AVATAR_IMAGE_FORMATS = ['jpg', 'jpeg', 'png'] + + +def _fullname_length_exceeds(first_name, last_name): + """ + Validates length of full name to be less than FULLNAME_MAX_LENGTH. + """ + + return len(first_name) + len(last_name) > FULLNAME_MAX_LENGTH + + +def _is_alphanumerical(value): + """ + Checks to see if the given value is alphanumerical or not. + An allowed set of digits, ASCII letters and underscore is created first, + if intersection between the given value and this allowed set has no difference + with value, then surely it's alphanumerical. + """ + + inter = set(value).intersection(set(string.digits+string.ascii_letters+'_')) + diff = set(value).difference(inter) + return len(diff) == 0 + + +def username_validator(value): + """ + Validates given username in terms of uniqeness and length. + """ + + if not _is_alphanumerical(value): + raise ValidationError(u'Username must only has alphanumerical or underscore characters.') + elif value in RESTRICTED_USERNAMES: + raise ValidationError(u'Username is a reserved word') + elif User.objects.filter(username=value).exists(): + raise ValidationError(u'Username already exists') + elif value[0].isdigit(): + raise ValidationError(u'Username should start with a letter') + elif len(value) < USERNAME_MIN_LENGTH or len(value) > USERNAME_MAX_LENGTH: + raise ValidationError(u'Username should be at least {0} and at most {1} characters long' + .format(USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH)) + + +def email_validator(value): + """ + Validates given email in terms of uniqeness and correctness. + """ + + if User.objects.filter(email=value).exists(): + raise ValidationError(u'Email already exists') + else: + validate_email(value) + + +def password_validator(value): + """ + Validates given password in terms of length. + """ + + if len(value) < PASSWORD_MIN_LENGTH: + raise ValidationError(u'Password length should be greater than {0}'.format(PASSWORD_MIN_LENGTH)) + + +def image_size_validator(value): + """ + Validates size of uploaded image. + """ + + if len(value) > AVATAR_IMAGE_MAX_SIZE: + raise forms.ValidationError(u'Avatar image size should not be greater than 640KiB.') + + +def image_content_type_validator(value): + """ + Validates content-type of uploaded image. + Image format should be of '.jpg' or '.jpeg' or '.png' + """ + + main, ext = value.content_type.split('/') + if not (main == 'image' and ext in VALID_AVATAR_IMAGE_FORMATS): + raise forms.ValidationError(u'Avatar image should of either jpeg or png formats.') + + +def image_dimensions_validator(value): + """ + Validates dimensions of uploaded image. + Avatar images should be in square aspect ratios and have size of less than 640*640. + """ + + w, h = get_image_dimensions(value) + if w != h: + raise forms.ValidationError(u'Avatar image should have same height and width.') + elif w > AVATAR_IMAGE_MAX_DIMENSION or h > AVATAR_IMAGE_MAX_DIMENSION: + raise forms.ValidationError(u'Avatar image width and height should be less than {0} pixels.' + .format(AVATAR_IMAGE_MAX_DIMENSION)) + elif w < AVATAR_IMAGE_MIN_DIMENSION or h < AVATAR_IMAGE_MIN_DIMENSION: + raise forms.ValidationError(u'Avatar image width and height should be greater than {0} pixels.' + .format(AVATAR_IMAGE_MIN_DIMENSION)) + + +class UserRegistrationForm(forms.Form): + """ + Form for registering users with username, email and password. + Avatar and other things are not required at first. User can + change those in profile settings. + """ + + username = forms.CharField(required=True, validators=[username_validator,]) + email = forms.EmailField(required=True, validators=[email_validator,], widget=forms.EmailInput) + password = forms.CharField(required=True, validators=[password_validator], widget=forms.PasswordInput) + + + def save(self): + """ + Save method to create a new user with provided information from registration form. + """ + + data = self.cleaned_data + + user = User.objects.create_user(username=(data['username']).lower(), email=(data['email']).lower()) + user.set_password(data['password']) + user.save() + + +class UserInfoForm(forms.ModelForm): + """ + Form for changing user's info. + """ + + old_password = forms.CharField(required=False, validators=[password_validator], widget=forms.PasswordInput) + new_password = forms.CharField(required=False, validators=[password_validator], widget=forms.PasswordInput) + + + class Meta: + model = User + fields = ['first_name', 'last_name', 'email'] + + + def _password_change_attempted(self, old_password, new_password): + """ + Returns True if user attempted to change his/her password. + """ + + assert old_password is not None, 'Old Password should not be None.' + assert new_password is not None, 'New Password should not be None.' + + if len(old_password) == 0 and len(new_password) == 0: + return False + elif len(old_password) == 0 and len(new_password) > 0: + raise forms.ValidationError(u'Old password is not entered.') + elif len(old_password) > 0 and len(new_password) == 0: + raise forms.ValidationError(u'New password is not entered.') + elif len(old_password) > 0 and len(new_password) > 0: + return True + + + def clean(self): + """ + Clean method to check password correctness before saving, if user attempted to change his/her password. + """ + + if self._password_change_attempted(self.cleaned_data['old_password'], self.cleaned_data['new_password']): + if self.instance.check_password(self.cleaned_data['old_password']): # Old password entered is correct + self.instance.set_password(self.cleaned_data['new_password']) # so new password can be set. + else: + raise forms.ValidationError(u'Old password entered is not correct.') + elif _fullname_length_exceeds(self.data['first_name'], self.data['last_name']): + raise forms.ValidationError(u'Your full name should be less than {0} characters.'.format(FULLNAME_MAX_LENGTH)) + + return self.cleaned_data + + +class UserProfileForm(forms.Form): + """ + Form for changing user's profile. + """ + + avatar = forms.ImageField(required=False, validators=[image_size_validator, + image_content_type_validator, image_dimensions_validator]) + birthdate = forms.DateField(required=False) + + + def __init__(self, *args, **kwargs): + self.user = kwargs.pop('user') + super(UserProfileForm, self).__init__(*args, **kwargs) + + + def save(self): + """ + Save method to commit changes to user's profile. + """ + + data = self.cleaned_data + profile = self.user.profile + + if 'avatar' in data: + profile.avatar = data['avatar'] if data['avatar'] is not None else profile.avatar + if 'birthdate' in data: + profile.birthdate = data['birthdate'] if data['birthdate'] is not None else profile.birthdate + + profile.save() + + +class UserArea51Form(forms.Form): + """ + Form for deleting user's account entirely. + """ + + confirmed_deletion = forms.BooleanField(required=False) + + + def __init__(self, *args, **kwargs): + self.user = kwargs.pop('user') + super(UserArea51Form, self).__init__(*args, **kwargs) + + + def save(self): + """ + Save method to commit dangerous operations to user's account. + """ + + data = self.cleaned_data + + if data['confirmed_deletion']: + self.user.delete() diff --git a/core/backend/user/models.py b/core/backend/user/models.py new file mode 100644 index 0000000..0ec41b2 --- /dev/null +++ b/core/backend/user/models.py @@ -0,0 +1,43 @@ +from django.db import models +from django.db.models import Q +from django.contrib.auth.models import User + +from djacket.storage import OverwriteStorage +from repository.models import Repository + +FULLNAME_MAX_LENGTH = 32 + + +def user_avatar_path(instance, filename): + """ + Returns the path for user's avatar image under MEDIA_ROOT folder. + + e.g. + MEDIA_ROOT/avatars/username/avatar.png + """ + + # Select username, 'avatar' static name and then file extention. + return 'avatars/{0}/{1}.{2}'.format(instance.user.username, 'avatar', filename.split('.')[-1]) + + +class UserProfile(models.Model): + """ + A one-to-one model for keeping each user's extra information. + This model is created with signal when a user signs up. + """ + + user = models.OneToOneField(User) + name = models.CharField(max_length=FULLNAME_MAX_LENGTH) + avatar = models.ImageField(upload_to=user_avatar_path, storage=OverwriteStorage()) + birthdate = models.DateTimeField(null=True) + + + def __str__(self): + return '{0}\'s Profile [id={1}]'.format(self.user.username, self.user.id) + + +User.profile = property(lambda u: UserProfile.objects.get_or_create(user=u)[0]) # Assign .profile property for each user object. + + +# Send signals for operations needed after user object is saved. +from user.signals import * diff --git a/core/backend/user/signals.py b/core/backend/user/signals.py new file mode 100644 index 0000000..83885fd --- /dev/null +++ b/core/backend/user/signals.py @@ -0,0 +1,54 @@ +import os +import random + +from django.contrib.auth.models import User +from django.db.models import signals +from django.conf import settings + +from user.models import UserProfile +from utils.system import remove_tree + + +def create_user_profile_signal(sender, instance, created, **kwargs): + """ + Create UserProfile object when user signs up for the first time. + """ + + if created: + stock_avatar_id = random.randint(0, 9) + 1 + UserProfile.objects.create(user=instance, avatar='./stock/{0}.png'.format(stock_avatar_id)) + + +def create_user_fullname(sender, instance, created, **kwargs): + """ + Create user's full name based on his/her first name and last name. + """ + + profile = UserProfile.objects.get(user_id=instance.id) + profile.name = '{0} {1}'.format(instance.first_name, instance.last_name) + profile.save() + + +def create_user_deposit_signal(sender, instance, created, **kwargs): + """ + Create user's deposit folder when he/she registers on site. + """ + + if created: + # Create user's deposit folder under GIT_DEPOSIT_ROOT/username + os.makedirs(os.path.join(settings.GIT_DEPOSIT_ROOT, instance.username)) + + +def delete_user_deposit_signal(sender, instance, using, **kwargs): + """ + Delete user's deposit folder when it's account is deleted. + """ + + # Remove user's deposit folder under GIT_DEPOSIT_ROOT/username + remove_tree(os.path.join(settings.GIT_DEPOSIT_ROOT, instance.username)) + + +signals.post_save.connect(create_user_profile_signal, sender=User, weak=False) +signals.post_delete.connect(delete_user_deposit_signal, sender=User, weak=False) +signals.post_save.connect(create_user_deposit_signal, sender=User, weak=False) +signals.post_save.connect(create_user_fullname, sender=User, weak=False) diff --git a/core/backend/user/tests.py b/core/backend/user/tests.py new file mode 100644 index 0000000..2e9cb5f --- /dev/null +++ b/core/backend/user/tests.py @@ -0,0 +1 @@ +from django.test import TestCase diff --git a/core/backend/user/urls.py b/core/backend/user/urls.py new file mode 100644 index 0000000..c193794 --- /dev/null +++ b/core/backend/user/urls.py @@ -0,0 +1,12 @@ +from django.conf.urls import url + +from user.views import user_login, user_register, user_settings, user_profile, user_area51 + +urlpatterns = [ + url(r'register$', user_register, name='user_register'), + url(r'login$', user_login, name='user_login'), + url(r'logout$', 'django.contrib.auth.views.logout', {'next_page': 'index'}, name='user_logout'), + url(r'settings/profile$', user_profile, name='user_profile'), + url(r'settings/area51$', user_area51, name='user_area51'), + url(r'settings$', user_settings, name='user_settings'), +] diff --git a/core/backend/user/views.py b/core/backend/user/views.py new file mode 100644 index 0000000..608fbd4 --- /dev/null +++ b/core/backend/user/views.py @@ -0,0 +1,122 @@ +from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login as auth_login +from django.views.decorators.http import require_http_methods +from django.shortcuts import render, redirect, resolve_url +from django.contrib.auth.decorators import login_required +from django.contrib.auth.forms import AuthenticationForm +from django.http import HttpResponseRedirect +from django.core.urlresolvers import reverse +from django.contrib.auth.models import User +from django.utils.http import is_safe_url +from django.conf import settings +from django.http import Http404 + +from repository.models import Repository +from user.decorators import redirect_if_authorized +from user.forms import UserRegistrationForm, UserInfoForm, UserProfileForm, UserArea51Form + + +@require_http_methods(['POST']) +@redirect_if_authorized +def user_login(request): + """ + View for logging users in. + """ + + redirect_to = request.POST.get(REDIRECT_FIELD_NAME, request.GET.get(REDIRECT_FIELD_NAME, '')) + login_form = AuthenticationForm(request, data=request.POST) + if login_form.is_valid(): + # Ensure the user-originating redirection url is safe. + if not is_safe_url(url=REDIRECT_FIELD_NAME, host=request.get_host()): + redirect_to = settings.LOGIN_REDIRECT_URL + # Okay, security check complete. Log the user in. + auth_login(request, login_form.get_user()) + return redirect(settings.LOGIN_REDIRECT_URL if redirect_to == '' else redirect_to) + else: + return render(request, 'index.html', {'login_form': login_form, 'display': 'block', 'active': 'login'}) + + +@require_http_methods(['POST']) +@redirect_if_authorized +def user_register(request): + """ + View for registering new users. If user is already authenticated view redirects + to index page. + """ + + register_form = UserRegistrationForm(request.POST) + if register_form.is_valid(): + register_form.save() + registered_user = authenticate(username=register_form.cleaned_data['username'], + password=register_form.cleaned_data['password']) + auth_login(request, registered_user) + return redirect(settings.LOGIN_REDIRECT_URL) + + return render(request, 'index.html', {'register_form': register_form, 'display': 'block', 'active': 'register'}) + + +@require_http_methods(['GET']) +def user_deposit(request, username): + """ + View for viewing user's deposit (git repositories). + """ + + try: + target_user = User.objects.get(username=username) + if target_user == request.user: + repos = Repository.objects.all_repositores(user=target_user) + else: + repos = Repository.objects.public_repositores(user=target_user) + except: + raise Http404() + + return render(request, 'user/deposit.html', {'repos': repos, 'target_user': target_user}) + + +@login_required +@require_http_methods(['GET', 'POST']) +def user_settings(request): + """ + View for viewing/changing user's info settings. + """ + + if request.method == 'POST': + info_form, profile_form = UserInfoForm(request.POST, instance=request.user), UserProfileForm(user=request.user) + if info_form.is_valid(): + info_form.save() + elif request.method == 'GET': + info_form, profile_form = UserInfoForm(instance=request.user), UserProfileForm(user=request.user) + + return render(request, 'user/user-settings.html', + {'active': 'info', 'info_form': info_form, 'profile_form': profile_form}) + + +@login_required +@require_http_methods(['POST']) +def user_profile(request): + """ + View for changing user's profile settings. + """ + + info_form = UserInfoForm(instance=request.user) + profile_form = UserProfileForm(request.POST, request.FILES, user=request.user) + + if profile_form.is_valid(): + profile_form.save() + + return render(request, 'user/user-settings.html', + {'active': 'profile', 'info_form': info_form, 'profile_form': profile_form}) + + +@login_required +@require_http_methods(['POST']) +def user_area51(request): + """ + View for deleting a user account entirely. + """ + + form = UserArea51Form(request.POST, user=request.user) + if form.is_valid(): + form.save() + return redirect('index') + else: + raise Http404() diff --git a/core/backend/utils/__init__.py b/core/backend/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/backend/utils/date.py b/core/backend/utils/date.py new file mode 100644 index 0000000..38f1011 --- /dev/null +++ b/core/backend/utils/date.py @@ -0,0 +1,47 @@ +from dateutil import tz +from datetime import datetime +import dateutil.parser as tparser + + +def time_to_utc(time): + """ + Returns 'UTC' conversion of the given time in "ISO-8601 like" format. + 'time' is a string data type. + """ + + utc_zone = tz.tzutc() + dt = tparser.parse(time) + in_utc = dt.astimezone(utc_zone).strftime("%Y-%m-%dT%H:%M:%S%z") + return in_utc + + +def get_year(utc_timestamp): + """ + Returns year of the given utc timestamp. + """ + + return tparser.parse(utc_timestamp).year + + +def get_month(utc_timestamp): + """ + Returns month of the given utc timestamp. + """ + + return tparser.parse(utc_timestamp).month + + +def get_weeknumber(utc_timestamp): + """ + Returns week number of the given utc timestamp. + """ + + return tparser.parse(utc_timestamp).isocalendar()[1] + + +def get_weekday(utc_timestamp): + """ + Returns week day of the given utc timestamp. + """ + + return tparser.parse(utc_timestamp).isocalendar()[2] diff --git a/core/backend/utils/decorators.py b/core/backend/utils/decorators.py new file mode 100644 index 0000000..ee38729 --- /dev/null +++ b/core/backend/utils/decorators.py @@ -0,0 +1,17 @@ +from functools import wraps + +from django.http import Http404 + + +def require_ajax(func): + """ + Checks to see if the request sent is of ajax type. + """ + + @wraps(func) + def _decorator(request, *args, **kwargs): + if request.is_ajax(): + return func(request, *args, **kwargs) + else: + raise Http404() + return _decorator diff --git a/core/backend/utils/system.py b/core/backend/utils/system.py new file mode 100644 index 0000000..054b6a2 --- /dev/null +++ b/core/backend/utils/system.py @@ -0,0 +1,45 @@ +import os +import shlex +import shutil +from subprocess import Popen, PIPE, STDOUT + + +def run_command(cmd, data, location, chw): + """ + Runs command specified in 'cmd' and provides the input with the given data. + Also if there is a location it will be appended to the end of command. + """ + + output = None + cwd = os.getcwd() + + if location is not None and chw is True: + cwd = location + elif location is not None and chw is False: + cmd = '{0} {1}'.format(cmd, location) + + r = Popen(shlex.split(cmd), stdout=PIPE, stdin=PIPE, stderr=PIPE, cwd=cwd) + + if data is None: + output = r.communicate()[0].decode('utf-8') + else: + output = r.communicate(input=data)[0] + + return output + + +def remove_tree(path): + """ + Removes a folder and it's whole tree from server storage. + """ + + shutil.rmtree(path, ignore_errors=True) + + +def rename_tree(path, new_name): + """ + Renames a folder to it's new name. + """ + + parent = os.sep.join(path.split(os.sep)[:-1]) + os.rename(path, os.path.join(parent, new_name)) diff --git a/core/backend/utils/urlparser.py b/core/backend/utils/urlparser.py new file mode 100644 index 0000000..0807892 --- /dev/null +++ b/core/backend/utils/urlparser.py @@ -0,0 +1,6 @@ +def partition_url(path_info): + """ + Returns sections of a request url path. + """ + + return [part for part in path_info.split('/') if part != ''] # remove '' items generated by spliting the url. diff --git a/core/frontend/.bowerrc b/core/frontend/.bowerrc new file mode 100644 index 0000000..7a97089 --- /dev/null +++ b/core/frontend/.bowerrc @@ -0,0 +1,5 @@ +{ + "directory": "public/build/static/libs", + "analytics": false, + "timeout": 120000 +} diff --git a/core/frontend/bower.json b/core/frontend/bower.json new file mode 100644 index 0000000..fa51963 --- /dev/null +++ b/core/frontend/bower.json @@ -0,0 +1,40 @@ +{ + "name": "djacket", + "authors": [ + "Moeen Zamani " + ], + "description": "A Git management system written in Python/Django. https://djacket.github.io/", + "main": "", + "moduleType": [ + "amd", + "node" + ], + "keywords": [ + "git", + "web-interface", + "source-control", + "djacket" + ], + "license": "MIT", + "homepage": "https://djacket.github.io/", + "private": false, + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "libs", + "test", + "tests" + ], + "devDependencies": { + "font-awesome": "~4.5.0", + "jquery": "~2.2.0", + "jquery-pjax": "~1.9.6", + "nprogress": "~0.2.0", + "devicons": "~1.8.0", + "highlightjs": "^9.1.0", + "moment": "^2.11.1", + "marked": "^0.3.5", + "Chart.js": "^1.0.2" + } +} diff --git a/core/frontend/gulpfile.js b/core/frontend/gulpfile.js new file mode 100644 index 0000000..314728e --- /dev/null +++ b/core/frontend/gulpfile.js @@ -0,0 +1,59 @@ +var gulp = require('gulp'); +var sass = require('gulp-sass'); +var uglify = require('gulp-uglify'); +var minifyHtml = require('gulp-minify-html'); + +var dev_folder = "./public/dev/"; +var build_folder = "./public/build/"; + +var paths = { + "dev": { + "styles": [ + dev_folder + "static/styles/**/*.css", + dev_folder + "static/styles/**/*.scss", + ], + "scripts": [ + dev_folder + "static/scripts/**/*.js", + ], + "views": [ + dev_folder + "views/**/*.html", + ] + }, + "build": { + "styles": build_folder + "static/styles", + "scripts": build_folder + "static/scripts", + "views": build_folder + "views", + } +}; + + +gulp.task('styles', function () { + gulp + .src(paths.dev.styles) + .pipe(sass({ + outputStyle: 'compressed' + }).on('error', sass.logError)) + .pipe(gulp.dest(paths.build.styles)); +}); + +gulp.task('scripts', function () { + gulp + .src(paths.dev.scripts) + .pipe(uglify()) + .pipe(gulp.dest(paths.build.scripts)); +}); + +gulp.task('views', function () { + gulp + .src(paths.dev.views) + .pipe(minifyHtml({empty: true})) + .pipe(gulp.dest(paths.build.views)); +}); + +gulp.task('watch', function () { + gulp.watch([paths.dev.styles, paths.dev.scripts, paths.dev.views], ['styles', 'scripts', 'views']); +}); + +gulp.task('default', ['watch']); // Default task is for watching development folder for any changes. + +gulp.task('compile', ['styles', 'scripts', 'views']); // Compile task will run all necessary tasks for frontend. diff --git a/core/frontend/package.json b/core/frontend/package.json new file mode 100644 index 0000000..b1bc22a --- /dev/null +++ b/core/frontend/package.json @@ -0,0 +1,31 @@ +{ + "name": "djacket", + "version": "0.1.0", + "description": "A Git management system written in Python/Django https://djacket.github.io/", + "main": "gulpfile.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Djacket/djacket.git" + }, + "keywords": [ + "git", + "web-interface", + "source-control", + "djacket" + ], + "author": "Moeen Zamani", + "license": "MIT", + "bugs": { + "url": "https://github.com/Djacket/djacket/issues" + }, + "homepage": "https://github.com/Djacket/djacket#readme", + "devDependencies": { + "gulp": "^3.9.0", + "gulp-minify-html": "^1.0.5", + "gulp-sass": "^2.1.1", + "gulp-uglify": "^1.5.1" + } +} diff --git a/core/frontend/public/build/static/android-icon-36x36.png b/core/frontend/public/build/static/android-icon-36x36.png new file mode 100644 index 0000000000000000000000000000000000000000..39829554c3369822f128129189beba7d027dd073 GIT binary patch literal 1438 zcmZ{h3rtg27{?D33YHeyAuBJTpmJp#)~lhZe>| zP`p?!g3UstGz_tY!3LpJ9VI4Xr7I(-5nt6Epc(_zZ6D<|7I`8NwuM1Sak34es=GjS zA5b@Zk@8R|F&={BEo<5EBRCh3GpYr?6U zKKL%4YSafVc^9n~sD1`meF$~SCuwas-ojH}VM(&1@K&BAE1EnWC?-T=OhohrlHUY~ z6|u@8mU57VH3cK<=fXxIB#BpE@{^>`Q(k6CGNTjeSn@Pm-o+8i;>5~WqGt|u-50GB zNOK~oVP9+qU-nTbVh=-HbEUbF_+GAbWh8krAaPNQqMxle%f@%}qy-Up8yAwsp&tuW zqd->m4!(z*m>i=x!xkfP5_J^1BSegf(5?KWqHwfEAk{=EzGEjDgw#!6)dN6XV@Z}p zC#?>bcLouMI7BB$-pRq5gOQ3*sWx(EW~OUwQ@wX~hW9hls)oN76*0veXXYvw070SN z@ByuD;SA86m6no6UB22}u);TmzgcJ&q=##N3vMDIEtbLErqT@=@~^0LFaH?oXmmaL zqTzLhVy-?)2QgSx7#g`Kun)Lxg`YoAy@ zP!ZbjsfIs**iW7O%>7)PxA<)C_JU4zx8iwLCFOPd48u{(w_hxZwEtj#?AXegV<#Jf$Bi^&cu+ z!+KBzHIM0}R_BMV{J~y3p4@CZxCJG%68t9ETw-eSr|bZ~XWH825x#Xp4Y~GN3I4B)-2^hn_h49eEaH2@4(~! z%`xSbyL15q&;F`CeDGK7Me&P^i~G-*@jl1?sm5zWU{J#Js;2=1`0sts{dWz}4~ioG z^e{6^aHki#i}h|3Z7ee}3E(KEfKeRA&^(l+Nd>c{C`zN~u`6v~{YPMT6kE+3{$Fse zX~7&uu*}=R*Q4XdMmAKtbS|;;& zJc!jxGr`b^qs%h4H^rInbWejm^VFXy2NC) zl(+$=!b3n=9m5D``TWBaSc}~yUgUaDTp)u)%tF*wyUD%5X##IvC3BWqmVBA7dCNzg z$4~$xND`&w6s8z&EV#fRA+vl|ST~vgnwt7~$9ogZRsO82mAP5KY_*w~@5}6^Zkxj* zGwUm?Mn|bj_U~Y1CJHZB81yJX8stWbG@&F$$Vs^p$BQXSNl^%+K$!O(U9+y8=>W3Q LHEBmubv1tjfluP> literal 0 HcmV?d00001 diff --git a/core/frontend/public/build/static/apple-icon-60x60.png b/core/frontend/public/build/static/apple-icon-60x60.png new file mode 100644 index 0000000000000000000000000000000000000000..8d1be17be8051af9d21965d87ec3a965a7bed177 GIT binary patch literal 1912 zcmZ{i3s6&M7RSGY1QH$zuMnC5fgt3?y}38!CN}|;r#wn{NmUA>Bm@W$f{++NT)U{H zDvDOISXdRc;s}Bu+EGRnE00RlRQamJ;vc$Z-S!xFjaTW)L}iA!v3BCwx&Pktl^xnkWFe8=U6ys6&<11@)QT+f^cR}beN6`{rk&1_%b5KnIWSgsU z)K2jgRjlE{Wi0%Al4^p4e@Di@BjI;Q_yh?uvILR?Elic?Vq_?<4P}32`1&!a{a3L$qvs zoD8q^RDMkfJ>e+H;Dnx}3spRMHx1eBhBh#vd^XnWptwqrXt?4;AGnl-e#(FhEMz-V zDEF6SzK5N4KzA`@jSO)LSG3pY?Su>_}I?#C41Y5f?l4GRSAHT$3l?6=PlmE>fA|;jmk_IezyNf zc1D!v(BGZ+CGA*gZ;PWOWyIXSb^QqWR6>vebvLxiBqGrmfAUoFvMHPGUlCJ3Z*qR* z{PF9D|9CmbJZO4c^~;8vbB`h_3lukN zG^?lAd?2b0POT5#@6l5VpV_Cot!^)E_OtcZP*XG#pZV>^QvDZi z>ulnh@Gl;YyoD1ygb&7QpVuD>w8jTSOl>}qT;19|p=xYr&n};!4HYC@ zXCosK=$ho`9toS-afeo4@mI>pGY=)xJ(2yKflNj1=(&>ViZ55H`w}O`(e__N&ZC45z25P?IFn@Op(%&Sb zjaaWPG9Te&*LTH|;{Dv=6|FgbFlk8rr~QUa*s(zG&w`Fd_B+90ak}huICZBqH}Rq% zXDPPsYQT*b8+Xl32iT_5p=J(DiATn`LaqNqyjA)zqtBeyh{29cUavgHv}F z^^NLfJ2tF(J4?4n>w=A;wND(-J3X;Wh6}>u9*nQ|{-DXZ?faJ}d%k{7ZPO^vsa`$* zXQRk??^@6?Mfia^SbIOdSdL7GyllO}8a7oF-u?I)Aqcz|6f=t~My*;MYtmVa`ZBXxokpOQ0BxJ^lm3DE z-BYbD(i;kk%m7u%n4n@S!LZ-a`46+gkZ&$pK*l-(Xaw@!JBY?$)SIiy_2BoZq~&FW zOL~^4{6R;PNl*ZlV;F*m;;7=Mh82SZ!hA<(nW0(_km%@rM;1RNu9m-Z^+84=C@_@j ziRVj=W#&>-p|l`xok3?RtCart$4K?KEMK9`L*$q?REJ}F1ViPaSg2Aa%g1pgj>AL; WOw4yWr{pl<021R;VmqVP)czYHtDo%v literal 0 HcmV?d00001 diff --git a/core/frontend/public/build/static/apple-icon.png b/core/frontend/public/build/static/apple-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f92e54d27b60af224c55d9bff8590d03b84204e7 GIT binary patch literal 7292 zcmcIpXH-*7w>}905?bgWH6TSqAasZz2_2~-C@RuRXoB=2Bs6Iv*g&ZPL=aI>Ku}s} z%BvuuBZx#rX#zodNpj=2?vL+&Yu$hMu5;$hIy28aGyBY*efFNou(L7aWD{Wn0D#lN z+}NIx6aOYC3*%}3C6JYoL3}aR7yx*g!+zw>%ywrOR>`HpNB71z&qnkxVO{3MtFhSI3Z|Tw+Y298cnRP z`8_`F;jWbX^(=PwX`Wtii%)+#Vv@s|XTFOy-G=6kl5g_sPL*aZN@bZ=f2cFpr;1ENyTgQcnDqI_phVPxfIj2Z2 z$qTWKt+zd(*jo1MrZr|roN2PW^_PJ=deL1=kwP-KZK4)amGI(qRa9ufQLy=3{i7YS zF1*40n}JA$-rJ-(rEeW4+znQ`;y))B)$`U=Zg*SZwN0khM@U_Pmq}a4rHJh78v%6Z zWR1JSwXDIHve|q&cD><^!>rVtRC+u-)M3%3yCOp2oE)*R{FAniUUY>$SmRhkQ~K7O zD*eR@o&g_bI#tm@5{{%Tl<|K1qPNwWeVfroB7k&fY9pf?T5i7cO~fI=Tz{jO>(jyAkpWAK`K`$cbOiHyuDABlSXH_y*?Ga4c1#n%i2*r z0D3KnGnAe2_2sznxA2pvqCda>rM%aKz@NqJFI3de@i=rACCw>bc`3ixeNh;0hz323 zkOflP-i%jQbk&+|d=vgT=Ul#*6+j|2du-1`89acAIH)L(Z^i~MB1kA99CZoI-zC%R zx^ctF&Y=|*Xm*{WH3WE;X$-PBJ?<%s*%%gX?`#L^ob)xXA>Zx@d}VjYECEz>tU+&c z!Pe4&xiRDAM_-#tQs6gk16M(u`U5r0gL`XoIbc#FUi;L5_B*m8dq~*%E-U;brk$rK zEOt*bI;E;}U3hF4DS6AiLz6`S8+b{BkIYHjv)A7dHnLQ9(K~SVA_>8eo=s|?kdwUz#(j9f z3=r#D6$m+hQyPp!+{BoHJI~|K2#v3)SoPe{Fr=@4a;JH5oaA#YHFOpFnWAEu zY|c<2HuHM)H_m4~h`#(QRp_C!dp(AtGwf}#b?z_^SPMi(qcfQ~p$-_-*%Cg{p*yi) z@t_ommu9RZRW*B&(&5YPUg17@QO*xEoq6|fTS4}a?W7;DD;h7OI>V;>w{&g=lbrAx(@Se@t;;r3lAd6rO*#`61rP~$sUoW^p*$wc6C zBYB(c1fTdWAEJt*^XlUFoTdvFh`ejXU+pn~1GZKbEl0dseVllgscvRQ&DAO;LZON! zp*6)h?8}KCWqH{OaZ%zJ(^`oNzS5@({g4*+E}ut2;sZCw)m+Pw>$aTE2=wZ-;DS0S zDB^^G*i+W0@q^ii$BpEO;@pVGXd6Yei8Ri9nuSf_dvN>ts5kWVmd4g zfdBzuu*D7k(;+tZ@e!;)qhRdAiM6wpU?O@XGSKlcYt#>xue3WbN@#kd-iCkfL0&pe zh%?PK^qpP z+bk_V&bCoxnb*OCIZnJ@Hj>T4l4ym`$3QhIcNR2O?*61zLldg-VnQw3$Q*zLDZpRO zG`ywM=l!aH{*k##Kfs)^g7PsTMnH+Rj1m_QaW#)(wrfBwzOjCyG1jC(dGOm+oxppR zNu6%yCfdn51veRRG=z|d*A99GMMKstHzX=A|KiN z1hdA<7(MKy^}aELay)4!yMq2E-eX&(^13<~{3Nvomf>>sP7|2}LV`m5^L^)kqLU`w_S7vfYD5CdVO{#)m zt`XBe3U+abCa{Jva8?Ef05~sl%8ecgcC-v9gr~eiwP>2Np@Iik}L}6L`oX z?8E?I`J{};9{gMIvQdsBT^=uVYQRUBT>PE85O@QXpA!1A*lLgq3tN%H?g#VD+0Ox2d6lGYY6UlbkeK36IBf%a7?6s%($%6v$Cgt6L)!; zrR5tBf)8kU6mO!=<@51oObg5l(YqKjJ$y=0<40D)f;u&P;VtgCG`}-QM zmd3(9mR@X1nW}zZ-n;PP^i=ejAf}ud*dJER4{ajDG(1m8m|gy*`e6S#d?eAg?|GWd zz!0hKiq5F6QK6Wt-i`Y11K5~Ke-iO~qIk_o2wQ$&r)BTx0t_v+|PrFfv zWKg!iqp=|s`7OE-QBt_29n5PW4u`b53!uH{X!B z!i)ix)0E4V6`8!e#Nd4YxrZ>41f^~CkGt)N*cvxNh=bk;C|lhqmEb!Nx^U`*J8NN) z@LsW6zvT2irU;tM!(U*DRmw{$7dUHt+-^)aE+6C17JAiAvv)KSI zM?FJH8o>6PcRh)&$S=#;-hb zX}*VdYxA`1yqX8?6sDuy@hMZdUE{kBQ#F>O1~5&=p>Fyf6<9n8k~>n5=}J6*%+10H zUB&TP+Z}9GyC6pjUvw3gyC0n6(7tRtFgVu)E0zVeA#E3yC;>KJ_@5x0M@6L`&|Minu8EGck+Fgcd*TLWzsj}#SJ9!4X>~>a%n+LKt-{Q*LyLs2r z$x$C>pd__$6!>8_TVt+`0o*bquImixxIeS`S&&crNhem=AV@m69V%C{i4kj1_TB${xhmyc!TpzNy)duuM24xIDs5c%0_i7VU$dMmfM(04(CoB>g>zFL%S*Q$5 zf1tH2!@vw5iC)$x`n=^&PfDzvQy}MjKcsJFI?~IfF)l9auAeR)3Mo%}<0FJnMgV{L z@!Y)U6GP`6a1hEQN3$rZx_k+jbR(`|WWO_`U@l-kS&ScT5~P0=&$TL#&V`4~9-c0) zrt-K)Z>6G$eLHlsV@H-kd&;(8F=Z+ zm}e2FAH?bUS^RxY88eKY;1vVcbSE~{hYQzE=^Z;}xeK;0u-p&f;!P|@UOB`z{ zmM)^bi{lZACE176Df3;eUS?*h&D*1QvOSjv5a?q^>iNzEPyoyPlZ2(}U4XQltGoOh zcmShXs%X;lvvyt^bZ{_`oJpXGqKo_J8c?*SJ?qcpJ#mLNGEr2}JYtVoJYn@U`>M+d z_cYZnV6%Fg5qW+ewI|Ub8->oH&a6Y+@? z5$Brs={u%CN|V|aIh)O#ACwNf;S%j{2+{Ri!m(iJ>I6zxXj->WLulCNX|6+<3Nv!*!u%+@w@v9^e3>(`5_aQ5m`>Dcs*+cbM}gcA1vC zd$O4RfRLExw29koOqt19be%>t-UlH3;D!<>{Uum}>-as&b>AC@A-u)(ph`a^Nn-|U^Auu&FjM^|iH zE9j4m6=4Vo?g~zs#``4t98~TOW|K6@t=tJi%v9%fN;K{E&pmUWMI}+^C zMyxJuoD_7y!JF~5I??@*Qy?*>wsT!=LQgv7Pg)7h~iqU=SE-5B_*$>NFktt4gem;$NI6z{c@`1q$0V>UW(MZ@$He~D)X z=QQr?_r>|j)=|BH&cd3`SGrgs(24s73-a?lPo;@(Zlg1&(cJ|!e#z{v&5ZJV$SR$_ z{cY#10`8%49Mom0ZOeftM0Pq>72C$`QvJa0_h(u9g1eJEX%_S7N-Qp~r`2 z(aZ}x_*j%d{;wWN7C);w7e$_7Xk=GFQ}N#-ww~@!2>(jHb5_!$Zm@8~7qlB=`&{(O z8S2jnQpw5O+zWPEM%xf`t)t74?EM&!Vegjjbj99Rfz0(=BTmiSh}+~dD_sdA7gup+ z0rY!V9*`kq%DlET3esG45Cc0ezrI-_6?@J7j9A&;8q`PeVN{>;92?>y00oLQITgw;= zk5Ly9+(3QjN*DD--2=(dwX(_Jpd-)&=W3#fh{?g7xYBvE53n7RZ0S$nn|FAzr`rT5ynD%;FHW zra9(ay+LUbbr8)Nw^=q>jE%g62COn(8+?-UNBFV%m(tJQq}HZw3{^1{mAU7|tyWS9 zm>WCY6Ue+-{XQYUCRS;ysQb)P*};C}wHvlck2S)`HX21hEI3drnQHMp`)7rTex*+x zdRuZ-n7+v!>hIuKu+fCwl0bt&42O9za$r)lMV}CcpHa4>e&ilkd4dWEKh(l%fb9CM ztxKMu9)ibvly*3O*RcrGy;#=#9tqJz36CJ$01MfjG4zqQam&e-W~qeDJD%?r<<+Zc z5!DivFVn)C#Rg>o_UO-YrN!Hec89UIp=)s28dokmYM{uPMct;Ld~mW4FAxh{u*KPG z9UZ84v$~cRgMXA-dmiPeQ-(gw^p)yTOtEsH6kvz!C4^B^x&J`9hTXpOr>!;((-Z}2z4*gxDyQV)( zb-`}0g9jP@?g6DX`SYvg(*1{MmHqfXFKM%^IiIFIHq3)A99;u-p6?D?sW9-2DE-kk zY@O=EgTVVFgB!JSiFxkby~+W>;No>xr^f*$57$b3WCf<$*c15EpywFLs#uxXbT2wuqRh+ zNy9zc#$&aJ?zF8A1B)0y5z={b&&!K{Pc~J$A6UcAM^cxJs6nwm%*bsbf$<(!Ls3?BHu@tpUI z^#+iJhWE?Q{AN_4|EE{`IElkB*N6kyi!yT!i>7nka mqv?&*RMk+|RM%Eh^U~4L*3l`loZMh=0Tw1U#xD%rlKu-@pgsZs literal 0 HcmV?d00001 diff --git a/core/frontend/public/build/static/browserconfig.xml b/core/frontend/public/build/static/browserconfig.xml new file mode 100644 index 0000000..2ce07d1 --- /dev/null +++ b/core/frontend/public/build/static/browserconfig.xml @@ -0,0 +1,2 @@ + +#ffffff \ No newline at end of file diff --git a/core/frontend/public/build/static/favicon.ico b/core/frontend/public/build/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..2c0246be8636f202f0be7ec672a90b64cc23a1db GIT binary patch literal 1150 zcmbW1PfHt76veM`q==ynNfacBnwfEC68|K1C^0kJwooXz^$Q4HbfufF+`DtH2uk}M z`UP4jx+v70P-qntEDFVTdwTAhluoRPBH_-=c`xtGy}x@CkpX^kIpK9mURfezA`&pT z#Fb>7Hxcd?Mv^d^7d!Bgj_Qn$blCOo{q%SLd-i;|W$#~<_&n*suHE9neaH1N5!aC2 z4wKjk@o@7o#+PT|ROxXo&n~V0e0Q}Gz&c!nT*Q!XZOD%Zav9^%MicH7J^Izcy4{M0 z?A8M0XN>%M!{jscv1R-;4|k%shca_nZ*g@V@+-#3UWj1WMR6_B+1;@3dCkpXj^!71 z$Vr63gO=e|c}9I4!J@7<15@XXJe7e@2D3g)&ZiMdFY9m|FVSbzOEXD7dU4>oVL4b^uw`8F6(C7VW2G%|q(AzNhP6pK4 zW9{`_8I+&b4U^0l&c)X@Do@Eo=c(6Bb{J^?bPklI^?@^R9wBqo#$>x@X0_+}&K!-t zo=4{MBCJm>3^MNr@1J07We$$r=iTWUXD@B#ta&o@+?{?)*FDrnLw#=G{`Y_2zV|;Y CxRY7{ literal 0 HcmV?d00001 diff --git a/core/frontend/public/build/static/libs/README.md b/core/frontend/public/build/static/libs/README.md new file mode 100644 index 0000000..7df2b33 --- /dev/null +++ b/core/frontend/public/build/static/libs/README.md @@ -0,0 +1,3 @@ +# Bower libraries + +All external frontend libraries from Bower will be installed here. diff --git a/core/frontend/public/build/static/manifest.json b/core/frontend/public/build/static/manifest.json new file mode 100644 index 0000000..15176d5 --- /dev/null +++ b/core/frontend/public/build/static/manifest.json @@ -0,0 +1,11 @@ +{ + "name": "Djacket", + "icons": [ + { + "src": "\/android-icon-36x36.png", + "sizes": "36x36", + "type": "image\/png", + "density": "0.75" + }, + ] +} \ No newline at end of file diff --git a/core/frontend/public/build/static/ms-icon-70x70.png b/core/frontend/public/build/static/ms-icon-70x70.png new file mode 100644 index 0000000000000000000000000000000000000000..6169002b3cc4de3a311726afe4066e422b9048d0 GIT binary patch literal 2021 zcmZ{j2~d;Q7RPT$0!c_nN{}T2Nl?f}lJEPH@5^FQfhYtCB1T2fwt<9@%2tR)G@?>O z1;HJZLBZA*v4OJHQdCe;#MTWItl)w=j;%|d*1FUM-bd%nJYQ$t%suzax%YSe=iIsX zZi9F1*77ZKa?sRJ56@Y@#X}XvjWycnedW&kkMUD+&(~gz_cx zII6QG&269=1)5txa}#K;1I;i&-S2|_;0Bd4#c>>ggfGpSr2d7VxdE`_WLd74K+1== zd8jTDWp=i*m4cllOX4Q0`be4)0Ig>U#6hxbFZES|vXLr8x$ssG?5HcUn}Jj@kbQL3 zPb93%RXodIsNpJGC;}N@q~V4p`6}Dpu+t>TT#gu*I!~0$=U}}gS&o-_h=8_JL}7t~ z;Gj^Gk9xob>miF{C&QHtso7h7#RWa$DvAgYO$$_hN0lvS%j{l4B~NvZC^7oM6-=R& zFUy*Q9(EIAT&0sLU%`gAd%(LG;%I;DG+Eh9Q4bTOR&VuX7qrt=6d52%;Hb`!Bn$nZ zjV$4`Kt(-GbB!P`^u$h)RF{a7Bwtycm(=2o9dnh$b0iD=piN9ryI0^rKodPHc;gbPXzBL z%3mBAAXIU&Ub1A7pEP3<+U_R$Fc58{VBM~v$-c@4nt0A+^hY=KAVHetCDH}RKJ${M zdt)ca;^@g(HyJ(V8v3!1#K=MF=mIfcv6qh2(2+e1sDw2>J|1>>X2isMOPLd;>o9bO zyubr$PW0kD0NmLVMgY|{z5ozI^ty0kV$b6$+kAF7t0cd&hY&2R|DAMccQk8T-`DFX zn}kJ6?v$^{rBDtEOM{9=2^&i{mfmR`-SqPL%i@fSt;LCh>~H^^^E&zPp<53M>))K+ zJoT?0Zqn9kx4$a&XB{l78!bSdhnE%?U--Eo%<|PZ^x_R zuYZm$iZK6p?al|Wa8oqhbAUfIwX7{ zV@g&TY6w=$sq=rB6s>G(vySA^J*HGGt}UzL7sL&6(&8#D*S_bknbOy4AbZ`qoVKC5 zmsa*o8^uuk%$l6?1Ub;5+chud=j&lRvm4hkM-Jb$#Og17(c1H1EWRtxr|;3tFJ6=w zf4?_mY|xu)8Ft2-ex!YEa^7fNwK?q6mg|MSH+C5J5~Np*otEg#fWA`!+xo@*&mAu~ zF2+5*UtO{|rVpF8UnRK@);M42k}JXs&djZ|q(9U*_kI>**go=#b$?X9zh!oVhA~vL z?vLS%U+(R12^qOpxxf5=-O-9?uTqpl%KVd6?vTSNp=BHL#kJLCpC%+RLpugmQD-<# zALra_?TnxLkhar$q_n=cmorpfRP~~MW&!c)llcmD59d|x-hZvS%5cmLbnY2`i8Ytp zpr7LI6fEnzB#pabdK+=R`I{4`8`sIEk(>=jAD`e1pWKEPMR`B27)&G%UBCUNPzBc9 z986;lnQz>6`aNHdzFdbg9Vz9{E;{|*j5`bZd=9PldQ7M8>!}`#a_kQrCoel7a2%|i z&fd+LJ46~OCqH%5(gF*akG0)E%?=>YYZvjOz0oN= z**>&!CrfPDoOi{1hfx`=ol#l*L(}oCWBJr2!DC@vzGsaWHfT8HQN!{@ABs5M}ad6OU4eBo(uTn_AXKVZ;_StS@*l9 zFL-x*%Nlk!p|JLegRvs!JpPS>?Xwc?W|Q5bP0h974j>SWNg+fELq-VIq8L6T5Tu2m zmUA`V|4%|@w%L|e@c$EzZ<|WR6J}3T$jeT%=bLgZK%Z%{S_~$uEp-_Wlc{8CsfLG_ zR7xRK3M0IEFvd%_+jF#Xd47Jr%r-&ehSRd_TnDTF&J?Qz)BQ_)Cx6>J=n77G>$Oe)0uBuX#r3~#Lh3L zKf<>bzPGg?K@ZYw85aEb^2{uIMz&R+X8PQgnw^y=|L?`fEo#J!rI;WknxaTmqZSB- kl?qg$LJ+fBtx~IHc!mt`uX)L`I=lkV&x+GEMSN29Z?kdYeE0){var e=t[0].split("/")[t[0].split("/").length-1];$.ajax({url:"/api"+t[0].replace(".git/blob/",".git/readme/"),type:"get",success:function(t){$("h2.markdown-title").html(' '+e),$(".markdown-preview .markdown-title").css("display","block"),$(".markdown-preview .arrow-down").css("display","block"),$(".markdown-preview .markdown-body").html(marked(t))},failure:function(t){console.error("djacket > failed to get README file.")}})}}function process_dates(){$(".moment-date").each(function(){$(this).hasClass("moment-dated")||($(this).text(moment($(this).text()).fromNow()),$(this).addClass("moment-dated"))})}function setup_pjax(){$(document).pjax("a[data-pjax]","#pjax-container"),$(document).on("pjax:start",function(){NProgress.start()}),$(document).on("pjax:end",function(){NProgress.done()})}function setup_view_tabs(){jQuery(".pjax-tabs .tab-links a").on("click",function(t){jQuery(this).attr("href");jQuery(this).parent("li").addClass("active").siblings().removeClass("active")})}function setup_static_tabs(){jQuery(".static-tabs .tab-links a").on("click",function(t){var e=jQuery(this).attr("href");jQuery(".static-tabs "+e).show().siblings().hide(),jQuery(this).parent("li").addClass("active").siblings().removeClass("active"),t.preventDefault()})}function setup_nprogress(){NProgress.configure({showSpinner:!1})}function setup_file_icons(){$("i.obj-icon").each(function(){var t=$(this).attr("ext");$(this).addClass(get_icon(t).icon)})}function _show_weekly_graphs(t){var e=["Monday","Tuesday","Wednsday","Thursday","Friday","Saturday","Sunday"],a=[];t=JSON.parse(t);for(data in t)a.push(t[data]);var s={labels:e,datasets:[{label:"Weekly Commits",fillColor:"rgba(57,221,0,0.2)",strokeColor:"rgba(49,170,8,1)",pointColor:"rgba(26, 103, 0, 1)",pointStrokeColor:"#fff",pointHighlightFill:"#fff",pointHighlightStroke:"rgba(20, 75, 1, 1)",data:a}]},o=$("#repo-graphs #weekly-commits-graph").get(0).getContext("2d");new Chart(o).Line(s)}function _show_monthly_graphs(t){var e=["January","February","March","April","May","June","July","August","September","October","November","December"],a=[];t=JSON.parse(t);for(data in t)a.push(t[data]);var s={labels:e,datasets:[{label:"Monthly Commits",fillColor:"rgba(57,221,0,0.2)",strokeColor:"rgba(49,170,8,1)",pointColor:"rgba(26, 103, 0, 1)",pointStrokeColor:"#fff",pointHighlightFill:"#fff",pointHighlightStroke:"rgba(20, 75, 1, 1)",data:a}]},o=$("#repo-graphs #monthly-commits-graph").get(0).getContext("2d");new Chart(o).Bar(s)}function show_graphs(t){var e=JSON.parse(""+t).weekly,a=JSON.parse(""+t).monthly;_show_weekly_graphs(e),_show_monthly_graphs(a)}function get_datasets(){var t=$("#repo-graphs");t.length&&$.ajax({url:"/api"+$("#repo-link").attr("href").trim()+"/commits_stats",type:"get",success:function(t){$("#graphs-loader").css("display","none"),show_graphs(t)},failure:function(t){console.error("djacket > failed to get repository graphs.")}})}$(document).on("ready",function(){setup_nprogress(),setup_view_tabs(),setup_static_tabs(),setup_pjax()}),$(document).on("ready pjax:end",function(){highlight_codes(),process_dates(),setup_markdown(),setup_file_icons(),get_datasets()}); \ No newline at end of file diff --git a/core/frontend/public/build/static/scripts/djacket.js b/core/frontend/public/build/static/scripts/djacket.js new file mode 100644 index 0000000..da9fcaf --- /dev/null +++ b/core/frontend/public/build/static/scripts/djacket.js @@ -0,0 +1 @@ +function setup_static_tabs(){jQuery(".static-tabs .tab-links a").on("click",function(t){var e=jQuery(this).attr("href");jQuery(".static-tabs "+e).show().siblings().hide(),jQuery(this).parent("li").addClass("active").siblings().removeClass("active"),t.preventDefault()})}$(document).on("ready",function(){setup_static_tabs(),$('a[href="#auth"]').on("click",function(){$("header#title-bar").css("display","none"),$("section#container").css("display","block")});var t=""+window.location,e=t.substring(t.lastIndexOf("/#")+2).trim();"login-tab"==e?($("header#title-bar").css("display","none"),$("section#container").css("display","block")):"register-tab"==e&&($("header#title-bar").css("display","none"),$("section#container").css("display","block"),$("section#container #login-tab-li").removeClass("active"),$("section#container #login-tab").removeClass("active"),$("section#container #register-tab-li").toggleClass("active"),$("section#container #register-tab").toggleClass("active"))}); \ No newline at end of file diff --git a/core/frontend/public/build/static/scripts/icon-selection.js b/core/frontend/public/build/static/scripts/icon-selection.js new file mode 100644 index 0000000..2d6320b --- /dev/null +++ b/core/frontend/public/build/static/scripts/icon-selection.js @@ -0,0 +1 @@ +function get_icon(o){try{return{color:icons[o].color,icon:icons[o].icon}}catch(c){return{color:"",icon:"fa fa-file-text-o"}}}icons={git:{color:"",icon:"devicons devicons-git"},js:{color:"",icon:"devicons devicons-nodejs_small"},css:{color:"",icon:"devicons devicons-css3"},html:{color:"",icon:"devicons devicons-html5"},java:{color:"",icon:"devicons devicons-java"},rb:{color:"",icon:"devicons devicons-ruby"},rails:{color:"",icon:"devicons devicons-ruby_on_rails"},py:{color:"",icon:"devicons devicons-python"},scala:{color:"",icon:"devicons devicons-scala"},md:{color:"",icon:"devicons devicons-markdown"},php:{color:"",icon:"devicons devicons-php"},mysql:{color:"",icon:"devicons devicons-mysql"},coffee:{color:"",icon:"devicons devicons-coffeescript"},sass:{color:"",icon:"devicons devicons-sass"},scss:{color:"",icon:"devicons devicons-sass"},less:{color:"",icon:"devicons devicons-less"},vs:{color:"",icon:"devicons devicons-visualstudio"},sh:{color:"",icon:"devicons devicons-terminal"},ps:{color:"",icon:"devicons devicons-photoshop"},vim:{color:"",icon:"devicons devicons-vim"}}; \ No newline at end of file diff --git a/core/frontend/public/build/static/styles/djacket-session.css b/core/frontend/public/build/static/styles/djacket-session.css new file mode 100644 index 0000000..5eb07df --- /dev/null +++ b/core/frontend/public/build/static/styles/djacket-session.css @@ -0,0 +1 @@ +@import url(https://fonts.googleapis.com/css?family=Oswald);@import url(https://fonts.googleapis.com/css?family=Droid+Sans+Mono);html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none;list-style-type:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:'';content:none}table{border-collapse:collapse;border-spacing:0}@-webkit-keyframes fadeIn{from{opacity:0}to{opacity:1}}@-moz-keyframes fadeIn{from{opacity:0}to{opacity:1}}@keyframes fadeIn{from{opacity:0}to{opacity:1}}.fade-in{opacity:0;-webkit-animation:fadeIn ease-in 1;-moz-animation:fadeIn ease-in 1;-ms-animation:fadeIn ease-in 1;animation:fadeIn ease-in 1;-webkit-animation-fill-mode:forwards;-moz-animation-fill-mode:forwards;-ms-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-duration:0.4s;-moz-animation-duration:0.4s;-ms-animation-duration:0.4s;animation-duration:0.4s;-webkit-animation-delay:0.1s;-moz-animation-delay:0.1s;-ms-animation-delay:0.1s;animation-delay:0.1s}input[type="hidden"]{display:none !important}.left{float:left}.right{float:right}.text-center{text-align:center}.clearfix{clear:both}.red-color{color:#F60F0F !important}.red-back{background:#F60F0F}.yellow-color{color:#fa0}.yellow-back{background:#fa0}.green-color{color:#39DD00}.green-back{background:#39DD00}.grey-color{color:#ccc}.grey-back{background:#ccc}#nprogress .bar{background:#39DD00;height:3px;box-shadow:0 0 3px #31aa08,0 0 2px #31aa08}.form-error{text-align:center}.border-top{border-top:2px solid #fff}.border-bottom{border-bottom:2px solid #fff}.seperator{border-bottom:2px solid #fff;width:146px;text-align:center;margin:0 auto}.seperator-left{border-bottom:2px solid #fff;width:146px;text-align:center}.arrow-down{width:0;height:0;margin:0 auto;text-align:center;border-left:32px solid transparent;border-right:32px solid transparent;border-top:8px solid #41443c;margin-bottom:24px}header#view-title{width:99%}header#view-title div.left h2{margin-top:1px}aside#sidebar{font-family:'Oswald', sans-serif;position:fixed;background:#1e1f1c;top:0px;left:0px;width:180px;height:100%;padding:24px;border-right:1px solid #000;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box}aside#sidebar section#box{position:fixed;left:0px;width:18px;height:18px;background:#39DD00}aside#sidebar section#title{position:fixed;left:18px;margin-left:8px}aside#sidebar section#title h4{font-style:italic}aside#sidebar section#title h3{margin-top:2px}aside#sidebar img#avatar-img{width:128px;border-radius:50%}aside#sidebar section#avatar{margin-top:64px}aside#sidebar section#sidebar-options{margin-top:32px}aside#sidebar section#sidebar-options ul i{width:18px;height:18px;margin-right:2px}aside#sidebar ul li{margin-bottom:12px}aside#sidebar footer{position:absolute;width:100%;font-size:0.8em;bottom:24px}body{background:#272822;background-size:100% 100%}section#container{font-family:'Droid Sans Mono', sans-serif;margin-left:180px;min-height:100%;height:auto !important;background:#272822;padding:16px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box}section#container header#view-title{font-family:'Oswald', sans-serif;font-size:32px}section#container header#view-title section#title{float:left}section#container header#view-title section#title div{float:left;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0}section#container header#view-title section#options{float:right}section#container header#view-title section#options ul li{float:right;margin-right:12px}section#container section#content{margin-top:84px;font-size:13px;box-sizing:border-box;clear:both}.sk-cube-grid{width:40px;height:40px;margin:100px auto}.sk-cube-grid .sk-cube{width:33%;height:33%;background-color:#39DD00;float:left;-webkit-animation:sk-cubeGridScaleDelay 1.3s infinite ease-in-out;animation:sk-cubeGridScaleDelay 1.3s infinite ease-in-out}.sk-cube-grid .sk-cube1{-webkit-animation-delay:0.2s;animation-delay:0.2s}.sk-cube-grid .sk-cube2{-webkit-animation-delay:0.3s;animation-delay:0.3s}.sk-cube-grid .sk-cube3{-webkit-animation-delay:0.4s;animation-delay:0.4s}.sk-cube-grid .sk-cube4{-webkit-animation-delay:0.1s;animation-delay:0.1s}.sk-cube-grid .sk-cube5{-webkit-animation-delay:0.2s;animation-delay:0.2s}.sk-cube-grid .sk-cube6{-webkit-animation-delay:0.3s;animation-delay:0.3s}.sk-cube-grid .sk-cube7{-webkit-animation-delay:0s;animation-delay:0s}.sk-cube-grid .sk-cube8{-webkit-animation-delay:0.1s;animation-delay:0.1s}.sk-cube-grid .sk-cube9{-webkit-animation-delay:0.2s;animation-delay:0.2s}@-webkit-keyframes sk-cubeGridScaleDelay{0%,70%,100%{-webkit-transform:scale3D(1, 1, 1);transform:scale3D(1, 1, 1)}35%{-webkit-transform:scale3D(0, 0, 1);transform:scale3D(0, 0, 1)}}@keyframes sk-cubeGridScaleDelay{0%,70%,100%{-webkit-transform:scale3D(1, 1, 1);transform:scale3D(1, 1, 1)}35%{-webkit-transform:scale3D(0, 0, 1);transform:scale3D(0, 0, 1)}}div#new-repo{display:block;text-align:center;width:840px;margin:48px auto}div#new-repo h2#new-repo-msg{font-weight:bold;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;font-size:16px;margin-bottom:8px;padding-bottom:2px}div#new-repo div.form-errors{margin-top:16px}input:-webkit-autofill{-webkit-box-shadow:0 0 0px 1000px #272822 inset;-webkit-background:#272822;-webkit-text-fill-color:#fff !important}form.djacket-form{width:100%;margin:32px auto;background:#272822;border-radius:2px;padding:20px}form.djacket-form span.last-span{margin-top:6px}form.djacket-form input{background:#272822;color:#fff}form.djacket-form h1{display:block;text-align:center;padding:0;margin:0px 0px 20px 0px;color:#5C5C5C;font-size:x-large}form.djacket-form ul{list-style:none;padding:0;margin:0}form.djacket-form li{display:block;padding:9px;border:1px solid #DDDDDD;margin-bottom:30px;border-radius:3px}form.djacket-form li:last-child{border:none;margin-bottom:0px;text-align:center}form.djacket-form li>label{display:block;float:left;margin-top:-19px;background:#272822;height:14px;padding:2px 5px 2px 5px;color:#B9B9B9;font-size:14px;overflow:hidden}form.djacket-form input[type="text"],form.djacket-form input[type="email"],form.djacket-form input[type="date"],form.djacket-form input[type="password"],form.djacket-form input[type="file"],form.djacket-form select{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;width:100%;display:block;outline:none;border:none;height:25px;line-height:25px;font-size:16px;padding:0}form.djacket-form input[type="file"]{margin:4px 0}form.djacket-form li>span{background:#000112;display:block;padding:3px;margin:0 -9px -9px -9px;text-align:center;color:#39DD00;font-size:11px}form.djacket-form input[type="submit"]{border:none;padding:10px 20px 10px 20px;color:#000112}form.djacket-form input[type="submit"]:hover{border:none;padding:10px 20px 10px 20px;color:#000112}form.djacket-form input[type="submit"].success{background:#39DD00;border-bottom:3px solid #31aa08}form.djacket-form input[type="submit"].success:hover{background:#31aa08;color:#000112}form.djacket-form input[type="submit"].danger{background:#F60F0F;border-bottom:3px solid #990a0a}form.djacket-form input[type="submit"].danger:hover{background:#990a0a;color:#000112}div#user-info-settings,div#user-profile-settings,div#user-area51-settings{display:block;text-align:center;width:840px;margin:0 auto}.tab-links:after{display:block;clear:both;content:''}.tab-links li{margin:0px 5px;margin-left:0px;float:left;list-style:none}.tab-links a{padding:9px 15px;display:inline-block;border-radius:3px 3px 0px 0px;background:#171814;font-size:16px;font-weight:600;color:#fff;transition:all linear 0.15s}.tab-links a:hover{background:#28C13C;text-decoration:none}li.active a,li.active a:hover{background:#272822;color:#35FF50}.tab-content{padding:15px;border-radius:3px;box-shadow:-1px 1px 1px rgba(0,0,0,0.15);background:#272822}.tab{display:none}.tab.active{display:block}section#user-deposit{padding-top:12px}section#user-deposit ul{margin-top:32px}section#user-deposit ul li.deposit-item{width:99%;padding:12px;margin:12px auto;box-sizing:border-box;box-shadow:0 0 2px #ccc}section#user-deposit ul li.deposit-item div.repo-info a.repo-link{float:left}section#user-deposit ul li.deposit-item div.repo-info div.repo-last-update{float:right}section#user-deposit ul li.deposit-item div.repo-description{clear:both;padding-top:6px}section#user-deposit div.empty-deposit{width:50%;margin:96px auto}section#user-deposit div.empty-deposit h2,section#user-deposit div.empty-deposit h4{text-align:center}section#user-deposit div.empty-deposit h2{font-weight:bold;font-size:18px}section#user-deposit div.empty-deposit h4{color:#ccc;margin-top:8px;font-size:12px}section#user-deposit div.empty-deposit h4 a{color:#ccc}div.empty-repo{width:100%}div.empty-repo h2#empty-repo-message{border-bottom:2px solid #fff;padding-bottom:8px}div.empty-repo h2,div.empty-repo h4{font-weight:bold}div.empty-repo h2{display:block;font-size:1.5em}div.empty-repo div#empty-and-no-repo{margin-top:36px}div.empty-repo div#empty-but-has-repo{margin-top:16px}body{color:white}body a{text-decoration:none;color:white}div#pjax-container .code-view pre{background:#272822}div#pjax-container p.repo-message{border-bottom:2px solid #fff;margin-top:4px;padding-bottom:6px;margin-bottom:6px}div#pjax-container table#repo-content{width:100%;border-spacing:4px;border-collapse:separate}div#pjax-container table#repo-content tbody tr td.repo-obj-name{width:33%;text-align:left}div#pjax-container table#repo-content tbody tr td.repo-obj-name i{width:13px;height:13px}div#pjax-container table#repo-content tbody tr td.repo-obj-subject{width:33%;text-align:center}div#pjax-container table#repo-content tbody tr td.repo-obj-commitdate{width:33%;text-align:right}div#pjax-container article.markdown-preview{width:100%;margin:0 auto;margin-top:128px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box}div#pjax-container article.markdown-preview .markdown-title{display:none;width:75%;text-align:center;font-size:1.5em;font-weight:bold;padding-bottom:8px;margin:0 auto;color:#fff;border-bottom:1px solid #41443c}div#pjax-container article.markdown-preview .arrow-down{display:none}div#pjax-container article.markdown-preview .markdown-body{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;min-width:25%;max-width:75%;margin:0 auto;padding:45px;padding-top:8px}ul#branches-list{margin-top:16px}ul.branch-commits{margin-top:16px;margin-left:16px;margin-bottom:32px}div#repo-graphs{width:100%}div#repo-graphs div#graphs-loader{position:absolute;width:40px;height:40px;top:50%;left:55%;-webkit-transform:translate(-50%, -50%);-ms-transform:translate(-50%, -50%);transform:translate(-50%, -50%)}div#repo-graphs canvas{display:block;margin:64px auto;width:840px;height:160px}div#repo-info-settings,div#repo-area51-settings{display:block;text-align:center;width:840px;margin:0 auto}.markdown-body{-webkit-text-size-adjust:100%;text-size-adjust:100%;color:#fff;font-size:16px;line-height:1.6;word-wrap:break-word}.markdown-body a{background-color:transparent}.markdown-body a:active,.markdown-body a:hover{outline:0}.markdown-body strong{font-weight:bold}.markdown-body h1{font-size:2em;margin:0.67em 0}.markdown-body img{border:0}.markdown-body hr{box-sizing:content-box;height:0}.markdown-body pre{overflow:auto}.markdown-body code,.markdown-body kbd,.markdown-body pre{font-family:monospace, monospace;font-size:1em}.markdown-body input{color:inherit;font:inherit;margin:0}.markdown-body html input[disabled]{cursor:default}.markdown-body input{line-height:normal}.markdown-body input[type="checkbox"]{box-sizing:border-box;padding:0}.markdown-body table{border-collapse:collapse;border-spacing:0}.markdown-body td,.markdown-body th{padding:0}.markdown-body *{box-sizing:border-box}.markdown-body input{font:13px / 1.4 Helvetica, arial, nimbussansl, liberationsans, freesans, clean, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"}.markdown-body a{color:#4078c0;text-decoration:none}.markdown-body a:hover,.markdown-body a:active{text-decoration:underline}.markdown-body hr{height:0;margin:15px 0;overflow:hidden;background:transparent;border:0;border-bottom:1px solid #ddd}.markdown-body hr:before{display:table;content:""}.markdown-body hr:after{display:table;clear:both;content:""}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{margin-top:15px;margin-bottom:15px;line-height:1.1}.markdown-body h1{font-size:30px}.markdown-body h2{font-size:21px}.markdown-body h3{font-size:16px}.markdown-body h4{font-size:14px}.markdown-body h5{font-size:12px}.markdown-body h6{font-size:11px}.markdown-body blockquote{margin:0}.markdown-body ul,.markdown-body ol{padding:0;margin-top:0;margin-bottom:0}.markdown-body ol ol,.markdown-body ul ol{list-style-type:lower-roman}.markdown-body ul ul ol,.markdown-body ul ol ol,.markdown-body ol ul ol,.markdown-body ol ol ol{list-style-type:lower-alpha}.markdown-body dd{margin-left:0}.markdown-body code{font-family:Consolas, "Liberation Mono", Menlo, Courier, monospace;font-size:12px}.markdown-body pre{margin-top:0;margin-bottom:0;font:12px Consolas, "Liberation Mono", Menlo, Courier, monospace}.markdown-body .select::-ms-expand{opacity:0}.markdown-body .octicon{font:normal normal normal 16px/1 octicons-link;display:inline-block;text-decoration:none;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.markdown-body .octicon-link:before{content:'\f05c'}.markdown-body:before{display:table;content:""}.markdown-body:after{display:table;clear:both;content:""}.markdown-body>*:first-child{margin-top:0 !important}.markdown-body>*:last-child{margin-bottom:0 !important}.markdown-body a:not([href]){color:inherit;text-decoration:none}.markdown-body .anchor{display:inline-block;padding-right:2px;margin-left:-18px}.markdown-body .anchor:focus{outline:none}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{margin-top:1em;margin-bottom:16px;font-weight:bold;line-height:1.4}.markdown-body h1 .octicon-link,.markdown-body h2 .octicon-link,.markdown-body h3 .octicon-link,.markdown-body h4 .octicon-link,.markdown-body h5 .octicon-link,.markdown-body h6 .octicon-link{color:#ffffff;vertical-align:middle;visibility:hidden}.markdown-body h1:hover .anchor,.markdown-body h2:hover .anchor,.markdown-body h3:hover .anchor,.markdown-body h4:hover .anchor,.markdown-body h5:hover .anchor,.markdown-body h6:hover .anchor{text-decoration:none}.markdown-body h1:hover .anchor .octicon-link,.markdown-body h2:hover .anchor .octicon-link,.markdown-body h3:hover .anchor .octicon-link,.markdown-body h4:hover .anchor .octicon-link,.markdown-body h5:hover .anchor .octicon-link,.markdown-body h6:hover .anchor .octicon-link{visibility:visible}.markdown-body h1{padding-bottom:0.3em;font-size:2.25em;line-height:1.2;border-bottom:1px solid #eee}.markdown-body h1 .anchor{line-height:1}.markdown-body h2{padding-bottom:0.3em;font-size:1.75em;line-height:1.225;border-bottom:1px solid #eee}.markdown-body h2 .anchor{line-height:1}.markdown-body h3{font-size:1.5em;line-height:1.43}.markdown-body h3 .anchor{line-height:1.2}.markdown-body h4{font-size:1.25em}.markdown-body h4 .anchor{line-height:1.2}.markdown-body h5{font-size:1em}.markdown-body h5 .anchor{line-height:1.1}.markdown-body h6{font-size:1em;color:#d6caca}.markdown-body h6 .anchor{line-height:1.1}.markdown-body p,.markdown-body blockquote,.markdown-body ul,.markdown-body ol,.markdown-body dl,.markdown-body table,.markdown-body pre{margin-top:0;margin-bottom:16px}.markdown-body hr{height:4px;padding:0;margin:16px 0;background-color:#e7e7e7;border:0 none}.markdown-body ul,.markdown-body ol{padding-left:2em}.markdown-body ul ul,.markdown-body ul ol,.markdown-body ol ol,.markdown-body ol ul{margin-top:0;margin-bottom:0}.markdown-body li>p{margin-top:16px}.markdown-body dl{padding:0}.markdown-body dl dt{padding:0;margin-top:16px;font-size:1em;font-style:italic;font-weight:bold}.markdown-body dl dd{padding:0 16px;margin-bottom:16px}.markdown-body blockquote{padding:0 15px;color:#bebbbb;border-left:4px solid #ddd}.markdown-body blockquote>:first-child{margin-top:0}.markdown-body blockquote>:last-child{margin-bottom:0}.markdown-body table{display:block;width:100%;overflow:auto;word-break:normal;word-break:keep-all}.markdown-body table th{font-weight:bold}.markdown-body table th,.markdown-body table td{padding:6px 13px;border:1px solid #ddd}.markdown-body table tr{background-color:#fff;border-top:1px solid #ccc}.markdown-body table tr:nth-child(2n){background-color:#f8f8f8}.markdown-body img{max-width:100%;box-sizing:content-box;background-color:#040404}.markdown-body code{padding:0;padding-top:0.2em;padding-bottom:0.2em;margin:0;font-size:85%;background-color:rgba(230,230,230,0.04);border-radius:3px}.markdown-body code:before,.markdown-body code:after{letter-spacing:-0.2em;content:"\00a0"}.markdown-body pre>code{padding:0;margin:0;font-size:100%;word-break:normal;white-space:pre;background:transparent;border:0}.markdown-body .highlight{margin-bottom:16px}.markdown-body .highlight pre,.markdown-body pre{padding:16px;overflow:auto;font-size:85%;line-height:1.45;background-color:#323232;border-radius:3px}.markdown-body .highlight pre{margin-bottom:0;word-break:normal}.markdown-body pre{word-wrap:normal}.markdown-body pre code{display:inline;max-width:initial;padding:0;margin:0;overflow:initial;line-height:inherit;word-wrap:normal;background-color:transparent;border:0}.markdown-body pre code:before,.markdown-body pre code:after{content:normal}.markdown-body kbd{display:inline-block;padding:3px 5px;font-size:11px;line-height:10px;color:#ebebeb;vertical-align:middle;background-color:#fcfcfc;border:solid 1px #ccc;border-bottom-color:#bbb;border-radius:3px;box-shadow:inset 0 -1px 0 #bbb}.markdown-body .pl-c{color:#969896}.markdown-body .pl-c1,.markdown-body .pl-s .pl-v{color:#0086b3}.markdown-body .pl-e,.markdown-body .pl-en{color:#795da3}.markdown-body .pl-s .pl-s1,.markdown-body .pl-smi{color:#b4b4b4}.markdown-body .pl-ent{color:#63a35c}.markdown-body .pl-k{color:#a71d5d}.markdown-body .pl-pds,.markdown-body .pl-s,.markdown-body .pl-s .pl-pse .pl-s1,.markdown-body .pl-sr,.markdown-body .pl-sr .pl-cce,.markdown-body .pl-sr .pl-sra,.markdown-body .pl-sr .pl-sre{color:#183691}.markdown-body .pl-v{color:#ed6a43}.markdown-body .pl-id{color:#b52a1d}.markdown-body .pl-ii{background-color:#b52a1d;color:#f8f8f8}.markdown-body .pl-sr .pl-cce{color:#63a35c;font-weight:bold}.markdown-body .pl-ml{color:#693a17}.markdown-body .pl-mh,.markdown-body .pl-mh .pl-en,.markdown-body .pl-ms{color:#1d3e81;font-weight:bold}.markdown-body .pl-mq{color:#008080}.markdown-body .pl-mi{color:#333;font-style:italic}.markdown-body .pl-mb{color:#333;font-weight:bold}.markdown-body .pl-md{background-color:#ffecec;color:#bd2c00}.markdown-body .pl-mi1{background-color:#eaffea;color:#55a532}.markdown-body .pl-mdr{color:#795da3;font-weight:bold}.markdown-body .pl-mo{color:#1d3e81}.markdown-body kbd{display:inline-block;padding:3px 5px;font:11px Consolas, "Liberation Mono", Menlo, Courier, monospace;line-height:10px;color:#555;vertical-align:middle;background-color:#fcfcfc;border:solid 1px #ccc;border-bottom-color:#bbb;border-radius:3px;box-shadow:inset 0 -1px 0 #bbb}.markdown-body .task-list-item{list-style-type:none}.markdown-body .task-list-item+.task-list-item{margin-top:3px}.markdown-body .task-list-item input{margin:0 0.35em 0.25em -1.6em;vertical-align:middle}.markdown-body :checked+.radio-label{z-index:1;position:relative;border-color:#4078c0} diff --git a/core/frontend/public/build/static/styles/djacket.css b/core/frontend/public/build/static/styles/djacket.css new file mode 100644 index 0000000..a2e1150 --- /dev/null +++ b/core/frontend/public/build/static/styles/djacket.css @@ -0,0 +1 @@ +@import url(https://fonts.googleapis.com/css?family=Oswald);@import url(https://fonts.googleapis.com/css?family=Droid+Sans+Mono);html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none;list-style-type:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:'';content:none}table{border-collapse:collapse;border-spacing:0}@-webkit-keyframes fadeIn{from{opacity:0}to{opacity:1}}@-moz-keyframes fadeIn{from{opacity:0}to{opacity:1}}@keyframes fadeIn{from{opacity:0}to{opacity:1}}.fade-in{opacity:0;-webkit-animation:fadeIn ease-in 1;-moz-animation:fadeIn ease-in 1;-ms-animation:fadeIn ease-in 1;animation:fadeIn ease-in 1;-webkit-animation-fill-mode:forwards;-moz-animation-fill-mode:forwards;-ms-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-duration:0.4s;-moz-animation-duration:0.4s;-ms-animation-duration:0.4s;animation-duration:0.4s;-webkit-animation-delay:0.1s;-moz-animation-delay:0.1s;-ms-animation-delay:0.1s;animation-delay:0.1s}input[type="hidden"]{display:none !important}.left{float:left}.right{float:right}.text-center{text-align:center}.clearfix{clear:both}.red-color{color:#F60F0F !important}.red-back{background:#F60F0F}.yellow-color{color:#fa0}.yellow-back{background:#fa0}.green-color{color:#39DD00}.green-back{background:#39DD00}.grey-color{color:#ccc}.grey-back{background:#ccc}#nprogress .bar{background:#39DD00;height:3px;box-shadow:0 0 3px #31aa08,0 0 2px #31aa08}.form-error{text-align:center}.border-top{border-top:2px solid #fff}.border-bottom{border-bottom:2px solid #fff}.seperator{border-bottom:2px solid #fff;width:146px;text-align:center;margin:0 auto}.seperator-left{border-bottom:2px solid #fff;width:146px;text-align:center}.arrow-down{width:0;height:0;margin:0 auto;text-align:center;border-left:32px solid transparent;border-right:32px solid transparent;border-top:8px solid #41443c;margin-bottom:24px}input:-webkit-autofill{-webkit-box-shadow:0 0 0px 1000px #272822 inset;-webkit-background:#272822;-webkit-text-fill-color:#fff !important}form.djacket-form{width:100%;margin:32px auto;background:#272822;border-radius:2px;padding:20px}form.djacket-form span.last-span{margin-top:6px}form.djacket-form input{background:#272822;color:#fff}form.djacket-form h1{display:block;text-align:center;padding:0;margin:0px 0px 20px 0px;color:#5C5C5C;font-size:x-large}form.djacket-form ul{list-style:none;padding:0;margin:0}form.djacket-form li{display:block;padding:9px;border:1px solid #DDDDDD;margin-bottom:30px;border-radius:3px}form.djacket-form li:last-child{border:none;margin-bottom:0px;text-align:center}form.djacket-form li>label{display:block;float:left;margin-top:-19px;background:#272822;height:14px;padding:2px 5px 2px 5px;color:#B9B9B9;font-size:14px;overflow:hidden}form.djacket-form input[type="text"],form.djacket-form input[type="email"],form.djacket-form input[type="date"],form.djacket-form input[type="password"],form.djacket-form input[type="file"],form.djacket-form select{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;width:100%;display:block;outline:none;border:none;height:25px;line-height:25px;font-size:16px;padding:0}form.djacket-form input[type="file"]{margin:4px 0}form.djacket-form li>span{background:#000112;display:block;padding:3px;margin:0 -9px -9px -9px;text-align:center;color:#39DD00;font-size:11px}form.djacket-form input[type="submit"]{border:none;padding:10px 20px 10px 20px;color:#000112}form.djacket-form input[type="submit"]:hover{border:none;padding:10px 20px 10px 20px;color:#000112}form.djacket-form input[type="submit"].success{background:#39DD00;border-bottom:3px solid #31aa08}form.djacket-form input[type="submit"].success:hover{background:#31aa08;color:#000112}form.djacket-form input[type="submit"].danger{background:#F60F0F;border-bottom:3px solid #990a0a}form.djacket-form input[type="submit"].danger:hover{background:#990a0a;color:#000112}.tab-links:after{display:block;clear:both;content:''}.tab-links li{margin:0px 5px;margin-left:0px;float:left;list-style:none}.tab-links a{padding:9px 15px;display:inline-block;border-radius:3px 3px 0px 0px;background:#171814;font-size:16px;font-weight:600;color:#fff;transition:all linear 0.15s}.tab-links a:hover{background:#28C13C;text-decoration:none}li.active a,li.active a:hover{background:#272822;color:#35FF50}.tab-content{padding:15px;border-radius:3px;box-shadow:-1px 1px 1px rgba(0,0,0,0.15);background:#272822}.tab{display:none}.tab.active{display:block}body{width:100%;height:100%;background:#272822;background-size:100% 100%;color:#fff;font-family:'Oswald', sans-serif;overflow:hidden}body a{text-decoration:none;color:#fff}body header#title-bar{position:relative;width:50%;text-align:center;top:40%;left:50%;-webkit-transform:translate(-50%, -50%);-ms-transform:translate(-50%, -50%);transform:translate(-50%, -50%)}body header#title-bar h1,body header#title-bar h2{text-align:center}body header#title-bar h1{font-weight:bold;font-style:italic;font-size:96px;color:#39DD00}body header#title-bar h2{margin-top:12px;font-size:32px}body header#title-bar ul{position:absolute;top:140%;left:50%;-webkit-transform:translate(-50%, -50%);-ms-transform:translate(-50%, -50%);transform:translate(-50%, -50%);margin-top:16px}body header#title-bar ul li{font-size:16px;float:left;margin-left:12px}body header#title-bar ul li i{width:13px;margin-left:6px;margin-right:6px}body header#title-bar p{font-size:14px;position:absolute;top:160%;left:50%;-webkit-transform:translate(-50%, -50%);-ms-transform:translate(-50%, -50%);transform:translate(-50%, -50%);color:#ccc;margin-top:16px}body header#title-bar p a{color:#ccc}body section#container{position:relative;width:75%;text-align:center;top:50%;left:50%;-webkit-transform:translate(-50%, -50%);-ms-transform:translate(-50%, -50%);transform:translate(-50%, -50%)}body section#container h1{text-align:center;font-weight:bold;font-style:italic;font-size:48px;color:#39DD00;margin-bottom:8px}body section#container .arrow-down{margin-bottom:12px}body section#container div#user-auth{font-family:'Droid Sans Mono', sans-serif;width:40%;margin:32px auto}body section#container div#user-auth .form-error{font-size:0.75em} diff --git a/core/frontend/public/build/views/404.html b/core/frontend/public/build/views/404.html new file mode 100644 index 0000000..063a99a --- /dev/null +++ b/core/frontend/public/build/views/404.html @@ -0,0 +1,37 @@ + + + + + + + Djacket / 404 + + + + + + + + + + + +
+

Djacket

+

You failed with ERR_CODE : 404

+
+ + + + diff --git a/core/frontend/public/build/views/500.html b/core/frontend/public/build/views/500.html new file mode 100644 index 0000000..288814e --- /dev/null +++ b/core/frontend/public/build/views/500.html @@ -0,0 +1,37 @@ + + + + + + + Djacket / 500 + + + + + + + + + + + +
+

Djacket

+

We failed with ERR_CODE : 500

+
+ + + + diff --git a/core/frontend/public/build/views/base/deposit-item.html b/core/frontend/public/build/views/base/deposit-item.html new file mode 100644 index 0000000..05d0464 --- /dev/null +++ b/core/frontend/public/build/views/base/deposit-item.html @@ -0,0 +1 @@ +{% if repos %}{% else %}

We found nothing here but here's a map

{% if request.user.username == target_user.username %}

> You can create repositories by clicking 'New Repo' <

{% endif %}
{% endif %} \ No newline at end of file diff --git a/core/frontend/public/build/views/base/empty-repo.html b/core/frontend/public/build/views/base/empty-repo.html new file mode 100644 index 0000000..cd909a8 --- /dev/null +++ b/core/frontend/public/build/views/base/empty-repo.html @@ -0,0 +1,15 @@ +
{% if request.user.username == repo_owner %}

Looks kinda lonesome in here!

You could create a local repository and then ->

+                
+                    # create some files and folders.
+                    git init
+                    git add .
+                    git commit -m "initial commit"
+                    git remote add origin http://{{request.get_host}}/{{repo_owner}}/{{repo_name}}.git
+                    git push -u origin master
+                
+            

or upload your currently working repository. ->

+                
+                    git remote add origin http://{{request.get_host}}/{{repo_owner}}/{{repo_name}}.git
+                    git push -u origin master
+                
+            
{% else %}

This repository is empty.

{% endif %}
\ No newline at end of file diff --git a/core/frontend/public/build/views/base/loading.html b/core/frontend/public/build/views/base/loading.html new file mode 100644 index 0000000..6c829f9 --- /dev/null +++ b/core/frontend/public/build/views/base/loading.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/core/frontend/public/build/views/base/repo-area51-form.html b/core/frontend/public/build/views/base/repo-area51-form.html new file mode 100644 index 0000000..c7d0fb7 --- /dev/null +++ b/core/frontend/public/build/views/base/repo-area51-form.html @@ -0,0 +1 @@ +{% if form.errors %}
{% for field in form %} {% for error in field.errors %}

*{{ error|escape }}

{% endfor %} {% endfor %} {% for error in form.non_field_errors %}

*{{ error|escape }}

{% endfor %}
{% endif %} {# Form design is borrowed from "http://www.sanwebe.com/2014/08/css-html-forms-designs" #}
{% csrf_token %}
  • Yes No You are stepping into Area 51, there is no going back.
{# Form design is borrowed from "http://www.sanwebe.com/2014/08/css-html-forms-designs" #} \ No newline at end of file diff --git a/core/frontend/public/build/views/base/repo-branches.html b/core/frontend/public/build/views/base/repo-branches.html new file mode 100644 index 0000000..a00311f --- /dev/null +++ b/core/frontend/public/build/views/base/repo-branches.html @@ -0,0 +1 @@ +{% if num_branches > 0 %}

{% if num_branches == 1 %} This repository has 1 branch {% else %} This repository has {{num_branches}} branches {% endif %}

{% else %} {% include 'base/repo-no-commits.html' %} {% endif %} \ No newline at end of file diff --git a/core/frontend/public/build/views/base/repo-browse.html b/core/frontend/public/build/views/base/repo-browse.html new file mode 100644 index 0000000..a33d447 --- /dev/null +++ b/core/frontend/public/build/views/base/repo-browse.html @@ -0,0 +1,3 @@ +{# This template is for viewing objects inside a repository. #} {% load djacket_filters %} {% if objects %}

{{ repo_lsmsg }}

{% if objects.is_blob %}{% else %} {% for obj in objects %}{% endfor %} {% endif %}
+                            
{{ objects.show }}
+
{% if obj.is_tree %} {% elif obj.is_blob %} {% else %} {% endif %} {{obj.get_path|obj_name:'/'}}{{obj.get_subject|ellipsize}}{{obj.get_committer_date}}
{% if objects.is_blob %} {% else %}

{% endif %}
{% endif %} \ No newline at end of file diff --git a/core/frontend/public/build/views/base/repo-commits.html b/core/frontend/public/build/views/base/repo-commits.html new file mode 100644 index 0000000..860dd2b --- /dev/null +++ b/core/frontend/public/build/views/base/repo-commits.html @@ -0,0 +1 @@ +{% if commits %} {% for branch, branch_commits in commits.items %}

{% if branch_commits|length == 0 %} This repository has no commits yet. {% elif branch_commits|length == 1 %} Branch "{{ branch }}", has 1 commit {% else %} Branch "{{ branch }}", has {{ branch_commits|length }} commits {% endif %}

    {% for commit in branch_commits %}
  • {% if commit.get_committer_name %} > {{ commit.get_committer_name }} committed {{ commit.get_sha1_hash }}, {{ commit.get_committer_date }} {% else %} > Someone committed {{ commit.get_sha1_hash }}, {{ commit.get_committer_date }} {% endif %}
  • {% endfor %}
{% endfor %} {% else %} {% include 'base/repo-no-commits.html' %} {% endif %} \ No newline at end of file diff --git a/core/frontend/public/build/views/base/repo-creation-form.html b/core/frontend/public/build/views/base/repo-creation-form.html new file mode 100644 index 0000000..51a8cdc --- /dev/null +++ b/core/frontend/public/build/views/base/repo-creation-form.html @@ -0,0 +1 @@ +{% if form.errors %}
{% for field in form %} {% for error in field.errors %}

*{{field.label}}: {{ error|escape }}

{% endfor %} {% endfor %} {% for error in form.non_field_errors %}

*{{ error|escape }}

{% endfor %}
{% endif %} {# Form design is borrowed from "http://www.sanwebe.com/2014/08/css-html-forms-designs" #} {% if form.instance %}
{% else %}{% endif %} {% csrf_token %}
  • Enter a name for repository.
  • Enter a description for repository.
  • {% if form.instance %} {% if form.instance.private %} Yes No {% else %} Yes No {% endif %} {% else %} Yes No {% endif %} Specify if repository should be private.
{# Form design is borrowed from "http://www.sanwebe.com/2014/08/css-html-forms-designs" #} \ No newline at end of file diff --git a/core/frontend/public/build/views/base/repo-graphs.html b/core/frontend/public/build/views/base/repo-graphs.html new file mode 100644 index 0000000..412526e --- /dev/null +++ b/core/frontend/public/build/views/base/repo-graphs.html @@ -0,0 +1 @@ +{% if num_branches > 0 %}

Graphs present number of commits in this week and year.

{% include 'base/loading.html' %}
{% else %} {% include 'base/repo-no-commits.html' %} {% endif %} \ No newline at end of file diff --git a/core/frontend/public/build/views/base/repo-no-commits.html b/core/frontend/public/build/views/base/repo-no-commits.html new file mode 100644 index 0000000..0f56833 --- /dev/null +++ b/core/frontend/public/build/views/base/repo-no-commits.html @@ -0,0 +1 @@ +

This repository has no commits yet.

\ No newline at end of file diff --git a/core/frontend/public/build/views/base/session-base.html b/core/frontend/public/build/views/base/session-base.html new file mode 100644 index 0000000..7c541e6 --- /dev/null +++ b/core/frontend/public/build/views/base/session-base.html @@ -0,0 +1 @@ +{# This template will be the parent for all views in a working session such as browsing, changing settings, etc. #} {% load staticfiles %}{% block page_title %}{% endblock %}
{% block view_title %}{% endblock %}
{% block content %}{% endblock %}
\ No newline at end of file diff --git a/core/frontend/public/build/views/base/sidebar.html b/core/frontend/public/build/views/base/sidebar.html new file mode 100644 index 0000000..9bb5927 --- /dev/null +++ b/core/frontend/public/build/views/base/sidebar.html @@ -0,0 +1 @@ +{% load djacket_filters %}

Djacket

{% if request.user.is_authenticated %}

{{user.profile.name|ellipsize:19}}

{% else %}

Anonymous

{% endif %}
{% if request.user.is_authenticated %} {{user.name}} {% else %} Anonymous {% endif %}

Copyright © 2016

\ No newline at end of file diff --git a/core/frontend/public/build/views/base/user-area51-form.html b/core/frontend/public/build/views/base/user-area51-form.html new file mode 100644 index 0000000..0046b78 --- /dev/null +++ b/core/frontend/public/build/views/base/user-area51-form.html @@ -0,0 +1 @@ +{% if form.errors %}
{% for field in form %} {% for error in field.errors %}

*{{ error|escape }}

{% endfor %} {% endfor %} {% for error in form.non_field_errors %}

*{{ error|escape }}

{% endfor %}
{% endif %} {# Form design is borrowed from "http://www.sanwebe.com/2014/08/css-html-forms-designs" #}
{% csrf_token %}
  • Yes No You are stepping into Area 51, there is no going back.
{# Form design is borrowed from "http://www.sanwebe.com/2014/08/css-html-forms-designs" #} \ No newline at end of file diff --git a/core/frontend/public/build/views/base/user-info-form.html b/core/frontend/public/build/views/base/user-info-form.html new file mode 100644 index 0000000..91d1f33 --- /dev/null +++ b/core/frontend/public/build/views/base/user-info-form.html @@ -0,0 +1 @@ +{% if info_form.errors %}
{% for field in info_form %} {% for error in field.errors %}

*{{field.label}}: {{ error|escape }}

{% endfor %} {% endfor %} {% for error in info_form.non_field_errors %}

*{{ error|escape }}

{% endfor %}
{% endif %} {# Form design is borrowed from "http://www.sanwebe.com/2014/08/css-html-forms-designs" #}
{% csrf_token %}
  • Enter your first name.
  • Enter your last name.
  • Enter your email address.
  • Enter your current password.
  • Enter your new password.
{# Form design is borrowed from "http://www.sanwebe.com/2014/08/css-html-forms-designs" #} \ No newline at end of file diff --git a/core/frontend/public/build/views/base/user-login-form.html b/core/frontend/public/build/views/base/user-login-form.html new file mode 100644 index 0000000..bb34b25 --- /dev/null +++ b/core/frontend/public/build/views/base/user-login-form.html @@ -0,0 +1 @@ +{% if login_form.errors %}
{% for field in login_form %} {% for error in field.errors %}

*{{field.label}}: {{ error|escape }}

{% endfor %} {% endfor %} {% for error in login_form.non_field_errors %}

*{{ error|escape }}

{% endfor %}
{% endif %} {# Form design is borrowed from "http://www.sanwebe.com/2014/08/css-html-forms-designs" #}
{% csrf_token %}
  • Enter your username.
  • Enter your password.
\ No newline at end of file diff --git a/core/frontend/public/build/views/base/user-profile-form.html b/core/frontend/public/build/views/base/user-profile-form.html new file mode 100644 index 0000000..a6f9de3 --- /dev/null +++ b/core/frontend/public/build/views/base/user-profile-form.html @@ -0,0 +1 @@ +{% if profile_form.errors %}
{% for field in profile_form %} {% for error in field.errors %}

*{{field.label}}: {{ error|escape }}

{% endfor %} {% endfor %} {% for error in profile_form.non_field_errors %}

*{{ error|escape }}

{% endfor %}
{% endif %} {# Form design is borrowed from "http://www.sanwebe.com/2014/08/css-html-forms-designs" #}
{% csrf_token %}
  • Choose an image for your avatar.
  • Let us know when this legend was born.
{# Form design is borrowed from "http://www.sanwebe.com/2014/08/css-html-forms-designs" #} \ No newline at end of file diff --git a/core/frontend/public/build/views/base/user-register-form.html b/core/frontend/public/build/views/base/user-register-form.html new file mode 100644 index 0000000..1b36cc0 --- /dev/null +++ b/core/frontend/public/build/views/base/user-register-form.html @@ -0,0 +1 @@ +{% if register_form.errors %}
{% for field in register_form %} {% for error in field.errors %}

*{{field.label}}: {{ error|escape }}

{% endfor %} {% endfor %} {% for error in register_form.non_field_errors %}

*{{ error|escape }}

{% endfor %}
{% endif %} {# Form design is borrowed from "http://www.sanwebe.com/2014/08/css-html-forms-designs" #}
{% csrf_token %}
  • Enter a username.
  • Enter your email.
  • Enter a password.
\ No newline at end of file diff --git a/core/frontend/public/build/views/index.html b/core/frontend/public/build/views/index.html new file mode 100644 index 0000000..fcadeb6 --- /dev/null +++ b/core/frontend/public/build/views/index.html @@ -0,0 +1,69 @@ + + + + + + + Djacket + + + + + + + + + + + +
+

Djacket

+

Wheels Re-invented

+ + +
+
+
+

Djacket

+
+
+
+ +
+
+ {% include 'base/user-login-form.html' %} +
+
+ {% include 'base/user-register-form.html' %} +
+
+
+
+ + + + + diff --git a/core/frontend/public/build/views/repository/new.html b/core/frontend/public/build/views/repository/new.html new file mode 100644 index 0000000..aabb817 --- /dev/null +++ b/core/frontend/public/build/views/repository/new.html @@ -0,0 +1 @@ +{% extends 'base/session-base.html' %} {% block page_title %} New Repository {% endblock %} {% block view_title%}

New Repository

{% endblock %} {% block content %}

Create a new repository for your project.

{% include 'base/repo-creation-form.html' %}
{% endblock %} \ No newline at end of file diff --git a/core/frontend/public/build/views/repository/repo-main.html b/core/frontend/public/build/views/repository/repo-main.html new file mode 100644 index 0000000..4b92fdb --- /dev/null +++ b/core/frontend/public/build/views/repository/repo-main.html @@ -0,0 +1 @@ +{# This template is the parent for viewing contents, settings, charts, commits of a repository. #} {% extends 'base/session-base.html' %} {% load djacket_filters %} {% block page_title %} {{repo_owner}} / {{repo_name}} {% endblock %} {% block view_title %}
{% block title_section %}{% endblock %}
    {% if request.user.is_authenticated and request.user.username == repo_owner %}
  • {% endif %}
{% endblock %} {% block content %}
{% include 'repository/repo-sections.html' %}
{% endblock %} \ No newline at end of file diff --git a/core/frontend/public/build/views/repository/repo-pjax.html b/core/frontend/public/build/views/repository/repo-pjax.html new file mode 100644 index 0000000..7ca9833 --- /dev/null +++ b/core/frontend/public/build/views/repository/repo-pjax.html @@ -0,0 +1 @@ +{# This template shows repository a if a normal request sent. If request is pjax then 'repo-content.html' will be replaced. #} {% extends "repository/repo-main.html, repository/repo-sections.html"|pjax:request %} \ No newline at end of file diff --git a/core/frontend/public/build/views/repository/repo-sections.html b/core/frontend/public/build/views/repository/repo-sections.html new file mode 100644 index 0000000..58c57d4 --- /dev/null +++ b/core/frontend/public/build/views/repository/repo-sections.html @@ -0,0 +1 @@ +{% if template == 'browse' %} {% if objects %} {% include 'base/repo-browse.html' %} {% else %} {% include 'base/empty-repo.html' %} {% endif %} {% elif template == 'branches' %} {% include 'base/repo-branches.html' %} {% elif template == 'commits' %} {% include 'base/repo-commits.html' %} {% elif template == 'graphs' %} {% include 'base/repo-graphs.html' %} {% endif %} \ No newline at end of file diff --git a/core/frontend/public/build/views/repository/repo-settings.html b/core/frontend/public/build/views/repository/repo-settings.html new file mode 100644 index 0000000..9e48d9d --- /dev/null +++ b/core/frontend/public/build/views/repository/repo-settings.html @@ -0,0 +1 @@ +{% extends 'repository/repo-main.html' %} {% block page_title %} {{ repo.name }}'s Settings {% endblock %} {% block title_section %}

{{ repo.name }}'s Settings

{% endblock %} {% block content %}
{% include 'base/repo-creation-form.html' %}
{% include 'base/repo-area51-form.html' %}
{% endblock %} \ No newline at end of file diff --git a/core/frontend/public/build/views/user/deposit.html b/core/frontend/public/build/views/user/deposit.html new file mode 100644 index 0000000..911123f --- /dev/null +++ b/core/frontend/public/build/views/user/deposit.html @@ -0,0 +1 @@ +{% extends 'base/session-base.html' %} {% block page_title %} Djacket / {{ target_user.username }} {% endblock %} {% block view_title %}

{{ target_user.username }}

{{ target_user.first_name }} {{ target_user.last_name }}

joined {{ target_user.date_joined|date:"c" }}
{% endblock %} {% block content %}
{% include 'base/deposit-item.html' %}
{% endblock %} \ No newline at end of file diff --git a/core/frontend/public/build/views/user/user-settings.html b/core/frontend/public/build/views/user/user-settings.html new file mode 100644 index 0000000..eb9aedd --- /dev/null +++ b/core/frontend/public/build/views/user/user-settings.html @@ -0,0 +1 @@ +{% extends 'base/session-base.html' %} {% block page_title %} {{ request.user.username }}'s Settings {% endblock %} {% block view_title %}

{{ request.user.username }}'s Settings

{% endblock %} {% block content %}
{% include 'base/user-info-form.html' %}
{% include 'base/user-profile-form.html' %}
{% include 'base/user-area51-form.html' %}
{% endblock %} \ No newline at end of file diff --git a/core/frontend/public/dev/static/scripts/djacket-session.js b/core/frontend/public/dev/static/scripts/djacket-session.js new file mode 100644 index 0000000..02c797c --- /dev/null +++ b/core/frontend/public/dev/static/scripts/djacket-session.js @@ -0,0 +1,212 @@ +function highlight_codes () { + $('.code-view pre').each(function (i, block) { + hljs.highlightBlock(block); + }); +} + + +function setup_markdown () { + var markdowns = []; + + marked.setOptions({ + renderer: new marked.Renderer(), + gfm: true, + tables: true, + breaks: true, + pedantic: false, + sanitize: true, + smartLists: true, + smartypants: false + }); + + $('table#repo-content tbody tr td.repo-obj-name[type="blob"] a').each(function () { + if ($(this).text().trim() == 'README.md' || $(this).text().trim() == 'README.rst') { + markdowns.push($(this).attr('href')); + } + }); + + if (markdowns.length > 0) { + var file_name = markdowns[0].split('/')[markdowns[0].split('/').length-1]; + $.ajax({ + url: '/api' + markdowns[0].replace('.git/blob/', '.git/readme/'), + type: 'get', + success: function(data) { + $('h2.markdown-title').html(' ' + file_name); + $('.markdown-preview .markdown-title').css('display', 'block'); + $('.markdown-preview .arrow-down').css('display', 'block'); + $('.markdown-preview .markdown-body').html(marked(data)); + }, + failure: function(data) { + console.error('djacket > failed to get README file.'); + } + }); + } +} + + +function process_dates () { + $('.moment-date').each(function () { + if (!$(this).hasClass('moment-dated')) { + $(this).text(moment($(this).text()).fromNow()); + $(this).addClass('moment-dated'); + } + }); +} + + +function setup_pjax () { + $(document).pjax('a[data-pjax]', '#pjax-container'); + + $(document).on('pjax:start', function() { + NProgress.start(); + }); + + $(document).on('pjax:end', function() { + NProgress.done(); + }); +} + + +function setup_view_tabs () { + jQuery('.pjax-tabs .tab-links a').on('click', function (e) { + var currentAttrValue = jQuery(this).attr('href'); + + // Change/remove current tab to active + jQuery(this).parent('li').addClass('active').siblings().removeClass('active'); + }); +} + + +function setup_static_tabs () { + jQuery('.static-tabs .tab-links a').on('click', function(e) { + var currentAttrValue = jQuery(this).attr('href'); + + // Show/Hide Tabs + jQuery('.static-tabs ' + currentAttrValue).show().siblings().hide(); + + // Change/remove current tab to active + jQuery(this).parent('li').addClass('active').siblings().removeClass('active'); + + e.preventDefault(); + }); +} + + +function setup_nprogress() { + NProgress.configure({ showSpinner: false }); +} + + +function setup_file_icons () { + $('i.obj-icon').each(function () { + var obj_extention = $(this).attr('ext'); + $(this).addClass(get_icon(obj_extention)["icon"]); + }); +} + + +function _show_weekly_graphs (dataset) { + var weekly_labels = ['Monday', 'Tuesday', 'Wednsday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; + var weekly_datas = []; + + dataset = JSON.parse(dataset); + for (data in dataset) { + // weekly_labels.push(data); + weekly_datas.push(dataset[data]); + } + + var weekly_data = { + labels: weekly_labels, + datasets: [ + { + label: "Weekly Commits", + fillColor: "rgba(57,221,0,0.2)", + strokeColor: "rgba(49,170,8,1)", + pointColor: "rgba(26, 103, 0, 1)", + pointStrokeColor: "#fff", + pointHighlightFill: "#fff", + pointHighlightStroke: "rgba(20, 75, 1, 1)", + data: weekly_datas + } + ] + }; + + var weekly_ctx = $("#repo-graphs #weekly-commits-graph").get(0).getContext("2d"); + var weekly_commits = new Chart(weekly_ctx).Line(weekly_data); +} + + +function _show_monthly_graphs (dataset) { + var monthly_labels = ['January', 'February', 'March', 'April', 'May', 'June', 'July', + 'August', 'September', 'October', 'November', 'December']; + var monthly_datas = []; + + dataset = JSON.parse(dataset); + for (data in dataset) { + // monthly_labels.push(data); + monthly_datas.push(dataset[data]); + } + + var monthly_data = { + labels: monthly_labels, + datasets: [ + { + label: "Monthly Commits", + fillColor: "rgba(57,221,0,0.2)", + strokeColor: "rgba(49,170,8,1)", + pointColor: "rgba(26, 103, 0, 1)", + pointStrokeColor: "#fff", + pointHighlightFill: "#fff", + pointHighlightStroke: "rgba(20, 75, 1, 1)", + data: monthly_datas + } + ] + }; + + var monthly_ctx = $("#repo-graphs #monthly-commits-graph").get(0).getContext("2d"); + var monthly_commits = new Chart(monthly_ctx).Bar(monthly_data); +} + + +function show_graphs (dataset) { + var weekly_dataset = JSON.parse('' + dataset + '')['weekly']; + var monthly_dataset = JSON.parse('' + dataset + '')['monthly']; + + _show_weekly_graphs(weekly_dataset); + _show_monthly_graphs(monthly_dataset); +} + + +function get_datasets () { + var repo_graphs = $('#repo-graphs'); + if (repo_graphs.length) { + $.ajax({ + url: '/api' + $('#repo-link').attr('href').trim() + '/commits_stats', + type: 'get', + success: function(data) { + $('#graphs-loader').css('display', 'none'); + show_graphs(data); + }, + failure: function(data) { + console.error('djacket > failed to get repository graphs.'); + } + }); + } +} + + +$(document).on('ready', function () { + setup_nprogress(); + setup_view_tabs(); + setup_static_tabs(); + setup_pjax(); +}); + + +$(document).on('ready pjax:end', function () { + highlight_codes(); + process_dates(); + setup_markdown(); + setup_file_icons(); + get_datasets(); +}); diff --git a/core/frontend/public/dev/static/scripts/djacket.js b/core/frontend/public/dev/static/scripts/djacket.js new file mode 100644 index 0000000..c4a06cb --- /dev/null +++ b/core/frontend/public/dev/static/scripts/djacket.js @@ -0,0 +1,36 @@ +function setup_static_tabs () { + jQuery('.static-tabs .tab-links a').on('click', function(e) { + var currentAttrValue = jQuery(this).attr('href'); + + // Show/Hide Tabs + jQuery('.static-tabs ' + currentAttrValue).show().siblings().hide(); + + // Change/remove current tab to active + jQuery(this).parent('li').addClass('active').siblings().removeClass('active'); + + e.preventDefault(); + }); +} + +$(document).on('ready', function () { + setup_static_tabs(); + + $('a[href="#auth"]').on('click', function () { + $('header#title-bar').css('display', 'none'); + $('section#container').css('display', 'block'); + }); + + var url = "" + window.location; + var id = url.substring(url.lastIndexOf('/#') + 2).trim(); + if (id == 'login-tab') { + $('header#title-bar').css('display', 'none'); + $('section#container').css('display', 'block'); + } else if (id == 'register-tab') { + $('header#title-bar').css('display', 'none'); + $('section#container').css('display', 'block'); + $('section#container #login-tab-li').removeClass('active'); + $('section#container #login-tab').removeClass('active'); + $('section#container #register-tab-li').toggleClass('active'); + $('section#container #register-tab').toggleClass('active'); + } +}); diff --git a/core/frontend/public/dev/static/scripts/icon-selection.js b/core/frontend/public/dev/static/scripts/icon-selection.js new file mode 100644 index 0000000..1312a65 --- /dev/null +++ b/core/frontend/public/dev/static/scripts/icon-selection.js @@ -0,0 +1,106 @@ +// icons and color of each file type. +icons = { + "git": { + "color": "", + "icon": "devicons devicons-git" + }, + "js": { + "color": "", + "icon": "devicons devicons-nodejs_small" + }, + "css": { + "color": "", + "icon": "devicons devicons-css3" + }, + "html": { + "color": "", + "icon": "devicons devicons-html5" + }, + "java": { + "color": "", + "icon": "devicons devicons-java", + }, + "rb": { + "color": "", + "icon": "devicons devicons-ruby" + }, + "rails": { + "color": "", + "icon": "devicons devicons-ruby_on_rails" + }, + "py": { + "color": "", + "icon": "devicons devicons-python", + }, + "scala": { + "color": "", + "icon": "devicons devicons-scala" + }, + "md": { + "color": "", + "icon": "devicons devicons-markdown", + }, + "php": { + "color": "", + "icon": "devicons devicons-php" + }, + "mysql": { + "color": "", + "icon": "devicons devicons-mysql" + }, + "coffee": { + "color": "", + "icon": "devicons devicons-coffeescript" + }, + "sass": { + "color": "", + "icon": "devicons devicons-sass" + }, + "scss": { + "color": "", + "icon": "devicons devicons-sass" + }, + "less": { + "color": "", + "icon": "devicons devicons-less" + }, + "vs": { + "color": "", + "icon": "devicons devicons-visualstudio" + }, + "sh": { + "color": "", + "icon": "devicons devicons-terminal" + }, + "ps": { + "color": "", + "icon": "devicons devicons-photoshop" + }, + "vim": { + "color": "", + "icon": "devicons devicons-vim" + } +} + +/* + Returns an icon object for the given file type. + 'icon' object is of a format like this: + icon = { + "color": "somecolor", + "icon": "someicon" + } + */ +function get_icon (file_type) { + // return an icon object. + try { + return { + "color": icons[file_type].color, + "icon": icons[file_type].icon + } + } catch (err) { + return { + "color": "", + "icon": "fa fa-file-text-o" + } + } +} diff --git a/core/frontend/public/dev/static/styles/_base.scss b/core/frontend/public/dev/static/styles/_base.scss new file mode 100644 index 0000000..6c8f8d7 --- /dev/null +++ b/core/frontend/public/dev/static/styles/_base.scss @@ -0,0 +1,121 @@ +@-webkit-keyframes fadeIn { from { opacity:0; } to { opacity:1; } } +@-moz-keyframes fadeIn { from { opacity:0; } to { opacity:1; } } +@keyframes fadeIn { from { opacity:0; } to { opacity:1; } } + +.fade-in { + opacity:0; + -webkit-animation:fadeIn ease-in 1; + -moz-animation:fadeIn ease-in 1; + -ms-animation:fadeIn ease-in 1; + animation:fadeIn ease-in 1; + + -webkit-animation-fill-mode:forwards; + -moz-animation-fill-mode:forwards; + -ms-animation-fill-mode:forwards; + animation-fill-mode:forwards; + + -webkit-animation-duration:0.4s; + -moz-animation-duration:0.4s; + -ms-animation-duration:0.4s; + animation-duration:0.4s; + + + -webkit-animation-delay: 0.1s; + -moz-animation-delay: 0.1s; + -ms-animation-delay: 0.1s; + animation-delay: 0.1s; +} + +input[type="hidden"] { + display: none !important; +} + +.left { + float: left; +} + +.right { + float: right; +} + +.text-center { + text-align: center; +} + +.clearfix { + clear: both; +} + +.red-color { + color: $red !important; +} + +.red-back { + background: $red; +} + +.yellow-color { + color: $yellow; +} + +.yellow-back { + background: $yellow; +} + +.green-color { + color: $green; +} + +.green-back { + background: $green; +} + +.grey-color { + color: $grey; +} + +.grey-back { + background: $grey; +} + +#nprogress .bar { + background: $green; + height: 3px; + box-shadow: 0 0 3px $green-hover, 0 0 2px $green-hover; +} + +.form-error { + text-align: center; +} + +.border-top { + @include border-top(); +} + +.border-bottom { + @include border-bottom(); +} + +.seperator { + @include border-bottom(); + width: 146px; + text-align: center; + margin: 0 auto; +} + +.seperator-left { + @include border-bottom(); + width: 146px; + text-align: center; +} + +.arrow-down { + width: 0; + height: 0; + margin: 0 auto; + text-align: center; + border-left: 32px solid transparent; + border-right: 32px solid transparent; + border-top: 8px solid #41443c; + margin-bottom: 24px; +} diff --git a/core/frontend/public/dev/static/styles/_branches.scss b/core/frontend/public/dev/static/styles/_branches.scss new file mode 100644 index 0000000..446795a --- /dev/null +++ b/core/frontend/public/dev/static/styles/_branches.scss @@ -0,0 +1,3 @@ +ul#branches-list { + margin-top: 16px; +} diff --git a/core/frontend/public/dev/static/styles/_commits.scss b/core/frontend/public/dev/static/styles/_commits.scss new file mode 100644 index 0000000..fc5c9c7 --- /dev/null +++ b/core/frontend/public/dev/static/styles/_commits.scss @@ -0,0 +1,5 @@ +ul.branch-commits { + margin-top: 16px; + margin-left: 16px; + margin-bottom: 32px; +} diff --git a/core/frontend/public/dev/static/styles/_container.scss b/core/frontend/public/dev/static/styles/_container.scss new file mode 100644 index 0000000..cb39abd --- /dev/null +++ b/core/frontend/public/dev/static/styles/_container.scss @@ -0,0 +1,46 @@ +body { + background: $container-background; + background-size: 100% 100%; +} + +section#container { + font-family: 'Droid Sans Mono', sans-serif; + margin-left: $sidebar-width; + min-height: 100%; + height: auto !important; + background: $container-background; + padding: 16px; + @include border-box(); + + header#view-title { + font-family: 'Oswald', sans-serif; + font-size: 32px; + + section#title { + float: left; + + div { + float: left; + @include border-box(); + margin: 0; + padding: 0; + } + } + + section#options { + float: right; + + ul li { + float: right; + margin-right: 12px; + } + } + } + + section#content { + margin-top: 84px; + font-size: 13px; + box-sizing: border-box; + clear: both; + } +} diff --git a/core/frontend/public/dev/static/styles/_deposit.scss b/core/frontend/public/dev/static/styles/_deposit.scss new file mode 100644 index 0000000..40bb21f --- /dev/null +++ b/core/frontend/public/dev/static/styles/_deposit.scss @@ -0,0 +1,55 @@ +section#user-deposit { + padding-top: 12px; + + ul { + margin-top: 32px; + + li.deposit-item { + width: $content-view-width; + padding: 12px; + margin: 12px auto; + box-sizing: border-box; + box-shadow: 0 0 2px #ccc; + + div.repo-info { + + a.repo-link { + float: left; + } + + div.repo-last-update { + float: right; + } + } + + div.repo-description { + clear: both; + padding-top: 6px; + } + } + } + + div.empty-deposit { + width: 50%; + margin: 96px auto; + + h2, h4 { + text-align: center; + } + + h2 { + font-weight: bold; + font-size: 18px; + } + + h4 { + color: $grey; + margin-top: 8px; + font-size: 12px; + + a { + color: $grey; + } + } + } +} diff --git a/core/frontend/public/dev/static/styles/_empty-repo.scss b/core/frontend/public/dev/static/styles/_empty-repo.scss new file mode 100644 index 0000000..c4e429d --- /dev/null +++ b/core/frontend/public/dev/static/styles/_empty-repo.scss @@ -0,0 +1,25 @@ +div.empty-repo { + width: 100%; + + h2#empty-repo-message { + border-bottom: 2px solid #fff; + padding-bottom: 8px; + } + + h2, h4 { + font-weight: bold; + } + + h2 { + display: block; + font-size: 1.5em; + } + + div#empty-and-no-repo { + margin-top: 36px; + } + + div#empty-but-has-repo { + margin-top: 16px; + } +} diff --git a/core/frontend/public/dev/static/styles/_forms.scss b/core/frontend/public/dev/static/styles/_forms.scss new file mode 100644 index 0000000..083199d --- /dev/null +++ b/core/frontend/public/dev/static/styles/_forms.scss @@ -0,0 +1,136 @@ +/* Form design is borrowed from "http://www.sanwebe.com/2014/08/css-html-forms-designs" */ +input:-webkit-autofill { + -webkit-box-shadow: 0 0 0px 1000px $container-background inset; + -webkit-background:$container-background; + -webkit-text-fill-color: #fff !important; +} + +form.djacket-form { + width: 100%; + margin:32px auto; + background:$container-background; + border-radius:2px; + padding:20px; + + span.last-span { + margin-top: 6px; + } +} + +form.djacket-form input { + background:$container-background; + color: #fff; +} + +form.djacket-form h1{ + display: block; + text-align: center; + padding: 0; + margin: 0px 0px 20px 0px; + color: #5C5C5C; + font-size:x-large; +} + +form.djacket-form ul{ + list-style:none; + padding:0; + margin:0; +} + +form.djacket-form li{ + display: block; + padding: 9px; + border:1px solid #DDDDDD; + margin-bottom: 30px; + border-radius: 3px; +} + +form.djacket-form li:last-child{ + border:none; + margin-bottom: 0px; + text-align: center; +} + +form.djacket-form li > label{ + display: block; + float: left; + margin-top: -19px; + background: $container-background; + height: 14px; + padding: 2px 5px 2px 5px; + color: #B9B9B9; + font-size: 14px; + overflow: hidden; +} + +form.djacket-form input[type="text"], + form.djacket-form input[type="email"], + form.djacket-form input[type="date"], + form.djacket-form input[type="password"], + form.djacket-form input[type="file"], + form.djacket-form select { + @include border-box(); + width: 100%; + display: block; + outline: none; + border: none; + height: 25px; + line-height: 25px; + font-size: 16px; + padding: 0; +} + +form.djacket-form input[type="text"]:focus, + form.djacket-form input[type="email"]:focus + form.djacket-form input[type="date"]:focus + form.djacket-form input[type="password"]:focus + form.djacket-form input[type="file"]:focus + form.djacket-form select:focus { +} + +form.djacket-form input[type="file"] { + margin: 4px 0; +} + +form.djacket-form li > span{ + background: $dark-blue; + display: block; + padding: 3px; + margin: 0 -9px -9px -9px; + text-align: center; + color: $green; + font-size: 11px; +} + +form.djacket-form input[type="submit"] { + border: none; + padding: 10px 20px 10px 20px; + color: $dark-blue; +} + +form.djacket-form input[type="submit"]:hover { + border: none; + padding: 10px 20px 10px 20px; + color: $dark-blue; +} + +form.djacket-form input[type="submit"].success { + background: $green; + border-bottom: 3px solid $green-hover; +} + +form.djacket-form input[type="submit"].success:hover { + background: $green-hover; + color: $dark-blue; +} + +form.djacket-form input[type="submit"].danger { + background: $red; + border-bottom: 3px solid $red-hover; +} + +form.djacket-form input[type="submit"].danger:hover { + background: $red-hover; + color: $dark-blue; +} +/* Form design is borrowed from "http://www.sanwebe.com/2014/08/css-html-forms-designs" */ diff --git a/core/frontend/public/dev/static/styles/_graphs.scss b/core/frontend/public/dev/static/styles/_graphs.scss new file mode 100644 index 0000000..19cdd9f --- /dev/null +++ b/core/frontend/public/dev/static/styles/_graphs.scss @@ -0,0 +1,19 @@ +div#repo-graphs { + width: 100%; + + div#graphs-loader { + position: absolute; + width: 40px; + height: 40px; + top: 50%; + left: 55%; + @include translate(-50%, -50%); + } + + canvas { + display: block; + margin: 64px auto; + width: $default-graph-width; + height: $default-graph-height; + } +} diff --git a/core/frontend/public/dev/static/styles/_loading.scss b/core/frontend/public/dev/static/styles/_loading.scss new file mode 100644 index 0000000..f3e8f18 --- /dev/null +++ b/core/frontend/public/dev/static/styles/_loading.scss @@ -0,0 +1,62 @@ +/* Design borrowed from http://tobiasahlin.com/spinkit/ */ +.sk-cube-grid { + width: 40px; + height: 40px; + margin: 100px auto; +} + +.sk-cube-grid .sk-cube { + width: 33%; + height: 33%; + background-color: $green; + float: left; + -webkit-animation: sk-cubeGridScaleDelay 1.3s infinite ease-in-out; + animation: sk-cubeGridScaleDelay 1.3s infinite ease-in-out; +} +.sk-cube-grid .sk-cube1 { + -webkit-animation-delay: 0.2s; + animation-delay: 0.2s; } +.sk-cube-grid .sk-cube2 { + -webkit-animation-delay: 0.3s; + animation-delay: 0.3s; } +.sk-cube-grid .sk-cube3 { + -webkit-animation-delay: 0.4s; + animation-delay: 0.4s; } +.sk-cube-grid .sk-cube4 { + -webkit-animation-delay: 0.1s; + animation-delay: 0.1s; } +.sk-cube-grid .sk-cube5 { + -webkit-animation-delay: 0.2s; + animation-delay: 0.2s; } +.sk-cube-grid .sk-cube6 { + -webkit-animation-delay: 0.3s; + animation-delay: 0.3s; } +.sk-cube-grid .sk-cube7 { + -webkit-animation-delay: 0s; + animation-delay: 0s; } +.sk-cube-grid .sk-cube8 { + -webkit-animation-delay: 0.1s; + animation-delay: 0.1s; } +.sk-cube-grid .sk-cube9 { + -webkit-animation-delay: 0.2s; + animation-delay: 0.2s; } + +@-webkit-keyframes sk-cubeGridScaleDelay { + 0%, 70%, 100% { + -webkit-transform: scale3D(1, 1, 1); + transform: scale3D(1, 1, 1); + } 35% { + -webkit-transform: scale3D(0, 0, 1); + transform: scale3D(0, 0, 1); + } +} + +@keyframes sk-cubeGridScaleDelay { + 0%, 70%, 100% { + -webkit-transform: scale3D(1, 1, 1); + transform: scale3D(1, 1, 1); + } 35% { + -webkit-transform: scale3D(0, 0, 1); + transform: scale3D(0, 0, 1); + } +} diff --git a/core/frontend/public/dev/static/styles/_markdown.scss b/core/frontend/public/dev/static/styles/_markdown.scss new file mode 100644 index 0000000..3125771 --- /dev/null +++ b/core/frontend/public/dev/static/styles/_markdown.scss @@ -0,0 +1,651 @@ +/* style borrowed from https://github.com/sindresorhus/github-markdown-css.git */ +.markdown-body { + -webkit-text-size-adjust: 100%; + text-size-adjust: 100%; + color: #fff; + font-size: 16px; + line-height: 1.6; + word-wrap: break-word; +} + +.markdown-body a { + background-color: transparent; +} + +.markdown-body a:active, +.markdown-body a:hover { + outline: 0; +} + +.markdown-body strong { + font-weight: bold; +} + +.markdown-body h1 { + font-size: 2em; + margin: 0.67em 0; +} + +.markdown-body img { + border: 0; +} + +.markdown-body hr { + box-sizing: content-box; + height: 0; +} + +.markdown-body pre { + overflow: auto; +} + +.markdown-body code, +.markdown-body kbd, +.markdown-body pre { + font-family: monospace, monospace; + font-size: 1em; +} + +.markdown-body input { + color: inherit; + font: inherit; + margin: 0; +} + +.markdown-body html input[disabled] { + cursor: default; +} + +.markdown-body input { + line-height: normal; +} + +.markdown-body input[type="checkbox"] { + box-sizing: border-box; + padding: 0; +} + +.markdown-body table { + border-collapse: collapse; + border-spacing: 0; +} + +.markdown-body td, +.markdown-body th { + padding: 0; +} + +.markdown-body * { + box-sizing: border-box; +} + +.markdown-body input { + font: 13px / 1.4 Helvetica, arial, nimbussansl, liberationsans, freesans, clean, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; +} + +.markdown-body a { + color: #4078c0; + text-decoration: none; +} + +.markdown-body a:hover, +.markdown-body a:active { + text-decoration: underline; +} + +.markdown-body hr { + height: 0; + margin: 15px 0; + overflow: hidden; + background: transparent; + border: 0; + border-bottom: 1px solid #ddd; +} + +.markdown-body hr:before { + display: table; + content: ""; +} + +.markdown-body hr:after { + display: table; + clear: both; + content: ""; +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + margin-top: 15px; + margin-bottom: 15px; + line-height: 1.1; +} + +.markdown-body h1 { + font-size: 30px; +} + +.markdown-body h2 { + font-size: 21px; +} + +.markdown-body h3 { + font-size: 16px; +} + +.markdown-body h4 { + font-size: 14px; +} + +.markdown-body h5 { + font-size: 12px; +} + +.markdown-body h6 { + font-size: 11px; +} + +.markdown-body blockquote { + margin: 0; +} + +.markdown-body ul, +.markdown-body ol { + padding: 0; + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body ol ol, +.markdown-body ul ol { + list-style-type: lower-roman; +} + +.markdown-body ul ul ol, +.markdown-body ul ol ol, +.markdown-body ol ul ol, +.markdown-body ol ol ol { + list-style-type: lower-alpha; +} + +.markdown-body dd { + margin-left: 0; +} + +.markdown-body code { + font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; + font-size: 12px; +} + +.markdown-body pre { + margin-top: 0; + margin-bottom: 0; + font: 12px Consolas, "Liberation Mono", Menlo, Courier, monospace; +} + +.markdown-body .select::-ms-expand { + opacity: 0; +} + +.markdown-body .octicon { + font: normal normal normal 16px/1 octicons-link; + display: inline-block; + text-decoration: none; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.markdown-body .octicon-link:before { + content: '\f05c'; +} + +.markdown-body:before { + display: table; + content: ""; +} + +.markdown-body:after { + display: table; + clear: both; + content: ""; +} + +.markdown-body>*:first-child { + margin-top: 0 !important; +} + +.markdown-body>*:last-child { + margin-bottom: 0 !important; +} + +.markdown-body a:not([href]) { + color: inherit; + text-decoration: none; +} + +.markdown-body .anchor { + display: inline-block; + padding-right: 2px; + margin-left: -18px; +} + +.markdown-body .anchor:focus { + outline: none; +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + margin-top: 1em; + margin-bottom: 16px; + font-weight: bold; + line-height: 1.4; +} + +.markdown-body h1 .octicon-link, +.markdown-body h2 .octicon-link, +.markdown-body h3 .octicon-link, +.markdown-body h4 .octicon-link, +.markdown-body h5 .octicon-link, +.markdown-body h6 .octicon-link { + color: #ffffff; + vertical-align: middle; + visibility: hidden; +} + +.markdown-body h1:hover .anchor, +.markdown-body h2:hover .anchor, +.markdown-body h3:hover .anchor, +.markdown-body h4:hover .anchor, +.markdown-body h5:hover .anchor, +.markdown-body h6:hover .anchor { + text-decoration: none; +} + +.markdown-body h1:hover .anchor .octicon-link, +.markdown-body h2:hover .anchor .octicon-link, +.markdown-body h3:hover .anchor .octicon-link, +.markdown-body h4:hover .anchor .octicon-link, +.markdown-body h5:hover .anchor .octicon-link, +.markdown-body h6:hover .anchor .octicon-link { + visibility: visible; +} + +.markdown-body h1 { + padding-bottom: 0.3em; + font-size: 2.25em; + line-height: 1.2; + border-bottom: 1px solid #eee; +} + +.markdown-body h1 .anchor { + line-height: 1; +} + +.markdown-body h2 { + padding-bottom: 0.3em; + font-size: 1.75em; + line-height: 1.225; + border-bottom: 1px solid #eee; +} + +.markdown-body h2 .anchor { + line-height: 1; +} + +.markdown-body h3 { + font-size: 1.5em; + line-height: 1.43; +} + +.markdown-body h3 .anchor { + line-height: 1.2; +} + +.markdown-body h4 { + font-size: 1.25em; +} + +.markdown-body h4 .anchor { + line-height: 1.2; +} + +.markdown-body h5 { + font-size: 1em; +} + +.markdown-body h5 .anchor { + line-height: 1.1; +} + +.markdown-body h6 { + font-size: 1em; + color: #d6caca; +} + +.markdown-body h6 .anchor { + line-height: 1.1; +} + +.markdown-body p, +.markdown-body blockquote, +.markdown-body ul, +.markdown-body ol, +.markdown-body dl, +.markdown-body table, +.markdown-body pre { + margin-top: 0; + margin-bottom: 16px; +} + +.markdown-body hr { + height: 4px; + padding: 0; + margin: 16px 0; + background-color: #e7e7e7; + border: 0 none; +} + +.markdown-body ul, +.markdown-body ol { + padding-left: 2em; +} + +.markdown-body ul ul, +.markdown-body ul ol, +.markdown-body ol ol, +.markdown-body ol ul { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body li>p { + margin-top: 16px; +} + +.markdown-body dl { + padding: 0; +} + +.markdown-body dl dt { + padding: 0; + margin-top: 16px; + font-size: 1em; + font-style: italic; + font-weight: bold; +} + +.markdown-body dl dd { + padding: 0 16px; + margin-bottom: 16px; +} + +.markdown-body blockquote { + padding: 0 15px; + color: #bebbbb; + border-left: 4px solid #ddd; +} + +.markdown-body blockquote>:first-child { + margin-top: 0; +} + +.markdown-body blockquote>:last-child { + margin-bottom: 0; +} + +.markdown-body table { + display: block; + width: 100%; + overflow: auto; + word-break: normal; + word-break: keep-all; +} + +.markdown-body table th { + font-weight: bold; +} + +.markdown-body table th, +.markdown-body table td { + padding: 6px 13px; + border: 1px solid #ddd; +} + +.markdown-body table tr { + background-color: #fff; + border-top: 1px solid #ccc; +} + +.markdown-body table tr:nth-child(2n) { + background-color: #f8f8f8; +} + +.markdown-body img { + max-width: 100%; + box-sizing: content-box; + background-color: #040404; +} + +.markdown-body code { + padding: 0; + padding-top: 0.2em; + padding-bottom: 0.2em; + margin: 0; + font-size: 85%; + background-color: rgba(230, 230, 230, 0.04); + border-radius: 3px; +} + +.markdown-body code:before, +.markdown-body code:after { + letter-spacing: -0.2em; + content: "\00a0"; +} + +.markdown-body pre>code { + padding: 0; + margin: 0; + font-size: 100%; + word-break: normal; + white-space: pre; + background: transparent; + border: 0; +} + +.markdown-body .highlight { + margin-bottom: 16px; +} + +.markdown-body .highlight pre, +.markdown-body pre { + padding: 16px; + overflow: auto; + font-size: 85%; + line-height: 1.45; + background-color: #323232; + border-radius: 3px; +} + +.markdown-body .highlight pre { + margin-bottom: 0; + word-break: normal; +} + +.markdown-body pre { + word-wrap: normal; +} + +.markdown-body pre code { + display: inline; + max-width: initial; + padding: 0; + margin: 0; + overflow: initial; + line-height: inherit; + word-wrap: normal; + background-color: transparent; + border: 0; +} + +.markdown-body pre code:before, +.markdown-body pre code:after { + content: normal; +} + +.markdown-body kbd { + display: inline-block; + padding: 3px 5px; + font-size: 11px; + line-height: 10px; + color: #ebebeb; + vertical-align: middle; + background-color: #fcfcfc; + border: solid 1px #ccc; + border-bottom-color: #bbb; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #bbb; +} + +.markdown-body .pl-c { + color: #969896; +} + +.markdown-body .pl-c1, +.markdown-body .pl-s .pl-v { + color: #0086b3; +} + +.markdown-body .pl-e, +.markdown-body .pl-en { + color: #795da3; +} + +.markdown-body .pl-s .pl-s1, +.markdown-body .pl-smi { + color: #b4b4b4; +} + +.markdown-body .pl-ent { + color: #63a35c; +} + +.markdown-body .pl-k { + color: #a71d5d; +} + +.markdown-body .pl-pds, +.markdown-body .pl-s, +.markdown-body .pl-s .pl-pse .pl-s1, +.markdown-body .pl-sr, +.markdown-body .pl-sr .pl-cce, +.markdown-body .pl-sr .pl-sra, +.markdown-body .pl-sr .pl-sre { + color: #183691; +} + +.markdown-body .pl-v { + color: #ed6a43; +} + +.markdown-body .pl-id { + color: #b52a1d; +} + +.markdown-body .pl-ii { + background-color: #b52a1d; + color: #f8f8f8; +} + +.markdown-body .pl-sr .pl-cce { + color: #63a35c; + font-weight: bold; +} + +.markdown-body .pl-ml { + color: #693a17; +} + +.markdown-body .pl-mh, +.markdown-body .pl-mh .pl-en, +.markdown-body .pl-ms { + color: #1d3e81; + font-weight: bold; +} + +.markdown-body .pl-mq { + color: #008080; +} + +.markdown-body .pl-mi { + color: #333; + font-style: italic; +} + +.markdown-body .pl-mb { + color: #333; + font-weight: bold; +} + +.markdown-body .pl-md { + background-color: #ffecec; + color: #bd2c00; +} + +.markdown-body .pl-mi1 { + background-color: #eaffea; + color: #55a532; +} + +.markdown-body .pl-mdr { + color: #795da3; + font-weight: bold; +} + +.markdown-body .pl-mo { + color: #1d3e81; +} + +.markdown-body kbd { + display: inline-block; + padding: 3px 5px; + font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace; + line-height: 10px; + color: #555; + vertical-align: middle; + background-color: #fcfcfc; + border: solid 1px #ccc; + border-bottom-color: #bbb; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #bbb; +} + +.markdown-body .task-list-item { + list-style-type: none; +} + +.markdown-body .task-list-item+.task-list-item { + margin-top: 3px; +} + +.markdown-body .task-list-item input { + margin: 0 0.35em 0.25em -1.6em; + vertical-align: middle; +} + +.markdown-body :checked+.radio-label { + z-index: 1; + position: relative; + border-color: #4078c0; +} diff --git a/core/frontend/public/dev/static/styles/_mixins.scss b/core/frontend/public/dev/static/styles/_mixins.scss new file mode 100644 index 0000000..8130eb3 --- /dev/null +++ b/core/frontend/public/dev/static/styles/_mixins.scss @@ -0,0 +1,27 @@ +@mixin translate($x, $y) { + -webkit-transform: translate($x, $y); + -ms-transform: translate($x, $y); + transform: translate($x, $y); +} + +@mixin border-radius($radius) { + -webkit-border-radius: $radius; + -moz-border-radius: $radius; + -ms-border-radius: $radius; + border-radius: $radius; +} + +@mixin border-box() { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; +} + +@mixin border-bottom() { + border-bottom: 2px solid #fff; +} + +@mixin border-top() { + border-top: 2px solid #fff; +} diff --git a/core/frontend/public/dev/static/styles/_new.scss b/core/frontend/public/dev/static/styles/_new.scss new file mode 100644 index 0000000..31816e2 --- /dev/null +++ b/core/frontend/public/dev/static/styles/_new.scss @@ -0,0 +1,18 @@ +div#new-repo { + display: block; + text-align: center; + width: $default-form-width; + margin: 48px auto; + + h2#new-repo-msg { + font-weight: bold; + @include border-box(); + font-size: 16px; + margin-bottom: 8px; + padding-bottom: 2px; + } + + div.form-errors { + margin-top: 16px; + } +} diff --git a/core/frontend/public/dev/static/styles/_repo-settings.scss b/core/frontend/public/dev/static/styles/_repo-settings.scss new file mode 100644 index 0000000..5eb33bd --- /dev/null +++ b/core/frontend/public/dev/static/styles/_repo-settings.scss @@ -0,0 +1,6 @@ +div#repo-info-settings, div#repo-area51-settings { + display: block; + text-align: center; + width: $default-form-width; + margin: 0 auto; +} diff --git a/core/frontend/public/dev/static/styles/_repo.scss b/core/frontend/public/dev/static/styles/_repo.scss new file mode 100644 index 0000000..6bd3f24 --- /dev/null +++ b/core/frontend/public/dev/static/styles/_repo.scss @@ -0,0 +1,85 @@ +body { + color: white; +} + +body a { + text-decoration: none; + color: white; +} + +div#pjax-container { + .code-view { + pre { + background: #272822; + } + } + + p.repo-message { + border-bottom: 2px solid #fff; + margin-top: 4px; + padding-bottom: 6px; + margin-bottom: 6px; + } + + table#repo-content { + width: 100%; + border-spacing: 4px; + border-collapse: separate; + + tbody { + tr { + td.repo-obj-name { + width: 33%; + text-align: left; + + i { + width: 13px; + height: 13px; + } + } + + td.repo-obj-subject { + width: 33%; + text-align: center; + } + + td.repo-obj-commitdate { + width: 33%; + text-align: right; + } + } + } + } + + article.markdown-preview { + width: 100%; + margin: 0 auto; + margin-top: 128px; + @include border-box(); + + .markdown-title { + display: none; + width: 75%; + text-align: center; + font-size: 1.5em; + font-weight: bold; + padding-bottom: 8px; + margin: 0 auto; + color: #fff; + border-bottom: 1px solid #41443c; + } + + .arrow-down { + display: none; + } + + .markdown-body { + @include border-box(); + min-width: 25%; + max-width: 75%; + margin: 0 auto; + padding: 45px; + padding-top: 8px; + } + } +} diff --git a/core/frontend/public/dev/static/styles/_reset.scss b/core/frontend/public/dev/static/styles/_reset.scss new file mode 100644 index 0000000..85d98e3 --- /dev/null +++ b/core/frontend/public/dev/static/styles/_reset.scss @@ -0,0 +1,49 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; + list-style-type: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/core/frontend/public/dev/static/styles/_sidebar.scss b/core/frontend/public/dev/static/styles/_sidebar.scss new file mode 100644 index 0000000..64c79e3 --- /dev/null +++ b/core/frontend/public/dev/static/styles/_sidebar.scss @@ -0,0 +1,67 @@ +aside#sidebar { + font-family: 'Oswald', sans-serif; + position: fixed; + background: $sidebar-background; + top: 0px; + left: 0px; + width: $sidebar-width; + height: 100%; + padding: 24px; + border-right: 1px solid $container-border-color; + @include border-box(); + + section#box { + position: fixed; + left: 0px; + width: 18px; + height: 18px; + background: $green; + } + + section#title { + position: fixed; + left: 18px; + margin-left: 8px; + + h4 { + font-style: italic; + } + + h3 { + margin-top: 2px; + } + } + + img#avatar-img { + width: 128px; + border-radius: 50%; + } + + section#avatar { + margin-top: 64px; + } + + section#sidebar-options { + margin-top: 32px; + + ul { + + i { + width: 18px; + height: 18px; + margin-right: 2px; + } + } + } + + ul li { + margin-bottom: 12px; + } + + footer { + position: absolute; + width: 100%; + font-size: 0.8em; + bottom: 24px; + } +} diff --git a/core/frontend/public/dev/static/styles/_title.scss b/core/frontend/public/dev/static/styles/_title.scss new file mode 100644 index 0000000..cc5b3aa --- /dev/null +++ b/core/frontend/public/dev/static/styles/_title.scss @@ -0,0 +1,9 @@ +header#view-title { + width: $content-view-width; + + div.left { + h2 { + margin-top: 1px; + } + } +} diff --git a/core/frontend/public/dev/static/styles/_user-settings.scss b/core/frontend/public/dev/static/styles/_user-settings.scss new file mode 100644 index 0000000..9b3ba10 --- /dev/null +++ b/core/frontend/public/dev/static/styles/_user-settings.scss @@ -0,0 +1,6 @@ +div#user-info-settings, div#user-profile-settings, div#user-area51-settings { + display: block; + text-align: center; + width: $default-form-width; + margin: 0 auto; +} diff --git a/core/frontend/public/dev/static/styles/_vars.scss b/core/frontend/public/dev/static/styles/_vars.scss new file mode 100644 index 0000000..46701c3 --- /dev/null +++ b/core/frontend/public/dev/static/styles/_vars.scss @@ -0,0 +1,21 @@ +/* Sizes */ +$sidebar-width: 180px; +$content-view-width: 99%; +$default-form-width: 840px; + +/* Colors and Backgrounds */ +$container-border-color: #000; + +$sidebar-background: #1e1f1c; +$container-background: #272822; + +$grey: #ccc; +$red: #F60F0F; +$yellow: #FFAA00; +$green: #39DD00; +$green-hover: #31aa08; +$red-hover: #990a0a; +$dark-blue: #000112; + +$default-graph-width: 840px; +$default-graph-height: 160px; diff --git a/core/frontend/public/dev/static/styles/_view-tabs.scss b/core/frontend/public/dev/static/styles/_view-tabs.scss new file mode 100644 index 0000000..78b80f2 --- /dev/null +++ b/core/frontend/public/dev/static/styles/_view-tabs.scss @@ -0,0 +1,48 @@ +.tab-links:after { + display:block; + clear:both; + content:''; +} + +.tab-links li { + margin:0px 5px; + margin-left: 0px; + float:left; + list-style:none; +} + + .tab-links a { + padding:9px 15px; + display:inline-block; + border-radius:3px 3px 0px 0px; + background:#171814; + font-size:16px; + font-weight:600; + color:#fff; + transition:all linear 0.15s; + } + + .tab-links a:hover { + background:#28C13C; + text-decoration:none; + } + +li.active a, li.active a:hover { + background:#272822; + color:#35FF50; +} + +.tab-content { + padding:15px; + border-radius:3px; + box-shadow:-1px 1px 1px rgba(0,0,0,0.15); + background:#272822; +} + + .tab { + display:none; + } + + .tab.active { + display:block; + } diff --git a/core/frontend/public/dev/static/styles/djacket-session.scss b/core/frontend/public/dev/static/styles/djacket-session.scss new file mode 100644 index 0000000..0def5da --- /dev/null +++ b/core/frontend/public/dev/static/styles/djacket-session.scss @@ -0,0 +1,27 @@ +@import url(https://fonts.googleapis.com/css?family=Oswald); +@import url(https://fonts.googleapis.com/css?family=Droid+Sans+Mono); + +/* base variables, mixins and styles. */ +@import 'reset'; +@import 'vars'; +@import 'mixins'; +@import 'base'; +@import 'title'; +@import 'sidebar'; +@import 'container'; +@import 'loading'; +/* user styles. */ +@import 'new'; +@import 'forms'; +@import 'user-settings'; +@import 'view-tabs'; +@import 'deposit'; +/* repository styles. */ +@import 'empty-repo'; +@import 'repo'; +@import 'branches'; +@import 'commits'; +@import 'graphs'; +@import 'repo-settings'; +/* markdown styles. */ +@import 'markdown'; diff --git a/core/frontend/public/dev/static/styles/djacket.scss b/core/frontend/public/dev/static/styles/djacket.scss new file mode 100644 index 0000000..0b5d879 --- /dev/null +++ b/core/frontend/public/dev/static/styles/djacket.scss @@ -0,0 +1,118 @@ +@import url(https://fonts.googleapis.com/css?family=Oswald); +@import url(https://fonts.googleapis.com/css?family=Droid+Sans+Mono); + +@import 'reset'; +@import 'vars'; +@import 'mixins'; +@import 'base'; +@import 'forms'; +@import 'view-tabs'; + +$default-text-color: #fff; + +body { + width: 100%; + height: 100%; + background: #272822; + background-size: 100% 100%; + color: $default-text-color; + font-family: 'Oswald', sans-serif; + overflow: hidden; + + a { + text-decoration: none; + color: $default-text-color; + } + + header#title-bar { + position: relative; + width: 50%; + text-align: center; + top: 40%; + left: 50%; + @include translate(-50%, -50%); + + h1, h2 { + text-align: center; + } + + h1 { + font-weight: bold; + font-style: italic; + font-size: 96px; + color: $green; + } + + h2 { + margin-top: 12px; + font-size: 32px; + } + + ul { + position: absolute; + top: 140%; + left: 50%; + @include translate(-50%, -50%); + margin-top: 16px; + li { + font-size: 16px; + float: left; + margin-left: 12px; + + i { + width: 13px; + margin-left: 6px; + margin-right: 6px; + } + } + } + + p { + font-size: 14px; + position: absolute; + top: 160%; + left: 50%; + @include translate(-50%, -50%); + color: $grey; + margin-top: 16px; + + a { + color: $grey; + } + } + + } + + section#container { + position: relative; + width: 75%; + text-align: center; + top: 50%; + left: 50%; + @include translate(-50%, -50%); + + h1 { + text-align: center; + font-weight: bold; + font-style: italic; + font-size: 48px; + color: $green; + margin-bottom: 8px; + } + + .arrow-down { + margin-bottom: 12px; + } + + div#user-auth { + font-family: 'Droid Sans Mono', sans-serif; + width: 40%; + margin: 32px auto; + + .form-error { + font-size: 0.75em; + } + } + } + +} diff --git a/core/frontend/public/dev/views/base/deposit-item.html b/core/frontend/public/dev/views/base/deposit-item.html new file mode 100644 index 0000000..ded5afc --- /dev/null +++ b/core/frontend/public/dev/views/base/deposit-item.html @@ -0,0 +1,30 @@ +{% if repos %} + +{% else %} +
+

We found nothing here but here's a map

+ {% if request.user.username == target_user.username %} +

> You can create repositories by clicking 'New Repo' <

+ {% endif %} +
+{% endif %} diff --git a/core/frontend/public/dev/views/base/empty-repo.html b/core/frontend/public/dev/views/base/empty-repo.html new file mode 100644 index 0000000..cb3c681 --- /dev/null +++ b/core/frontend/public/dev/views/base/empty-repo.html @@ -0,0 +1,31 @@ +
+ {% if request.user.username == repo_owner %} +

Looks kinda lonesome in here!

+
+

You could create a local repository and then ->

+
+                
+                    # create some files and folders.
+                    git init
+                    git add .
+                    git commit -m "initial commit"
+                    git remote add origin http://{{request.get_host}}/{{repo_owner}}/{{repo_name}}.git
+                    git push -u origin master
+                
+            
+
+
+

or upload your currently working repository. ->

+
+                
+                    git remote add origin http://{{request.get_host}}/{{repo_owner}}/{{repo_name}}.git
+                    git push -u origin master
+                
+            
+
+ {% else %} +

+ This repository is empty. +

+ {% endif %} +
diff --git a/core/frontend/public/dev/views/base/loading.html b/core/frontend/public/dev/views/base/loading.html new file mode 100644 index 0000000..f0918d1 --- /dev/null +++ b/core/frontend/public/dev/views/base/loading.html @@ -0,0 +1,11 @@ +
+
+
+
+
+
+
+
+
+
+
diff --git a/core/frontend/public/dev/views/base/repo-area51-form.html b/core/frontend/public/dev/views/base/repo-area51-form.html new file mode 100644 index 0000000..7084c41 --- /dev/null +++ b/core/frontend/public/dev/views/base/repo-area51-form.html @@ -0,0 +1,28 @@ +{% if form.errors %} +
+ {% for field in form %} + {% for error in field.errors %} +

*{{ error|escape }}

+ {% endfor %} + {% endfor %} + {% for error in form.non_field_errors %} +

*{{ error|escape }}

+ {% endfor %} +
+{% endif %} +{# Form design is borrowed from "http://www.sanwebe.com/2014/08/css-html-forms-designs" #} +
+ {% csrf_token %} +
    +
  • + + Yes + No + You are stepping into Area 51, there is no going back. +
  • +
  • + +
  • +
+
+{# Form design is borrowed from "http://www.sanwebe.com/2014/08/css-html-forms-designs" #} diff --git a/core/frontend/public/dev/views/base/repo-branches.html b/core/frontend/public/dev/views/base/repo-branches.html new file mode 100644 index 0000000..68c8a8f --- /dev/null +++ b/core/frontend/public/dev/views/base/repo-branches.html @@ -0,0 +1,20 @@ +{% if num_branches > 0 %} +

+ {% if num_branches == 1 %} + This repository has 1 branch + {% else %} + This repository has {{num_branches}} branches + {% endif %} +

+ +{% else %} + {% include 'base/repo-no-commits.html' %} +{% endif %} diff --git a/core/frontend/public/dev/views/base/repo-browse.html b/core/frontend/public/dev/views/base/repo-browse.html new file mode 100644 index 0000000..ee52386 --- /dev/null +++ b/core/frontend/public/dev/views/base/repo-browse.html @@ -0,0 +1,49 @@ +{# This template is for viewing objects inside a repository. #} +{% load djacket_filters %} + +{% if objects %} +

+ {{ repo_lsmsg }} +

+ + + {% if objects.is_blob %} + + + + {% else %} + {% for obj in objects %} + + + + + + {% endfor %} + {% endif %} + +
+
+                            
{{ objects.show }}
+
+
+ + {% if obj.is_tree %} + + {% elif obj.is_blob %} + + {% else %} + + {% endif %} + {{obj.get_path|obj_name:'/'}} + + {{obj.get_subject|ellipsize}}{{obj.get_committer_date}}
+
+ {% if objects.is_blob %} + {% else %} +

+
+
+
+ {% endif %} +
+{% endif %} diff --git a/core/frontend/public/dev/views/base/repo-commits.html b/core/frontend/public/dev/views/base/repo-commits.html new file mode 100644 index 0000000..539e11a --- /dev/null +++ b/core/frontend/public/dev/views/base/repo-commits.html @@ -0,0 +1,28 @@ +{% if commits %} + {% for branch, branch_commits in commits.items %} +

+ + {% if branch_commits|length == 0 %} + This repository has no commits yet. + {% elif branch_commits|length == 1 %} + Branch "{{ branch }}", has 1 commit + {% else %} + Branch "{{ branch }}", has {{ branch_commits|length }} commits + {% endif %} + +

+
    + {% for commit in branch_commits %} +
  • + {% if commit.get_committer_name %} + > {{ commit.get_committer_name }} committed {{ commit.get_sha1_hash }}, {{ commit.get_committer_date }} + {% else %} + > Someone committed {{ commit.get_sha1_hash }}, {{ commit.get_committer_date }} + {% endif %} +
  • + {% endfor %} +
+ {% endfor %} +{% else %} + {% include 'base/repo-no-commits.html' %} +{% endif %} diff --git a/core/frontend/public/dev/views/base/repo-creation-form.html b/core/frontend/public/dev/views/base/repo-creation-form.html new file mode 100644 index 0000000..a42cb62 --- /dev/null +++ b/core/frontend/public/dev/views/base/repo-creation-form.html @@ -0,0 +1,52 @@ +{% if form.errors %} +
+ {% for field in form %} + {% for error in field.errors %} +

*{{field.label}}: {{ error|escape }}

+ {% endfor %} + {% endfor %} + {% for error in form.non_field_errors %} +

*{{ error|escape }}

+ {% endfor %} +
+{% endif %} +{# Form design is borrowed from "http://www.sanwebe.com/2014/08/css-html-forms-designs" #} +{% if form.instance %} +
+{% else %} + +{% endif %} + {% csrf_token %} +
    +
  • + + + Enter a name for repository. +
  • +
  • + + + Enter a description for repository. +
  • +
  • + + {% if form.instance %} + {% if form.instance.private %} + Yes + No + {% else %} + Yes + No + {% endif %} + {% else %} + Yes + No + {% endif %} + Specify if repository should be private. +
  • +
  • + +
  • +
+
+{# Form design is borrowed from "http://www.sanwebe.com/2014/08/css-html-forms-designs" #} diff --git a/core/frontend/public/dev/views/base/repo-graphs.html b/core/frontend/public/dev/views/base/repo-graphs.html new file mode 100644 index 0000000..28d68b1 --- /dev/null +++ b/core/frontend/public/dev/views/base/repo-graphs.html @@ -0,0 +1,14 @@ +{% if num_branches > 0 %} +

+ Graphs present number of commits in this week and year. +

+
+
+ {% include 'base/loading.html' %} +
+ + +
+{% else %} + {% include 'base/repo-no-commits.html' %} +{% endif %} diff --git a/core/frontend/public/dev/views/base/repo-no-commits.html b/core/frontend/public/dev/views/base/repo-no-commits.html new file mode 100644 index 0000000..ac57125 --- /dev/null +++ b/core/frontend/public/dev/views/base/repo-no-commits.html @@ -0,0 +1,3 @@ +

+ This repository has no commits yet. +

diff --git a/core/frontend/public/dev/views/base/session-base.html b/core/frontend/public/dev/views/base/session-base.html new file mode 100644 index 0000000..e88a6b6 --- /dev/null +++ b/core/frontend/public/dev/views/base/session-base.html @@ -0,0 +1,43 @@ +{# This template will be the parent for all views in a working session such as browsing, changing settings, etc. #} +{% load staticfiles %} + + + + + {% block page_title %}{% endblock %} + + + + + + + + + + + + + + +
+
+ {% block view_title %}{% endblock %} +
+
+ {% block content %}{% endblock %} +
+
+ + + + + + + + + + + + diff --git a/core/frontend/public/dev/views/base/sidebar.html b/core/frontend/public/dev/views/base/sidebar.html new file mode 100644 index 0000000..dc8837d --- /dev/null +++ b/core/frontend/public/dev/views/base/sidebar.html @@ -0,0 +1,35 @@ +{% load djacket_filters %} +
+
+
+

Djacket

+ {% if request.user.is_authenticated %} +

{{user.profile.name|ellipsize:19}}

+ {% else %} +

Anonymous

+ {% endif %} +
+
+
+ {% if request.user.is_authenticated %} + {{user.name}} + {% else %} + Anonymous + {% endif %} +
+ +
+

Copyright © 2016

+
diff --git a/core/frontend/public/dev/views/base/user-area51-form.html b/core/frontend/public/dev/views/base/user-area51-form.html new file mode 100644 index 0000000..21d35ad --- /dev/null +++ b/core/frontend/public/dev/views/base/user-area51-form.html @@ -0,0 +1,28 @@ +{% if form.errors %} +
+ {% for field in form %} + {% for error in field.errors %} +

*{{ error|escape }}

+ {% endfor %} + {% endfor %} + {% for error in form.non_field_errors %} +

*{{ error|escape }}

+ {% endfor %} +
+{% endif %} +{# Form design is borrowed from "http://www.sanwebe.com/2014/08/css-html-forms-designs" #} +
+ {% csrf_token %} +
    +
  • + + Yes + No + You are stepping into Area 51, there is no going back. +
  • +
  • + +
  • +
+
+{# Form design is borrowed from "http://www.sanwebe.com/2014/08/css-html-forms-designs" #} diff --git a/core/frontend/public/dev/views/base/user-info-form.html b/core/frontend/public/dev/views/base/user-info-form.html new file mode 100644 index 0000000..34ea316 --- /dev/null +++ b/core/frontend/public/dev/views/base/user-info-form.html @@ -0,0 +1,47 @@ +{% if info_form.errors %} +
+ {% for field in info_form %} + {% for error in field.errors %} +

*{{field.label}}: {{ error|escape }}

+ {% endfor %} + {% endfor %} + {% for error in info_form.non_field_errors %} +

*{{ error|escape }}

+ {% endfor %} +
+{% endif %} +{# Form design is borrowed from "http://www.sanwebe.com/2014/08/css-html-forms-designs" #} +
+ {% csrf_token %} +
    +
  • + + + Enter your first name. +
  • +
  • + + + Enter your last name. +
  • +
  • + + + Enter your email address. +
  • +
  • + + + Enter your current password. +
  • +
  • + + + Enter your new password. +
  • +
  • + +
  • +
+
+{# Form design is borrowed from "http://www.sanwebe.com/2014/08/css-html-forms-designs" #} diff --git a/core/frontend/public/dev/views/base/user-login-form.html b/core/frontend/public/dev/views/base/user-login-form.html new file mode 100644 index 0000000..b2b34b8 --- /dev/null +++ b/core/frontend/public/dev/views/base/user-login-form.html @@ -0,0 +1,33 @@ +{% if login_form.errors %} +
+ {% for field in login_form %} + {% for error in field.errors %} +

*{{field.label}}: {{ error|escape }}

+ {% endfor %} + {% endfor %} + {% for error in login_form.non_field_errors %} +

*{{ error|escape }}

+ {% endfor %} +
+{% endif %} +{# Form design is borrowed from "http://www.sanwebe.com/2014/08/css-html-forms-designs" #} +
+ {% csrf_token %} +
    +
  • + + + Enter your username. +
  • +
  • + + + Enter your password. +
  • +
  • + + +
  • +
+
+ diff --git a/core/frontend/public/dev/views/base/user-profile-form.html b/core/frontend/public/dev/views/base/user-profile-form.html new file mode 100644 index 0000000..bb4bec3 --- /dev/null +++ b/core/frontend/public/dev/views/base/user-profile-form.html @@ -0,0 +1,32 @@ +{% if profile_form.errors %} +
+ {% for field in profile_form %} + {% for error in field.errors %} +

*{{field.label}}: {{ error|escape }}

+ {% endfor %} + {% endfor %} + {% for error in profile_form.non_field_errors %} +

*{{ error|escape }}

+ {% endfor %} +
+{% endif %} +{# Form design is borrowed from "http://www.sanwebe.com/2014/08/css-html-forms-designs" #} +
+ {% csrf_token %} +
    +
  • + + + Choose an image for your avatar. +
  • +
  • + + + Let us know when this legend was born. +
  • +
  • + +
  • +
+
+{# Form design is borrowed from "http://www.sanwebe.com/2014/08/css-html-forms-designs" #} diff --git a/core/frontend/public/dev/views/base/user-register-form.html b/core/frontend/public/dev/views/base/user-register-form.html new file mode 100644 index 0000000..c5e6b2c --- /dev/null +++ b/core/frontend/public/dev/views/base/user-register-form.html @@ -0,0 +1,38 @@ +{% if register_form.errors %} +
+ {% for field in register_form %} + {% for error in field.errors %} +

*{{field.label}}: {{ error|escape }}

+ {% endfor %} + {% endfor %} + {% for error in register_form.non_field_errors %} +

*{{ error|escape }}

+ {% endfor %} +
+{% endif %} +{# Form design is borrowed from "http://www.sanwebe.com/2014/08/css-html-forms-designs" #} +
+ {% csrf_token %} +
    +
  • + + + Enter a username. +
  • +
  • + + + Enter your email. +
  • +
  • + + + Enter a password. +
  • +
  • + + +
  • +
+
+ diff --git a/core/frontend/public/dev/views/repository/new.html b/core/frontend/public/dev/views/repository/new.html new file mode 100644 index 0000000..ed7979e --- /dev/null +++ b/core/frontend/public/dev/views/repository/new.html @@ -0,0 +1,17 @@ +{% extends 'base/session-base.html' %} + +{% block page_title %} + New Repository +{% endblock %} + +{% block view_title%} +

New Repository

+{% endblock %} + +{% block content %} +
+

Create a new repository for your project.

+
+ {% include 'base/repo-creation-form.html' %} +
+{% endblock %} diff --git a/core/frontend/public/dev/views/repository/repo-main.html b/core/frontend/public/dev/views/repository/repo-main.html new file mode 100644 index 0000000..6095246 --- /dev/null +++ b/core/frontend/public/dev/views/repository/repo-main.html @@ -0,0 +1,60 @@ +{# This template is the parent for viewing contents, settings, charts, commits of a repository. #} +{% extends 'base/session-base.html' %} +{% load djacket_filters %} + +{% block page_title %} + {{repo_owner}} / {{repo_name}} +{% endblock %} + +{% block view_title %} +
+ {% block title_section %} + + {% endblock %} +
+
+
    + {% if request.user.is_authenticated and request.user.username == repo_owner %} +
  • + {% endif %} +
+
+{% endblock %} + +{% block content %} +
+ +
+
+ {% include 'repository/repo-sections.html' %} +
+
+
+{% endblock %} diff --git a/core/frontend/public/dev/views/repository/repo-pjax.html b/core/frontend/public/dev/views/repository/repo-pjax.html new file mode 100644 index 0000000..f74ac49 --- /dev/null +++ b/core/frontend/public/dev/views/repository/repo-pjax.html @@ -0,0 +1,2 @@ +{# This template shows repository a if a normal request sent. If request is pjax then 'repo-content.html' will be replaced. #} +{% extends "repository/repo-main.html, repository/repo-sections.html"|pjax:request %} diff --git a/core/frontend/public/dev/views/repository/repo-sections.html b/core/frontend/public/dev/views/repository/repo-sections.html new file mode 100644 index 0000000..5beb9f7 --- /dev/null +++ b/core/frontend/public/dev/views/repository/repo-sections.html @@ -0,0 +1,13 @@ +{% if template == 'browse' %} + {% if objects %} + {% include 'base/repo-browse.html' %} + {% else %} + {% include 'base/empty-repo.html' %} + {% endif %} +{% elif template == 'branches' %} + {% include 'base/repo-branches.html' %} +{% elif template == 'commits' %} + {% include 'base/repo-commits.html' %} +{% elif template == 'graphs' %} + {% include 'base/repo-graphs.html' %} +{% endif %} diff --git a/core/frontend/public/dev/views/repository/repo-settings.html b/core/frontend/public/dev/views/repository/repo-settings.html new file mode 100644 index 0000000..ef61d10 --- /dev/null +++ b/core/frontend/public/dev/views/repository/repo-settings.html @@ -0,0 +1,30 @@ +{% extends 'repository/repo-main.html' %} + +{% block page_title %} + {{ repo.name }}'s Settings +{% endblock %} + +{% block title_section %} +

{{ repo.name }}'s Settings

+{% endblock %} + +{% block content %} +
+ +
+
+
+ {% include 'base/repo-creation-form.html' %} +
+
+
+
+ {% include 'base/repo-area51-form.html' %} +
+
+
+
+{% endblock %} diff --git a/core/frontend/public/dev/views/user/deposit.html b/core/frontend/public/dev/views/user/deposit.html new file mode 100644 index 0000000..2821958 --- /dev/null +++ b/core/frontend/public/dev/views/user/deposit.html @@ -0,0 +1,23 @@ +{% extends 'base/session-base.html' %} + +{% block page_title %} + Djacket / {{ target_user.username }} +{% endblock %} + +{% block view_title %} +
+
+

{{ target_user.username }}

+

{{ target_user.first_name }} {{ target_user.last_name }}

+
+
+ joined {{ target_user.date_joined|date:"c" }} +
+
+{% endblock %} + +{% block content %} +
+ {% include 'base/deposit-item.html' %} +
+{% endblock %} diff --git a/core/frontend/public/dev/views/user/user-settings.html b/core/frontend/public/dev/views/user/user-settings.html new file mode 100644 index 0000000..c76e564 --- /dev/null +++ b/core/frontend/public/dev/views/user/user-settings.html @@ -0,0 +1,40 @@ +{% extends 'base/session-base.html' %} + +{% block page_title %} + {{ request.user.username }}'s Settings +{% endblock %} + +{% block view_title %} +
+

{{ request.user.username }}'s Settings

+
+
+
+{% endblock %} + +{% block content %} +
+ +
+
+
+ {% include 'base/user-info-form.html' %} +
+
+
+
+ {% include 'base/user-profile-form.html' %} +
+
+
+
+ {% include 'base/user-area51-form.html' %} +
+
+
+
+{% endblock %} diff --git a/core/media/avatars/README.md b/core/media/avatars/README.md new file mode 100644 index 0000000..f471f9e --- /dev/null +++ b/core/media/avatars/README.md @@ -0,0 +1,3 @@ +# core/media/avatars + +Users avatars will be uploaded here. diff --git a/core/media/stock/1.png b/core/media/stock/1.png new file mode 100644 index 0000000000000000000000000000000000000000..8698c9688510b086fe4af74a2f1e91817b754e64 GIT binary patch literal 14000 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 literal 0 HcmV?d00001 diff --git a/core/media/stock/10.png b/core/media/stock/10.png new file mode 100644 index 0000000000000000000000000000000000000000..49a966e175cf2b6bb314e5b3b0c59b9210ac97fa GIT binary patch literal 11921 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 H#v%U$o9e$0 literal 0 HcmV?d00001 diff --git a/core/media/stock/11.png b/core/media/stock/11.png new file mode 100644 index 0000000000000000000000000000000000000000..51abd7b20a4cc88b031d640b95381dd0e5340335 GIT binary patch literal 9106 zcmaia1yEb@(`^V&(c(^l;#Q!z7WWpHP)cxj_fovLLxJMOwYU|hI24xv0a84;!eeK-dUQAuwXhX8n^)fF9`o#NPw(wWC$a=yONq5`UVgS9}DRtgO4%*KnGBgmDcuN zKFaY)p`HtXNnY#D`90oURy%TX{`ADetR%*#^Ta1x@dk*JMK+W9fro^lmXzzEd`*02qR@=RIz9oRI2_C-gV3*Z7+<9n-&s49&G~h&L!NANB=`4}G&&W5n1*ZJ zlZg`_^yb(*tIM4p)9%~5DoP2oHGM-YTE;H$DU}=C&t_xbM~>75 zA|i+ce9|Y;2smPAG{l2HnuzsvhTHLiGY4@K6GK&SG?39(UQ7mexZxT^(fUwu}9z=)} z3VS}bqgp#HC`y*2(@H;_qKo*rh}9F+0+T2Rx71VM-t`Hz4w!`ILT-UZ-M5l|y#pfj zUlI)eajTj^#+(Vol=l-sx{1$wnC_J8fLu>)bD!??$oBS%h%Qj-C6X|< zjR!fmStl@uX2I%muq%=#qkzqwuCb*HvH8X?NFD_w+sZxlh5Bz-LU9I)|J@vCF zP3H))`#IP`kUMjNNjCyf@^2eFzy(4A9eMmG^0Bm$J;H7QeXP06(OouTH_!oUHgQvO|@3 z6HX8M`ty_KgDH92*mBVw;bJB>bxrrXEqI{3uSi`(X2Vkr`+=3q!O63Y0l?8(&JLV$ zU#&0W;J{f}Sm+%$zDb(9v&BjK-S-Lv#GHSP=2ME{Pb-=&ZA7AA;@j2u*ksi7^TV)F{jEmN}s99D<-C@r$-tk``O&}fxj+iG;Yb$o`-07%?mP)7j)AHU#(U-xPs~XtdZ=) zxQW>5fj(BN?4rr?ta4jZT{AMxrcil+Yg3;TlS;o3LC{U^(2iG7lD~+Y-3JycWv1l( zs;b!ZkG@gQ_iznzDz*B(^?S^RGjgWA&*M@eqgBo8hD`y_4e^imt}*xKFiX< z3}^=gTTJDwS5Cn;_dDqyv83*&SWIckzwIx%`8?-6cs;q?bL}qZq=S>8(w=|#d2nL$ z`H_&)Oy8SkYU?w_a>leYxo{b~=}t-TN1ynZIhD*eGKIH*_ki=&#-qn;?S0$92hQgA z0=ghKzG?4NeFB1EpNWr4sKCiSWM8~PPKWNha`u?&2^QZ1q<53hrALLK0L4Bv<|gz3 z2P7nv@2S1~Ot_RdRo)HldF*Li8xx`@9GEB@J@d@^O}D*t9isv6x}giz|@L0pEYcOb0^ zrFN6*t|K2>L+BIEDT7*Cslv(g-LB*6jVpAkfZy%@uK8j3$!jiF4-LjrM#&di+~Z0- zV|PInf7W1ZKMc}VYf0IuJtxW(AC0DQ%8-sK`zGyqU5e7EV*{Pf#|6_JG_E|8DO44I z>!G+l!I5bvT}I+?Q*CPi0>&<=fWAzs%-$92DQDO5IV0~`%5X~!Jz#MAahu)P({X3J z^ZM3J49Z@w4W_~v@?AU)*Gk~y0EUnggKqUqNOu-v)VJfePsk22W-XX?P)r;BCh#CiIJwX&4$ZO zuREgrPCi45P8xuIk7giV6Qxt3n_BE=+%6^Y_MJs_h;@hZaT&(;D%#&TV*pSSIcnx= zT+dkN&g=fY>(-RCbbWh_%u2H>;vDkISglX}ny{)nH<$^1yf}o&yh`2v9shWhF?|7b z8Lp1|s5@x~zTBU)03s<~GXJpsCev7MXi14UVz|iekRdPXdWJp=yj`8_8m=~hLZL3f z(*Xh1+~?zG%SrA)fifX*9a){e{fxW)O5714?hWfF4J(!n6|PqEu_yBa^wI={q)xE( zghTN0BED38X@&hgj^G!hyUqTMB5GBlO?Um^@oYi2C5!aBz(-%*YS74Pt6QdIki@ig zqbgY(bZTyHz^ttgj zBZC|p8w)5s`c8HuVdwaagNsv~al+2qB*Sa+T{6{_C&x@Eo54=h97`Y$w&^V)yzznaPQf|dfe`4&EI@YKg2v7tv4D#5Q(u3vaIbWka8U%eAn`DHY#(l zbfZkqUPs2C!pCNk*Ym|}S#*CKmA6_pqG6X*-Rww>`mbq~dsN3uq_XxS6&_22I; z`n0yvC%Dn!Lqh4u_ySE0NB}u~T5h=mxf2pxAKOjGJVleys^C~nt002mm7#0@spvma z5Y{?B>$hzRnp$Umc;@x8R+E(M2_TX%5eEVL5(a}xK9a}?U9`+!A#Z@E{&}0K$BlB) zgz|!NiKx<28-6HV@h7DB#s-#kKX?v*dN&cbq|LjN0^J!u2Wd_hB#(xbri8m9#^{5%{N|C_L_zX!gB4;!EBhBDQi|$J z;x3df&04pu7k7!2PNStJloDuy^XvF_l4Da$XT+04?zxVYr<$LlYef zs8sAn>|lk^^1N1-qu=1KJOS&qC#u^&{l5Y{i#l#WD{-VQXTtMc&oWj!w!*C(sUDX* zZxki;UP4Fa2S;GT#Yr3Y8PhCv%f1AW&Xz#TbT$?|Tb$VQdpD zK6`R@X-Ub+i8~-502>=SLA_LoDY>QE=&3gE$p36>yHc^E&A!2kv=_hVFDY8rq@syn z2rN-0?mJGhwa!~*8#Y^8IXb>wX>JLCvR!4ga%FThpD!gk(vM*0kcnhken{y7r)8}` zrOD*Un(LfRe1k)FTpVuK>69d2+o!kE8+ok~7<{_ZTC2Q}%V6i$FgiH-zEE_|_*w6D z*9X^u0-ZU~X?C9Jz%J#fm35Pyd@)bTJT{fxPES+m-r^(M$kWGgk@y^)@>$8^Z|02I zr6-)8ZxCF)FHiD{J$EfAB{U!uyTomq+H1@uQSpn#Xjer~pKQGS&JUV#L}hBWnor7eL7cGf#<@*FcWiC9j@hX>j>f2QSS zMw66a^41PtsBtw2hC|}G9-7IczP3`OvNO7@OpuRk*1%>v-JDvZL?6MdpdB{~-^_h1rbWiuBMKcdRATZS6dzKNe88oBnC+%96Ce1j%78rzUI`7- zYT^_W8FPa?v4s<}nvi?ao4h8olcrgFqDVWv%A&O{uKijocyb!P5C=;h3oC{r?}5$G zJ+3l&5lGBRt$rnP>q|!R9pOA3$lMQoJ$h#uaZi}xR+jR1gBXl1-a!Vm6;K2m{SAbA zh^^3gJx9@Xw#Y8C>Xw`<9;8%9;`q~QFUA&+c|^)%MkB}n<{62@R(~0UO2lhJFnoa{ zLA2&{oauZ$t=DWOPmeh7Z9@)YWU?o}I<-7~_;iRO-^K2gSnKrDVertK235^I;x6;Z z(#;CEp^jB&+@M*7qitXhzWGc?Hp^z!Qg~FUStGrXOiBGJ&(~^R1JA#kzw5&;SdIZj z8bO2J8xmAi=fbI`y@?aam+7tLG5S40$KCfbgvVazt#D3}sD`4(-fNKUZVKsB?1g)n z4U{hmkLauW&t!5MI`IUI(qr|DR`A0Hk*kv(D-0S-epc%4QB!HdlG>y{jb+IR64(@; zH*U=w#?CQ#%Ll5Y``%UJt2ru#uWAS_YLwo%9kVsyWty5~R!}}?XX+c-f!m|Y2@NzQ zC=sagn2xBIlJD7CHC7}a8eB|@>?{mk%Y-bJ`gdDjp62Q!)wQ9KR4(xx3rn0O#>3$< zcyF@CqoqnThVy?1SfZZZ^k1qBRajgW0jKk)NVsSkoQf(!$2K->sv@<93przc#TA;i zw$4LesIX!!@f%XIRE@;Ob;uoTzS}^yRb5i<2-L015`5^is?i_Exqjvngm@z(*c}+b z*u<_D%^q%sBzF@Ru5uc`fu!o2DIw>|?Dy(cC2K;FCq9P(`zCu%;ytfVIzxroxzUJX z)nWOf(6EABQd~p9@=mD6KO=I!Huwuj}#=FneLZN#YA0Z4KPy4o#7f2z{c+L z#SnsXYilp05u>s7CcWf)iAj3nZhxqHR|yS<26TuG(KI-#TNumKW$6*%e@_9QzHN41 zIq}c+<7#hlU!C|5fNpdlf@`XfLVQ=zm4BHS8JHRr5OQlCwg30E;n8j@{^b3~9dZa?N zKG}x%fVPxqT5dH--HauG0X*B&R-$6AS2c^j44|#TX?pHH`6OT10qgWipr&Rr)CusE zi@v3@mh+_|<U4SB*Zf_)!~!5YMA#U3 zk*iAxR*|CR=+1aKv{!1==c&*5ed1P@KC#ZaKj{uyoviA*ecTKcUkc;>3a%^A7`;0a zUN4j7uFfeHOEt@pel)DI6GKy3Wh;<2!d=gQ?|1yxFYGmRg}9dM(WwD{ujRKs70#)9R_-*?#ri-!qTy+dHzD zz1B^PJM-?#Pe$x3U60*2zpNyC*`F^S36BjPIY=Bjw)%ylF}8bqYU%qaEk7)*y7umf z+%Vne-U@Tm?15K)dEVU<`rnS4?9S#r@yAQvGM+qka_4-A{eXW{9IX%WFz}8h7OuTi zs#Hwzw{`zYd@7FD{GLvHQIbg>j@-Q(`NR4Bq+3pH9c6;X>r)Wv=U{V_$vv&#%88WZ z@-|^Plf2%Cic810>6Hxd)v4ld8w20flBp#h6>yz`sxQO=<(6+_YP?Sx@>oz?93|QCs<;-r-bA;2IDNnM=M+Z-@tJCSpppX#vIP~TtA@;nkg7WZH z#YKL{J>mry=and-qUXP_!rMg=Ok2&DNBZd7?o8{EJ(@$-MKcYDsEMb0MwYNyc3W%f zVMgMKb`Y)3WkYR)jq3;jo9#Z6e@nm+QyzsJC6mI(WzXNCPSY*ssJYNj@&$yf*v#n` z;|^XkUT`(Bq5LKvq)|c0aPMV0bQv{8;78_)*pXftufVFyNuC<}fL{MpNtg~n;mK4N zuh*-&1vUFiw5DUp-U@~G3Ezit9?r8&#v7zW{nuqrUG);3j}dsaXUm*ItyIda^mWQ9 zGezixEX7NYqj_7)%m^Wpw%zhrkG&>?s71cZ6|ejvNr^tGjj~!PU0e z$(3TC!_pKQOdB2((xOcZ!JS;Z#QLSF_FZ0I~959P7#?O>D!b|N)$@cA$C8caa zLsxRS2pN0i1lQ#^e5^-%c^C*=%5~qE?LwDO{TF_DP$9_5A8vis@ozqSmaKa=!nCri zY7XZ>fa86iBwlGk787E99k~=X?oEjgjD)=8)TlG_rM0JxmmZ=fPZ4~_U^(b;?&v0N z+`=meD45qb4x2KZt+QLmaKrIv{4i|EgUtY{>AO}z3WN#mz?{Txxn(QLtqr_{yAG0Q zsq+(*%;XD9znQ^Qs*Dz&AB`pd9SNTW#X{3*Oh|x50E5T)i@(ba4eqM- zH4#mE&03I-gz^$$M#9dHhpAAggnsL^DdNqpPhsI<_;4k0x%j2w#`o&zm7%c=w7??& zfC%Nr(dCy0ktO3hTe@EeHe+j0FO|)-5P0{ZIK3}ZKLkpQ$v0s5)eq;Nf?V=^AQcuC zeuETl(Rn2J9kBy@_T0A{$`M?XO(zdH>Rla{N|d`cqkprxKaQT+Ili+CW#E?7xSudd zKXTG<`I3KCXt7eBy<2-K)2-CkTRbrBoR>I(dnNHm7S(q@^gyc@u;s?jX&}9E-%m*~ z3eqqwd9f{~Uz$H*_L|*Q^DF{J`yh$!MV)Kub8*THSg2yT%zp^s=)E?Pb0Z zUqxbD>-nj1^Tm~JxFxI*FW#jL5pFEHLmvN-#7hdtEnc1n?fzA|1SQvPPTcGJ??v5| zbXlB!%^_z2`jEn@vNtEpoa}yAHK~3B#r>S6 ziE%h%MzHa=w%ts7s92`HurBr}WRsjO-x2EcC?_7DunW}a4oH@-TB0B!IQ zeLBU!ge82Z6 zNL#3ve}hm*0;Y|9RVtpEu_YPShkA zzGZf?mh__uLJ5AFNA5q^H*i_wJbSNBfIYf3{d|{4Zf%FUF$j62kKta!PA;Ou9>pqs zEi{or(VnTvr7I*o^ksJJvLYLz>8U!tdV8j=&Pl0J-d%Gs+hr9PWvc$bg)0`H_ps@7 z{y6EM(Z~H{tK2%SRkCVKiRPE%p796 zAAN580Pcs&A*Kc#tq=Q{@y*~=j?VqN861L=6xobsUryTRm568@I94)^xZf=sTe;nE zX_9`Tbsc<`FeD`)QOlQqC%Mq{Ri1wM3th%zp@4+c`L9>*A({wLe}kCWckO)cWTxxf zVz;`Zs8J16nH)K3D{B{O?nL@x-hMm!{+M=wHzN^axpBf**xb~13gVj(bbEGEcamwc zv0cxh?`NaGQ~B0ooHJuOn)NcE^egJAak@t{WPA`WfC%C-SbY-%5=>a!Tehx!6jaAf00kuPW zvUat^7!vIL3;d6^Zl*Ve>9bznFaw=i{Z!m9YW7DG0;_F4AN z9}a6rrqsW3l$I{k?+~h)EC7i830j=a`)7_uo%0IhZ&E@UBZVTh=}m*k5U{EX$h=Da z9E`c3N{Z-5g$65}S6IMt5g>IE(rMb(oRA*F03-k)GBzI!-KOP8T@HVy*U=LXoZ?5x z$kG83YA;gDet89%kT1pp*T%M6{dy~+M;0M2Y8j>K)?EsK2xL(SA7ydrD~O695)t69 z<#d8Pmn;BO0k=Mt(O9m93|p2_QYghl?IOMg&5qRn^x|zT)^8w^1($7Ll++i5CIPT# zv1~v@IiGp4UHd{6FAFft2MJM-MB^^uzwiR_gXhzmOyvu ze5SkmMW^xFLEPMre}BzPHl){p*Z1F!1x=#uesot7is<(+cbRp&oM@X$`;?t#xys{x zMHwUqv*XUk3034*_V9SCrlvNQBbvUyZ(CkoJ`6i&e2KZdva)x4%p^MDzFJbuuJ695 zgK(!Jw!34=nmz=>VH)^79pQ%i?CgYWdNfZ6=GvS8CMso>L zLBS7{6=c^9#>U2C5AeQ+9h0ZmeSLiZP{)p^K+sl$H$5Un>ppaja6-&9u~Ree`*vbr zZhKpn6cucvac;)xD^A@x%z6GVgLm{G!#H{O$YN!%mmy;u*Bm`pVzz7rpjEMKMB3-m zY|?G<<7hf}>Cd0U!A3#Pqlh*0ZMC)vsoRY6dsgM@LwyL>!qT!6ll<<*KR&9^=54=# z1SW{FXrLT6=Y3e|exwAHJxS<7E+lLk>I~rk05ajfT7c*P62&a9w$4*`A*QoXQj^kG zG-d<8Q;P#a9jaVICSH=Wd@GSv4b3}T9JPM)9vv>OR~ez7Ku!^JBcXgypLaiN@N^vi zT+~=q-HPgvag)nTs*1wc8+>P~@xDKkpiD^A1oLuy#ScKBQt~drzfV_!^k$ z=pCnak}rsehBbJnRoc4a%?n_Sc@1pf%62G?;kl~cgh z*SNf&)~qT5#xkSJDyJayNe@J}7K7nsVhk%VGZ3ZcL1n(-d^$pTKZu;0E5zYjW!}OM z8H=Q}khccacQ7VImuJoN)u<)w8wa!;F;*bNcT4QR4K2RX+b?oaIwT$&TB1%!%RJlk zIp{?s2f%0ty~N z{<)rxX->^;DqIPXb&?+)_>5sDTR?moZa|NV5USZiP6(Z>lUigDUdB$`oEqU2{?FQx s|7StTe`-hmw>G0FWN7ap#T5? literal 0 HcmV?d00001 diff --git a/core/media/stock/2.png b/core/media/stock/2.png new file mode 100644 index 0000000000000000000000000000000000000000..b223f3f4cf1a3eb5f81e531978d24d76f55a10ef GIT binary patch literal 9467 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 literal 0 HcmV?d00001 diff --git a/core/media/stock/4.png b/core/media/stock/4.png new file mode 100644 index 0000000000000000000000000000000000000000..8ab15ed2968c2b1300a0a86c03c1a2038f48a0da GIT binary patch literal 14831 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 literal 0 HcmV?d00001 diff --git a/core/media/stock/5.png b/core/media/stock/5.png new file mode 100644 index 0000000000000000000000000000000000000000..3876b2ab23d5467a1cd7693253bfef0b9caf5c42 GIT binary patch literal 15375 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

hpcD#Ee`Ic7;0aN9<2Mmq;-=x z&4$A;D+K9uZ?Od56K4@YQ|%FN1RSpc<#yh-mdiY3mgxe9g=Bawmtm%zJ3JH35NZTTb zO4&(LMC3tx4Gi$PO$AC{=s}_+38MGj-i~v?e@M-uTA%6ya4b! z95l(*p6}LpK1_jb&GrccO5{?f`;rK(2?6-`Ef=WKJW;&<_mh55LH}mP+LO$sLn;v5 ze~xdlv1A-!n3yp?Q9U;6=ga>JohJKt*mMV69B|6<)I?stx@Ws&^QutEkrvYK8zeD7 zS=l4@(x3O~ceX)|sHt0q#=BwaSfVqzUQC6ci`}QaDezTjILlT8T6YRbrqA)gf6$m* zxsGgMe~|F3SqyhgIEY*}1f!hj#gQDuw0l!@rag<%kt*=(Y}X;bI}3*~?IxM*BPO*< zi^J!j>XK9_va)GZLb?(*Fwi-57{j@;eemyodJJkqh`Hmr^ZhQV#n&AhbH*1N6=sq} z^FRJ^zw?2tp|5~BC0L=!I zKiFN!a%GV<13e>9@_g1OfZqt)e}syer}J{6z_MeGO8}sNs|E)$l9tIt1Qy*}$c3iv zu-_v6tAkksku$#QoUuL$1%-43R!ZaJIt#u$%g=NKW=7|7?|wH@tIsb2t?zzyjn|06 zH{G>Ym={!ad(<&zCtD}s{g@$Tw7EKfeHDBus&rYPMSrp_9H6~)2VzHc@`{eJl83@O zppk)7Ml~K1D`HBK86F2bOR+=N7llLN2a9V|`pBv;Rt!8V&jax3&XBDJKer$B{sKyB z$^-zWIJpOHay3@l3zNxwP7p5)@loBu6<1v#1>gt!j2CG&WzL#08v6_%nt=LypU(1| znv^#Hnvg19)tO~*nC3bcgGy25qTP2r`m1tXR3;+oVS+?{IOVX_&Wtyk@o#kcLnqo9 zxt$@RM6yf%Y!_N(KV?OHibzy1pYzMF-N?Y~nPCa|q5Fw7!G!>-vL+*C$19ut5*6w~ z%otDpn7bop`yo(|aB>RsMbU1&>SdTLU+kcHQWT5&C2T!DtbE6hUj7y;S2O{RhdR^y zw%y9NTilk}JpHgLQ@C5|w7$QFF7y6>zY(FGzfe%kuU}?`h1hNm^0OA%Miu9L4nc=g zqJyWvXL3)hMZNqpM7ERTbwORKMxATfSh1bWa8{AZ)ygb`4t4WtN%%kxi7Q*l6-~H! zQ#$|AM?m68zQ;oqI*{{-`NuSYqAfP)jDlN1=_|1+ktI#84sh$ErW)$Dunp(DaBzbq zDbZJdzqp=`2}$eo)UozNzF!-LG5Q4d7V>%Xh|Pf6RF^~Rv)-O2-SC>H)jbi>k<%Z- zK2Qf1YDSbggb9%Ab3R9{vk48A(LaTHdy_{O_B1AMU6Yz}NHzbCe`)VgaL5$^{deJ- zch!8=KOaqGJ+?VgoK5wm%K4rEz&cQ*^!~*^G4>zOB{XB6wFGge*=z)hURJAgFf&<$ zM(oo3lhP-A1f*CcfV7W2l~V7#j2|Q4G$^$Gpo#A<#o5H?A_Wb_z#F{I@VtK9W~=uf zm@GhyBNMzcWYK&7Xt2G9*=6vP-BRP>cQ?O40h{$FpiIt6^jIj3OQpOrEyHFKWpC#S z3i(>K3i8MjFoc?xcZQP*zxl6s?)S)bFk^R`cq6L7pcuecwo6LkL2X3JyAH&uBvd_+ zX0^(@Y8_->DH6zhmgQ42L36r%PH$WxMx`C!I&l9=iJPKYN8JDJwZ(bnlkgQ9rJjEx zjw;Sl##B!qyTQ!+VkRL_RHpY z>j^*>AK%kFW{sAoszF5Zt-waE3&rx?4>mI~c`u%2NXc63&kI{Xh8!1XZpB((7rY2p zwN?>MT(!Kw+jgN1_O569|9akFby4Py_r2vR|mfcMA9l*>YHbBcW&! zykXUF`?kv7WP&Q{>MNYi^iJ2a`h+0w@7`}GyJcEEa{B1|e!QDs=XI#{ZS_hH8j3#0 zdwIImwwSpI`^Wn>D#I5te?tOkayuY?dY;H_6w@!r&0}X~>q@i>zhJYc5tsxkhSYpG zBTrdS>~Udu{MC3Br^A#O8+VZ&Nu5xr-8=XDm~7taIN1Xdc|MM*sD0Nqr>z&AX-@{6 zYL`~_d(tTGs9;|7NaXKfQ6i0F;ZCqJFlDvNUe|`enpg$I{26EQ|<3) zW;PZx-$;$?yjOXWL1H93LsZJ##DqO^+ei_YMz9i0Y*+lFlULEvM9(i)0G)7vK13Bc z&7EC!S#Ac5sZPI~G;XcHJff?li6{3(Ns#+h?|k(p)tOTtAe;zXH%7f~J`lA_%a1A5 zWHD`VRnRuZa(!ye0`R|Z5YSG1i5Ag&rw0r{?>#V~%w(?YVxqxh^&b<%8PtB{H$<{q z^$B=t4-w&g;c<%47!8HBVk-=Wt(7X6=CX3-(to@|fRq-W={rU;!ai@FqK^vk{_#fzr=ZK~s?*kWpMB4nai$TMt!y7X9Te9^ zO>CkuO_3!ZNyrNL31nu!^U0FEx^Ta6WKlrFOXx^*7L7Vte-7=}9NYA?yK)G%#RqGS zACneuA(YkycXm}c*|vyk;sNlpMXm%w`2M3HrZN_dNbsPE@^wgW#e=>-KdUORFS8K% z{(mnD9dzCU=!7FpZu~{~G2F5ga$hE?b6|nY)^`U+yl>8-n0%TYJ*?=!gnW^8}Sd0=r@|)@m3U{bhB=G40^h=B8z$ zA_XVrxR^wDy&&$hN=S;J&5^*roEl1A`pZei_}Iv^n983Tu#g7CvqK=02DYBB+3zIl zwwx4`T(WH|GS3N_mHlIb>X5;{Xsn^qyf7}hw!;#NT)8UUb8CWiM?R9coUDr&9x^MM z6!y$xXNSV`Pe!$>kHW?&`$y@sKU)^;B50Uk0nYpPr-LH(hnqQU0<4ebinN-R4MK0We7yvfujd2Ny# zqsTO%Wuz)Smg2IyP+GWi@4BXAOTZciF@V+eSi z5%t0z7cZ#y%YSzK72X+P&dS2&9bq#(i7P7v!Ol4+IQA`d}M zUu7W1kdTL?$*B`AQfKvMYxiB;mhbC+br=5rQ2|d)CUWw1pPQ-VCv8<7w`;}~BOYRXj0%k>uK#{p5^CFSH)rkZ$HN-!H*!9Cw`9IC_yNYi(? z1jFH<<2F?YqN?xn%B_s?WJ6MlUgnb!7VCIgKZ{-SM@!8`o5kiaJ_@{wV9E?fR&hC( zVc6;8a#@Pp>n)G?vyN19C$|wg-2<6@A9L{P?B_B^yLhJS*EL^jvY zFPzSUJQ_NmH2%!YIZ~rjVB??&a6TAUf8Ls)EKRy&ilpg@u#(sKP5{QfQsl3m#7I$! zsNjME>X|4b@&h#0dztcMD4g7~(}T+z8BIL8r=`o(K3+|RFDf%&)9UypSO=lXFRju_ z1x{8!%on8W zR+EvD$4whvD#^*_tTs+PPF-@`U@B^fR8sd?K?qT^>Q5UfqbVKq$BA_XrGts=IjZP+ zEJ;=O>>5i0s%_CgFbo^_0bot4i#zpsnwYx}=zOb~L`O)d{IJ?84YxS^rtrLN^}9Ei zpwdS{AX_$dIAH|wzxaAbsa);$*0%XCt(5>xVsN4o~1la1Rpq0kLsZ^93Se z4Ej?Ei~Ku0AfX7cE!aLFVDOkRSnXe5mA*gVgrgjE}_0A+v_D-&!Y50j7CrLx_Wvx*472}^@$)RX+cujTxG2I5oZHl z>p`=2f)Z>L6cpb*Mx(9fm2VRCp{Su+0uP;DH;EB^(`D*VfkDzJH%t zTT}X+TtTKSP|Hqvnj1vAbN`FAuKNuPoR%9Ru;|{xg_yfw{F7vN691JurS7gL5|h*= zcIi3@8Geuic?5g$tPL(Rz=7Z}f|_h`sbsHxr!khwnyLQQA)9%q@y^2@4Ba?d|QdpOfK8v@1052?K2htI3Cjh5c)Frh*vG2yRA0Ms^xN;dL6q(Jp8j{=T-!sURmvTGr5z zJT;~K-|#ORAO7xV%R{le>!Y+HPjX@5;Sz-~-{sqp^dA2VGd^w}@g{3bdRkgWnoA>6 z!{6b$?KS_0y|cCBQeJ-TeLBboFInYJMjO4F0>g>Q$c*$I)Z>~dg49*#j0D_I%N_G$ z;K+RIghP_lLC=*<((+@aO4)~JXEhJ_W{GO1FQu1d4NZTG>EnGb}aSl669@ViIQ_PZbrnI=~KYL7Cl2YtNW`Jng->xF&W z5h3-Vx)9ov8PueDN6uWzde6UvV1m___m1vbZTJZIanxW{;oOdwqgD~P8 zl*vesW;@a3N9udoCn3Bxa?TiH{BXXu>IxRx$=eU_yl{HdsL*(P0tXn1fF!7;#2`F|%bx|2(G&Bayl2%4`_tsn&mS8od4p#I zGilN7BI)}`42-C?&yfD4#IjE{Ex=Q>KJ8~e?T~#$oV17ExqU2w{bAX@fjBXm&NiN= zJWAcpm;DR^-tO#|e(x2~zJY8}J_6xLH!G`tRf4l@YEEjGr%zn-UxO~2pVvUU&{Il1qe3Pll-36UcX&vOOW+Kr*eh#-Utfc7NUKv(HbyA zTSK>%$jBA&xCR!|`o3;HNB9a3cy}fBJ}g8CSAfeC$p^chH7zRZsLIp(KaCq*@ALL# z3i;d@q*V5J`}jQd!U^}vh&<1<^W{xi{vDP;NcZXJ83KxUm1WIYf0*r@Ka~2u*DqQ-Iv)ItW-;vxg1NoFcX>H$X$bi6 zFHR}E*wMiSAp{PJ>7mgOGdbN=@B&p6m6yvU+UpAyUSSPcn49A3 z)yNc+-%a+$?tkNpcCpXL=)?OWkv)ydw4S%0pjKV=TYnM;T+P$-V$W3j_Y>4*tCNKq z-`gUxz2}o!k<**_u3?Rhe0U1zTF|S9ps*9w%e6{aBG|QcKc)jTT^DuDb(c5+9|&Sf zDp-HCBI8S6YYRtnM^^3yNxb~`PL8W;Edf_|clPPir&X76X5S+Pqix{0`F;)~6`?c% z&Y8D!3neGu{b?dkD7(8`7+4m|x7ICKj?4*+{GNNAVPzKUn*63k`1oSnnV0kA0CBVG zTt^3QxVMrs2qtU6(D)o@S{tkT#+;=r45*ofvYGFXGqd@-r3Q=2UmCw8)Kl8n*q8t> z4e+)z8yn!^SfZn|GZ@qh8$k@P`>Ukk(?OUhx%{B#K|2{+tqIA}AAkS)w}1vtA6AHy z71jg4vR7z_V3`>=`B%0x7}IKsJ$at{wZ88}Mxh9cZe%RswgpWl|7IeJq|fM$h=yZ@ z9_I4yNAi}{wi*SpJ!)PE5X$HeL#u&Of6*D>HS0iz&4&Y>dxK#0>yg?#-$KqBlF(to zZTa6KJ=u=mycO!2C1gWX3*Xr9qz$saSE8ufc1N30#1L{91)Or{vjUiwDMKhW2dIV) zrdH=U00bbAJu5F3-%g(K{(eJBXMWkxh{@#X8g0ahKuQ1WUE0r^=Zlp2$G_MKKpTHs ztY-`lTw;mA*o{VHrOnrMEh+^9~riq+MyX&oH!u^1 z`X%9Lmm#qk8nJskZsx^bDvW_3dk}urGF zbDpBfS$_{RhFQbz=d~mp#!qp390_=)E)fdd+6QF#{5RcY!RDa; zWXl+GUh%}bOevbp8Z&Np=6$8L=w!p$oEWSD`q!E_uKmghAh&~>gl56cP0eY=w&ve# zaqw6=z4etVBEG)g4+MM?1zSc(v9AStpqXaD4^k#-#QukceS-H@H!&R0U2i%McPrI~ zTaxMTvXkQrMF%dGvP}R907}P%=~8f)9hRgU4l^2E$v@}un0H|RL7`f;LZ6i+7b|** zRVn>V7Hy(A9{M_I(_<>OBY#Vg36l}V^VqQ9)55R8qN zNWR~pJ*+~HegiIcZYC@b1)yp7V zBK6!SC%LtP21gI6L z;5c+~wR;pRkM2yI1~5tk(o~L^$0VD`$Lr10c#kiw@xMnZF?SJ8m;1`$My%09HKa^g zK6b-~UELHX{F*-Yu8KS2*w|R#r}kR(=#EprRkNGBAdvU|Jt;bZIk6hupKcR_69s;= zbE04l8_r%Ykix@FeY{^x(~0s={Yocrz>f+<%ifXv6oU6Fv{Ehi&3Of3F*WYk3K~A4 zM78Y~VXHi^&jD|NJ&?Yn&v`N*JM1}g6X!yJz)(z=lAvKi->-WLBH+p(thQ-Z!(O-!v(%Su4L%-9#Yk!!ijX_;4r#6(Q-V^XgCAoKcM) z2V3O8@&$J{E2I?`p^Lg^F}*{ zykL+28lan!XqUTsbjX?y<#m2z4Nx@OsrKeB6XAwKG#f)uXgyaj+JETjh>m9IMrX5c zUw7HrkcVxS+Rk8@{nj^kB@It!$zA(a{IlZ@m8EO<4jBb)K6djUD~q=?4YR4JDEOiC zMX2*Y=%S>n2<69m88=#hLjB1%CsT5(&E;z<8qCox%(U@xZ0_^`Eq1O0j9;DG3_OlY zZmMQN-&)YaB4Z|>*O!*YR@R?+*5A$_>a3kr2dEp28lJqz(z2oVFoLGPE9{=|Ap>wE z0us}eZG4_(O5yZ>gWR?9IM?WIR_G=}#sca-^b_Ho3y>IXKsd$V=# z(>R8R6Y0UHZLGJzO&X(4?|j8v_nu$z9I9fWNR)+On=4s*CK{V$vxWqz#vFG0AL#BfHA3=nnSdM7Ur|=95t$az3R@o zeMq-uHrgWv&L5dqI`WDStxA2@!@^*-szH79WjTKo3~w=~_%DbTstItUN{pBMYE4!g zlKkQkE4`eO10mxg#iiqL0KFazC3=(*qIdFwW|;a0xz2aFX-<5!X5powQf8IQ+k4uh zql?l6u=+0zHfbvK!EN^C6#-DYIf6nuv&H;zDwBSrvA!~^L8zaKb^AGv+ZyZGDeB<Yhq=QFAwg4OZcSG& zu+$(J@noc>DqlvGRH!iP3236V4uxdWB**oGJXw!ovM7Bl{v!lr zjsZoyXI*5tv``G{>&PM}(kX%T8N&EnxGH+FUK4(blWZH}MsHeSN1M$-**~LygDc9f z?rg5G0Ixt@idiW7bKQ)RGEh(^#^Ex2-DcHDU4*weFPyKGIxb~bD1OA-e{${RJ$vNE z@$;|0uS3fhv)K7EW8lA8`r_)+9p9z1kX=AtCzrmWNif_Jpv zQ<+!e1uW_L3wwtQDeQlgT&>X)jP7=H`6E?hnHC!(4=v^hHXtI$ADu4LCZdGN4H`n& zSmYVSCgai%M0bhNkCWyrgs!Qv^3f3AW(Y-3ppU;azF-!de)DTaeoKp*T8v+1dr;Ev zJPdB-5Yw@;QK>0l#~eukU}qTU0)YA^Lg*I>38pfo+u9y2un$5{D*dOgPw$*6Np#7K zPeiydxBVN{J`)RJByNd8LhMHobXLD!8jN z{8(;?7G?mG1+M_i^k9f2VuG1v4Qiv@uyW%J1+P@&ZcxuSv1FRbc1 zUkZFq6>GO*;uv)npKSqEV-T<8vO_6%TS$hy>}EMbY$OYimJOyP)VZGt|C54An{q@}CGk`oHjr@iWYnE>9+u4q~um5Gy-=taJm3%W~0Dd)?k9- z5Lv=Cyy7fL91P1CQ9%vhovpUL7%UNV)>qb%_6Y-)ZlCSk{A8y2%orlADm_aj0V^S$ zRe-pb{}X|ijcX_+RojA(xPC7+-+NximIoKsr2HG(c42oWhc zhR>cUiAY=LSgwQ^@)2od@NP7*Z5tEYwr$%J+vb@h6Wewswryi#+rH=bziZvcyViZU5Br>#-n+Z1 zyQ;scuPRDOK@tHL7ZwBr1VLI#Oa=Hq`riW$3H+_rs>uNX;hvBd6IS!gy6p1u)nD?e z{jO?`V70j+Uc0jkv8Tj?$7TwF2`WnDCYMiQBS9copxlftN_~2wyp;D2kj{)E|7;?$ zPC7~r0)hE~S~3el0*P0L6IpY43zwcH+X(UlNyC;caGiuh8JXlM#k4lt@1pIheE* zc7zx)49tEaG&zYeK57u~CDH%K!I~g(eTjAlz6^=b2j&u{qtWQ`b!dQ2NGXUrmICGj zv5b)r`zEMtE^<<@<|N<{k;1VU_-8fFo3S7~942wQbm)U61=7S+&^SuEc{?@6yRd#F z&#mzouLbt&JSBP~>GPECuK!m5E0`JE&hAY<=v()NL?HV6@i`!34pJXMC{ptc6AdWS zD9o_YD0FJBCsG~n$-%hnb}t2%VgI&I?#Ng%lTOe4-?ab4S0Kg})oHa;^=YgAU;-if zoOF9(gje3?jC57pe2^hUNnGV(1c(t+AqVvuy`(&2|4`psY?JjGxb6b@JqsYIBd9o$ zuGfkwF;TEVO0J%Wk&gh?ixw$X$IsO}(8mhWdA)T#4rXPQM08LH>E)=Bc)KNn5tf*swG)hCquST39 z!T^i({c(?O7*P0X!9XeP2>IHx0JOPOF(VS&9Tso;FI+fd7}^jDohr&2&wUJN2*I*V z$-jiXl+JR52>*QxS3dV=o|2u)1`6~~o=acMY6FGaAp;p*9{`O{XwXkmlEF#?LIDa@ z<6iVr_K=4Y(6>oq@RHF)8dVPUiWh1&TvasHOJ9)!TF?|MD9;t5PCO>N=jXfoamT`P zjefCu!&AI8IW6cnITUkrD0#t+2SWI8x}QDx^e0|UoWh1bI+(-e@`WIR8_HGKF!<0o z-aPr3-2{io6sRGiD9BN9;-JHmove}9)*qyGFrhe`8lMJi(>Xl^e4a0N5wB5}TTAOF zGjin`DS;chA2~c*8txQ2u<(T_Bm4s^v)Qo|J}^k~DXc$|$2lZl7fFd(9UXG!1wI|M z%R5qlRje-=hs8YHH7dGX;$5m21rulYHyTccSp1L%9$b}n>pTgCUF@6|O-x_4{(*}2 zbkB|N(}x`rKm0EbH-48!1~cidUqp+xkz&LnFi}Njv}$F0M)$6D;DN;qG2EBItaCX9 zH)>cK>%(bIq4Yn)34 zRQNG_HU8AtKLhp;H}4vCktJ^M9La3ASh=SRB4d(b$t8(VMx8~3u>0eO2$zNsyNCiC zXZRx?9_Ya{dCD#bK}Pzm;=<(D)Lv8P18()|&eLkB!onyhFb;3;&jvdA{ZL%0Fn!U8 z>4R_|s2#74+#Fl+N8E3~a|K}ew6+F)*T&hb<7_Xe$icWBAgrR>W9jfYUKv$}!2ey` zPlFoWlq0J3jX1c@jT^y@CDH&Vh*1-mTIzXrKbp>s;4r=^SkAC%mTA+oaB@lv>J#SS zmTrcQ1N4p~ph1xr&|xC?P3wrX7l)czji`bB3tMSIXpDOraWm58ZJiM@+V{7A zZ*K!$CQ)il0}E(b*|@|UJ&<_aul{*>2#oH3KkZaCrPj^n#94mqD)$NPB)z9OYB--3 zr`{cu#Bt4+aANQM{bHSL)RBV2N~gDomj*h-NRrq#+6-7s%nmiE%L=r>a69mcX)Qz{ zFI}SkKZ|%M61FuHJrSFt3>RNV`)RXl1P&-DVCfC@^BnVkplJXtgJbP%M=Fz6X;bAc z$If$iLv~Y)VBi>?8CBx0#$-!E?mV3rzQ-VYSnMN{hAUo*12ROLS{N~&kbRPiC&?n9kNJXA|y0;9Y9CpYtt ztyIEyL!RcMGbSabhoCAsu1U0~-CVad4yP!Q3_lFnKw|xOUXRR7c-N#|PM_Ct4wk_J ztmDn!ZC2M)0WdYvL}uXigbD!glOVg639P$Ose}@pmzQS23OV}_Rh)#d4}T$DlT)zu!O{ofV?GdPVuAq~mjLH%} z*=yd`*R7U6p<#z989R2G48elPFg}^mf{GAsjqn?Uuv5MfK0`pBO2OhGk%k&wnx~Q$ zi_xgVaeMXeNGPW=pP0epg#CB$6yL4bHq=kvXC-#I5G}Kuc|eMfeJB@sM{3KF)bJVT znPaz5-GpNczj5N2Ba`K=Z5jO~qgXYwhkjGYN^~BD88H&HGcwPQ5Hrskg5`_n-zq1| zh3Qv-i>)>?X7vOn{mF)>)am<$l<<$h>t_%IdPc?V?agf*DKRuSDgrdhkTJLR(d9?gIdj2n*9qmT`v6BJ4o@RpVSDhhkzP0p?SrN#BL@?F38M_C&Vt$dg71* z8SGU0DdIxt?C|ZP3q&fdhI!16^Af@I5l?*Vh^ifV($y<9ZufJ)$bElC6K)st+_(mk zWt(NVK}QK0fT0T0*fbHeW95S2;l)`zwpD^+d$yzX{0(LwCrnIhD4(x};wpPzt@Yu` z^I5I+K4JL;dSdf^aWa`eW+NEsaCB`zf%ZX~{nS5F%OCbvu>++t!^HSwvu}vV!H1FRSplbySl56ELFQsK zlz)Zvu6UpL&9?Di;QffKQ%RX&GF!1aU7N-S(HPM!jS41C{= zlD93*3gOVSJF4zE?^E_>ZVCX@5+F33VVN?3nPchF1xEO!^ zf2aS%{}dl7GZSj)w?;Kl_-m#IjXW`bKfo?mzrjtashcbe*v|34(=%D;> ziIJeoaB~$Fuxemzw7Pw{?Oirziv#zT1}|3ErCt}V90C10<4w#442x8(lfaxdwGT7C>< zhpXgOMqll;xXi1^r15$vP$!N$a$7~iXP^=av$*4n09|BYe;zZse3$`yWF3SAo<@yU z5OQ!ImiNsQ5=oZRiXlI$@DglmUVE&npbtv!TMwsP)rx~%QFC*Ox$K|2aMquumuPU> zJ>MipM;47rlNHLyf<`=<^JZg<@=02?oZ@lSV zwkSHMZ*&p7AVdk<4@|-0XrUlN)PsLV?5GaY!Yv=oP%_z=kvsAO2w0lh;CR4~7`2gi z#Ox_5HIecQtq#P92Y@%C1O&1ea9>f=cs*j9rFq=~ zB7efAu|o?6%>i`JWFie}Uik^Fq}!hKe?dcFw8iC?cioeg?KwS!5;v2Wn<1$2MATuK z!0v7nB%L{HU3E;03M&Hws|24ei+Yo?+HIkpE(|bKNsD~;vqjH=Iavr{9|^J#ONX1d z3;}LUKhGxAU9^$VDrQO!=@Xnf=1dkAKgzZzw+#P*WVrVp42(3yuh_xUE!yG28FXw1 zrQPoo{m+wM$ZN!C$Fo7?n_;|J(GL{%cBpwDAdaStF?h!jNaCfH*o^C3;wAx$0PNEd zIcfmQk`sosSh)S-o($tdz8{A7+{nhFD|9`yT(Sn6P@()wrUV%>SHA=)b6R0cHVY)6 zftr6qRCg9%5JCtex0f1OI#*i}xKHHC5&3o%o|HnQN=)>L*`K>7&gcttQAJkcuIHe9 z`;vfuXGg-T@4B!sGCMsx_{8C2=_5+_AnJ>vb!}oa@s_mFA;URB48Dvp#TWllFrb0s zfJ=zEc*GI-CD$B+C~dPNs{=Ad)8R2OMN3ApCIn4VuY^d$pM$W2D9Ek#RJgSINke2S zvxX_AWi%!Z(Y7^zB11?dsXJV*gZQJfu<`{X9D~IhYJT6V0fWxL8;9;Fl;bgkSlnCB zr0ft-QLmYQp_+Ajz8&u&gv;tV*)Q^No|6Ibj-0;1I2xv{TOMH7#7v+yPEK`XJ(4f} zv?bB%nuNlnw(n|Sg-k*kMWa!R$p9ewj$FkKCwoq;C$TNis`AF4=)0gnhF2@3RgrX% zK@GA5!_;nk{r>t0P}Me4KEW;8H-h^7{B(VGRi#a#Mvq2^6@~B@DptwYqES|(h-HFi zWww?jSLY)SZgMKLi*;TL1Cfnfx16hlqDfpm> zYD`86PoTI@NU})U01MIlN!;vWBkCpD8f<>3ipDXEfD4tW_<1@~KW!~=;T1MqsYZht z4biPwueVn}vZDH#J2SVw5NlaU7EK1c@ zW+&TURj0|j(c@fKjVvs`qbHZ3@qdRBEO_TRfOVgZN5{wd?}#0uX|<#dx;1a ztRdh`r(hBPVUj2{uJ-!gkEC_&2ue(Wzx`WhAf^0XS!aPZQ<6Y>4*&^9I)ci-h z17xRCmDmOTY7iEvE9~)YLU=;tY0c!Ks+{l1`#YK)$r z)0%(Rc7`aX$4}bE=n`p8F<>tdb0t44ZMzIxkw`BXt}6s&ek2I+yRM?s@(BR(6$bb6 z9-*({4k#@64|*@xAr@PEzUc;RC?*}S-BBm~#kTRM?JBM+YL8H3*liPQ%z719r3K_{R^9~*R?Ius$D)&nu`P{pEfbZpbKeR(9&ql8-p z13EeGAS3CDAzCVZZ=TX@7f7L#TCUKB^2%oCQI}mkJ~qA5D9B%=_~Ih@z0*d5DP(N! z1upO>s6J{`+3gALOWY+=dko#57G*~rT)2DB$ayHFRSDRN^&s=}6r!+kBoz2M=B!gJ z2^179=48c#E%wP8Tr{aMD|%uSWu#un=jAYkza&7z25tM)dBW$Lyj9ElKB%pWjSrP_D6kl5J zGb6%6QyT^>DJZ*SH~`YH5D8P``reEo?W-~BY8bV^&?($LU^qO4Czb%DRKxVM%Cx+d&m=mIConl|15;8?(TIri~*2)nVb2bV) zPFTfNfR6n@LZ22B+P5F@FYqjX1-?P5MVUh#3|%8zirjcobI5a5`E~|WwMbkp(`kkQ z`m%_tu3-m$X}X$;?e`D{Vn1{!GTRv6%DU!!lLg0TwSf2R{-(?HVZg^DRrVx zu*x1^aRA|?m3)}aqp!bwKznX;D%Ah17b4DALRq8Z;EK6#zHgLvyvp2KK*|Bwr%yk$ zpQ=5FsmN3Yg&BkE2Ejsu<#tt=%q?a|=NRg7`F!cg^=&|~F};%}_alw>_43o{EPjdJ z82s_wQz_ABL$VV-JSAKXc+dbdoJ*dlAM*)Pg!fO<{)L-6`3Y+Oi>JE4>-VTBDjd0k z)(`!ol6if;E5{#3-5cYXTzIll8d>$EdO7T5WSXk|=56e`c4`@RP1{V{smiky3N1&! zv2b9O-2n~d4VrGulGAx|DfAI5AZ*xRg`Tq;gMl=p(6;cyGfRbq9*y#she2l&)2<48 z{iD(=#0}K(oqaTu{RIPW=lvFj$+YYx>Kvvcww-fOWqKzYn|;H7zhghFS(PPJee@NO;_kpS0Iy4irQF9)Y@Ww`{Ory!vwus<;xHuu(Di)#A%_YQL zDw)N8n?!zB_qcqWlb{n`&3vcK(?zPI@eI07y|&1`vXDxpx`;uRl4+0bQ(`iAuZ%wt z=vQ_OVyA2WW*W;To@_NSw+p4f@jW9xEcLNs(`IRdPw>%wUHtcVt>lRqxoO;54hsPg z$Lf{V1Qt|BrLfO5k)g3+`RV!S#Zm0R;yFWjbe{fk|F#H zr_4VH)|m)@s8Z6g@L_a92Zo6Un&fAR$Jj{})(`D=BgK)}+qJyuf7qVJqs(ykAL*Aj z>;21;7*o^6juT4?8zXyUuB+!-(VoV6G66&9z~ub|_f`3(>+wDi$m#r`%4+l>+<6%Y z__vigWAO$q;7_|b#z_bn5+kZmQf`xrxi*=uz!j|}g`Uw-kw6+|){9P)E>ms!sO-63 zDv30U_ILmyn%rN&?dFe&!!SmR4ql?w;oospMT)9KpkUGe@)gKhakt?2IT?zFHgwU%TO+s|TzWqSb7=v!z|4#rrBVW|tf{LbtM z?^_mGehQ#n{PXx~ta8(6kR&k6XV1)%TEH4&zEsw4)@y~7|BIVvMGK!!%AG_?p*KEk zsuYn~fLOM@?v{(-erHz~gYkq4^hjM5F>SvnYn*Jzq&w>?MW4lkQn?9lfW{e7aHuaY zQ3O=3!qCfsI4+#C7I-T3Q>6j$CBaF(zD!kHDY%HNQDp3}1e5M1k$r60^;&bvPiPY+ zGqLF6Cq983AI@6YZ1Ip$BNA)Y1soamwUt%H9#ztD{<; z7cwY4D~6Av4_gDZ_c7>H{WB0KS57Fm)C=={&#LNv*HsJ z6^bMY!r*7AU0VA|n4&2GWDNaGd)5s>LmIkMbKGlb4`fnug`*8()4K`tn!k#DfXCDL zj~%7Go_C0_j@i92J70V#B)%Q4SUBOnDl?&@qM=-ez$oiiJT;1FdTolsZUU?09{sx+ zFeAc&NkPkpbrC&nYBPM5)CZSk&sZvjW8VK3u^^6y7lLK8wjqzM=c)93A2(m^OZyDP z77ZU#BGz$MH58#9r?{pzV1~pp^c>w&Fau8l$k!*)+fL-cnt|Zzr=(giy$Y~K*8Zh| zI}@6YMPO_yX&b^Lqs)er4itc2o<%ao&H{KQlB(NUTdCPt&C#k0(hCt1;#P`r#k(y$ zpvhhuCv~|83}Qu$NpNO=bnATJd~0{73aA3TK2gebsp|8R_XXF5Y4$TdV;TP>`c#oL zvp(Bg4dO4u$EbDo{aKtSqtvs1bVHeIr5)Co2>H}*QNsFSc=Ac3%0tHyhd$cJj)cx# zbgIX!hAm1ZGD)NROzs-bFJJSd;u=NH1>NlK@d_G);0rxG+{J>B&O8LUfxT9CB+iA0 z>!#VH&=5eN5iW-VYD}VN3&!D>FCH8mt& z%ByP2amwvffBqf19r?Lr$u0@{`1FRTnM1+J{jd8EV6q1#j<7#(fO}c4*GcG6DY{1Y zyB#>-+~wLd+%C_=7gMW|G~wf52gI#O4hQc*DZW&=ocTaJyh%!#V`*JRsV~{9%Lg;2 zYHd?pe`KN0y+XNOK|YPmB&;#CQZ?17fj{;4?Tf){Dtpin;X6}Irv-pLQ*fp`CDy5J zkw58o`7f8l9_6`nir>I{oPSlbV@EGaH|2Gzk`S z$;ACQ@&}I#Rj7LGZKSe*Wn7ab zz2r#-2Fe~@C8f%NkpuKxu5}@P6`8`SMOL)U>a}aju0P#1@4P)Q=Amglf!DFAg}pR3 zC6L1*SEy{1B#52&lz<^9IF`KX#H*pjz7G3aaN`wKb!i;rgcO10HNMY>kg6JG}sqC8ZkK`Px{0YI>zAjXjp!EUW2E*U!M0qPVzjmR(Xp`Rlas$|iSf_N7{sRvI zP@sVqjxx(CBt*L^Tph0Miv!s;Ir`sT4PgSzV#=Cjdn;M~&cp>_@=25P^teFA*|}yD zaZ7XwUh-wwKcTLvgqTdFY1HD5G^8&POB=Q%6pH{2?*XzP_WF0{=q&Xen?&YcRA;#? zAj=!r>O6xi56n8kC^Zv{AraxlbdDyX=6_SrC!?*S!BumNmjqk(pAqj?r1oKQa04i* zSmtQ8S4Z*0D0RYc5z}jPd6q0iq(hw_fZ4gAL4ZF}T49fKXw zb60%aW#=*i66D?KUKGphPHjmAHefCnGW@S_`a0~gr$B@^%%Y_>cg8uvn`(bO0LYVz z;op6W8D!p!vlItoprOY9`M(qOC0tS9&c3KYa?X2~!!U(O&}Px7SO?nIj?yMg|8e8! z6w{fH1LJ-VC&F3EEVPv5K&l9v4QH37Eaqc50BBhhcx4j*Gfn|x(PHY~(>U4g)j$z9 zzQkRWobsMGd1aP)Q>DW4`wRes28f+vfh-_81;_kM>{-U9iWieiQMQueo!6psr^_Vn z85##Zze#NEZH;=s)NLX(pYIuXI@QREvwS^zDyp^&V6hc(c_WEt?;?X+}&@^P76; z9CgyR!N8r$UUDm3hDslodqq%ne(LX~?$&4z6JMs^{@u4={Y>Y z8GSRcXppZ2A)(q+dvgPzt2uXIqTJ4~J-aIiJq;*=9x^gEh0{(74jaV8+)W)sZD?Wi zGqXskJG&g3mg#1^+PymSu2HBpuV>GcQMotmoZyC5FAmYLz*T3RpmBAPQ^lv z?i@6=yA>GB^<5*P9Odnq5EuxuQ%23)Qn3MJu<{Zo6uHZPL^hHn8i0h?$PiA1=XFn6 zr4d%4epW3RIk12r-GA0;t=GUJzx0x2H*-k(4DYY+_d9dqdur!E5X3XW&=Hp9@CTT& zO4wB2PqXAg8L`h#rvcaiq{ak@^0b#Z1z;(6p;eL!K5;WTWduUZz^wL=f z%Ca>CTAPa?7#p%^k^H7%Nfc~#Jb9baO52xPN9UaFmos3-Eld;^N^=#crXf?Q6p%i) zwO-tJw*S`P(}mDE)RTxYy{Gf^Qa+iC6-g{N^lDd!_Fn~u@?#N`tO?imbNSvr4$=&S z6&q&5GMZ>wNQDU>q#Sit^ewe&u^Fv#2%9>r8ub~hxau%fo?Qic!CQzk?Yt*jG?m?W z8a4Ph01Nr7n2RqTb+wV&JY_P7?3$>9L<>exzy(|;c=@t zEjg@*oF*e@9faKve-5QiGy~NEnnsCtBd2B1W;L#S$F7WrG2dTo;tPy-^eb`|wnqGy zfC#8i7KzCSRxS2BdXRGoZ{_g|ekauA!aUO5t?tP)Ex#ThzHB$`qF`R^hYAcX&OaGG z8s4a{<@EdWXymP-_L4EBw8%Dh%Yg|+O8SutlZAXAnsi!Vv@e?`_m+2R`gdo{y*WX@ zA_=@bv*8v|@bo5@{;hf28+(mR_YZ{fx7OmIyuPVmkyRULEKJ;UJ#ysD=53r9M=T7| zu-Ng6#6mUH$uc-FM5SO*Y3BVfIMviUsERxbFQehj0LnVp%(?tJvrMRKF+;_m@^D7d zQqIpu)YQnZIzYG-rl=Ia^rtCNB$%1HdQO6<<%oA)j_9>WgUTYA85^uP{)K0vq5lU| z^&I6S6#Opjs_@o`;slbD{uS^70>g}0QOt#v(+(P3s043M9qFobXceoQ3jhEy!i5UK z!)gA_5LhV2I=+r|@?0Jnc*btH)dq)roV;44I_yw?*$Re;QEXoWeNFort;KYfv_NiY zem>}mn{=_t!7px=a`gE9(70BIjQAIn0`+Q2z|_={evF0fcOwZZrG*B?g1>5tcYYin!c zzK1q#he6DcRiCHbDi@Lh9){qON0p`au?e}3^{YE}Cd?($gZ$S22f9$o-wc!QzI z)qVXhhn7qSUcEc@Dw-*0cav?7Cey2Qnxo+uI}?{GHC^6LOM!FX>#YzxZhndy6S$Hf z=KjSqt+UT0hW0$kkKj7aa)Ay8*=QMQrrR?uPJVcLYOr3Q1Zi51;NRKWn!5bEGu^TM z_%m6BOQF?PNk*DBrJ%YxuBC-Tqt;+*MBs_p{c3ZJz+~r~gyIN-~Y7a|s2*Ar$fu*6yuv^KMX-thIczNbT?EdkqqU#b|N4-mF%+gi;Wn+QdFz|;#HxSU} z!+*Pi`{WGbVAC%R&! zQxScNjGoIBd$LUN`g?*hKq?2i<@T@W&fz!P2eY-xi>es8s%2H|EVc^Q#T?YD`607W z(KoS!(9Y|$2h;GscYpijI(xi{hC~&@<+S}3;P;Iux4XWYMWEfUx2MPw_*e=!iDEq8 z)T%YwE?YKij&Tr3Ab=++!T+HU9v+^!m~RquUZ=2mxPjiGbr&+n>nJ@V-=Df(isNPG z-XrW91zC(8o2uuP=Mch#bZ4YGCE|orRJrYt2P#tqDByWb$II5Wf5bkQ7#}(bMECW5xuPHJ z3|w~5{uF=*^*&EL&k{Ez36PB^>l`i3 zeMK;$e4h|>|~Wy_ti`TzsIGI|0XkW^gUtM zsMZjyV z+@3F7G(TT9h~Q`QpO)Ew)3eV&P^FV5Z{vm&oPV>Yny8t0H!xE`pso-iLBCY?)dHQo zORK39`WeFId2LV5f5&GsSFq>F*TaMJdD8`H=Vh2vtH=M7ncV=}p$q{5q4Svk`aknE zUvIL;=krXAAoPyhaO}zc`uZ{z2>38K!@^2dk;G?5JiLDuEYRk-0ea81(socSld0X| z;d84*9ADN~la`WDA@mFj!2a;O{YRcJBWAEv1 zBCN=05gEeUoZZ@pFNO>qf={qREd;il@Gf-c!ND$HzPL)k=eeh+kr>Zsvyjodws52* zDumF2SFcCot7?Z{BTvOQH(#D-16az^*Cliw-Wr{>Z2};O=+pjE=EcXU%FQJoOkRd68MJ#=HWu zK$;FYq)$YJ9n_HeBhi6hDN{#3vn|@E^YKEHzj;OBBrAgLU%6P)=&n?zH5-#E$5M&v zwUpr?#uYaoik&!KoMo^y8DiQX*c|p9>5w3n<59X16e6ZTbn{huCwFr=A#3mD8Oj0r zEi@6Gid3>PJ+}+DeF}GL2e?!aoAGFYB^b;9MC`!Y(Yx{O8)u8V>Isx}f;iHN&uz4i zsyjS-a)D?s4Z48D?pOi-(Zeh5$=?gJCp0Lku2oiSQZKU+f?|~p0`zx6#7fw(CFpBJ zNiot?iK|yunozIDdsBmb$kw|zlIe8PyDBUgzy!{7acx4$eK&d2*+CI}NUqv7brWxYR-+;9SDFETWLTJpi`$usLOo_8%Fp{tZMF$M zU?#eAn%{PwL7tGYX<2K2@BaJvTKSTqQ{;!Wdi`i@_} z@7_W#MBex9=V(({`euDp1hX{+DL^fDCV_U>f;2Y}hyVpvZj3Q?8tV&|Ds~@ob~P2u zT1U0}XnnLW9_BzjM};X?X`O2q&`e1ODCRPo^~67pn@B-j2=zRl@1`LFq7F zd_3p=k<+Sp9N=opK7ZMUpBzCygX!T z^p=1%>d555(YIO-jQ`Fqit~ji6D^LLrTCgNIP7sb{f;T;*WjT7I+8xWKgbnCVO{V! zL%KAiAy^;}^ZKE5YRyiohO7z)c!-lC-?jidU8fyYT5Z89&&V5sUm$;=R#km$Fj(8Z z&PHH@gtRkC`p`M`Zm^1Q`~6mvtjVJYg1=ve0uHh@d(ev6Hw52_i%Z)9+&(gRM~~!S zyEhDf!Ge-8!4F9^JQ(*f{&oha#F4hx7bfqm+twbw%@`zrK7F1`@O5EG5-8-ZQ|BpkGOc^p~&pc^tvsvyw|Ns+%CsT zmo&E*>>f(na|tQg8GVtGd(Drj{uD}(mO9~Ef}4Y-8op`8mOrV==%ns#sA`NbAV9GU zeVL!P{TO8xbb?njE$-Pp=6W0i-IFr04^0!H&A^~ZZsddx4O{N36+h9|j(n?!hbvRK z=sHzktJ!nE9!lGeHJ|I8Zbd4xe{cl+Q}Djph+<_(!9Lu-Is1g`$(V)Y%q(!3Fu>d~ zzTI*RfTB9gYxZA?BFB`-I=K3{pE>?|zdD{U{9;-2{nJ;*jancrG*vm zrO7N|J-^`6={X^>?4aFm@9`BJ9v-g4UaFZP8dmQ)XX5ZN9LM*N3Dx!yJ;B_&eQjs5 zkQpGcTOP7158N(#ZGtH2+|IXFvL8!1q_*Af?|d#6SdKk=3=dC_OR@h_Kix?kJ+n~V zmaDXV4;TgdCBOE_nx-9ow9zt%56w#QgVzy0IQUm#2!I9MZ#hn{E0Rc zaSlGOeHmx$y{_tpz55+VVA0BF-nN0ZPvmzZ>U%IF*#9982SR;yX1dcOFcfqx7_y?; zXZ=f5P_iMJ*BCVj9}ZOsa_3(Ae&KnyrzS|z?_mF9B*32ty9=~O?Mzm21_|*pGwxsc zTph&(e2Wu)Z4ET1YX1qFGcFG+M3JQr*Rpyv$7D%|?|B~H*%w{W8%WXA7w176jz;L3 z6+K4cT(~VfroP7Q9p@QMCXhI!3^+9XuLhR{HEKD7BEW?trq&R=m%wAY?{D{${MF~9 z9~m0|1k-TkHd269)M>p8z1cj@H1{+9w(nA=*ZaoKba`p6j!(^XeV6Np6`X#LXVBSK zK96yl8P6lCG{l_>3Ro-&wI^E^YmE3xwZ)Tqg1_rI!4hcLMu zxigKfrz+DrUX-@WdZQGndXqGz2r>OcRr7u`?mPi^e11rmBqy~-hxkRd6z;>$0(mpjmLgd&W|~a@x?c1>kYCuby2Lo%}y&FGG?&9`*^Sk>NelgdzlDPacQUU`qF3!bnD= z*m7X7*~GmckbkI}v}8@ot%(KZ4DwzlWJ@P`KP-viW7wJ#RAq(0z#mroCFfisit<7b z{}2iP9&v{sU;JYd#()wp#)I0?7uJg3%z@3$jPz2+!~*05XMJGtVkFXmg{d5v+<>Ux z?o_c7zD3HK-FGyC2f<-TWaH|7~GV(b48dph|z=5U>XF zHT#*|Lu0vqdPh#8bu;msnAMDoLe-)rtB-9Cdzn}cI|*3n9n^Z-ll9str=RVCm)Yrb zE7u$U4+QCb<|glNlzkr|2oQO^OkD6<0T!s!VO`AOrW#04Mj(JKQRwiDZ5iu#R$8Wm z5+I-z%b775dhrQ%a$D>aU~nhMa6|t(CgU&Sq88^__atne3jJ~YOq43lZ?dh z_Yct-!O8p?I0)3BIAB?!9lb9YkjcaQ*aFrbH%|M+gST4_I?GmZY-e;QHeQ-1%VQ7? ziP&O#0i*|=(Sd1l*f+Nz5>*KhOw5)m3o25&T z3#~Q4WE6cYh8{>O)9g|%R1}imhR7kK47DMpvw)XKX=~mnQx}d!qh~I<`)hTl`s=DAw?YsMgj@^9sZqG0*GBY$p5MqHWpoRw%I#jn3x zibbnlh5uh*)O{O8OOni8;6X}Hvm7Mi@vBC_-Y z%Zqk(8m@0!ha?xx8EJ}T2DLH{_?avdMx&Oj_*hs_P&4u9(}t+dQS!oJS>kjP0t#nS zmpbP#v~X{uGTek%>A4J>BNLAw{N9Nn?Foom6T(}Hxr8RO=rkuR;^6LA zcIU1Az;vu!DguxRIT}r;Z?XVtl}Y}nzTP&zJJ~6XBvWdC54uC+o_u%pzmit?T8*%x zP+6JQ7YLw^;nzmTEPmq32T~IEvI<~;(GJ4}a1g5dCM!cw^Pz=4qJ)u$#L(g2k;)sG zjm5B81Nu=g=fyHhk=gUtmF=rvwug0)rMP&2mH{DABQPWY1E(2EREb&A5kRy8WE%5 F{{>ggM7{t3 literal 0 HcmV?d00001 diff --git a/core/media/stock/7.png b/core/media/stock/7.png new file mode 100644 index 0000000000000000000000000000000000000000..78ead635a4663fc7fca047a6a9fc2fb44ef0bcae GIT binary patch literal 11640 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 literal 0 HcmV?d00001 diff --git a/core/media/stock/8.png b/core/media/stock/8.png new file mode 100644 index 0000000000000000000000000000000000000000..320940dc16da6677b756b347364964a483869d45 GIT binary patch literal 12987 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 literal 0 HcmV?d00001 diff --git a/core/media/stock/9.png b/core/media/stock/9.png new file mode 100644 index 0000000000000000000000000000000000000000..0c969552a5b776c8850dad975f2ceecf18a5d523 GIT binary patch literal 11619 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/dev/null 1>&2; then + echo 'Git not found. Please install Git, then run the script again.'; + issue_error; + fi + if ! which python3 2>/dev/null 1>&2; then + echo 'Python 3.x not found. Please install Python 3.x, then run the script again'; + issue_error; + fi + if ! which pip3 2>/dev/null 1>&2; then + echo 'pip3 package manager (part of Python 3.x) not found. Please install pip3, then run the script again'; + issue_error; + fi + if ! which gunicorn 2>/dev/null 1>&2; then + echo 'Installing Gunicorn ------------------------------------------------ [ STARTED ]'; + sudo pip3 install gunicorn + echo 'Installing Gunicorn ------------------------------------------------ [ DONE ]'; + fi + printf "\nRequirements all met ------------------------------------------- [ OK ]\n"; +} + + +function chk_installation () { + printf "Installation date: "; + if ! cat $DJACKETPATH/run/INSTALLATION; then + echo "Seems like you haven't run 'setup-djacket.sh' file for installation"; + echo "Please do it by executing 'bash setup-djacket.sh', then run the script again"; + echo; + exit 1; + else + echo; + fi +} + + +function prepare_environment () { + export PYTHONPATH=$PYTHONPATH:${DJACKETPATH}/libs; +} + + +function validate_port () { + if ! [[ ${port} =~ ^[1-9][0-9]{1,4}$ ]] ; then + printf "\033[033m${port}\033[m is not a valid port number\n\n"; + exit 1 + fi +} + + +function is_djacket_running () { + if pgrep -f "${gunicorn_conf}" 2>/dev/null 1>&2; then + echo "Djacket is already running." + exit 1; + fi +} + + +function start_djacket () { + chk_installation; + echo "Starting Djacket v0.1.0 on port '${port}' ..."; + prepare_environment; + cd core/backend; + gunicorn djacket.wsgi:application -c "${gunicorn_conf}" -b 0.0.0.0:"${port}"; + cd ../..; + sleep 3 + if ! pgrep -f "${gunicorn_conf}" 2>/dev/null 1>&2; then + printf "\033[33mError:Djacket failed to start.\033[m\n" + echo "Please try to run \"./djacket.sh start\" again" + exit 1; + fi + echo; + echo "Djacket started successfully."; + echo; +} + + +function stop_djacket () { + echo; + if [[ -f ${pidfile} ]]; then + pid=$(cat "${pidfile}") + echo "Stopping Djacket ..."; + echo; + kill ${pid} + rm -f ${pidfile} + echo "Djacket stopped successfully."; + return 0 + else + echo "Djacket is stopped." + fi + echo; +} + + +if [[ $1 != "start" && $1 != "stop" && $1 != "restart" ]]; then + echo; + echo 'You can start/stop/restart Djacket by running one of these commands'; + echo './djacket.sh start port_number'; + echo './djacket.sh stop'; + echo './djacket.sh restart'; + echo ' e.g.'; + echo ' ./djacket start 8080'; + echo ' ./djacket stop'; + echo ' ./djacket restart 8585'; + echo; +else + if [[ $1 == "start" ]]; then + port=${2} + validate_port; + is_djacket_running; + start_djacket; + elif [[ $1 == "stop" ]]; then + stop_djacket; + elif [[ $1 == "restart" ]]; then + if [[ $2 == "" ]]; then + echo "Port for restarting server is not specified"; + else + port=${2}; + validate_port; + stop_djacket; + sleep 3; + is_djacket_running; + start_djacket; + fi + else + echo; + fi +fi diff --git a/index.png b/index.png new file mode 100644 index 0000000000000000000000000000000000000000..537e09811dd564428cd5a50c289b6dbe1c487e2b GIT binary patch literal 14858 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 TToeLBSeb&1s&tv;yTAVzjC)Xn literal 0 HcmV?d00001 diff --git a/libs/README.md b/libs/README.md new file mode 100644 index 0000000..b46c21f --- /dev/null +++ b/libs/README.md @@ -0,0 +1,21 @@ +# Djacket /libs + +This folder contains all of required python runtime libraries. + +These libraries will be accessible when the environment variable PYTHONPATH + is set during startup script (djacket.sh) execution. + + +``` +List of current runtime libraries are: + + - dateutil + - django + - django_easy_pjax-1.2.0.dist-info + - Django-1.8.dist-info + - easy_pjax + - gunicorn + - gunicorn-19.4.5-dist-info +``` + +Above-mentioned Runtime libraries will be added inside each release package. diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..36963da --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +python-dateutil==2.4.2 +django==1.8 +django-easy-pjax==1.2.0 +docutils==0.12 diff --git a/run/README.md b/run/README.md new file mode 100644 index 0000000..6275a5f --- /dev/null +++ b/run/README.md @@ -0,0 +1,5 @@ +# Djacket /run + +This folder contains configurations for gunicorn WSGI server. + +Also errorlog, accesslog and pidfile will be kept here. diff --git a/run/djacket.gunicorn.conf b/run/djacket.gunicorn.conf new file mode 100644 index 0000000..bbcc8af --- /dev/null +++ b/run/djacket.gunicorn.conf @@ -0,0 +1,15 @@ +import os +import multiprocessing + +daemon = True +workers = multiprocessing.cpu_count() * 2 + 1 # Recommended setting for number of workers + +# Logging +run_dir = os.path.dirname(__file__) +pidfile = os.path.join(run_dir, 'djacket.pid') +errorlog = os.path.join(run_dir, 'error.log') +accesslog = os.path.join(run_dir, 'access.log') + +# Set timeout for worker. +# 10 minutes will be enough for git uploading. +timeout = 600 # (unit is seconds) diff --git a/setup-djacket.sh b/setup-djacket.sh new file mode 100644 index 0000000..28fce0e --- /dev/null +++ b/setup-djacket.sh @@ -0,0 +1,195 @@ +#!/bin/bash + +CURRENTDIR=$(readlink -f "$0") +DJACKETPATH=$(dirname "${CURRENTDIR}") +SEK=`python3 -c 'import random; import string; print ("".join([random.SystemRandom().choice(string.digits + string.ascii_letters + "!@#$%^&*()_+-=>= v1.8 (http://git-scm.com) |'; + echo '| - Python >= 3.x (http://python.org) |'; + echo '| - Pillow imaging library (https://pypi.python.org/pypi/Pillow) |'; + echo '| |'; + echo '| installed and configured properly. |'; + echo '| |'; + echo '| If these requirements are not met, Please install them first, |'; + echo '| then run this script again. |'; + echo '.----------------------------------------------------------------------.'; + printf ' All set? Should we continue? '; + if ! read_yes_no; then + exit 0; + fi +} + + +function chk_reqs () { + echo; + if ! which git 2>/dev/null 1>&2; then + echo 'Git not found.'; + issue_error; + fi + if ! which python3 2>/dev/null 1>&2; then + echo 'Python 3.x not found.'; + issue_error; + fi + if ! which pip3 2>/dev/null 1>&2; then + echo 'pip3 package manager (part of Python 3.x) not found.'; + issue_error; + fi + if ! python -c "from PIL import Image"; then + echo 'Pillow is not installed.'; + issue_error; + fi + if ! which gunicorn 2>/dev/null 1>&2; then + echo 'Installing Gunicorn ----------------------------------------- [ STARTED ]'; + sudo pip3 install gunicorn + echo 'Installing Gunicorn ----------------------------------------- [ DONE ]'; + fi + printf "\nRequirements all met ---------------------------------------- [ OK ]\n"; +} + + +function setup_deposit () { + echo; + echo 'Creating deposit folder --------------------------------------- [ STARTED ]'; + echo; + echo 'This folder is where your repositories will be kept'; + echo "It's highly recommended for this folder to be outside of installation folder "; + echo " where you have read/write access privileges."; + echo; + printf "Deposit folder path: "; + read deposit_path; + mkdir "${deposit_path}"; + echo 'Deposit folder created ---------------------------------------- [ DONE ]'; +} + + +function setup_django () { + echo; + echo 'Installing Djacket -------------------------------------------- [ STARTED ]'; + prepare_environment; + echo "GIT_DEPOSIT_ROOT = '${deposit_path}'" >> ${DJACKETPATH}/core/backend/djacket/settings.py; + cd ${DJACKETPATH}/core/backend; + ./manage.py makemigrations; + ./manage.py migrate; + ./manage.py collectstatic --noinput; + echo; + echo; + echo 'Creating super user -------------------------------------------- [ STARTED ]'; + echo; + echo 'A super user should be created for management and administration'; + echo ' Please provide requested information to create one.'; + ./manage.py createsuperuser; + cd ../..; + chmod +x djacket.sh; + echo; + echo 'Installing Djacket -------------------------------------------- [ DONE ]'; +} + + +function setup_security () { + echo; + echo 'Setting up security -------------------------------------------- [ STARTED ]'; + echo; + echo 'These settings here will affect your Djacket server security so Please follow the instructions exactly.'; + echo "Enter your server IP address or domain name or both in one of these formats"; + echo " e.g."; + echo " 123.123.123.123"; + echo " www.example.com"; + echo " .example.com"; + echo; + printf "IP address/domain name: "; + read thishost; + echo; + printf "You entered '${thishost}' as your host. Do you confirm? "; + if ! read_yes_no; then + echo "Please enter your server IP address or domain name in correct format."; + printf "IP address/domain name: "; + read thishost; + fi + echo; + echo "ALLOWED_HOSTS = ['${thishost}']" >> ${DJACKETPATH}/core/backend/djacket/settings.py; + echo 'Added your host/domain name to settings.py --------------------- [ DONE ]'; + echo; + echo 'Installing secret key ------------------------------------------ [ STARTED ]'; + echo; + echo "SECRET_KEY = '$SEK'" >> ${DJACKETPATH}/core/backend/djacket/settings.py; + echo 'Installing secret key ------------------------------------------ [ DONE ]'; + echo; + echo 'Setting up security -------------------------------------------- [ DONE ]'; + echo; + +} + + +function djacket_ready () { + printf "\n\n\n"; + echo "$(date)" > ${DJACKETPATH}/run/INSTALLATION; + echo ' v0.1.0 successfully installed'; + echo '.--------------------------------------------------------------------------.'; + echo '| |'; + echo '| You can now start Djacket by executing "./djacket.sh start port_number" |'; + echo '| e.g. |'; + echo '| ./djacket.sh start 8080 |'; + echo '| |'; + echo '.--------------------------------------------------------------------------.'; + echo; + printf "\n\033[33mYour web server should point to these locations after installation:\033[m\n"; + echo '------------------------------------------------------------------------'; + printf "\033[33m /static location should be set to: ${DJACKETPATH}/core/static/ \033[m\n"; + printf "\033[33m /media location should be set to: ${DJACKETPATH}/core/media/ \033[m\n"; + echo '------------------------------------------------------------------------'; + printf "\033[33mRead more about configurations on http://github.com/Djacket/djacket \033[m\n"; + echo; +} + + +print_djacket; +welcome; +chk_reqs; +setup_deposit; +setup_security; +setup_django; +djacket_ready;