Release 0.1.0

main
Nikita Manovich 8 years ago
commit eb9fba3685

@ -0,0 +1,8 @@
/.git
/share
/data
/media
/.env
/.vscode
/db.sqlite3
/keys

15
.gitignore vendored

@ -0,0 +1,15 @@
# Project Specific
/data/
/share/
/static/
/db.sqlite3
/.env
/keys
/logs
# Ignore temporary files
docker-compose.override.yml
/.vscode
__pycache__
*.pyc
._*

@ -0,0 +1,4 @@
# How to contribute to Computer Vision Annotation Tool (CVAT)
When contributing to this repository, please first discuss the change you wish to make via issue,
email, or any other method with the owners of this repository before making a change.

@ -0,0 +1,99 @@
FROM ubuntu:16.04
ARG http_proxy
ARG https_proxy
ARG no_proxy
ENV TERM=xterm \
http_proxy=${http_proxy} \
https_proxy=${https_proxy} \
no_proxy=${no_proxy}
ENV LANG='C.UTF-8' \
LC_ALL='C.UTF-8'
ARG USER
ARG TF_ANNOTATION
ENV TF_ANNOTATION=${TF_ANNOTATION}
ARG DJANGO_CONFIGURATION
ENV DJANGO_CONFIGURATION=${DJANGO_CONFIGURATION}
# Install necessary apt packages
RUN apt-get update && \
apt-get install -yq \
python-software-properties \
software-properties-common \
wget && \
add-apt-repository ppa:mc3man/xerus-media -y && \
add-apt-repository ppa:mc3man/gstffmpeg-keep -y && \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -yq \
apache2 \
apache2-dev \
libapache2-mod-xsendfile \
supervisor \
ffmpeg \
gstreamer0.10-ffmpeg \
libldap2-dev \
libsasl2-dev \
python3-dev \
python3-pip \
unzip \
unrar \
p7zip-full \
vim && \
rm -rf /var/lib/apt/lists/*
# Add a non-root user
ENV USER=${USER}
ENV HOME /home/${USER}
WORKDIR ${HOME}
RUN adduser --shell /bin/bash --disabled-password --gecos "" ${USER}
# Install tf annotation if need
COPY cvat/apps/tf_annotation/docker_setup_tf_annotation.sh /tmp/tf_annotation/
COPY cvat/apps/tf_annotation/requirements.txt /tmp/tf_annotation/
ENV TF_ANNOTATION_MODEL_PATH=${HOME}/rcnn/frozen_inference_graph.pb
RUN if [ "$TF_ANNOTATION" = "yes" ]; then \
/tmp/tf_annotation/docker_setup_tf_annotation.sh; \
fi
ARG WITH_TESTS
RUN if [ "$WITH_TESTS" = "yes" ]; then \
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \
echo 'deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main' | tee /etc/apt/sources.list.d/google-chrome.list && \
wget -qO- https://deb.nodesource.com/setup_9.x | bash - && \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -yq \
google-chrome-stable \
nodejs && \
rm -rf /var/lib/apt/lists/*; \
mkdir tests && cd tests && npm install \
eslint \
eslint-detailed-reporter \
karma \
karma-chrome-launcher \
karma-coverage \
karma-junit-reporter \
karma-qunit \
qunit; \
echo "export PATH=~/tests/node_modules/.bin:${PATH}" >> ~/.bashrc; \
fi
# Install and initialize CVAT, copy all necessary files
COPY cvat/requirements/ /tmp/requirements/
COPY supervisord.conf mod_wsgi.conf wait-for-it.sh manage.py ${HOME}/
RUN pip3 install --no-cache-dir -r /tmp/requirements/${DJANGO_CONFIGURATION}.txt
COPY cvat/ ${HOME}/cvat
COPY tests ${HOME}/tests
RUN chown -R ${USER}:${USER} .
# RUN all commands below as 'django' user
USER ${USER}
RUN mkdir data share media keys logs /tmp/supervisord
RUN python3 manage.py collectstatic
EXPOSE 8080 8443
ENTRYPOINT ["/usr/bin/supervisord"]

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 annotation
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.

@ -0,0 +1,106 @@
# Computer Vision Annotation Tool (CVAT)
CVAT is completely re-designed and re-implemented version of [Video Annotation Tool from Irvine, California](http://carlvondrick.com/vatic/) tool. It is free, online, interactive video and image annotation tool for computer vision. It is being used by our team to annotate million of objects with different properties. Many UI and UX decisions are based on feedbacks from professional data annotation team.
## LICENSE
Code released under the [MIT License](https://opensource.org/licenses/MIT).
## INSTALLATION
These instructions below should work for Ubuntu 16.04. Probably it will work on other OSes as well with minor modifications.
### Install [Docker CE](https://www.docker.com/community-edition) or [Docker EE](https://www.docker.com/enterprise-edition) from official site
Please read official manual [here](https://docs.docker.com/engine/installation/linux/docker-ce/ubuntu/).
### Install the latest driver for your graphics card
The step is necessary only to run tf_annotation app. If you don't have a Nvidia GPU you can skip the step.
```bash
sudo add-apt-repository ppa:graphics-drivers/ppa
sudo apt-get update
sudo apt-cache search nvidia-* # find latest nvidia driver
sudo apt-get install nvidia-* # install the nvidia driver
sudo apt-get install mesa-common-dev
sudo apt-get install freeglut3-dev
sudo apt-get install nvidia-modprobe
```
Reboot your PC and verify installation by `nvidia-smi` command.
### Install [Nvidia-Docker](https://github.com/NVIDIA/nvidia-docker)
The step is necessary only to run tf_annotation app. If you don't have a Nvidia GPU you can skip the step. See detailed installation instructions on repository page.
### Install docker-compose (1.19.0 or newer)
```bash
sudo pip install docker-compose
```
### Build docker images
To build all necessary docker images run `docker-compose build` command. By default, in production mode the tool uses PostgreSQL as database, Redis for caching.
### Run containers without tf_annotation app
To start all containers run `docker-compose up -d` command. Go to [localhost:8080](http://localhost:8080/). You should see a login page.
### Run containers with tf_annotation app
If you would like to enable tf_annotation app first of all be sure that nvidia-driver, nvidia-docker and docker-compose>=1.19.0 are installed properly (see instructions above) and `docker info | grep 'Runtimes'` output contains `nvidia`.
Run following command:
```bash
docker-compose -f docker-compose.yml -f docker-compose.nvidia.yml up -d --build
```
### Create superuser account
You can [register a user](http://localhost:8080/auth/register) but by default it will not have rights even to view list of tasks. Thus you should create a superuser. The superuser can use admin panel to assign correct groups to the user. Please use the command below:
```bash
docker exec -it cvat sh -c '/usr/bin/python3 ~/manage.py createsuperuser'
```
Type your login/password for the superuser [on the login page](http://localhost:8080/auth/login) and press **Login** button. Now you should be able to create a new annotation task. Please read documentation for more details.
### Stop all containers
The command below will stop and remove containers, networks, volumes, and images
created by `up`.
```bash
docker-compose down
```
### Advanced settings
If you want to access you instance of CVAT outside of your localhost you should specify [ALLOWED_HOSTS](https://docs.djangoproject.com/en/2.0/ref/settings/#allowed-hosts) environment variable. The best way to do that is to create [docker-compose.override.yml](https://docs.docker.com/compose/extends/) and put all your extra settings here.
```yml
version: "2.3"
services:
cvat:
environment:
ALLOWED_HOSTS: .example.com
ports:
- "80:8080"
```
### Annotation logs
It is possible to proxy annotation logs from client to another server over http. For examlpe you can use Logstash.
To do that set DJANGO_LOG_SERVER_URL environment variable in cvat section of docker-compose.yml
file (or add this variable to docker-compose.override.yml).
```yml
version: "2.3"
services:
cvat:
environment:
DJANGO_LOG_SERVER_URL: https://annotation.example.com:5000
```

@ -0,0 +1 @@
default_app_config = 'cvat.apps.authentication.apps.AuthenticationConfig'

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

@ -0,0 +1,18 @@
from django.apps import AppConfig
from django.db.models.signals import post_migrate, post_save
from .settings.authentication import DJANGO_AUTH_TYPE
class AuthenticationConfig(AppConfig):
name = 'cvat.apps.authentication'
def ready(self):
from . import signals
from django.contrib.auth.models import User
post_migrate.connect(signals.create_groups)
if DJANGO_AUTH_TYPE == 'SIMPLE':
post_save.connect(signals.create_user, sender=User, dispatch_uid="create_user")
import django_auth_ldap.backend
django_auth_ldap.backend.populate_user.connect(signals.update_ldap_groups)

@ -0,0 +1,32 @@
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.shortcuts import resolve_url, reverse
from django.http import JsonResponse
from urllib.parse import urlparse
from django.contrib.auth.views import redirect_to_login
from functools import wraps
from django.conf import settings
def login_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None, redirect_methods=['GET']):
def decorator(view_func):
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
if request.user.is_authenticated:
return view_func(request, *args, **kwargs)
else:
if request.method not in redirect_methods:
return JsonResponse({'login_page_url': reverse('login')}, status=403)
path = request.build_absolute_uri()
resolved_login_url = resolve_url(login_url or settings.LOGIN_URL)
# If the login url is the same scheme and net location then just
# use the path as the "next" url.
login_scheme, login_netloc = urlparse(resolved_login_url)[:2]
current_scheme, current_netloc = urlparse(path)[:2]
if ((not login_scheme or login_scheme == current_scheme) and
(not login_netloc or login_netloc == current_netloc)):
path = request.get_full_path()
return redirect_to_login(path, resolved_login_url, redirect_field_name)
return _wrapped_view
return decorator(function) if function else decorator

@ -0,0 +1,55 @@
from django.contrib.auth.forms import (
UsernameField,
AuthenticationForm,
UserCreationForm,
)
from django.utils.translation import gettext, gettext_lazy as _
from django.contrib.auth.models import User
from django import forms
class AuthForm(AuthenticationForm):
username = UsernameField(
widget=forms.TextInput(attrs={'autofocus': True, 'placeholder': "Username"}),
)
password = forms.CharField(
label=_("Password"),
strip=False,
widget=forms.PasswordInput(attrs={'placeholder': "Password"}),
)
class NewUserForm(UserCreationForm):
username = UsernameField(
widget=forms.TextInput(attrs={'autofocus': True, 'placeholder': "Username (required)"}),
required=True,
)
first_name = UsernameField(
widget=forms.TextInput(attrs={'placeholder': "First name"}),
required=False,
)
last_name = UsernameField(
widget=forms.TextInput(attrs={'placeholder': "Last name"}),
required=False,
)
email = forms.EmailField(
widget=forms.EmailInput(attrs={'placeholder': "Email (required)"}),
required=True,
)
password1 = forms.CharField(
label=_("Password"),
strip=False,
widget=forms.PasswordInput(attrs={'placeholder': "Password (required)"}),
)
password2 = forms.CharField(
label=_("Password confirmation"),
widget=forms.PasswordInput(attrs={'placeholder': "Password confirmation (required)"}),
strip=False,
)
class Meta:
model = User
fields = ('username', 'first_name', 'last_name', 'email', )

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

@ -0,0 +1,51 @@
from django.conf import settings
import ldap
from django_auth_ldap.config import LDAPSearch, NestedActiveDirectoryGroupType
# Baseline configuration.
settings.AUTH_LDAP_SERVER_URI = ""
# Credentials for LDAP server
settings.AUTH_LDAP_BIND_DN = ""
settings.AUTH_LDAP_BIND_PASSWORD = ""
# Set up basic user search
settings.AUTH_LDAP_USER_SEARCH = LDAPSearch("dc=example,dc=com",
ldap.SCOPE_SUBTREE, "(sAMAccountName=%(user)s)")
# Set up the basic group parameters.
settings.AUTH_LDAP_GROUP_SEARCH = LDAPSearch("dc=example,dc=com",
ldap.SCOPE_SUBTREE, "(objectClass=group)")
settings.AUTH_LDAP_GROUP_TYPE = NestedActiveDirectoryGroupType()
# # Simple group restrictions
settings.AUTH_LDAP_REQUIRE_GROUP = "cn=cvat,ou=Groups,dc=example,dc=com"
# Populate the Django user from the LDAP directory.
settings.AUTH_LDAP_USER_ATTR_MAP = {
"first_name": "givenName",
"last_name": "sn",
"email": "mail",
}
settings.AUTH_LDAP_ALWAYS_UPDATE_USER = True
# Cache group memberships for an hour to minimize LDAP traffic
settings.AUTH_LDAP_CACHE_GROUPS = True
settings.AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600
settings.AUTH_LDAP_AUTHORIZE_ALL_USERS = True
# Keep ModelBackend around for per-user permissions and maybe a local
# superuser.
settings.AUTHENTICATION_BACKENDS.append('django_auth_ldap.backend.LDAPBackend')
AUTH_LDAP_ADMIN_GROUPS = [
"cn=cvat_admins,ou=Groups,dc=example,dc=com"
]
AUTH_LDAP_DATA_ANNOTATORS_GROUPS = [
]
AUTH_LDAP_DEVELOPER_GROUPS = [
"cn=cvat_users,ou=Groups,dc=example,dc=com"
]

@ -0,0 +1,2 @@
# Specify groups that new users will have
AUTH_SIMPLE_DEFAULT_GROUPS = []

@ -0,0 +1,53 @@
from django.conf import settings
import os
settings.LOGIN_URL = 'login'
settings.LOGIN_REDIRECT_URL = '/'
settings.AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
]
AUTH_LDAP_DEVELOPER_GROUPS = []
AUTH_LDAP_DATA_ANNOTATORS_GROUPS = []
AUTH_LDAP_ADMIN_GROUPS = []
DJANGO_AUTH_TYPE = 'LDAP' if os.environ.get('DJANGO_AUTH_TYPE', '') == 'LDAP' else 'SIMPLE'
if DJANGO_AUTH_TYPE == 'LDAP':
from .auth_ldap import *
else:
from .auth_simple import *
# Definition of CVAT groups with permissions for task and annotation objects
# Annotator - can modify annotation for task, but cannot add, change and delete tasks
# Developer - can create tasks and modify (delete) owned tasks and any actions with annotation
# Admin - can any actions for task and annotation, can login to admin area and manage groups and users
cvat_groups_definition = {
'user': {
'description': '',
'permissions': {
'task': ['view', 'add', 'change', 'delete'],
'annotation': ['view', 'change'],
},
'ldap_groups': AUTH_LDAP_DEVELOPER_GROUPS,
},
'annotator': {
'description': '',
'permissions': {
'task': ['view'],
'annotation': ['view', 'change'],
},
'ldap_groups': AUTH_LDAP_DATA_ANNOTATORS_GROUPS,
},
'admin': {
'description': '',
'permissions': {
'task': ['view', 'add', 'change', 'delete'],
'annotation': ['view', 'change'],
},
'ldap_groups': AUTH_LDAP_ADMIN_GROUPS,
},
}

@ -0,0 +1,57 @@
from django.db import models
from django.conf import settings
from .settings import authentication
from django.contrib.auth.models import User, Group
def setup_group_permissions(group):
from cvat.apps.engine.models import Task
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
def append_permissions_for_model(model):
content_type = ContentType.objects.get_for_model(model)
for perm_target, actions in authentication.cvat_groups_definition[group.name]['permissions'].items():
for action in actions:
codename = '{}_{}'.format(action, perm_target)
try:
perm = Permission.objects.get(codename=codename, content_type=content_type)
group_permissions.append(perm)
except:
pass
group_permissions = []
append_permissions_for_model(Task)
group.permissions.set(group_permissions)
group.save()
def create_groups(sender, **kwargs):
for cvat_role, _ in authentication.cvat_groups_definition.items():
Group.objects.get_or_create(name=cvat_role)
def update_ldap_groups(sender, user=None, ldap_user=None, **kwargs):
user_groups = []
for cvat_role, role_settings in authentication.cvat_groups_definition.items():
group_instance, _ = Group.objects.get_or_create(name=cvat_role)
setup_group_permissions(group_instance)
for ldap_group in role_settings['ldap_groups']:
if ldap_group.lower() in ldap_user.group_dns:
user_groups.append(group_instance)
user.save()
user.groups.set(user_groups)
user.is_staff = user.is_superuser = user.groups.filter(name='admin').exists()
def create_user(sender, instance, created, **kwargs):
if instance.is_superuser and instance.is_staff:
admin_group, _ = Group.objects.get_or_create(name='admin')
admin_group.user_set.add(instance)
if created:
for cvat_role, _ in authentication.cvat_groups_definition.items():
group_instance, _ = Group.objects.get_or_create(name=cvat_role)
setup_group_permissions(group_instance)
if cvat_role in authentication.AUTH_SIMPLE_DEFAULT_GROUPS:
instance.groups.add(group_instance)

@ -0,0 +1,12 @@
{% extends "auth_base.html" %}
{% block title %}Forbidden{% endblock %}
{% block content %}
<h1>Forbidden</h1>
{% if user.is_authenticated %}
<p align="center">Your account doesn't have access to this page. To proceed,
please login with an account that has access or contact your admin.<br>
<big><a href="{% url 'login' %}">Login</a><big></p>
{% endif %}
{% endblock %}

@ -0,0 +1,99 @@
<!--
Copyright (c) 2018 by Tyler Fry (https://codepen.io/frytyler/pen/EGdtg)
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.
-->
<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8">
<title>{% block title %}{% endblock %}</title>
<style>
.btn { display: inline-block; *display: inline; *zoom: 1; padding: 4px 10px 4px; margin-bottom: 0; font-size: 13px; line-height: 18px; color: #333333; text-align: center;text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); vertical-align: middle; background-color: #f5f5f5; background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); background-image: -ms-linear-gradient(top, #ffffff, #e6e6e6); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); background-image: linear-gradient(top, #ffffff, #e6e6e6); background-repeat: repeat-x; filter: progid:dximagetransform.microsoft.gradient(startColorstr=#ffffff, endColorstr=#e6e6e6, GradientType=0); border-color: #e6e6e6 #e6e6e6 #e6e6e6; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); border: 1px solid #e6e6e6; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); cursor: pointer; *margin-left: .3em; }
.btn:hover, .btn:active, .btn.active, .btn.disabled, .btn[disabled] { background-color: #e6e6e6; }
.btn-large { padding: 9px 14px; font-size: 15px; line-height: normal; -webkit-border-radius: 5px; -moz-border-radius: 5px; border-radius: 5px; }
.btn:hover { color: #333333; text-decoration: none; background-color: #e6e6e6; background-position: 0 -15px; -webkit-transition: background-position 0.1s linear; -moz-transition: background-position 0.1s linear; -ms-transition: background-position 0.1s linear; -o-transition: background-position 0.1s linear; transition: background-position 0.1s linear; }
.btn-primary, .btn-primary:hover { text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); color: #ffffff; }
.btn-primary.active { color: rgba(255, 255, 255, 0.75); }
.btn-primary { background-color: #4a77d4; background-image: -moz-linear-gradient(top, #6eb6de, #4a77d4); background-image: -ms-linear-gradient(top, #6eb6de, #4a77d4); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#6eb6de), to(#4a77d4)); background-image: -webkit-linear-gradient(top, #6eb6de, #4a77d4); background-image: -o-linear-gradient(top, #6eb6de, #4a77d4); background-image: linear-gradient(top, #6eb6de, #4a77d4); background-repeat: repeat-x; filter: progid:dximagetransform.microsoft.gradient(startColorstr=#6eb6de, endColorstr=#4a77d4, GradientType=0); border: 1px solid #3762bc; text-shadow: 1px 1px 1px rgba(0,0,0,0.4); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.5); }
.btn-primary:hover, .btn-primary:active, .btn-primary.active, .btn-primary.disabled, .btn-primary[disabled] { filter: none; background-color: #4a77d4; }
.btn-block { width: 100%; display:block; }
* { -webkit-box-sizing:border-box; -moz-box-sizing:border-box; -ms-box-sizing:border-box; -o-box-sizing:border-box; box-sizing:border-box; }
html { width: 100%; height:100%; overflow:hidden; }
body {
width: 100%;
height:100%;
font-family: 'Open Sans', sans-serif;
background: #092756;
background: -moz-linear-gradient(-45deg, #afcfec 0%, #17458f 100%);
background: -webkit-linear-gradient(-45deg, #afcfec 0%,#17458f 100%);
}
.login {
position: absolute;
top: 50%;
left: 50%;
margin: -150px 0 0 -150px;
width:300px;
height:300px;
}
.login h1 { color: #fff; text-shadow: 0 0 10px rgba(0,0,0,0.3); letter-spacing:1px; text-align:center; }
.note {
color: #fff;
text-shadow: 0 0 10px rgba(0,0,0,0.3);
position: absolute;
bottom: 10%;
width: 100%;
margin: 0 auto;
font-size: large;
text-align: center;
}
input {
width: 100%;
margin-bottom: 10px;
background: rgba(0,0,0,0.3);
border: none;
outline: none;
padding: 10px;
font-size: 13px;
color: #fff;
text-shadow: 1px 1px 1px rgba(0,0,0,0.3);
border: 1px solid rgba(0,0,0,0.3);
border-radius: 4px;
box-shadow: inset 0 -5px 45px rgba(100,100,100,0.2), 0 1px 1px rgba(255,255,255,0.2);
-webkit-transition: box-shadow .5s ease;
-moz-transition: box-shadow .5s ease;
-o-transition: box-shadow .5s ease;
-ms-transition: box-shadow .5s ease;
transition: box-shadow .5s ease;
}
input::placeholder {
color: rgb(168, 168, 168);
}
input:focus { box-shadow: inset 0 -5px 45px rgba(100,100,100,0.4), 0 1px 1px rgba(255,255,255,0.2); }
</style>
</head>
<body>
<div class="login">
{% block content %} {% endblock %}
</div>
<div class="note">
{% block note %} {% endblock %}
</div>
</body>
</html>

@ -0,0 +1,22 @@
{% extends "auth_base.html" %}
{% block title %}Login{% endblock %}
{% block content %}
<h1>Login</h1>
{% if form.errors %}
<small>Your username and password didn't match. Please try again.</small>
{% endif %}
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
{% for field in form %}
{{ field }}
{% endfor %}
<input type="hidden" name="next" value="{{ next }}" />
<button type="submit" class="btn btn-primary btn-block btn-large">Login</button>
</form>
{% endblock %}
{% block note%}
<p>Have not registered yet? <a href="{% url 'register' %}">Register here</a>.</p>
{% endblock %}

@ -0,0 +1,22 @@
{% extends "auth_base.html" %}
{% block title %}Login{% endblock %}
{% block content %}
<h1>Login</h1>
{% if form.errors %}
<small>Your username and password didn't match. Please try again.</small>
{% endif %}
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
{% for field in form %}
{{ field }}
{% endfor %}
<input type="hidden" name="next" value="{{ next }}" />
<button type="submit" class="btn btn-primary btn-block btn-large">Login</button>
</form>
{% endblock %}
{% block note %}
{% include "note.html" %}
{% endblock %}

@ -0,0 +1,27 @@
{% extends "auth_base.html" %}
{% block title %}Create user{% endblock %}
{% block content %}
<div class="login">
<h1>Create user</h1>
<form method="post" action="{% url 'register' %}">
{% csrf_token %}
{% for field in form %}
{{ field }}
{% if field.errors %}
{% for error in field.errors %}
<small>{{ error }}<small>
{% endfor %}
{% endif %}
{% endfor %}
<input type="hidden" name="next" value="{{ next }}" />
<button type="submit" class="btn btn-primary btn-block btn-large">Register</button>
</form>
</div>
<div class="note">
</div>
{% endblock %}

@ -0,0 +1,16 @@
<div class='userProfile'>
{% if user.is_authenticated %}
{% if user.ldap_user and 'thumbnailPhoto' in user.ldap_user.attrs %}
{# TODO insert photo from ldap #}
{% endif %}
{% if user.first_name and user.last_name %}
<p>{{ user.first_name }} {{ user.last_name }}</p>
{% else %}
<p>{{ user.username }}</p>
{% endif %}
<form action="{% url 'logout' %}">
<input type="submit" class="menuButton semiBold h2" value="Logout">
</form>
{% endif %}
</div>

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

@ -0,0 +1,15 @@
from django.urls import path
import os
from django.contrib.auth import views as auth_views
from . import forms
from . import views
from .settings.authentication import DJANGO_AUTH_TYPE
login_page = 'login{}.html'.format('_ldap' if DJANGO_AUTH_TYPE == 'LDAP' else '')
urlpatterns = [
path('login', auth_views.LoginView.as_view(form_class=forms.AuthForm, template_name=login_page), name='login'),
path('logout', auth_views.LogoutView.as_view(next_page='login'), name='logout'),
path('register', views.register_user, name='register'),
]

@ -0,0 +1,21 @@
from django.shortcuts import render
from django.contrib.auth.views import LoginView
from django.http import HttpResponseRedirect
from . import forms
from django.contrib.auth import login, authenticate
from django.shortcuts import render, redirect
def register_user(request):
if request.method == 'POST':
form = forms.NewUserForm(request.POST)
if form.is_valid():
form.save()
username = form.cleaned_data.get('username')
raw_password = form.cleaned_data.get('password1')
user = authenticate(username=username, password=raw_password)
login(request, user)
return redirect('/')
else:
form = forms.NewUserForm()
return render(request, 'register.html', {'form': form})

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

@ -0,0 +1,5 @@
from django.apps import AppConfig
class DashboardConfig(AppConfig):
name = 'dashboard'

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

@ -0,0 +1,546 @@
"use strict";
/* Dashboard entrypoint */
window.cvat = window.cvat || {};
window.cvat.dashboard = window.cvat.dashboard || {};
window.cvat.dashboard.uiCallbacks = window.cvat.dashboard.uiCallbacks || [];
window.cvat.dashboard.uiCallbacks.push(function(elements) {
elements.each(function(idx) {
let elem = $(elements[idx]);
let taskID = +elem.attr('id').split('_')[1];
let taskName = $.trim($( elem.find('label.dashboardTaskNameLabel')[0] ).text());
let buttonsUI = elem.find('div.dashboardButtonsUI')[0];
let dumpButton = $( $(buttonsUI).find('button.dashboardDumpAnnotation')[0] );
let uploadButton = $( $(buttonsUI).find('button.dashboardUploadAnnotation')[0] );
let updateButton = $( $(buttonsUI).find('button.dashboardUpdateTask')[0] );
let deleteButton = $( $(buttonsUI).find('button.dashboardDeleteTask')[0] );
let bugTrackerButton = $(buttonsUI).find('.dashboardOpenTrackerButton');
if (bugTrackerButton.length) {
bugTrackerButton = $(bugTrackerButton[0]);
bugTrackerButton.on('click', function() {
window.open($(buttonsUI).find('a.dashboardBugTrackerLink').attr('href'));
});
}
dumpButton.on('click', function() {
window.cvat.dashboard.taskID = taskID;
window.cvat.dashboard.taskName = taskName;
dumpAnnotationRequest(dumpButton, taskID, taskName);
});
uploadButton.on('click', function() {
window.cvat.dashboard.taskID = taskID;
window.cvat.dashboard.taskName = taskName;
confirm('The current annotation will be lost. Are you sure?', uploadAnnotationRequest);
});
updateButton.on('click', function() {
window.cvat.dashboard.taskID = taskID;
window.cvat.dashboard.taskName = taskName;
$('#dashboardUpdateModal').removeClass('hidden');
$('#dashboardUpdateModal')[0].loadCurrentLabels();
});
deleteButton.on('click', function() {
window.cvat.dashboard.taskID = taskID;
window.cvat.dashboard.taskName = taskName;
RemoveTaskRequest();
});
});
});
document.addEventListener("DOMContentLoaded", buildDashboard);
function buildDashboard() {
/* Setup static content */
setupTaskCreator();
setupTaskUpdater();
setupSearch();
$(window).on('click', function(e) {
let target = $(e.target);
if ( target.hasClass('modal') ) {
target.addClass('hidden');
}
});
/* Setup task UIs */
for (let callback of window.cvat.dashboard.uiCallbacks) {
callback( $('.dashboardTaskUI') );
}
$('#loadingOverlay').remove();
}
function setupTaskCreator() {
let dashboardCreateTaskButton = $('#dashboardCreateTaskButton');
let createModal = $('#dashboardCreateModal');
let nameInput = $('#dashboardNameInput');
let labelsInput = $('#dashboardLabelsInput');
let bugTrackerInput = $('#dashboardBugTrackerInput');
let localSourceRadio = $('#dashboardLocalSource');
let shareSourceRadio = $('#dashboardShareSource');
let selectFiles = $('#dashboardSelectFiles');
let filesLabel = $('#dashboardFilesLabel');
let localFileSelector = $('#dashboardLocalFileSelector');
let shareFileSelector = $('#dashboardShareBrowseModal');
let shareBrowseTree = $('#dashboardShareBrowser');
let cancelBrowseServer = $('#dashboardCancelBrowseServer');
let submitBrowseServer = $('#dashboardSubmitBrowseServer');
let flipImagesBox = $('#dashboardFlipImages');
let segmentSizeInput = $('#dashboardSegmentSize');
let customSegmentSize = $('#dashboardCustomSegment');
let overlapSizeInput = $('#dashboardOverlap');
let customOverlapSize = $('#dashboardCustomOverlap');
let imageQualityInput = $('#dashboardImageQuality');
let customCompressQuality = $('#dashboardCustomQuality');
let taskMessage = $('#dashboardCreateTaskMessage');
let submitCreate = $('#dashboardSubmitTask');
let cancelCreate = $('#dashboardCancelTask');
let name = nameInput.prop('value');
let labels = labelsInput.prop('value');
let bugTrackerLink = bugTrackerInput.prop('value');
let source = 'local';
let flipImages = false;
let segmentSize = 5000;
let overlapSize = 0;
let compressQuality = 50;
let files = [];
dashboardCreateTaskButton.on('click', function() {
$('#dashboardCreateModal').removeClass('hidden');
});
nameInput.on('change', (e) => {name = e.target.value;});
bugTrackerInput.on('change', (e) => {bugTrackerLink = e.target.value;});
labelsInput.on('change', (e) => {labels = e.target.value;});
localSourceRadio.on('click', function() {
if (source == 'local') return;
source = 'local';
files = [];
updateSelectedFiles();
});
shareSourceRadio.on('click', function() {
if (source == 'share') return;
source = 'share';
files = [];
updateSelectedFiles();
});
selectFiles.on('click', function() {
if (source == 'local') {
localFileSelector.click();
}
else {
shareBrowseTree.jstree("refresh");
shareFileSelector.removeClass('hidden');
shareBrowseTree.jstree({
core: {
data: {
url: 'get_share_nodes',
data: (node) => { return {'id' : node.id}; }
}
},
plugins: ['checkbox', 'sort'],
});
}
});
localFileSelector.on('change', function(e) {
files = e.target.files;
updateSelectedFiles();
});
cancelBrowseServer.on('click', () => shareFileSelector.addClass('hidden'));
submitBrowseServer.on('click', function() {
files = shareBrowseTree.jstree(true).get_selected();
cancelBrowseServer.click();
updateSelectedFiles();
});
flipImagesBox.on('click', (e) => {flipImages = e.target.checked;});
customSegmentSize.on('change', (e) => segmentSizeInput.prop('disabled', !e.target.checked));
customOverlapSize.on('change', (e) => overlapSizeInput.prop('disabled', !e.target.checked));
customCompressQuality.on('change', (e) => imageQualityInput.prop('disabled', !e.target.checked));
segmentSizeInput.on('change', function() {
let value = Math.clamp(
+segmentSizeInput.prop('value'),
+segmentSizeInput.prop('min'),
+segmentSizeInput.prop('max')
);
segmentSizeInput.prop('value', value);
segmentSize = value;
});
overlapSizeInput.on('change', function() {
let value = Math.clamp(
+overlapSizeInput.prop('value'),
+overlapSizeInput.prop('min'),
+overlapSizeInput.prop('max')
);
overlapSizeInput.prop('value', value);
overlapSize = value;
});
imageQualityInput.on('change', function() {
let value = Math.clamp(
+imageQualityInput.prop('value'),
+imageQualityInput.prop('min'),
+imageQualityInput.prop('max')
);
imageQualityInput.prop('value', value);
compressQuality = value;
});
submitCreate.on('click', function() {
if (!validateName(name)) {
taskMessage.css('color', 'red');
taskMessage.text('Invalid task name');
return;
}
if (!validateLabels(labels)) {
taskMessage.css('color', 'red');
taskMessage.text('Invalid task labels');
return;
}
if (!validateSegmentSize(segmentSize)) {
taskMessage.css('color', 'red');
taskMessage.text('Segment size out of range');
return;
}
if (!validateOverlapSize(overlapSize, segmentSize)) {
taskMessage.css('color', 'red');
taskMessage.text('Overlap size must be positive and not more then segment size');
return;
}
if (files.length <= 0) {
taskMessage.css('color', 'red');
taskMessage.text('Need specify files for task');
return;
}
else if (files.length > maxUploadCount && source == 'local') {
taskMessage.css('color', 'red');
taskMessage.text('Too many files. Please use share functionality');
return;
}
else if (source == 'local') {
let commonSize = 0;
for (let file of files) {
commonSize += file.size;
}
if (commonSize > maxUploadSize) {
taskMessage.css('color', 'red');
taskMessage.text('Too big size. Please use share functionality');
return;
}
}
let taskData = new FormData();
taskData.append('task_name', name);
taskData.append('bug_tracker_link', bugTrackerLink);
taskData.append('labels', labels);
taskData.append('flip_flag', flipImages);
taskData.append('storage', source);
if (customSegmentSize.prop('checked')) {
taskData.append('segment_size', segmentSize);
}
if (customOverlapSize.prop('checked')) {
taskData.append('overlap_size', overlapSize);
}
if (customCompressQuality.prop('checked')) {
taskData.append('compress_quality', compressQuality);
}
for (let file of files) {
taskData.append('data', file);
}
submitCreate.prop('disabled', true);
createTaskRequest(taskData,
() => {
taskMessage.css('color', 'green');
taskMessage.text('Successful request! Creating..');
},
() => window.location.reload(),
(response) => {
taskMessage.css('color', 'red');
taskMessage.text(response);
},
() => submitCreate.prop('disabled', false));
});
function updateSelectedFiles() {
switch (files.length) {
case 0:
filesLabel.text('No Files');
break;
case 1:
filesLabel.text(typeof(files[0]) == 'string' ? files[0] : files[0].name);
break;
default:
filesLabel.text(files.length + ' files');
}
}
function validateName(name) {
let math = name.match('[a-zA-Z0-9()_ ]+');
return math != null;
}
function validateLabels(labels) {
let tmp = labels.replace(/\s/g,'');
return tmp.length > 0;
// to do good validator
}
function validateSegmentSize(segmentSize) {
return (segmentSize >= 100 && segmentSize <= 50000);
}
function validateOverlapSize(overlapSize, segmentSize) {
return (overlapSize >= 0 && overlapSize <= segmentSize - 1);
}
cancelCreate.on('click', () => createModal.addClass('hidden'));
}
function setupTaskUpdater() {
let updateModal = $('#dashboardUpdateModal');
let oldLabels = $('#dashboardOldLabels');
let newLabels = $('#dashboardNewLabels');
let submitUpdate = $('#dashboardSubmitUpdate');
let cancelUpdate = $('#dashboardCancelUpdate');
updateModal[0].loadCurrentLabels = function() {
$.ajax({
url: '/get/task/' + window.cvat.dashboard.taskID,
success: function(data) {
let labels = new LabelsInfo(data.spec);
oldLabels.attr('value', labels.normalize());
},
error: function(response) {
oldLabels.attr('value', 'Bad request');
let message = 'Bad task request: ' + response.responseText;
throw Error(message);
}
});
};
cancelUpdate.on('click', function() {
$('#dashboardNewLabels').prop('value', '');
updateModal.addClass('hidden');
});
submitUpdate.on('click', () => UpdateTaskRequest(newLabels.prop('value')));
}
function setupSearch() {
let searchInput = $("#dashboardSearchInput");
let searchSubmit = $("#dashboardSearchSubmit");
let line = getUrlParameter('search') || "";
searchInput.val(line);
searchSubmit.on('click', function() {
let e = $.Event('keypress');
e.keyCode = 13;
searchInput.trigger(e);
});
searchInput.on('keypress', function(e) {
if (e.keyCode != 13) return;
let filter = e.target.value;
if (!filter) window.location.search = "";
else window.location.search = `search=${filter}`;
});
function getUrlParameter(name) {
let regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
let results = regex.exec(window.location.search);
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
}
}
/* Server requests */
function createTaskRequest(oData, onSuccessRequest, onSuccessCreate, onError, onComplete) {
$.ajax({
url: '/create/task',
type: 'POST',
data: oData,
contentType: false,
processData: false,
success: function(data) {
onSuccessRequest();
requestCreatingStatus(data);
},
error: function(data) {
onComplete();
onError(data.responseText);
}
});
function requestCreatingStatus(data) {
let tid = data.tid;
let request_frequency_ms = 1000;
let done = false;
let requestInterval = setInterval(function() {
$.ajax({
url: '/check/task/' + tid,
success: receiveStatus,
error: function(data) {
clearInterval(requestInterval);
onComplete();
onError(data.responseText);
}
});
}, request_frequency_ms);
function receiveStatus(data) {
if (done) return;
if (data['state'] == 'created') {
done = true;
clearInterval(requestInterval);
onComplete();
onSuccessCreate();
}
else if (data['state'] == 'error') {
done = true;
clearInterval(requestInterval);
onComplete();
onError(data.stderr);
}
}
}
}
function UpdateTaskRequest(labels) {
let oData = new FormData();
oData.append('labels', labels);
$.ajax({
url: '/update/task/' + window.cvat.dashboard.taskID,
type: 'POST',
data: oData,
contentType: false,
processData: false,
success: function() {
$('#dashboardNewLabels').prop('value', '');
showMessage('Task successfully updated.');
},
error: function(data) {
showMessage('Task update error. ' + data.responseText);
},
complete: () => $('#dashboardUpdateModal').addClass('hidden')
});
}
function RemoveTaskRequest() {
confirm('The action can not be undone. Are you sure?', confirmCallback);
function confirmCallback() {
$.ajax ({
url: '/delete/task/' + window.cvat.dashboard.taskID,
success: function() {
$(`#dashboardTask_${window.cvat.dashboard.taskID}`).remove();
showMessage('Task removed.');
},
error: function(response) {
let message = 'Abort. Reason: ' + response.responseText;
showMessage(message);
throw Error(message);
}
});
}
}
function uploadAnnotationRequest() {
let input = $('<input>').attr({
type: 'file',
accept: 'text/xml'
}).on('change', loadXML).click();
function loadXML(e) {
input.remove();
let overlay = showOverlay("File uploading..");
let file = e.target.files[0];
let fileReader = new FileReader();
fileReader.onload = (e) => parseFile(e, overlay);
fileReader.readAsText(file);
}
function parseFile(e, overlay) {
let xmlText = e.target.result;
overlay.setMessage('Request task data from server..');
$.ajax({
url: '/get/task/' + window.cvat.dashboard.taskID,
success: function(data) {
let labels = new LabelsInfo(data.spec);
let fakeJob = {
start: 0,
stop: data.size
};
let annotationParser = new AnnotationParser(labels, fakeJob);
let parsed = null;
try {
parsed = annotationParser.parse(xmlText);
}
catch(error) {
let message = "Parsing errors was occured. " + error;
showMessage(message);
overlay.remove();
return;
}
overlay.setMessage('Annotation saving..');
$.ajax({
url: '/save/annotation/task/' + window.cvat.dashboard.taskID,
type: 'POST',
data: JSON.stringify(parsed),
contentType: 'application/json',
success: function() {
let message = 'Annotation successfully uploaded';
showMessage(message);
},
error: function(response) {
let message = 'Annotation uploading errors was occured. ' + response.responseText;
showMessage(message);
},
complete: () => overlay.remove()
});
},
error: function(response) {
overlay.remove();
let message = 'Bad task request: ' + response.responseText;
showMessage(message);
throw Error(message);
}
});
}
}

