Browse Source

Merge pull request #80

pull/92/head
Hironsan 6 years ago
parent
commit
9ec7014f69
25 changed files with 347 additions and 66 deletions
  1. 15
      .travis.yml
  2. 2
      Dockerfile
  3. 14
      README.md
  4. 15
      app/app/settings.py
  5. 18
      app/server/migrations/0003_shortcut.py
  6. 6
      app/server/models.py
  7. 2
      app/server/static/css/forum.css
  8. 3
      app/server/static/js/document_classification.js
  9. 10
      app/server/static/js/filter.js
  10. 48
      app/server/static/js/label.js
  11. 48
      app/server/static/js/mixin.js
  12. 3
      app/server/static/js/sequence_labeling.js
  13. 38
      app/server/templates/admin/label.html
  14. 4
      app/server/templates/annotation/document_classification.html
  15. 4
      app/server/templates/annotation/sequence_labeling.html
  16. 12
      app/server/templates/base.html
  17. 14
      app/server/templates/tags/azure_appinsights.html
  18. 10
      app/server/templates/tags/google_analytics.html
  19. 0
      app/server/templatetags/__init__.py
  20. 18
      app/server/templatetags/analytics.py
  21. 6
      app/server/tests/test_models.py
  22. 10
      app/server/utils.py
  23. 93
      azuredeploy.json
  24. 5
      requirements.txt
  25. 15
      tools/cd.sh

15
.travis.yml

@ -1,7 +1,22 @@
language: python
services:
- docker
python:
- "3.6"
cache: pip
install:
- pip install -r requirements.txt
script:
- tools/ci.sh
deploy:
- provider: script
script: tools/cd.sh
on:
repo: chakki-works/doccano
tags: true

2
Dockerfile

@ -24,6 +24,8 @@ ENV DEBUG="True"
ENV SECRET_KEY="change-me-in-production"
ENV PORT="80"
ENV WORKERS="2"
ENV GOOGLE_TRACKING_ID=""
ENV AZURE_APPINSIGHTS_IKEY=""
EXPOSE ${PORT}

14
README.md

@ -68,10 +68,10 @@ cd doccano
To install doccano, there are two options:
**Option1: Build the Docker image**
**Option1: Pull the Docker image**
```bash
docker build -t doccano:1 .
docker pull chakkiworks/doccano
```
**Option2: Setup Python environment**
@ -135,8 +135,16 @@ Depending on your installation method, there are two options:
**Option1: Running the Docker image as a Container**
First, run a Docker container:
```bash
docker run -d --name doccano -p 8000:80 chakkiworks/doccano
```
Then, execute `create-admin.sh` script for creating a superuser.
```bash
docker run -p 8000:80 doccano:1
docker exec doccano tools/create-admin.sh "admin" "admin@example.com" "password"
```
**Option2: Running Django development server**

15
app/app/settings.py

