diff --git a/.gitignore b/.gitignore index 5e672a3..f55a249 100644 --- a/.gitignore +++ b/.gitignore @@ -15,11 +15,25 @@ docs/_build build *.swp .\#* -\#* -*.pyc +\#* __pycache__ /csv/article/ -/static/ /media/ config_app/settings/prod.env + +# Unit test / coverage reports +.pytest_cache/ +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Celery +celerybeat-schedule +/config_app/settings/dev.env diff --git a/Envoy.blade.php b/Envoy.blade.php index 04fd919..d3d66be 100644 --- a/Envoy.blade.php +++ b/Envoy.blade.php @@ -11,6 +11,7 @@ @story('deploy', ['on' => 'localhost']) clone_repository create_symlinks + install_req run_tests update_symlinks deployment_option_cleanup @@ -35,11 +36,19 @@ echo '>> Создание симлинков' @endif @endtask +@task('install_req', ['on' => 'localhost']) +echo '>> Подтягиваем зависимости' +@if ($branch) + cd {{ $new_release_dir }} + source /env/bin/activate && pip install -r requirements.txt +@endif +@endtask + @task('run_tests', ['on' => 'localhost']) -echo '>> Запускаем тесты' +echo '>> Запускаем миграции тесты' @if ($branch) cd {{ $new_release_dir }} - source /www/servers/python-server/bin/activate && python manage.py migrate && python manage.py test + source /env/bin/activate && python manage.py migrate && python manage.py test @endif @endtask @@ -59,4 +68,4 @@ echo '>> Запускаем тесты' find . -maxdepth 1 -name "20*{{ $branch }}" -mmin +30 | head -n 3 | xargs rm -Rf echo "Cleaned up old deployments" @endif -@endtask \ No newline at end of file +@endtask diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..01ec3f2 --- /dev/null +++ b/Makefile @@ -0,0 +1,39 @@ +.PHONY: all help migrate run qa clean coverage + +# target: all - Default target. Does nothing. +all: + @clear + @echo "Hello $(LOGNAME), nothing to do by default" + @echo "Try 'make help'" + +# target: help - Display callable targets. +help: + @clear + @egrep "^# target:" [Mm]akefile + +# target: migrate - Run migration +migrate: + python3 manage.py migrate + +# target: run - Run django server +run: + python3 manage.py runserver 0.0.0.0:8000 + +# target: qa - Run pytest +qa: + pytest + +# target: clean - delete pycache +clean: + echo "### Cleaning *.pyc and .DS_Store files " + find . -name '*.pyc' -exec rm -f {} \; + find . -name '.DS_Store' -exec rm -f {} \; + find . -name "__pycache__" -type d -exec rm -rf {} + + +# target: worker - Run rq workers +worker: + celery -A lms worker -l info -E -B + +# target: coverage - Test coverage +coverage: + py.test --cov=. diff --git a/access/models/user.py b/access/models/user.py index ef82924..ac1ac54 100644 --- a/access/models/user.py +++ b/access/models/user.py @@ -5,7 +5,7 @@ import uuid from django.conf import settings from django.contrib.auth.base_user import BaseUserManager, AbstractBaseUser from django.contrib.auth.models import Group, PermissionsMixin -from django.core.mail import send_mail +from django.core.mail import send_mail, EmailMessage from django.db import models from django.utils import timezone from django.utils.translation import ugettext_lazy as _ @@ -70,16 +70,17 @@ class CustomUserManager(BaseUserManager): hash=''.join(random.choice(string.ascii_letters) for x in range(15))) body = { "subject": 'Спасибо за регистрацию', - "message": ''' + "body": ''' Вы были успешны зарегистрированны на портале go.skillbox.ru - ваш пароль %s + ваш пароль (он будет дествителен после активации по ссылке) %s для подтверждения регистрации перейдите по ссылке %s/api/v1/users/registration/?hash=%s''' % (password, settings.DOMAIN, invite.hash), "from_email": 'robo@skillbox.ru', - "recipient_list": [user.email], + "to": [user.email], + "bcc": [settings.SUPPORT_EMAIL], } - send_mail( + EmailMessage( **body ) return user diff --git a/access/serializers.py b/access/serializers.py index e59a1c1..e33b3f6 100644 --- a/access/serializers.py +++ b/access/serializers.py @@ -1,5 +1,6 @@ from django.contrib.auth import get_user_model from rest_framework import serializers +from rest_framework.generics import get_object_or_404 from access.models.other import Account from achievements.serialers import DiplomaSerializer, AchievementsSerializer @@ -101,3 +102,28 @@ class UserSearchSerializer(serializers.ModelSerializer): @staticmethod def get_last_request(self): return self.useractivity.last_request + + +class UserEmailSerializer(serializers.Serializer): + """ + Serializer for set new password to the student in admin area by manager. + """ + email = serializers.EmailField() + + def __init__(self, *args, **kwargs): + super(UserEmailSerializer, self).__init__(*args, **kwargs) + self.user = None + self.password = None + + def validate_email(self, email): + self.user = get_object_or_404(get_user_model(), email=email) + if not self.user.is_active: + raise serializers.ValidationError( + 'Учетная запись еще не активирована. ' + 'Пользователь должен сначала подтвердить свой email.') + return email + + def save(self): + self.password = get_user_model().objects.make_random_password() + self.user.set_password(self.password) + self.user.save() diff --git a/access/urls.py b/access/urls.py index 75ef29a..579905d 100644 --- a/access/urls.py +++ b/access/urls.py @@ -18,4 +18,10 @@ urlpatterns = [ url(r'logout/$', views.LogoutView.as_view()), url(r'reset/$', views.ResetPasswordView.as_view()), url(r'progress_detail/upload/(?P[0-9A-Fa-f-]+)/$', progress.views.UploadCourseProgressUserView.as_view()), -] \ No newline at end of file + url( + r'management/password/$', + views.ManagementPassword.as_view(), + name='management-password' + ) + +] diff --git a/access/views.py b/access/views.py index b782575..2489aba 100644 --- a/access/views.py +++ b/access/views.py @@ -1,22 +1,29 @@ import datetime import random import string +import logging from django.contrib import auth from django.conf import settings from django.contrib.auth import get_user_model -from django.core.mail import send_mail +from django.core.mail import send_mail, EmailMessage from django.db.models import Q from django.shortcuts import redirect + from rest_framework.renderers import JSONRenderer from rest_framework.response import Response from rest_framework.views import APIView +from rest_framework import permissions, generics, status from access.models.other import Invite, ResetPassword, Account -from access.serializers import UserSelfSerializer, UserSearchSerializer +from access.serializers import (UserSelfSerializer, UserSearchSerializer, + UserEmailSerializer) from lms.tools import decode_base64 +logger = logging.getLogger(__name__) + + class TeacherListView(APIView): renderer_classes = (JSONRenderer,) status_code = 200 @@ -137,7 +144,7 @@ class DetailUserView(APIView): user.first_name = f_n if not l_n is None: - user.first_name = l_n + user.last_name = l_n user.save() @@ -145,10 +152,10 @@ class DetailUserView(APIView): if not acc['b_day'] is None: try: - b_day = datetime.datetime.strptime(acc['b_day'], '%d.%m.%Y') # TODO вынести форматы в настройки + b_day = datetime.datetime.strptime(acc['b_day'], '%Y-%m-%d') except ValueError: - b_day = datetime.datetime.strptime(acc['b_day'], '%d-%m-%Y') - acc['b_day'] = b_day.strftime('%Y-%m-%d') + return Response("Дата в не верном формате", status=400) + acc['b_day'] = b_day acc['gender'] = 0 if acc['gender'] == "undefined" else 1 if acc['gender'] == "male" else 2 @@ -283,3 +290,45 @@ class MinUserView(APIView): return Response(UserSearchSerializer(get_user_model().objects.get(out_key=out_key)).data, status=200) except get_user_model().DoesNotExist: return Response("User not found", status=404) + + +class ManagementPassword(generics.GenericAPIView): + permission_classes = (permissions.IsAuthenticated, permissions.IsAdminUser) + + def post(self, request): + """ + Set password to the student in admin area by manager + --- + Generate new password to the student and send email with new password + """ + email = request.JSON.get('email', None) + password = request.JSON.get('password', None) + + if email is None: + return Response('email not set', status=400) + + if password is None: + password = ''.join(random.choice(string.ascii_letters) for _x in range(8)) + + try: + user = get_user_model().objects.get(email=email) + except get_user_model().DoesNotExist: + return Response('user not found', status=404) + + user.set_password(password) + user.save() + + logger.info('''set password: %s to the + student with email: %s''' % (password, user.email)) + EmailMessage( + subject='Установлен новый пароль', + body='''Ваш новый пароль %s + (в последствии вы сможите сменить его в личном кабинете).''' % password, + from_email='robo@skillbox.ru', + to=[user.email], + bcc=[request.user.email], + ) + return Response( + data={'message': 'Письмо с новым паролем отправлено на email студента.'}, + status=201 + ) diff --git a/api_v1/urls.py b/api_v1/urls.py index 0dcd8e6..d4b257b 100644 --- a/api_v1/urls.py +++ b/api_v1/urls.py @@ -1,10 +1,14 @@ from django.conf.urls import url, include +from rest_framework_swagger.views import get_swagger_view + +schema_view = get_swagger_view(title='Skillbox LMS API') urlpatterns = [ url(r'courses/', include('courses.urls')), - url(r'users/', include('access.urls')), + url(r'users/', include('access.urls', namespace='users')), url(r'library/', include('library.urls')), url(r'finance/', include('finance.urls')), url(r'storage/', include('storage.urls')), url(r'progress/', include('progress.urls')), -] \ No newline at end of file + url(r'^docs/$', schema_view, name='api-docs'), +] diff --git a/config_app/settings/dev.env b/config_app/settings/dev.env new file mode 100644 index 0000000..4ab6c59 --- /dev/null +++ b/config_app/settings/dev.env @@ -0,0 +1,5 @@ +DEBUG=True +SECRET_KEY='!eiquy7_+2#vn3z%zfp51$m-=tmvtcv*cj*@x$!v(_9btq0w=$' +DATABASE_URL='psql://lms_dev_user:HJin2dt3@127.0.0.1:5432/lms_dev' +EMAIL_URL='smtp+tls://9ae31a1a770138:a7d79ee373a14c@smtp.mailtrap.io:2525' +CACHE_URL=rediscache://127.0.0.1:6379/1?client_class=django_redis.client.DefaultClient \ No newline at end of file diff --git a/config_app/settings/test.env b/config_app/settings/test.env index ece0044..5b922cf 100644 --- a/config_app/settings/test.env +++ b/config_app/settings/test.env @@ -1,5 +1,5 @@ DEBUG=True SECRET_KEY='!eiquy7_+2#vn3z%zfp51$m-=tmvtcv*cj*@x$!v(_9btq0w=$' -DATABASE_URL='sqlite:///None' +DATABASE_URL='psql://postgres@127.0.0.1:5432/test_lms' EMAIL_URL='smtp+tls://9ae31a1a770138:a7d79ee373a14c@smtp.mailtrap.io:2525' CACHE_URL=rediscache://127.0.0.1:6379/1?client_class=django_redis.client.DefaultClient \ No newline at end of file diff --git a/courses/factories/__init__.py b/courses/factories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/courses/views.py b/courses/views.py index 9babeb5..ea9536f 100644 --- a/courses/views.py +++ b/courses/views.py @@ -26,6 +26,24 @@ class CourseListView(APIView): status_code = 200 def post(self, request): + """ + This API endpoint create/update course. + --- + parameters: + - name: level + type: string + required: true + location: form + - name: direction + type: string + required: true + location: form + - name: statistic + type: string + required: true + location: form + ... + """ # TODO: Костыль teachers_emails = request.JSON.get('teachers', []) request.JSON['teachers'] = [get_user_model().objects.get(email=i).out_key for i in teachers_emails] @@ -33,6 +51,10 @@ class CourseListView(APIView): return Response(CourseDetailSerializer(course).data, status=self.status_code) def get(self, request): + """ + This API endpoint return list of courses. + --- + """ res = [CourseDetailSerializer(course).data for course in Course.objects.all()] return Response(res, self.status_code) diff --git a/factories/__init__.py b/factories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/factories/users.py b/factories/users.py new file mode 100644 index 0000000..a3c859f --- /dev/null +++ b/factories/users.py @@ -0,0 +1,30 @@ +import pytz + +import factory +import factory.fuzzy + +from functools import partial + +from django.contrib.auth import get_user_model + + +USER_PASSWORD = 'test' + +Faker = partial(factory.Faker, locale='ru_RU') + + +class UserFactory(factory.django.DjangoModelFactory): + first_name = Faker('first_name') + last_name = Faker('last_name') + email = Faker('email') + password = factory.PostGenerationMethodCall('set_password', USER_PASSWORD) + is_active = True + is_staff = False + date_joined = Faker( + 'past_datetime', + start_date='-30d', + tzinfo=pytz.UTC + ) + + class Meta: + model = get_user_model() diff --git a/lms/settings.py b/lms/settings.py index 5ca938e..6f326f9 100644 --- a/lms/settings.py +++ b/lms/settings.py @@ -8,7 +8,8 @@ import socket root = environ.Path(__file__) - 2 env = environ.Env() -MOD = os.environ.get('MOD', 'Dev') +MOD = os.environ.get('MOD', 'Prod') +DEBUG = os.environ.get('DEBUG', 'False') if MOD == 'Test': environ.Env.read_env(str(root) + '/config_app/settings/test.env') @@ -25,6 +26,9 @@ else: EMAIL_CONFIG = env.email_url('EMAIL_URL',) vars().update(EMAIL_CONFIG) +#TODO: Ответственый работник, который ставится в копию ко многим рассылкам, костыль +SUPPORT_EMAIL = 'anastasiya.katyuhina@skillbox.ru' + BROKER_URL = 'amqp://guest:guest@localhost:5672//' CELERY_RESULT_BACKEND = 'django-db' CELERY_CACHE_BACKEND = 'django-cache' @@ -90,6 +94,8 @@ libs = ( 'yandex_money', 'phonenumber_field', 'raven.contrib.django.raven_compat', + 'rest_framework', + 'rest_framework_swagger', ) apps = ( @@ -122,7 +128,12 @@ MIDDLEWARE_CLASSES = [ REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.SessionAuthentication', - ) + ), + 'DEFAULT_PARSER_CLASSES': [ + 'rest_framework.parsers.FormParser', + 'rest_framework.parsers.MultiPartParser', + 'rest_framework.parsers.JSONParser', + ], } ROOT_URLCONF = 'lms.urls' @@ -174,6 +185,7 @@ USE_TZ = False MEDIA_ROOT = os.path.join(BASE_DIR, 'media') MEDIA_URL = '/media/' STATIC_ROOT = os.path.join(BASE_DIR, 'static') + STATIC_URL = '/static/' RAVEN_CONFIG = { @@ -221,4 +233,23 @@ LOGGING = { 'propagate': False }, }, -} \ No newline at end of file +} + +# Configure loggers for all local apps +LOCAL_APPS_LOGGERS = {} +for app in apps: + LOCAL_APPS_LOGGERS[app] = { + 'handlers': ['console'], + 'level': 'DEBUG', + 'propagate': True, + } +LOGGING['loggers'].update(LOCAL_APPS_LOGGERS) + +SWAGGER_SETTINGS = { + 'USE_SESSION_AUTH': True, + 'LOGIN_URL': 'admin:login', + 'LOGOUT_URL': 'admin:logout', + 'SHOW_REQUEST_HEADERS': True, + 'JSON_EDITOR': True, + 'DOC_EXPANSION': 'list' +} diff --git a/lms/urls.py b/lms/urls.py index 54536ca..7e20a14 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -3,7 +3,7 @@ from django.contrib import admin from django.views.static import serve from yandex_money.views import CheckOrderFormView, NoticeFormView -from lms import settings +from django.conf import settings urlpatterns = [ url(r'^api/v1/', include('api_v1.urls')), @@ -12,4 +12,5 @@ urlpatterns = [ url(r'^static/(?P.*)/$', serve, {'document_root': settings.STATIC_ROOT}), url(r'^wallet/pay/check/$', CheckOrderFormView.as_view(), name='yandex_money_check'), url(r'^wallet/pay/result/$', NoticeFormView.as_view(), name='yandex_money_notice'), + url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) ] diff --git a/lms/wsgi.py b/lms/wsgi.py new file mode 100644 index 0000000..5e9e9dc --- /dev/null +++ b/lms/wsgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "lms.settings") + +application = get_wsgi_application() diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..2127292 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,18 @@ +[pytest] +DJANGO_SETTINGS_MODULE = lms.settings +norecursedirs = env/* docs/* misc/* static/* + +;addopts = --flake8 -vvs +addopts = -vvs + +python_files = + test_*.py + +flake8-max-line-length = 120 + +# E731 - do not assign a lambda expression, use a def +# F405 - name may be undefined, or defined from star imports: module +flake8-ignore = + *.py E731 F405 + **/migrations/** ALL + **/templates/** ALL \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 696753c..8ae31c9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,29 +17,17 @@ requests==2.18.4 Unidecode==0.4.21 PyJWT==1.5.3 -# amqp==2.2.2 -# Babel==2.5.1 -# billiard==3.5.0.3 -# bson==0.5.0 -# certifi==2017.11.5 -# chardet==3.0.4 -# django-appconf==1.0.2 -# django-model-utils==3.0.0 -# environ==1.0 -# flower==0.9.2 -# future==0.16.0 -# idna==2.6 -# kombu==4.1.0 -# lxml==4.1.1 -# Naked==0.1.31 -# olefile==0.44 -# phonenumberslite==8.8.8 -# pkg-resources==0.0.0 -# python-gitlab==1.1.0 -# pytz==2017.2 -# PyYAML==3.12 -# redis==2.10.6 -# six==1.11.0 -# tornado==4.5.2 -# urllib3==1.22 -# vine==1.1.4 +# testing +flake8==3.5.0 +pytest==3.4.1 +pytest-sugar==0.9.1 +pytest-django==3.1.2 +coverage==4.5.1 +pytest-cov==2.5.1 +mock==2.0.0 +pytest-mock==1.7.0 +# factories +Faker==0.8.11 +factory-boy==2.10.0 +# docs +django-rest-swagger==2.1.2 \ No newline at end of file diff --git a/static/09Cg13KjU7/bg.jpg b/static/09Cg13KjU7/bg.jpg new file mode 100644 index 0000000..e6733b5 Binary files /dev/null and b/static/09Cg13KjU7/bg.jpg differ diff --git a/static/09Cg13KjU7/button.png b/static/09Cg13KjU7/button.png new file mode 100644 index 0000000..7053ce7 Binary files /dev/null and b/static/09Cg13KjU7/button.png differ diff --git a/static/09Cg13KjU7/footer.png b/static/09Cg13KjU7/footer.png new file mode 100644 index 0000000..1c5686a Binary files /dev/null and b/static/09Cg13KjU7/footer.png differ diff --git a/static/09Cg13KjU7/skillbox.png b/static/09Cg13KjU7/skillbox.png new file mode 100644 index 0000000..6e524bc Binary files /dev/null and b/static/09Cg13KjU7/skillbox.png differ diff --git a/static/admin/css/base.css b/static/admin/css/base.css index b2a40c4..a37555a 100644 --- a/static/admin/css/base.css +++ b/static/admin/css/base.css @@ -187,15 +187,11 @@ p.mini { margin-top: -3px; } -.help, p.help, form p.help, div.help, form div.help, div.help li { +.help, p.help, form p.help { font-size: 11px; color: #999; } -div.help ul { - margin-bottom: 0; -} - .help-tooltip { cursor: help; } @@ -276,10 +272,6 @@ tfoot td { border-top: 1px solid #eee; } -thead th.required { - color: #000; -} - tr.alt { background: #f6f6f6; } @@ -414,9 +406,6 @@ input, textarea, select, .form-row p, form .button { font-weight: normal; font-size: 13px; } -.form-row div.help { - padding: 2px 3px; -} textarea { vertical-align: top; @@ -738,7 +727,7 @@ a.deletelink:focus, a.deletelink:hover { .object-tools a.viewsitelink, .object-tools a.golink,.object-tools a.addlink { background-repeat: no-repeat; - background-position: right 7px center; + background-position: 93% center; padding-right: 26px; } diff --git a/static/admin/css/changelists.css b/static/admin/css/changelists.css index 17690a3..fd9e784 100644 --- a/static/admin/css/changelists.css +++ b/static/admin/css/changelists.css @@ -166,8 +166,6 @@ #changelist-filter a { display: block; color: #999; - text-overflow: ellipsis; - overflow-x: hidden; } #changelist-filter li.selected { @@ -306,7 +304,6 @@ vertical-align: top; height: 24px; background: none; - color: #000; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; diff --git a/static/admin/css/dashboard.css b/static/admin/css/dashboard.css index 1560c7b..05808bc 100644 --- a/static/admin/css/dashboard.css +++ b/static/admin/css/dashboard.css @@ -21,6 +21,9 @@ ul.actionlist li { list-style-type: none; +} + +ul.actionlist li { overflow: hidden; text-overflow: ellipsis; -o-text-overflow: ellipsis; diff --git a/static/admin/css/forms.css b/static/admin/css/forms.css index 77985d5..2a21257 100644 --- a/static/admin/css/forms.css +++ b/static/admin/css/forms.css @@ -83,7 +83,7 @@ form ul.inline li { height: 26px; } -.aligned label + p, .aligned label + div.help, .aligned label + div.readonly { +.aligned label + p { padding: 6px 0; margin-top: 0; margin-bottom: 0; @@ -115,32 +115,26 @@ form .aligned ul.radiolist { padding: 0; } -form .aligned p.help, -form .aligned div.help { +form .aligned p.help { clear: left; margin-top: 0; margin-left: 160px; padding-left: 10px; } -form .aligned label + p.help, -form .aligned label + div.help { +form .aligned label + p.help { margin-left: 0; padding-left: 0; } -form .aligned p.help:last-child, -form .aligned div.help:last-child { +form .aligned p.help:last-child { margin-bottom: 0; padding-bottom: 0; } form .aligned input + p.help, form .aligned textarea + p.help, -form .aligned select + p.help, -form .aligned input + div.help, -form .aligned textarea + div.help, -form .aligned select + div.help { +form .aligned select + p.help { margin-left: 160px; padding-left: 10px; } @@ -162,8 +156,7 @@ form .aligned table p { padding: 0 0 5px 5px; } -.aligned .vCheckboxLabel + p.help, -.aligned .vCheckboxLabel + div.help { +.aligned .vCheckboxLabel + p.help { margin-top: -4px; } @@ -171,8 +164,7 @@ form .aligned table p { width: 610px; } -.checkbox-row p.help, -.checkbox-row div.help { +.checkbox-row p.help { margin-left: 0; padding-left: 0; } @@ -188,22 +180,14 @@ fieldset .field-box { width: 200px; } -form .wide p, -form .wide input + p.help, -form .wide input + div.help { +form .wide p, form .wide input + p.help { margin-left: 200px; } -form .wide p.help, -form .wide div.help { +form .wide p.help { padding-left: 38px; } -form div.help ul { - padding-left: 0; - margin-left: 0; -} - .colM fieldset.wide .vLargeTextField, .colM fieldset.wide .vXMLLargeTextField { width: 450px; } diff --git a/static/admin/css/rtl.css b/static/admin/css/rtl.css index ef39781..8c1ceb4 100644 --- a/static/admin/css/rtl.css +++ b/static/admin/css/rtl.css @@ -166,10 +166,6 @@ thead th.sorted .text { margin-left: 5px; } -form .aligned p.help, form .aligned div.help { - clear: right; -} - form ul.inline li { float: right; padding-right: 0; @@ -230,10 +226,6 @@ form .form-row p.datetime { overflow: hidden; } -.related-widget-wrapper { - float: right; -} - /* MISC */ .inline-related h2, .inline-group h2 { diff --git a/static/admin/js/SelectBox.js b/static/admin/js/SelectBox.js index 1a14959..95825d8 100644 --- a/static/admin/js/SelectBox.js +++ b/static/admin/js/SelectBox.js @@ -1,4 +1,4 @@ -(function($) { +(function() { 'use strict'; var SelectBox = { cache: {}, @@ -7,10 +7,8 @@ var node; SelectBox.cache[id] = []; var cache = SelectBox.cache[id]; - var boxOptions = box.options; - var boxOptionsLength = boxOptions.length; - for (var i = 0, j = boxOptionsLength; i < j; i++) { - node = boxOptions[i]; + for (var i = 0, j = box.options.length; i < j; i++) { + node = box.options[i]; cache.push({value: node.value, text: node.text, displayed: 1}); } }, @@ -18,8 +16,7 @@ // Repopulate HTML select box from cache var box = document.getElementById(id); var node; - $(box).empty(); // clear all options - var new_options = box.outerHTML.slice(0, -9); // grab just the opening tag + box.options.length = 0; // clear all options var cache = SelectBox.cache[id]; for (var i = 0, j = cache.length; i < j; i++) { node = cache[i]; @@ -27,11 +24,9 @@ var new_option = new Option(node.text, node.value, false, false); // Shows a tooltip when hovering over the option new_option.setAttribute("title", node.text); - new_options += new_option.outerHTML; + box.options[box.options.length] = new_option; } } - new_options += ''; - box.outerHTML = new_options; }, filter: function(id, text) { // Redisplay the HTML select box, displaying only the choices containing ALL @@ -42,13 +37,11 @@ for (var i = 0, j = cache.length; i < j; i++) { node = cache[i]; node.displayed = 1; - var node_text = node.text.toLowerCase(); var numTokens = tokens.length; for (var k = 0; k < numTokens; k++) { token = tokens[k]; - if (node_text.indexOf(token) === -1) { + if (node.text.toLowerCase().indexOf(token) === -1) { node.displayed = 0; - break; // Once the first token isn't found we're done } } } @@ -64,7 +57,11 @@ break; } } - cache.splice(delete_index, 1); + var k = cache.length - 1; + for (i = delete_index; i < k; i++) { + cache[i] = cache[i + 1]; + } + cache.length--; }, add_to_cache: function(id, option) { SelectBox.cache[id].push({value: option.value, text: option.text, displayed: 1}); @@ -85,13 +82,11 @@ var from_box = document.getElementById(from); var option; var boxOptions = from_box.options; - var boxOptionsLength = boxOptions.length; - for (var i = 0, j = boxOptionsLength; i < j; i++) { + for (var i = 0, j = boxOptions.length; i < j; i++) { option = boxOptions[i]; - var option_value = option.value; - if (option.selected && SelectBox.cache_contains(from, option_value)) { - SelectBox.add_to_cache(to, {value: option_value, text: option.text, displayed: 1}); - SelectBox.delete_from_cache(from, option_value); + if (option.selected && SelectBox.cache_contains(from, option.value)) { + SelectBox.add_to_cache(to, {value: option.value, text: option.text, displayed: 1}); + SelectBox.delete_from_cache(from, option.value); } } SelectBox.redisplay(from); @@ -101,13 +96,11 @@ var from_box = document.getElementById(from); var option; var boxOptions = from_box.options; - var boxOptionsLength = boxOptions.length; - for (var i = 0, j = boxOptionsLength; i < j; i++) { + for (var i = 0, j = boxOptions.length; i < j; i++) { option = boxOptions[i]; - var option_value = option.value; - if (SelectBox.cache_contains(from, option_value)) { - SelectBox.add_to_cache(to, {value: option_value, text: option.text, displayed: 1}); - SelectBox.delete_from_cache(from, option_value); + if (SelectBox.cache_contains(from, option.value)) { + SelectBox.add_to_cache(to, {value: option.value, text: option.text, displayed: 1}); + SelectBox.delete_from_cache(from, option.value); } } SelectBox.redisplay(from); @@ -133,12 +126,10 @@ }, select_all: function(id) { var box = document.getElementById(id); - var boxOptions = box.options; - var boxOptionsLength = boxOptions.length; - for (var i = 0; i < boxOptionsLength; i++) { - boxOptions[i].selected = 'selected'; + for (var i = 0; i < box.options.length; i++) { + box.options[i].selected = 'selected'; } } }; window.SelectBox = SelectBox; -})(django.jQuery); +})(); diff --git a/static/admin/js/SelectFilter2.js b/static/admin/js/SelectFilter2.js index 0f9a188..acf3996 100644 --- a/static/admin/js/SelectFilter2.js +++ b/static/admin/js/SelectFilter2.js @@ -2,7 +2,7 @@ /* SelectFilter2 - Turns a multiple-select box into a filter interface. -Requires jQuery, core.js, and SelectBox.js. +Requires core.js, SelectBox.js and addevent.js. */ (function($) { 'use strict'; @@ -75,15 +75,15 @@ Requires jQuery, core.js, and SelectBox.js. filter_input.id = field_id + '_input'; selector_available.appendChild(from_box); - var choose_all = quickElement('a', selector_available, gettext('Choose all'), 'title', interpolate(gettext('Click to choose all %s at once.'), [field_name]), 'href', '#', 'id', field_id + '_add_all_link'); + var choose_all = quickElement('a', selector_available, gettext('Choose all'), 'title', interpolate(gettext('Click to choose all %s at once.'), [field_name]), 'href', 'javascript:void(0);', 'id', field_id + '_add_all_link'); choose_all.className = 'selector-chooseall'; //