@ -0,0 +1,121 @@
.dashboardTaskUI {
margin: 5px auto;
width: 1200px;
height: 335px;
background-color: #e7edf5;
border: 1px solid;
border-radius: 5px;
}
.dashboardTaskIntro {
width: 25%;
height: 75%;
float: left;
margin-left: 20px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.dashboardButtonsUI {
margin-top: 1%;
width: 33%;
height: 75%;
float: left;
overflow-y: auto;
}
.dashboardButtonUI {
display: block;
width: 70%;
height: 2.5em;
margin: auto;
margin-top: 0.1em;
font-size: 1em;
}
.dashboardJobsUI {
width: 40%;
height: 75%;
float: left;
text-align: center;
overflow-y: scroll;
}
.dashboardJobList {
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
width: 100%;
margin-top: 15px;
}
.dashboardTitleWrapper {
overflow: hidden;
}
#dashboardSearchInput {
padding: 10px;
border: 1px solid grey;
float: left;
width: 70%;
background: #f1f1f1;
outline: 0;
}
#dashboardSearchSubmit {
float: left;
width: 20%;
padding: 10px;
background: #0b7dda;
color: white;
border: 1px solid grey;
border-left: none;
cursor: pointer;
outline: 0;
}
#dashboardSearchSubmit:hover {
background: #4c92ee;
}
#dashboardSearchSubmit:active {
background: #0b7fff;
}
#dashboardCreateTaskButton {
font-size: 2em;
}
#dashboardCreateContent {
width: 500px;
display: table;
text-align: center;
}
#dashboardShareBrowser {
width: 100%;
height: 80%;
border: 1px solid black;
border-radius: 5px;
margin-bottom: 10px;
overflow: auto;
}
#dashboardUpdateContent {
width: 450px;
height: 120px;
}
#dashboardOldLabels {
width: 97%;
margin-bottom: 10px;
}
#dashboardNewLabels {
width: 97%;
margin-bottom: 10px;
}