@ -40,6 +40,7 @@ if os.environ.get('DEBUG') == 'False':
# Application definition
INSTALLED_APPS = [
'whitenoise.runserver_nostatic',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
@ -55,6 +56,7 @@ INSTALLED_APPS = [
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
@ -62,6 +64,7 @@ MIDDLEWARE = [
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'social_django.middleware.SocialAuthExceptionMiddleware',
'applicationinsights.django.ApplicationInsightsMiddleware',
]
ROOT_URLCONF = 'app.urls'
@ -80,6 +83,9 @@ TEMPLATES = [
'social_django.context_processors.backends',
'social_django.context_processors.login_redirect',
],
'libraries': {
'analytics': 'server.templatetags.analytics',
},
},
},
]
@ -88,6 +94,8 @@ STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'server/static'),
]
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
WSGI_APPLICATION = 'app.wsgi.application'
AUTHENTICATION_BACKENDS = [
@ -181,4 +189,11 @@ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# on the import phase
IMPORT_BATCH_SIZE = 500
GOOGLE_TRACKING_ID = os.getenv('GOOGLE_TRACKING_ID', 'UA-125643874-2')
AZURE_APPINSIGHTS_IKEY = os.getenv('AZURE_APPINSIGHTS_IKEY')
APPLICATION_INSIGHTS = {
'ikey': AZURE_APPINSIGHTS_IKEY if AZURE_APPINSIGHTS_IKEY else None,
}
django_heroku.settings(locals(), test_runner=False)

18
app/server/migrations/0003_shortcut.py

@ -0,0 +1,18 @@
# Generated by Django 2.1.5 on 2019-02-06 02:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('server', '0002_document_metadata'),
]
operations = [
migrations.AlterField(
model_name='label',
name='shortcut',
field=models.CharField(blank=True, choices=[('a', 'a'), ('b', 'b'), ('c', 'c'), ('d', 'd'), ('e', 'e'), ('f', 'f'), ('g', 'g'), ('h', 'h'), ('i', 'i'), ('j', 'j'), ('k', 'k'), ('l', 'l'), ('m', 'm'), ('n', 'n'), ('o', 'o'), ('p', 'p'), ('q', 'q'), ('r', 'r'), ('s', 's'), ('t', 't'), ('u', 'u'), ('v', 'v'), ('w', 'w'), ('x', 'x'), ('y', 'y'), ('z', 'z'), ('ctrl a', 'ctrl a'), ('ctrl b', 'ctrl b'), ('ctrl c', 'ctrl c'), ('ctrl d', 'ctrl d'), ('ctrl e', 'ctrl e'), ('ctrl f', 'ctrl f'), ('ctrl g', 'ctrl g'), ('ctrl h', 'ctrl h'), ('ctrl i', 'ctrl i'), ('ctrl j', 'ctrl j'), ('ctrl k', 'ctrl k'), ('ctrl l', 'ctrl l'), ('ctrl m', 'ctrl m'), ('ctrl n', 'ctrl n'), ('ctrl o', 'ctrl o'), ('ctrl p', 'ctrl p'), ('ctrl q', 'ctrl q'), ('ctrl r', 'ctrl r'), ('ctrl s', 'ctrl s'), ('ctrl t', 'ctrl t'), ('ctrl u', 'ctrl u'), ('ctrl v', 'ctrl v'), ('ctrl w', 'ctrl w'), ('ctrl x', 'ctrl x'), ('ctrl y', 'ctrl y'), ('ctrl z', 'ctrl z'), ('shift a', 'shift a'), ('shift b', 'shift b'), ('shift c', 'shift c'), ('shift d', 'shift d'), ('shift e', 'shift e'), ('shift f', 'shift f'), ('shift g', 'shift g'), ('shift h', 'shift h'), ('shift i', 'shift i'), ('shift j', 'shift j'), ('shift k', 'shift k'), ('shift l', 'shift l'), ('shift m', 'shift m'), ('shift n', 'shift n'), ('shift o', 'shift o'), ('shift p', 'shift p'), ('shift q', 'shift q'), ('shift r', 'shift r'), ('shift s', 'shift s'), ('shift t', 'shift t'), ('shift u', 'shift u'), ('shift v', 'shift v'), ('shift w', 'shift w'), ('shift x', 'shift x'), ('shift y', 'shift y'), ('shift z', 'shift z'), ('ctrl shift a', 'ctrl shift a'), ('ctrl shift b', 'ctrl shift b'), ('ctrl shift c', 'ctrl shift c'), ('ctrl shift d', 'ctrl shift d'), ('ctrl shift e', 'ctrl shift e'), ('ctrl shift f', 'ctrl shift f'), ('ctrl shift g', 'ctrl shift g'), ('ctrl shift h', 'ctrl shift h'), ('ctrl shift i', 'ctrl shift i'), ('ctrl shift j', 'ctrl shift j'), ('ctrl shift k', 'ctrl shift k'), ('ctrl shift l', 'ctrl shift l'), ('ctrl shift m', 'ctrl shift m'), ('ctrl shift n', 'ctrl shift n'), ('ctrl shift o', 'ctrl shift o'), ('ctrl shift p', 'ctrl shift p'), ('ctrl shift q', 'ctrl shift q'), ('ctrl shift r', 'ctrl shift r'), ('ctrl shift s', 'ctrl shift s'), ('ctrl shift t', 'ctrl shift t'), ('ctrl shift u', 'ctrl shift u'), ('ctrl shift v', 'ctrl shift v'), ('ctrl shift w', 'ctrl shift w'), ('ctrl shift x', 'ctrl shift x'), ('ctrl shift y', 'ctrl shift y'), ('ctrl shift z', 'ctrl shift z'), ('', '')], max_length=15, null=True),
),
]

