Skip to content

Commit

Permalink
(no-ticket) Create Tags
Browse files Browse the repository at this point in the history
Change-Id: I67c968af97ffded24e2eb6f7f31a86b0ed90b8a4
  • Loading branch information
Marek Sommer authored and Gerrit Code Review committed Jun 23, 2015
1 parent 05d9182 commit 214f9da
Show file tree
Hide file tree
Showing 17 changed files with 483 additions and 23 deletions.
19 changes: 19 additions & 0 deletions oioioi/base/utils/tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
def get_tag_colors(tag):
"""This function decides how to color a certain tag.
For a given tag name (as a string),
it returns a tuple of strings (background color, text color).
For example, get_tag_colors('swag') may return ('#bac010', '#1e48c0').
It computes the background color basing on the tag name's hash.
The text color is matched to the background color so that
it will look as nice as it is possible on this background.
"""
color = hash(tag) % (256 * 256 * 256)
colors = [((color // 256**i) % 256) for i in (0, 1, 2)]
if sum(colors) > 128 * 3:
textcolors = (0, 0, 0)
else:
textcolors = (255, 255, 255)
return ('#' + ''.join('%02x' % i for i in colors),
'#' + ''.join('%02x' % i for i in textcolors))
2 changes: 2 additions & 0 deletions oioioi/default_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@

PROBLEMSET_LINK_VISIBLE = True

PROBLEM_TAGS_VISIBLE = False

TEMPLATE_CONTEXT_PROCESSORS = (
'django.contrib.auth.context_processors.auth',
'django.core.context_processors.debug',
Expand Down
3 changes: 3 additions & 0 deletions oioioi/deployment/settings.py.template
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,9 @@ INSTALLED_APPS = (
# Set to True to show the link to the problemset with contests on navbar.
PROBLEMSET_LINK_VISIBLE = True

# Comment out to show tags on the list of problems
#PROBLEM_TAGS_VISIBLE = True

TEMPLATE_CONTEXT_PROCESSORS += (
# 'oioioi.contestlogo.processors.logo_processor',
# 'oioioi.contestlogo.processors.icon_processor',
Expand Down
18 changes: 14 additions & 4 deletions oioioi/problems/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@
from oioioi.contests.models import ProblemInstance, ProblemStatementConfig
from oioioi.contests.utils import is_contest_admin
from oioioi.problems.models import Problem, ProblemStatement, \
ProblemAttachment, ProblemPackage, ProblemSite, MainProblemInstance
ProblemAttachment, ProblemPackage, ProblemSite, MainProblemInstance, \
Tag
from oioioi.problems.utils import can_add_problems, can_admin_problem, \
is_problem_author
from oioioi.problems.forms import ProblemStatementConfigForm, ProblemSiteForm
from oioioi.problems.forms import ProblemStatementConfigForm, \
ProblemSiteForm, TagThroughForm


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -112,9 +114,17 @@ class ProblemSiteInline(admin.StackedInline):
form = ProblemSiteForm


class TagInline(admin.StackedInline):
model = Tag.problems.through
form = TagThroughForm
extra = 0
verbose_name = _("Tag")
verbose_name_plural = _("Tags")


class ProblemAdmin(admin.ModelAdmin):
inlines = [StatementInline, AttachmentInline, ProblemInstanceInline,
ProblemSiteInline]
inlines = [TagInline, StatementInline, AttachmentInline,
ProblemInstanceInline, ProblemSiteInline]
readonly_fields = ['author', 'name', 'short_name', 'controller_name',
'package_backend_name', 'main_problem_instance']
exclude = ['contest']
Expand Down
4 changes: 3 additions & 1 deletion oioioi/problems/fixtures/test_problem_packages.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
"name": "XYZ",
"contest": "c",
"short_name": "xyz",
"controller_name": "oioioi.sinolpack.controllers.SinolProblemController"
"controller_name": "oioioi.sinolpack.controllers.SinolProblemController",
"author": 1000,
"is_public": true
}
},
{
Expand Down
24 changes: 24 additions & 0 deletions oioioi/problems/fixtures/test_tags.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[
{
"pk": 1,
"model": "problems.tag",
"fields": {
"name": "mrowkowiec"
}
},
{
"pk": 2,
"model": "problems.tag",
"fields": {
"name": "mrowka"
}
},
{
"pk": 1,
"model": "problems.tagthrough",
"fields": {
"problem": 1,
"tag": 1
}
}
]
64 changes: 63 additions & 1 deletion oioioi/problems/forms.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from collections import OrderedDict

from django import forms
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from django.utils.safestring import mark_safe

from oioioi.base.utils.input_with_generate import TextInputWithGenerate
from oioioi.contests.models import ProblemStatementConfig
from oioioi.problems.models import ProblemSite
from oioioi.problems.models import ProblemSite, Tag, TagThrough


class ProblemUploadForm(forms.Form):
Expand Down Expand Up @@ -61,3 +63,63 @@ def __init__(self, url_key, *args, **kwargs):
super(ProblemsetSourceForm, self).__init__(*args, **kwargs)
if url_key:
self.initial = {'url_key': url_key}


# TagSelectionWidget is designed to work with django-admin

class TagSelectionWidget(forms.Widget):
html_template = "<div>" \
"<input type=\"text\" autocomplete=\"off\" " \
"id=\"%(id)s\" " \
"name=\"%(name)s\" " \
"onfocus=\"init_tag_addition(this.id, '%(data-hints-url)s')\" " \
"value=\"%(value)s\" />" \
"<span id=\"%(id)s-hints\" style=\"margin-left: 5px;\"></span>" \
"</div>"

def __init__(self, hints_url=None, *args, **kwargs):
self.hints_url = hints_url
super(TagSelectionWidget, self).__init__(*args, **kwargs)

def render(self, name, value, attrs=None):
# Value can either be an integer (Tag's id) or a string (Tag's name)
if value is None:
value = ''
elif isinstance(value, int):
value = Tag.objects.get(id=value).name
arguments = {
'id': attrs['id'],
'name': name,
'value': value,
'data-hints-url': reverse(self.hints_url),
}
return mark_safe(self.html_template % arguments)

class Meta(object):
js = ('common/tag_selection.js',)


class TagSelectionField(forms.ModelChoiceField):
def __init__(self, data_hints_url):
for field in Tag._meta.fields:
if field.name == 'name':
self.default_validators = field.validators
break
else:
self.default_validators = []
self.widget = TagSelectionWidget(data_hints_url)
super(TagSelectionField, self).__init__(Tag.objects,
to_field_name='name')

def clean(self, value):
for validator in self.default_validators:
validator(value)
return Tag.objects.get_or_create(name=value)[0]


class TagThroughForm(forms.ModelForm):
tag = TagSelectionField('get_tag_hints')

class Meta(object):
fields = ['problem']
model = TagThrough
58 changes: 58 additions & 0 deletions oioioi/problems/migrations/0005_add_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations
import re
import django.core.validators


class Migration(migrations.Migration):

dependencies = [
('problems', '0004_problem_main_problem_instance'),
]

operations = [
migrations.CreateModel(
name='Tag',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(unique=True, max_length=20, verbose_name='name', validators=[django.core.validators.MinLengthValidator(3), django.core.validators.MaxLengthValidator(20), django.core.validators.RegexValidator(re.compile('^[-a-zA-Z0-9_]+$'), "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.", 'invalid')])),
],
options={
'verbose_name': 'tag',
'verbose_name_plural': 'tags',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='TagThrough',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('problem', models.ForeignKey(to='problems.Problem')),
('tag', models.ForeignKey(to='problems.Tag')),
],
options={
},
bases=(models.Model,),
),
migrations.AlterUniqueTogether(
name='tagthrough',
unique_together=set([('problem', 'tag')]),
),
migrations.AddField(
model_name='tag',
name='problems',
field=models.ManyToManyField(to='problems.Problem', through='problems.TagThrough'),
preserve_default=True,
),
migrations.CreateModel(
name='MainProblemInstance',
fields=[
],
options={
'proxy': True,
},
bases=('contests.probleminstance',),
),
]
31 changes: 31 additions & 0 deletions oioioi/problems/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from contextlib import contextmanager
from traceback import format_exception

from django.core import validators
from django.core.validators import validate_slug
from django.core.files.base import ContentFile
from django.db import models, transaction
Expand Down Expand Up @@ -297,3 +298,33 @@ class Meta(object):
class MainProblemInstance(ProblemInstance):
class Meta(object):
proxy = True


class Tag(models.Model):
name = models.CharField(max_length=20, unique=True,
verbose_name=_("name"), null=False, blank=False,
validators=[
validators.MinLengthValidator(3),
validators.MaxLengthValidator(20),
validators.validate_slug,
])
problems = models.ManyToManyField(Problem, through='TagThrough')

class Meta(object):
verbose_name = _("tag")
verbose_name_plural = _("tags")

def __unicode__(self):
return str(self.name)


class TagThrough(models.Model):
problem = models.ForeignKey(Problem)
tag = models.ForeignKey(Tag)

# This string will be visible in admin form
def __unicode__(self):
return unicode(self.tag.name)

class Meta(object):
unique_together = ('problem', 'tag')
92 changes: 92 additions & 0 deletions oioioi/problems/static/common/tag_selection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
function init_tag_addition(id, hints_url) {
$(function(){
var input = $('#' + id);
var hints = $('#' + id + '-hints');

/* Prevent multiple-initialization */
if(input.data("init"))
return;
input.data("init", true);

var changeInput = function() {
var make_hint = function(tag, query) {
var index = tag.indexOf(query);
var pre = tag.substr(0, index);
var mid = tag.substr(index, query.length);
var suf = tag.substr(index + query.length);

var label = $("<strong></strong>");
label.html(pre + "<u>" + mid + "</u>" + suf);
label.click(function() { input.val(tag); changeInput(); });
label.css('cursor', 'pointer');
return label;
};

var html_escape = function(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
};

var query = input.val();

if(query.length >= 2) {
$.getJSON(hints_url, {substr: query},
function(items) {
var exists = false;
var hint_chain = $('<span></span>');
var count = 0;
for(var i = 0; i < items.length; i++) {
if(items[i] == query)
exists = true;
else {
if(count !== 0)
hint_chain.append(", ");
else
hint_chain.append(gettext("Try: "));
hint_chain.append(make_hint(items[i], query));
count++;
}
}
if(!exists) {
hints.html(gettext(
"Tag '%(query)s' doesn't exist." +
" It will be added if you save the problem.")
.fmt({query: html_escape(query)}) + " ");
} else {
hints.html(gettext("Tag exists.") + " ");
}
hints.append(hint_chain);
});
} else {
hints.html(gettext("Type tag name."));
}
};

input.keyup(changeInput);
input.change(changeInput);

changeInput();
});
}

function init_tag_selection(id) {
$(function(){
var input = $('#' + id);
var form = $('#' + id + '-form');
input.typeahead({
source: function(query, process) {
$.getJSON(input.data("hintsUrl"), {substr: query}, process);
},
minLength: 2,
updater: function(item) {
input.val(item);
form.submit();
return item;
},
});
});
}
Loading

0 comments on commit 214f9da

Please sign in to comment.