@ -0,0 +1,187 @@
{% extends 'engine/base.html' %}
{% load static %}
{% load pagination_tags %}
{% block head_title %}
CVAT Dashboard
{% endblock %}
{% block head_css %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static 'dashboard/stylesheet.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dashboard/js/3rdparty/jstree/themes/default/style.css' %}">
{% endblock %}
{% block head_js_3rdparty %}
{{ block.super }}
<script type="text/javascript" src="{% static 'dashboard/js/3rdparty/jstree/jstree.js' %}"></script>
{% for js_file in js_3rdparty %}
<script type="text/javascript" src="{% static js_file %}" defer></script>
{% endfor %}
{% endblock %}
{% block head_js_cvat %}
{{ block.super }}
<script type="text/javascript" src="{% static 'dashboard/js/dashboard.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/listener.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/labelsInfo.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/annotationParser.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/trackModel.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/collectionModel.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/server.js' %}"></script>
<script>
window.maxUploadSize = {{ max_upload_size }};
window.maxUploadCount = {{ max_upload_count }};
</script>
{% endblock %}
{% block content %}
<div id="content">
<center>
<table style="width: 65%;">
<tr>
<td><button id="dashboardCreateTaskButton" class="regular h1"> Create New Task </button></td>
<td>
<div style="width: 300px; float: right;">
<input type="text" id="dashboardSearchInput" class="regular h1" placeholder="Search.." name="search">
<button id="dashboardSearchSubmit" class="regular h1"> &#x1F50D; </button>
</div>
</td>
</tr>
</table>
</center>
{% autopaginate data %}
<div style="float: left; width: 100%">
{% for item in data %}
{% include "dashboard/task.html" %}
{% endfor %}
</div>
<center>{% paginate %}</center>
</div>
<div id="dashboardCreateModal" class="modal hidden">
<form id="dashboardCreateContent" class="modal-content" autocomplete="on" onsubmit="return false">
<center>
<label class="semiBold h1"> Task Configuration </label>
</center>
<table style="width: 100%; text-align: left;">
<tr>
<td style="width: 25%"> <label class="regular h2"> Name: </label> </td>
<td> <input type="text" id="dashboardNameInput" class="regular" style="width: 90%"/> </td>
</tr>
<tr>
<td> <label class="regular h2"> Labels: </label> </td>
<td> <input type="text" id="dashboardLabelsInput" class="regular" style="width: 90%" title='
Example:
car @select=color:blue,red,black ~checkbox=parked:true
@text=plate:"" @select=model:volvo,mazda,bmw
~radio=quality:good,bad
Specification:
<prefix>checkbox=id:true/false
<prefix>radio=id:name1,name2,...
<prefix>number=id:min,max,step
<prefix>text=id:"default value"
<prefix>select=id:value1,value2,...
<prefix> can be @ for unique properties and ~ for properties
which can change its value during lifetime of the object.
Default value for all elements is the first value after ":".
For "select" and "radio" types the special value is available: "__undefined__".
Specify the value FIRST if the attribute should be annotated explicitly.
Example: @select=race:__undefined__,skip,asian,black,caucasian,other'/>
</td>
</tr>
<tr>
<td> <label class="regular h2"> Bug Tracker: </label> </td>
<td> <input type="text" id="dashboardBugTrackerInput" class="regular" style="width: 90%", placeholder="Please specify full URL"/> </td>
</tr>
<tr>
<td> <label class="regular h2"> Source: </label> </td>
<td>
<input id="dashboardLocalSource" type="radio" name="sourceType" value="local" checked=true/> <label class="regular h2" for="localSource"> Local </label>
<br> <input id="dashboardShareSource" type="radio" name="sourceType" value="share"/> <label class="regular h2" for="shareSource"> Share </label>
</td>
</tr>
<tr>
<td>
<label class="regular h2"> Flip images </label>
</td>
<td>
<input type="checkbox" id="dashboardFlipImages"/>
</td>
</tr>
<tr>
<td>
<label class="regular h2"> Overlap Size </label>
</td>
<td>
<input type="number" id="dashboardOverlap" class="regular" max="50000" min="0" value="0" disabled=true/>
<input type="checkbox" id="dashboardCustomOverlap" title="Custom overlap size"/>
</td>
</tr>
<tr>
<td>
<label class="regular h2"> Segment Size </label>
</td>
<td>
<input type="number" id="dashboardSegmentSize" class="regular" max="50000" min="100" value="5000" disabled=true/>
<input type="checkbox" id="dashboardCustomSegment" title="Custom segment size"/>
</td>
</tr>
<tr>
<td>
<label class="regular h2"> Image Quality </label>
</td>
<td>
<input type="number" id="dashboardImageQuality" class="regular" style="width: 4.5em;" max="95" min="1" value="50" disabled=true/>
<input type="checkbox" id="dashboardCustomQuality" title="Custom image quality"/>
</td>
</tr>
</table>
<div style="text-align: left;">
<div>
<button id="dashboardSelectFiles" class="regular h2"> Select Files </button>
<label id="dashboardFilesLabel" class="regular h2" style="margin-left: 10px"> No Files </label>
<input id="dashboardLocalFileSelector" type="file" style="display: none" multiple/>
</div>
</div>
<div style="width: 100%; height: 14%; padding-top: 10px;">
<div style="float: left; height: 50px; overflow: auto; width: 63%; height: auto;">
<label id="dashboardCreateTaskMessage" class="regular h2 selectable" style="float:left;"> </label>
</div>
<div style="float: right; width: 35%; height: 50px;">
<button id="dashboardCancelTask" class="regular h2"> Cancel </button>
<button id="dashboardSubmitTask" class="regular h2"> Submit </button>
</div>
</div>
</form>
</div>
<div id="dashboardShareBrowseModal" class="modal hidden">
<div style="width: 600px; height: 400px;" class="modal-content noSelect">
<center> <label class="regular h1"> //icv-cifs/icv_projects/cvat/data </label> </center>
<div id="dashboardShareBrowser"> </div>
<center>
<button id="dashboardCancelBrowseServer" class="regular h2" style="margin: 0px 10px"> Cancel </button>
<button id="dashboardSubmitBrowseServer" class="regular h2" style="margin: 0px 10px"> Submit </button>
</center>
</div>
</div>
<div id="dashboardUpdateModal" class="modal hidden">
<div id="dashboardUpdateContent" class="modal-content">
<input id="dashboardOldLabels" type="text" readonly=true placeholder="Please Wait.." class="regular h2">
<input id="dashboardNewLabels" type="text" placeholder="New Labels" class="regular h2">
<center>
<button id="dashboardCancelUpdate" class="regular h2"> Cancel </button>
<button id="dashboardSubmitUpdate" class="regular h2"> Update </button>
</center>
</div>
</div>
{% endblock %}

