class CodeSubmitForm(forms.Form): SUBMIT_PATH = config_info.get_config('path', 'submission_code_path') LANGUAGE_CHOICE = tuple(config_info.get_config_items('compiler_option')) BACKEND_VERSION = config_info.get_config('system_version', 'backend') GCC_VERSION = config_info.get_config('system_version', 'gcc') GPP_VERSION = config_info.get_config('system_version', 'gpp') pid = forms.CharField(label='Problem ID') language = forms.ChoiceField(choices=LANGUAGE_CHOICE, initial=Submission.CPP, help_text="Backend: %s<br>gcc: %s<br>g++: %s" % (BACKEND_VERSION, GCC_VERSION, GPP_VERSION)) code = forms.CharField(max_length=40 * 1024, widget=forms.Textarea(attrs={'id': 'code_editor'})) def clean_pid(self): pid = self.cleaned_data['pid'] if not unicode(pid).isnumeric(): raise forms.ValidationError("Problem ID must be a number") try: problem = Problem.objects.get(id=pid) if not user_info.has_problem_auth(self.user, problem): raise forms.ValidationError( "You don't have permission to submit that problem") except Problem.DoesNotExist: logger.warning('Pid %s doe not exist' % pid) raise forms.ValidationError('Problem of this pid does not exist') return pid def submit(self): pid = self.cleaned_data['pid'] code = self.cleaned_data['code'] language = self.cleaned_data['language'] problem = Problem.objects.get(id=pid) problem.total_submission += 1 problem.save() submission = Submission.objects.create( user=self.user, problem=problem, language=language) try: filename = '%s.%s' % ( submission.id, file_info.get_extension(submission.language)) f = open('%s%s' % (self.SUBMIT_PATH, filename), 'w') f.write(code.encode('utf-8')) f.close() except IOError: logger.warning('Sid %s fail to save code' % submission.id) if problem.judge_source == Problem.OTHER: # Send to other judge pass def __init__(self, *args, **kwargs): self.user = kwargs.pop('user', User()) super(CodeSubmitForm, self).__init__(*args, **kwargs)
def testcase(request, pid, tid=None): if request.method == 'POST': try: problem = Problem.objects.get(pk=pid) except Problem.DoesNotExist: logger.warning("problem %s does not exist" % (pid)) raise Http404("problem %s does not exist" % (pid)) if tid is None: testcase = Testcase() testcase.problem = problem else: try: testcase = Testcase.objects.get(pk=tid) except Testcase.DoesNotExist: logger.warning("testcase %s does not exist" % (tid)) raise Http404("testcase %s does not exist" % (tid)) if testcase.problem != problem: logger.warning( "testcase %s does not belong to problem %s" % (tid, pid)) raise Http404( "testcase %s does not belong to problem %s" % (tid, pid)) has_message = False if 'time_limit' in request.POST: testcase.time_limit = request.POST['time_limit'] testcase.memory_limit = request.POST['memory_limit'] testcase.save() logger.info("testcase saved, tid = %s by %s" % (testcase.pk, request.user)) messages.success(request, "testcase %s saved" % testcase.pk) has_message = True if 't_in' in request.FILES: TESTCASE_PATH = config_info.get_config('path', 'testcase_path') try: input_filename = '%s%s.in' % (TESTCASE_PATH, testcase.pk) output_filename = '%s%s.out' % (TESTCASE_PATH, testcase.pk) with open(input_filename, 'w') as t_in: for chunk in request.FILES['t_in'].chunks(): t_in.write(chunk) check_call(['dos2unix', input_filename]) logger.info("testcase %s.in saved by %s" % (testcase.pk, request.user)) with open(output_filename, 'w') as t_out: for chunk in request.FILES['t_out'].chunks(): t_out.write(chunk) check_call(['dos2unix', output_filename]) logger.info("testcase %s.out saved by %s" % (testcase.pk, request.user)) if not has_message: messages.success( request, "testcase %s saved" % testcase.pk) except IOError, OSError: logger.error("saving testcase error") return HttpResponse(json.dumps({'tid': testcase.pk}), content_type="application/json")
def testcase(request, pid, tid=None): if request.method == 'POST': try: problem = Problem.objects.get(pk=pid) except Problem.DoesNotExist: logger.warning("problem %s does not exist" % (pid)) raise Http404("problem %s does not exist" % (pid)) if tid is None: testcase = Testcase() testcase.problem = problem else: try: testcase = Testcase.objects.get(pk=tid) except Testcase.DoesNotExist: logger.warning("testcase %s does not exist" % (tid)) raise Http404("testcase %s does not exist" % (tid)) if testcase.problem != problem: logger.warning("testcase %s does not belong to problem %s" % (tid, pid)) raise Http404("testcase %s does not belong to problem %s" % (tid, pid)) has_message = False if 'time_limit' in request.POST: testcase.time_limit = request.POST['time_limit'] testcase.memory_limit = request.POST['memory_limit'] testcase.save() logger.info("testcase saved, tid = %s by %s" % (testcase.pk, request.user)) messages.success(request, "testcase %s saved" % testcase.pk) has_message = True if 't_in' in request.FILES: TESTCASE_PATH = config_info.get_config('path', 'testcase_path') try: input_filename = '%s%s.in' % (TESTCASE_PATH, testcase.pk) output_filename = '%s%s.out' % (TESTCASE_PATH, testcase.pk) with open(input_filename, 'w') as t_in: for chunk in request.FILES['t_in'].chunks(): t_in.write(chunk) check_call(['dos2unix', input_filename]) logger.info("testcase %s.in saved by %s" % (testcase.pk, request.user)) with open(output_filename, 'w') as t_out: for chunk in request.FILES['t_out'].chunks(): t_out.write(chunk) check_call(['dos2unix', output_filename]) logger.info("testcase %s.out saved by %s" % (testcase.pk, request.user)) if not has_message: messages.success(request, "testcase %s saved" % testcase.pk) except IOError, OSError: logger.error("saving testcase error") return HttpResponse(json.dumps({'tid': testcase.pk}), content_type="application/json")
class UserCreationForm(forms.ModelForm): """A form for creating new users. Includes all the required fields, plus a repeated password.""" USERNAME_BLACK_LIST = get_config('username', 'black_list', filename='user_auth.cfg').splitlines() username = forms.CharField(label='Username', validators=[ RegexValidator( regex='^\w+$', message='Username must be Alphanumeric') ]) email = forms.EmailField(label='Email') password1 = forms.CharField(label='Password', widget=forms.PasswordInput()) password2 = forms.CharField(label='Password Confirmation', widget=forms.PasswordInput()) class Meta: model = User fields = ('username', 'email', 'password1', 'password2') def clean_password2(self): # Check that the two password entries match password1 = self.cleaned_data.get("password1") password2 = self.cleaned_data.get("password2") if password1 and password2 and password1 != password2: raise forms.ValidationError("Passwords don't match") return password2 def clean_username(self): username = self.cleaned_data.get("username") username_lower = username.lower() for token in self.USERNAME_BLACK_LIST: if token.lower() in username_lower: raise forms.ValidationError("Username shouldn't contain %s." % token) return username def save(self, commit=True): user = super(UserCreationForm, self).save(commit=False) user.set_password(self.cleaned_data["password1"]) user.email = self.cleaned_data["email"] if commit: user.save() return user
def user_login(request): next_page = get_next_page(request.GET.get('next')) if request.user.is_authenticated(): return redirect(next_page) if request.method == 'POST': user_form = AuthenticationForm(data=request.POST) if user_form.is_valid(): user = authenticate( username=user_form.cleaned_data['username'], password=user_form.cleaned_data['password']) user.backend = 'django.contrib.auth.backends.ModelBackend' ip = get_ip(request) logger.info('user %s @ %s logged in' % (str(user), ip)) hours = int(get_config('session_expiry', 'expiry')) expiry = hours * 60 * 60 request.session.set_expiry(expiry) logger.info('user %s set session timeout %d-hour' % (str(user), hours)) login(request, user) return redirect(next_page) else: return render_index(request, 'users/auth.html', {'form': user_form, 'title': 'Login'}) return render_index(request, 'users/auth.html', {'form': AuthenticationForm(), 'title': 'Login'})
class User(AbstractBaseUser): ADMIN = 'ADMIN' JUDGE = 'JUDGE' SUB_JUDGE = 'SUB_JUDGE' USER = '******' USER_LEVEL_CHOICE = ( (ADMIN, 'Admin'), (JUDGE, 'Judge'), (SUB_JUDGE, 'Sub-judge'), (USER, 'User'), ) THEME_CHOICE = tuple(get_config_items('web_theme')) DEFAULT_THEME = get_config('theme_settings', 'default') username = models.CharField(max_length=15, default='', unique=True, primary_key=True) email = models.CharField(max_length=100, default='') register_date = models.DateField(default=date.today, auto_now_add=True) user_level = models.CharField(max_length=9, choices=USER_LEVEL_CHOICE, default=USER) theme = models.CharField(max_length=10, choices=THEME_CHOICE, default=DEFAULT_THEME) USERNAME_FIELD = 'username' is_active = models.BooleanField(default=False) is_admin = models.BooleanField(default=False) objects = UserManager() def has_admin_auth(self): has_auth = (self.user_level == self.ADMIN) return has_auth def has_judge_auth(self): has_auth = ((self.user_level == self.ADMIN) or (self.user_level == self.JUDGE)) return has_auth def has_subjudge_auth(self): has_auth = ((self.user_level == self.ADMIN) or (self.user_level == self.JUDGE) or (self.user_level == self.SUB_JUDGE)) return has_auth def get_full_name(self): return self.username def get_short_name(self): return self.username def has_perm(self, perm, obj=None): # Simplest possible answer: Yes, always (To be constructed later) return True def has_module_perms(self, app_label): # Simplest possible answer: Yes, always (To be constructed later) return True def __unicode__(self): return self.username @property def is_superuser(self): return self.is_admin @property def is_staff(self): return self.is_admin
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. """ import requests, base64, json from problem.models import Submission, Problem from vjudge.models import VjudgeID from vjudge.status import status_url from utils.config_info import get_config from utils.log_info import get_logger logger = get_logger() vjudge_username = get_config('vjudge', 'username') vjudge_password = get_config('vjudge', 'password') login_url = 'http://acm.hust.edu.cn/vjudge/user/login.action' submit_url = 'http://acm.hust.edu.cn/vjudge/problem/submit.action' LANGUAGE_CHOICE = { Problem.UVA_JUDGE: {Problem.C: 1 ,Problem.CPP: 3, Problem.CPP11: 5}, Problem.ICPC_JUDGE: {Problem.C: 1 ,Problem.CPP: 3, Problem.CPP11: 5}, Problem.POJ_JUDGE: {Problem.C: 1 ,Problem.CPP: 0, Problem.CPP11: 0} } def submit_to_vjudge(code, submission): try: # Convert to vjudge problem id problem = submission.problem vjudge_id = VjudgeID.objects.get(
""" import time from datetime import datetime from django.shortcuts import render from django.template import RequestContext from django.http import Http404 from django.core.exceptions import PermissionDenied from django.core.exceptions import SuspiciousOperation from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.core.urlresolvers import resolve, reverse from users.models import Notification from utils.config_info import get_config DEFAULT_THEME = get_config('theme_settings', 'default') class CustomHttpExceptionMiddleware(object): def process_exception(self, request, exception): message = unicode(exception) if isinstance(exception, Http404): return render_index(request, 'index/404.html', {'error_message': message}, status=404) elif isinstance(exception, SuspiciousOperation): return render_index(request, 'index/400.html', {'error_message': message}, status=400) elif isinstance(exception, PermissionDenied): return render_index(request, 'index/403.html', {'error_message': message}, status=403) elif isinstance(exception, Exception): return render_index(request, 'index/500.html', {'error_message': message}, status=500) def render_index(request, *args, **kwargs):
STATIC_URL = '/static/' MEDIA_ROOT = os.path.join(PROJECT_ROOT, 'media') MEDIA_URL = '/media/' # django-axes 1.3.8 configurations # https://pypi.python.org/pypi/django-axes/ # redirect to broken page when exceed wrong-try limits AXES_LOCKOUT_URL = '/users/block_wrong_tries' # freeze login access for that ip for 0.1*60 = 6 minites AXES_COOLOFF_TIME = 0.1 EMAIL_USE_TLS = True EMAIL_HOST = 'smtp.gmail.com' EMAIL_HOST_USER = get_config('email', 'user') EMAIL_HOST_PASSWORD = get_config('email', 'password') EMAIL_PORT = 587 # django-ckeditor configurations CKEDITOR_UPLOAD_PATH = 'uploads/' CKEDITOR_IMAGE_BACKEND = 'pillow' CKEDITOR_CONFIGS = { 'default': { 'toolbar': 'full', }, } # django-bower settings BOWER_COMPONENTS_ROOT = os.path.join(PROJECT_ROOT, 'components')
""" import time from datetime import datetime from django.shortcuts import render from django.template import RequestContext from django.http import Http404 from django.core.exceptions import PermissionDenied from django.core.exceptions import SuspiciousOperation from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.core.urlresolvers import resolve, reverse from users.models import Notification from utils.config_info import get_config DEFAULT_THEME = get_config('theme_settings', 'default') class CustomHttpExceptionMiddleware(object): def process_exception(self, request, exception): message = unicode(exception) if isinstance(exception, Http404): return render_index(request, 'index/404.html', {'error_message': message}, status=404) elif isinstance(exception, SuspiciousOperation): return render_index(request, 'index/400.html', {'error_message': message}, status=400) elif isinstance(exception, PermissionDenied): return render_index(request, 'index/403.html', {'error_message': message}, status=403) elif isinstance(exception, Exception): return render_index(request, 'index/500.html', {'error_message': message}, status=500)
import os.path from utils import config_info from problem.models import Problem, Testcase from django.db.models import Q from datetime import datetime SPECIAL_PATH = config_info.get_config('path', 'special_judge_path') PARTIAL_PATH = config_info.get_config('path', 'partial_judge_path') TESTCASE_PATH = config_info.get_config('path', 'testcase_path') def get_testcase(problem): return Testcase.objects.filter(problem=problem).order_by('id') def get_problem_list(user): if user.is_anonymous(): return Problem.objects.filter(visible=True).order_by('id') else: if user.is_admin: return Problem.objects.all().order_by('id') else: return Problem.objects.filter(Q(visible=True) | Q(owner=user)).order_by('id') def get_owner_problem_list(user): return Problem.objects.filter(owner=user).order_by('id') def get_problem_file_extension(problem):
from django.core.urlresolvers import reverse from django.core.mail import EmailMultiAlternatives from django.template.loader import render_to_string from django.http import HttpResponseRedirect from contest.models import Contest from contest.models import Contestant from problem.models import Submission, SubmissionDetail from users.models import User, UserProfile, Notification from utils.log_info import get_logger from utils.config_info import get_config from django.conf import settings EMAIL_HOST_USER = get_config("email", "user") logger = get_logger() def has_contest_ownership(curr_user, curr_contest): curr_user = validate_user(curr_user) if curr_user == curr_contest.owner: return True contest_coowners = curr_contest.coowner.all() return curr_user in contest_coowners def has_group_ownership(curr_user, curr_group):
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. """ import requests, json, time, datetime from problem.models import * from utils.config_info import get_config from utils.log_info import get_logger logger = get_logger() vjudge_username = get_config('vjudge', 'username') status_url = 'http://acm.hust.edu.cn/vjudge/problem/fetchStatus.action?draw=1&columns%5B0%5D%5Bdata%5D=0&columns%5B0%5D%5Bname%5D=&columns%5B0%5D%5Bsearchable%5D=true&columns%5B0%5D%5Borderable%5D=false&columns%5B0%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B0%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B1%5D%5Bdata%5D=1&columns%5B1%5D%5Bname%5D=&columns%5B1%5D%5Bsearchable%5D=true&columns%5B1%5D%5Borderable%5D=false&columns%5B1%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B1%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B2%5D%5Bdata%5D=2&columns%5B2%5D%5Bname%5D=&columns%5B2%5D%5Bsearchable%5D=true&columns%5B2%5D%5Borderable%5D=false&columns%5B2%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B2%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B3%5D%5Bdata%5D=3&columns%5B3%5D%5Bname%5D=&columns%5B3%5D%5Bsearchable%5D=true&columns%5B3%5D%5Borderable%5D=false&columns%5B3%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B3%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B4%5D%5Bdata%5D=4&columns%5B4%5D%5Bname%5D=&columns%5B4%5D%5Bsearchable%5D=true&columns%5B4%5D%5Borderable%5D=false&columns%5B4%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B4%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B5%5D%5Bdata%5D=5&columns%5B5%5D%5Bname%5D=&columns%5B5%5D%5Bsearchable%5D=true&columns%5B5%5D%5Borderable%5D=false&columns%5B5%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B5%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B6%5D%5Bdata%5D=6&columns%5B6%5D%5Bname%5D=&columns%5B6%5D%5Bsearchable%5D=true&columns%5B6%5D%5Borderable%5D=false&columns%5B6%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B6%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B7%5D%5Bdata%5D=7&columns%5B7%5D%5Bname%5D=&columns%5B7%5D%5Bsearchable%5D=true&columns%5B7%5D%5Borderable%5D=false&columns%5B7%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B7%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B8%5D%5Bdata%5D=8&columns%5B8%5D%5Bname%5D=&columns%5B8%5D%5Bsearchable%5D=true&columns%5B8%5D%5Borderable%5D=false&columns%5B8%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B8%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B9%5D%5Bdata%5D=9&columns%5B9%5D%5Bname%5D=&columns%5B9%5D%5Bsearchable%5D=true&columns%5B9%5D%5Borderable%5D=false&columns%5B9%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B9%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B10%5D%5Bdata%5D=10&columns%5B10%5D%5Bname%5D=&columns%5B10%5D%5Bsearchable%5D=true&columns%5B10%5D%5Borderable%5D=false&columns%5B10%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B10%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B11%5D%5Bdata%5D=11&columns%5B11%5D%5Bname%5D=&columns%5B11%5D%5Bsearchable%5D=true&columns%5B11%5D%5Borderable%5D=false&columns%5B11%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B11%5D%5Bsearch%5D%5Bregex%5D=false&order%5B0%5D%5Bcolumn%5D=0&order%5B0%5D%5Bdir%5D=desc&start=0&length=20&search%5Bvalue%5D=&search%5Bregex%5D=false&un={0}&OJId=All&probNum=&res=0&language=&orderBy=run_id'.format(vjudge_username) status_keywords = ['accept', 'answer', 'compilation', 'compile', 'error', 'exceed', 'limit', 'memory', 'runtime', 'time', 'wrong'] # Fetch status every 3 seconds # Should run as daemon def status_daemon(): while True: try: raw_status = requests.get(status_url, timeout=5).text.encode("utf-8") raw_status = json.loads(raw_status)['data'] refined_status = {}
from django.core.urlresolvers import reverse from django.core.mail import EmailMultiAlternatives from django.template.loader import render_to_string from django.http import HttpResponseRedirect from contest.models import Contest from contest.models import Contestant from problem.models import Submission, SubmissionDetail from users.models import User, UserProfile, Notification from utils.log_info import get_logger from utils.config_info import get_config from django.conf import settings EMAIL_HOST_USER = get_config('email', 'user') logger = get_logger() def has_contest_ownership(curr_user, curr_contest): curr_user = validate_user(curr_user) if curr_user == curr_contest.owner: return True contest_coowners = curr_contest.coowner.all() return curr_user in contest_coowners def has_group_ownership(curr_user, curr_group):
import os.path from utils import config_info from problem.models import Problem, Testcase from django.db.models import Q from datetime import datetime SPECIAL_PATH = config_info.get_config('path', 'special_judge_path') PARTIAL_PATH = config_info.get_config('path', 'partial_judge_path') TESTCASE_PATH = config_info.get_config('path', 'testcase_path') def get_testcase(problem): return Testcase.objects.filter(problem=problem).order_by('id') def get_problem_list(user): if user.is_anonymous(): return Problem.objects.filter(visible=True).order_by('id') else: if user.is_admin: return Problem.objects.all().order_by('id') else: return Problem.objects.filter(Q(visible=True) | Q(owner=user)).order_by('id') def get_owner_problem_list(user): return Problem.objects.filter(owner=user).order_by('id') def get_problem_file_extension(problem): if problem.judge_language == problem.C: return ".c" if problem.judge_language == problem.CPP: return ".cpp" if problem.judge_language == problem.CPP11:
import hashlib import random from django.core.urlresolvers import reverse from django.core.mail import EmailMultiAlternatives from django.template.loader import render_to_string from contest.models import Contest from contest.models import Contestant from problem.models import Submission, SubmissionDetail from users.models import User, UserProfile, Notification from utils.log_info import get_logger from utils.config_info import get_config from django.conf import settings EMAIL_HOST_USER = get_config('email', 'user') logger = get_logger() def has_contest_ownership(curr_user, curr_contest): curr_user = validate_user(curr_user) if curr_user == curr_contest.owner: return True contest_coowners = curr_contest.coowner.all() return curr_user in contest_coowners
STATIC_URL = "/static/" MEDIA_ROOT = os.path.join(PROJECT_ROOT, "media") MEDIA_URL = "/media/" # django-axes 1.3.8 configurations # https://pypi.python.org/pypi/django-axes/ # redirect to broken page when exceed wrong-try limits AXES_LOCKOUT_URL = "/users/block_wrong_tries" # freeze login access for that ip for 0.1*60 = 6 minites AXES_COOLOFF_TIME = 0.1 EMAIL_USE_TLS = True EMAIL_HOST = "smtp.gmail.com" EMAIL_HOST_USER = get_config("email", "user") EMAIL_HOST_PASSWORD = get_config("email", "password") EMAIL_PORT = 587 # django-ckeditor configurations CKEDITOR_UPLOAD_PATH = "uploads/" CKEDITOR_IMAGE_BACKEND = "pillow" CKEDITOR_CONFIGS = {"default": {"toolbar": "full"}} # django-bower settings BOWER_COMPONENTS_ROOT = os.path.join(PROJECT_ROOT, "components") BOWER_INSTALLED_APPS = ( "Chart.js", "jquery",