6
app/server/models.py

@ -1,10 +1,10 @@
import json
import string
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.contrib.auth.models import User
from django.contrib.staticfiles.storage import staticfiles_storage
from .utils import get_key_choices
class Project(models.Model):
@ -120,11 +120,11 @@ class Project(models.Model):
class Label(models.Model):
KEY_CHOICES = ((u, c) for u, c in zip(string.ascii_lowercase, string.ascii_lowercase))
KEY_CHOICES = get_key_choices()
COLOR_CHOICES = ()
text = models.CharField(max_length=100)
shortcut = models.CharField(max_length=10, choices=KEY_CHOICES)
shortcut = models.CharField(max_length=15, blank=True, null=True, choices=KEY_CHOICES)
project = models.ForeignKey(Project, related_name='labels', on_delete=models.CASCADE)
background_color = models.CharField(max_length=7, default='#209cee')
text_color = models.CharField(max_length=7, default='#ffffff')

2
app/server/static/css/forum.css

@ -649,7 +649,7 @@ p {
}
.mr5 {
margin-left: 5px!important
margin-right: 5px!important
}
.mr10 {

3
app/server/static/js/document_classification.js

@ -1,11 +1,14 @@
import Vue from 'vue';
import annotationMixin from './mixin';
import HTTP from './http';
import simpleShortcut from './filter';
Vue.use(require('vue-shortkey'), {
prevent: ['input', 'textarea'],
});
Vue.filter('simpleShortcut', simpleShortcut);
const vm = new Vue({
el: '#mail-app',

10
app/server/static/js/filter.js

@ -0,0 +1,10 @@
export default function simpleShortcut(shortcut) {
if (shortcut === null) {
shortcut = '';
} else {
shortcut = shortcut.replace('ctrl', 'C');
shortcut = shortcut.replace('shift', 'S');
shortcut = shortcut.split(' ').join('-');
}
return shortcut;
}

48
app/server/static/js/label.js

@ -1,5 +1,8 @@
import Vue from 'vue';
import HTTP from './http';
import simpleShortcut from './filter';
Vue.filter('simpleShortcut', simpleShortcut);
const vm = new Vue({
@ -8,16 +11,53 @@ const vm = new Vue({
data: {
labels: [],
labelText: '',
selectedShortkey: '',
selectedKey: '',
checkedKey: [],
shortcutKey: '',
backgroundColor: '#209cee',
textColor: '#ffffff',
},
computed: {
/**
* combineKeys: Combine selectedKey and checkedKey to get shortcutKey
* saveKeys: Save null to database if shortcutKey is empty string
*/
combineKeys: function () {
this.shortcutKey = '';
// If checkedKey exits, add it to shortcutKey
if (this.checkedKey.length > 0) {
this.checkedKey.sort();
this.shortcutKey = this.checkedKey.join(' ');
// If selectedKey exist, add it to shortcutKey
if (this.selectedKey.length !== 0) {
this.shortcutKey = this.shortcutKey + ' ' + this.selectedKey;
}
}
// If only selectedKey exist, assign to shortcutKey
if (this.shortcutKey.length === 0 && this.selectedKey.length !== 0) {
this.shortcutKey = this.selectedKey;
}
return this.shortcutKey;
},
saveKeys: function () {
this.shortcutKey = this.combineKeys;
if (this.shortcutKey === '') {
return null;
}
return this.shortcutKey;
},
},
methods: {
addLabel() {
const payload = {
text: this.labelText,
shortcut: this.selectedShortkey,
shortcut: this.saveKeys,
background_color: this.backgroundColor,
text_color: this.textColor,
};
@ -37,7 +77,9 @@ const vm = new Vue({
reset() {
this.labelText = '';
this.selectedShortkey = '';
this.selectedKey = '';
this.checkedKey = [];
this.shortcutKey = '';
this.backgroundColor = '#209cee';
this.textColor = '#ffffff';
},

48
app/server/static/js/mixin.js

@ -1,5 +1,37 @@
import HTTP from './http';
const getOffsetFromUrl = function(url) {
const offsetMatch = url.match(/[?#].*offset=(\d+)/);
if (offsetMatch == null) {
return 0;
}
return parseInt(offsetMatch[1], 10);
};
const storeOffsetInUrl = function(offset) {
let href = window.location.href;
const fragmentStart = href.indexOf('#') + 1;
if (fragmentStart === 0) {
href += '#offset=' + offset;
} else {
const prefix = href.substring(0, fragmentStart);
const fragment = href.substring(fragmentStart);
const newFragment = fragment.split('&').map(function(fragmentPart) {
const keyValue = fragmentPart.split('=');
return keyValue[0] === 'offset'
? 'offset=' + offset
: fragmentPart;
}).join('&');
href = prefix + newFragment;
}
window.location.href = href;
};
const annotationMixin = {
data() {
return {
@ -12,6 +44,7 @@ const annotationMixin = {
remaining: 0,
searchQuery: '',
url: '',
offset: getOffsetFromUrl(window.location.href),
picked: 'all',
count: 0,
isActive: false,
@ -56,6 +89,7 @@ const annotationMixin = {
const doc = this.docs[i];
this.annotations.push(doc.annotations);
}
this.offset = getOffsetFromUrl(this.url);
});
},
@ -71,7 +105,7 @@ const annotationMixin = {
async submit() {
const state = this.getState();
this.url = `docs/?q=${this.searchQuery}&is_checked=${state}`;
this.url = `docs/?q=${this.searchQuery}&is_checked=${state}&offset=${this.offset}`;
await this.search();
this.pageNumber = 0;
},
@ -83,6 +117,14 @@ const annotationMixin = {
this.annotations[this.pageNumber].splice(index, 1);
});
},
replaceNull(shortcut) {
if (shortcut === null) {
shortcut = '';
}
shortcut = shortcut.split(' ');
return shortcut;
},
},
watch: {
@ -97,6 +139,10 @@ const annotationMixin = {
this.remaining = response.data.remaining;
});
},
offset() {
storeOffsetInUrl(this.offset);
},
},
created() {

3
app/server/static/js/sequence_labeling.js

@ -1,11 +1,14 @@
import Vue from 'vue';
import annotationMixin from './mixin';
import HTTP from './http';
import simpleShortcut from './filter';
Vue.use(require('vue-shortkey'), {
prevent: ['input', 'textarea'],
});
Vue.filter('simpleShortcut', simpleShortcut);
Vue.component('annotator', {
template: '<div @click="setSelectedRange">\
<span class="text-sequence"\

38
app/server/templates/admin/label.html

@ -16,7 +16,6 @@
<div class="card-content">
<div class="has-text-right">
<div class="field is-grouped is-grouped-multiline has-text-weight-bold pbrem075">
<div class="control" v-for="label in labels">
<div class="tags has-addons">
@ -24,7 +23,7 @@
<button class="delete is-small tweaked-margin" @click="removeLabel(label)"></button>
[[ label.text ]]
</span>
<span class="tag is-medium">[[ label.shortcut ]]</span>
<span class="tag is-medium"><kbd>[[ label.shortcut | simpleShortcut ]]</kbd></span>
</div>
</div>
</div>
@ -36,23 +35,29 @@
<a class="tag is-medium" v-bind:style="{ color: textColor, backgroundColor: backgroundColor }">
[[ labelText ]]
</a>
<span class="tag is-medium">[[ selectedShortkey ]]</span>
<span class="tag is-medium"><kbd>[[ combineKeys | simpleShortcut ]]</kbd></span>
</div>
</div>
</div>
<div class="field is-horizontal bordered-row">
<label class="label column is-3 mb0">Label Name</label>
<div class="column is-3 mb0">
<label class="label mb0">Label Name</label>
<p class="is-small has-text-grey">required</p>
</div>
<div class="control column is-6">
<input class="input" type="text" placeholder="Text input" v-model="labelText">
</div>
</div>
<div class="field is-horizontal bordered-row">
<label class="label column is-3 mb0">Shortcut Key</label>
<div class="control column is-6">
<div class="column is-3 mb0">
<label class="label mb0">Shortcut Key</label>
<p class="is-small has-text-grey">optional</p>
</div>
<div class="control column is-narrow">
<div class="select">
<select v-model="selectedShortkey">
<select v-model="selectedKey">
<option disabled value="">Please select one</option>
{% for ch in 'abcdefghijklmnopqrstuvwxyz' %}
<option>{{ ch }}</option>
@ -60,18 +65,31 @@
</select>
</div>
</div>
<div class="is-narrow">
<input class="is-checkradio" type="checkbox" id="ctrl" value="ctrl" v-model="checkedKey">
<label for="ctrl"><kbd>C: Ctrl</kbd></label>
</div>
<div class="is-narrow">
<input class="is-checkradio" type="checkbox" id="shift" value="shift" v-model="checkedKey">
<label for="shift"><kbd>S: Shift</kbd></label>
</div>
</div>
<div class="field is-horizontal bordered-row">
<label class="label column is-3 mb0">Background Color</label>
<div class="column is-3 mb0">
<label class="label mb0">Background Color</label>
<p class="is-small has-text-grey">optional</p>
</div>
<div class="control column is-6">
<input class="input" type="color" v-model="backgroundColor">
</div>
</div>
<div class="field is-horizontal bordered-row">
<label class="label column is-3 mb0">Text Color</label>
<div class="column is-3 mb0">
<label class="label mb0">Text Color</label>
<p class="is-small has-text-grey">optional</p>
</div>
<div class="control column is-6">
<input class="input" type="color" v-model="textColor">
</div>

4
app/server/templates/annotation/document_classification.html

@ -8,10 +8,10 @@
<div class="control" v-for="(label, item) in labels">
<div class="tags has-addons">
<a class="tag is-medium" v-bind:style="{ color: label.text_color, backgroundColor: label.background_color }" v-on:click="addLabel(label)"
v-shortkey.once="[ label.shortcut ]" @shortkey="addLabel(label)">
v-shortkey.once=" replaceNull(label.shortcut) " @shortkey="addLabel(label)">
[[ label.text ]]
</a>
<span class="tag is-medium">[[ label.shortcut ]]</span>
<span class="tag is-medium"><kbd>[[ label.shortcut | simpleShortcut ]]</kbd></span>
</div>
</div>
</div>

4
app/server/templates/annotation/sequence_labeling.html

@ -8,10 +8,10 @@
<div class="control" v-for="label in labels">
<div class="tags has-addons">
<a class="tag is-medium" v-bind:style="{ color: label.text_color, backgroundColor: label.background_color }" v-on:click="annotate(label.id)"
v-shortkey.once="[ label.shortcut ]" @shortkey="annotate(label.id)">
v-shortkey.once=" replaceNull(label.shortcut) " @shortkey="annotate(label.id)">
[[ label.text ]]
</a>
<span class="tag is-medium">[[ label.shortcut ]]</span>
<span class="tag is-medium"><kbd>[[ label.shortcut | simpleShortcut ]]</kbd></span>
</div>
</div>
</div>

12
app/server/templates/base.html

@ -1,15 +1,9 @@
{% load static %}
{% load analytics %}
<!DOCTYPE html>
<html>
<head>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-125643874-2"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-125643874-2');
</script>
{% google_analytics %}
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
@ -20,6 +14,7 @@
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,700" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.1/css/bulma.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma-extensions@4.0.1/bulma-divider/dist/css/bulma-divider.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma-extensions@4.0.1/bulma-checkradio/dist/css/bulma-checkradio.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="{% static 'css/forum.css' %}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/4.3.3/css/swiper.min.css">
<!-- favicon settings -->
@ -41,6 +36,7 @@
<meta name="msapplication-TileImage" content="{% static 'images/favicons/ms-icon-144x144.png' %}">
<meta name="theme-color" content="#ffffff">
{% block header %}{% endblock %}
{% azure_appinsights %}
</head>
<body>

14
app/server/templates/tags/azure_appinsights.html

@ -0,0 +1,14 @@
{# See https://apmtips.com/blog/2015/03/18/javascript-snippet-explained/ #}
{% if azure_appinsights_ikey %}
<script type="text/javascript">
var appInsights=window.appInsights||function(a){
function b(a){c[a]=function(){var b=arguments;c.queue.push(function(){c[a].apply(c,b)})}}var c={config:a},d=document,e=window;setTimeout(function(){var b=d.createElement("script");b.src=a.url||"https://az416426.vo.msecnd.net/scripts/a/ai.0.js",d.getElementsByTagName("script")[0].parentNode.appendChild(b)});try{c.cookie=d.cookie}catch(a){}c.queue=[];for(var f=["Event","Exception","Metric","PageView","Trace","Dependency"];f.length;)b("track"+f.pop());if(b("setAuthenticatedUserContext"),b("clearAuthenticatedUserContext"),b("startTrackEvent"),b("stopTrackEvent"),b("startTrackPage"),b("stopTrackPage"),b("flush"),!a.disableExceptionTracking){f="onerror",b("_"+f);var g=e[f];e[f]=function(a,b,d,e,h){var i=g&&g(a,b,d,e,h);return!0!==i&&c["_"+f](a,b,d,e,h),i}}return c
}({
{% if DEBUG %}
enableDebug: true,
{% endif %}
instrumentationKey: "{{ azure_appinsights_ikey }}"
});
window.appInsights=appInsights,appInsights.queue&&0===appInsights.queue.length&&appInsights.trackPageView();
</script>
{% endif %}

10
app/server/templates/tags/google_analytics.html

@ -0,0 +1,10 @@
{% if google_tracking_id %}
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id={{ google_tracking_id }}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag("js", new Date());
gtag("config", "{{ google_tracking_id }}");
</script>
{% endif %}

0
app/server/templatetags/__init__.py

18
app/server/templatetags/analytics.py

@ -0,0 +1,18 @@
from django import template
from app import settings
register = template.Library()
@register.inclusion_tag('tags/google_analytics.html')
def google_analytics():
return {'google_tracking_id': settings.GOOGLE_TRACKING_ID}
@register.inclusion_tag('tags/azure_appinsights.html')
def azure_appinsights():
return {
'DEBUG': settings.DEBUG,
'azure_appinsights_ikey': settings.APPLICATION_INSIGHTS['ikey'],
}

6
app/server/tests/test_models.py

@ -21,11 +21,15 @@ class TestProject(TestCase):
class TestLabel(TestCase):
def test_shortcut_uniqueness(self):
label = mixer.blend('server.Label')
label = mixer.blend('server.Label', shortcut='a')
mixer.blend('server.Label', shortcut=label.shortcut)
with self.assertRaises(IntegrityError):
Label(project=label.project, shortcut=label.shortcut).save()
def test_create_none_shortcut(self):
label = mixer.blend('server.Label', shortcut=None)
self.assertEqual(label.shortcut, None)
def test_text_uniqueness(self):
label = mixer.blend('server.Label')
mixer.blend('server.Label', text=label.text)

10
app/server/utils.py

@ -0,0 +1,10 @@
import string
def get_key_choices():
selectKey, shortKey = [c for c in string.ascii_lowercase], [c for c in string.ascii_lowercase]
checkKey = 'ctrl shift'
shortKey += [ck + ' ' + sk for ck in checkKey.split() for sk in selectKey]
shortKey += [checkKey + ' ' + sk for sk in selectKey]
shortKey += ['']
KEY_CHOICES = ((u, c) for u, c in zip(shortKey, shortKey))
return KEY_CHOICES

93
azuredeploy.json

@ -2,35 +2,35 @@
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"appName":{
"appName": {
"type": "string",
"minLength": 1,
"minLength": 1,
"metadata": {
"description": "The name for the webapp. Must be globally unique."
}
},
"secretKey":{
"secretKey": {
"type": "securestring",
"minLength": 16,
"minLength": 16,
"metadata": {
"description": "The value to use as the Django secret key."
}
},
"adminUserName":{
"adminUserName": {
"type": "string",
"minLength": 1,
"minLength": 1,
"metadata": {
"description": "The user name for the admin account."
}
},
"adminContactEmail":{
"adminContactEmail": {
"type": "string",
"minLength": 1,
"minLength": 1,
"metadata": {
"description": "The contact email address for the admin account."
}
},
"adminPassword":{
"adminPassword": {
"type": "securestring",
"minLength": 16,
"metadata": {
@ -86,7 +86,7 @@
"dockerImageName": {
"type": "string",
"minLength": 1,
"defaultValue": "cwolff/doccano:latest",
"defaultValue": "chakkiworks/doccano:latest",
"metadata": {
"description": "The Docker image to deploy."
}
@ -102,26 +102,20 @@
"databaseServerName": "[concat(parameters('appName'),'-state')]",
"setupScriptName": "[concat(parameters('appName'),'-setup')]",
"appServicePlanName": "[concat(parameters('appName'),'-hosting')]",
"env": [
{
"name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE",
"value": "false"
},
{
"name": "DEBUG",
"value": "False"
},
{
"name": "SECRET_KEY",
"value": "[parameters('secretKey')]"
},
{
"name": "DATABASE_URL",
"value": "[variables('databaseConnectionString')]"
}
]
"analyticsName": "[concat(parameters('appName'),'-analytics')]"
},
"resources": [
{
"type": "Microsoft.Insights/components",
"apiVersion": "2015-05-01",
"name": "[variables('analyticsName')]",
"location": "[variables('location')]",
"tags": {},
"kind": "web",
"properties": {
"Application_Type": "web"
}
},
{
"apiVersion": "2017-08-01",
"type": "Microsoft.Web/serverfarms",
@ -199,7 +193,20 @@
"[parameters('adminContactEmail')]",
"[parameters('adminPassword')]"
],
"environmentVariables": "[variables('env')]",
"environmentVariables": [
{
"name": "DEBUG",
"value": "False"
},
{
"name": "SECRET_KEY",
"value": "[parameters('secretKey')]"
},
{
"name": "DATABASE_URL",
"value": "[variables('databaseConnectionString')]"
}
],
"resources": {
"requests": {
"cpu": "1",
@ -224,6 +231,7 @@
"location": "[variables('location')]",
"dependsOn": [
"[resourceId('Microsoft.DBforPostgreSQL/servers/', variables('databaseServerName'))]",
"[resourceId('Microsoft.Insights/components', variables('analyticsName'))]",
"[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]"
],
"properties": {
@ -231,7 +239,32 @@
"siteConfig": {
"linuxFxVersion": "[concat('DOCKER|', parameters('dockerImageName'))]",
"alwaysOn": true,
"appSettings": "[variables('env')]"
"appSettings": [
{
"name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE",
"value": "false"
},
{
"name": "AZURE_APPINSIGHTS_IKEY",
"value": "[reference(resourceId('Microsoft.Insights/components', variables('analyticsName')), '2014-04-01').InstrumentationKey]"
},
{
"name": "GOOGLE_TRACKING_ID",
"value": ""
},
{
"name": "DEBUG",
"value": "False"
},
{
"name": "SECRET_KEY",
"value": "[parameters('secretKey')]"
},
{
"name": "DATABASE_URL",
"value": "[variables('databaseConnectionString')]"
}
]
},
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]"
}

5
requirements.txt

@ -1,5 +1,6 @@
applicationinsights==0.11.7
dj-database-url==0.5.0
Django==2.1.5
Django==2.1.7
django-filter==2.0.0
django-heroku==0.3.1
django-widget-tweaks==1.4.2
@ -17,4 +18,4 @@ social-auth-app-django==3.1.0
social-auth-core[azuread]==3.0.0
text-unidecode==1.2
tornado==5.0.2
whitenoise==3.3.1
whitenoise[brotli]==4.1.2

15
tools/cd.sh

@ -0,0 +1,15 @@
#!/usr/bin/env bash
if [[ -z "${DOCKER_USERNAME}" ]]; then echo "Missing DOCKER_USERNAME environment variable" >&2; exit 1; fi
if [[ -z "${DOCKER_PASSWORD}" ]]; then echo "Missing DOCKER_PASSWORD environment variable" >&2; exit 1; fi
if [[ -z "${TRAVIS_TAG}" ]]; then echo "Missing TRAVIS_TAG environment variable" >&2; exit 1; fi
set -o errexit
docker build -t "${DOCKER_USERNAME}/doccano:latest" .
docker build -t "${DOCKER_USERNAME}/doccano:${TRAVIS_TAG}" .
echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin
docker push "${DOCKER_USERNAME}/doccano:latest"
docker push "${DOCKER_USERNAME}/doccano:${TRAVIS_TAG}"
Loading…
Cancel
Save