@ -0,0 +1,31 @@
<div class="dashboardTaskUI" id="dashboardTask_{{item.task_id}}">
<center class="dashboardTitleWrapper">
<label class="semiBold h1 dashboardTaskNameLabel selectable"> {{ item.name }} </label>
</center>
<center class="dashboardTitleWrapper">
<label class="regular dashboardStatusLabel"> {{ item.status }} </label>
</center>
<div class="dashboardTaskIntro" style='background-image: url("/get/task/{{item.task_id}}/frame/0")'> </div>
<div class="dashboardButtonsUI">
<button class="dashboardDumpAnnotation semiBold dashboardButtonUI"> Dump Annotation </button>
<button class="dashboardUploadAnnotation semiBold dashboardButtonUI"> Upload Annotation </button>
<button class="dashboardUpdateTask semiBold dashboardButtonUI"> Update Task </button>
<button class="dashboardDeleteTask semiBold dashboardButtonUI"> Delete Task </button>
{%if item.has_bug_tracker %}
<button class="dashboardOpenTrackerButton semiBold dashboardButtonUI"> Open Bug Tracker </button>
<a class="dashboardBugTrackerLink" href='{{item.bug_tracker_link}}' style="display: none;"> </a>
{% endif %}
</div>
<div class="dashboardJobsUI">
<center class="dashboardTitleWrapper">
<label class="regular h1"> Jobs </label>
</center>
<table class="dashboardJobList regular">
{% for segment in item.segments %}
<tr>
<td> <a href="{{segment.url}}"> {{segment.url}} </a> </td>
</tr>
{% endfor %}
</table>
</div>
</div>

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

@ -0,0 +1,7 @@
from django.urls import path
from . import views
urlpatterns = [
path('get_share_nodes', views.JsTreeView),
path('', views.DashboardView),
]

@ -0,0 +1,112 @@
from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest
from django.shortcuts import redirect
from django.shortcuts import render
from django.conf import settings
from django.contrib.auth.decorators import permission_required
from cvat.apps.authentication.decorators import login_required
from cvat.apps.engine.models import Task as TaskModel
from cvat.settings.base import JS_3RDPARTY
import os
def ScanNode(directory):
if '..' in directory.split(os.path.sep):
return HttpResponseBadRequest('Permission Denied')
act_dir = os.path.normpath(settings.SHARE_ROOT + directory)
result = []
nodes = os.listdir(act_dir)
files = filter(os.path.isfile, map(lambda f: os.path.join(act_dir, f), nodes))
dirs = filter(os.path.isdir, map(lambda d: os.path.join(act_dir, d), nodes))
for d in dirs:
name = os.path.basename(d)
children = len(os.listdir(d)) > 0
node = {'id': directory + name + '/', 'text': name, 'children': children}
result.append(node)
for f in files:
name = os.path.basename(f)
node = {'id': directory + name, 'text': name, "icon" : "jstree-file"}
result.append(node)
return result
@login_required
@permission_required('engine.add_task', raise_exception=True)
def JsTreeView(request):
node_id = None
if 'id' in request.GET:
node_id = request.GET['id']
if node_id is None or node_id == '#':
node_id = '/'
response = [{"id": node_id, "text": node_id, "children": ScanNode(node_id)}]
else:
response = ScanNode(node_id)
return JsonResponse(response, safe=False,
json_dumps_params=dict(ensure_ascii=False))
def MainTaskInfo(task, dst_dict):
dst_dict["status"] = task.status
dst_dict["num_of_segments"] = task.segment_set.count()
dst_dict["mode"] = task.mode.capitalize()
dst_dict["name"] = task.name
dst_dict["task_id"] = task.id
dst_dict["created_date"] = task.created_date
dst_dict["updated_date"] = task.updated_date
dst_dict["bug_tracker_link"] = task.bug_tracker
dst_dict["has_bug_tracker"] = len(task.bug_tracker) > 0
dst_dict["owner"] = 'undefined'
dst_dict["id"] = task.id
dst_dict["segments"] = []
def DetailTaskInfo(request, task, dst_dict):
scheme = request.scheme
host = request.get_host()
dst_dict['segments'] = []
for segment in task.segment_set.all():
for job in segment.job_set.all():
segment_url = "{0}://{1}/?id={2}".format(scheme, host, job.id)
dst_dict["segments"].append({
'id': job.id,
'start': segment.start_frame,
'stop': segment.stop_frame,
'url': segment_url
})
db_labels = task.label_set.prefetch_related('attributespec_set').all()
attributes = {}
for db_label in db_labels:
attributes[db_label.id] = {}
for db_attrspec in db_label.attributespec_set.all():
attributes[db_label.id][db_attrspec.id] = db_attrspec.text
dst_dict['labels'] = attributes
@login_required
@permission_required('engine.view_task', raise_exception=True)
def DashboardView(request):
filter_name = request.GET['search'] if 'search' in request.GET else None
tasks_query_set = list(TaskModel.objects.prefetch_related('segment_set').order_by('-created_date').all())
if filter_name is not None:
tasks_query_set = list(filter(lambda x: filter_name.lower() in x.name.lower(), tasks_query_set))
data = []
for task in tasks_query_set:
task_info = {}
MainTaskInfo(task, task_info)
DetailTaskInfo(request, task, task_info)
data.append(task_info)
return render(request, 'dashboard/dashboard.html', {
'data': data,
'max_upload_size': settings.LOCAL_LOAD_MAX_FILES_SIZE,
'max_upload_count': settings.LOCAL_LOAD_MAX_FILES_COUNT,
'js_3rdparty': JS_3RDPARTY.get('dashboard', [])
})

@ -0,0 +1,3 @@
from cvat.settings.base import JS_3RDPARTY
JS_3RDPARTY['dashboard'] = JS_3RDPARTY.get('dashboard', []) + ['documentation/js/shortcuts.js']

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

@ -0,0 +1,5 @@
from django.apps import AppConfig
class DocumentationConfig(AppConfig):
name = 'cvat.apps.documentation'

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save