diff options
author | Alvin Li <liweitianux@gmail.com> | 2013-10-09 15:52:53 +0800 |
---|---|---|
committer | Alvin Li <liweitianux@gmail.com> | 2013-10-09 15:52:53 +0800 |
commit | 02afd8a32edb13ea7fc2266ac80092ea15c0930c (patch) | |
tree | c7a2a3f50378c017b425da47e04a71c6beaae56c /97suifangqa/apps/recommend | |
parent | fafce2cfc72f4e1cd14ff6cb693c8ec7854159c5 (diff) | |
download | 97dev-02afd8a32edb13ea7fc2266ac80092ea15c0930c.tar.bz2 |
* treat 'apps/utils' as regular django app; which used to store
general tools for used in other apps
* moved 'templatetags' from 'apps/indicator' to 'apps/utils'
* '.gitignore' to ignore 'fixtures_bak'
* moved js plugins from 'apps/indicator/static/plugins' to
'staticfiles/plugins'
apps/recommend:
* updated 'recommend.models';
o commented 'recommend.models.ResearchCombination' (not used)
* implemented views 'add_edit_blog_info' and 'ajax_add_edit_configs';
* added pages 'templates/recommend/add_edit_blog_info.html',
'add_edit_blog_info_error.html';
o related css and javascripts files
* added 'tools.py';
* added 'utils/tools.py' for placing generic functions;
* deleted 'initial_data.json' (mv 'fixtures' to 'fixtures_bak');
* small fixes to 'indicator.models', 'sciblog.models' and
'sfaccount.views'
* fixed automatically show 'proper_nouns' annotation in blog:
recovered the line 'import signals' in 'sciblog.models'
* added 'is_ok()' method for 'recommend.models.ResearchConfig';
Diffstat (limited to '97suifangqa/apps/recommend')
10 files changed, 1381 insertions, 7 deletions
diff --git a/97suifangqa/apps/recommend/models.py b/97suifangqa/apps/recommend/models.py index 98ae64d..0c24c67 100644 --- a/97suifangqa/apps/recommend/models.py +++ b/97suifangqa/apps/recommend/models.py @@ -8,8 +8,12 @@ models for apps/recommend from django.db import models from django.contrib import admin +from utils.tools import format_float -class TreatRespnse(models.Model): # {{{ +import re + + +class TreatResponse(models.Model): # {{{ """ 治疗反应/结果的描述,以及结果的价值/权重 """ @@ -25,11 +29,11 @@ class TreatRespnse(models.Model): # {{{ verbose_name_plural = u"治疗反应" def __unicode__(self): - return u"< TreatRespnse: %s >" % self.name + return u"< TreatResponse: %s >" % self.name def save(self, **kwargs): if self.is_valid(): - super(TreatRespnse, self).save(**kwargs) + super(TreatResponse, self).save(**kwargs) else: return self @@ -37,14 +41,433 @@ class TreatRespnse(models.Model): # {{{ # check weight range if (self.weight < 0.0) or (self.weight > 10.0): print u"Error: weight < 0.0 / weight > 10.0" + raise ValueError(u"Error: weight<0.0 / weight>10.0") return False # return True + + def dump(self, **kwargs): + dump_data = { + 'id': self.id, + 'name': self.name, + 'description': self.description, + 'weight': self.weight, + } + return dump_data +# }}} + + +class ResearchIndicator(models.Model): # {{{ + """ + model to record the indicators which researched in the paper + """ + MEAN_TYPE = 'MEAN' + SCOPE_TYPE = 'SCOP' + KIND_TYPE = 'KIND' + DATA_TYPES = ( + (MEAN_TYPE, u'平均值'), + (SCOPE_TYPE, u'范围'), + (KIND_TYPE, u'种类'), + ) + blog = models.ForeignKey("sciblog.SciBlog", + related_name="research_indicators", + verbose_name=u"待关联文章") + indicator = models.ForeignKey("indicator.Indicator", + related_name="research_indicators", + verbose_name=u"待关联指标") + dataType = models.CharField(u"指标值类型", max_length=4, + choices=DATA_TYPES) + # datetime + created_at = models.DateTimeField(u"创建时间", auto_now_add=True) + updated_at = models.DateTimeField(u"更新时间", + auto_now_add=True, auto_now=True) + + class Meta: + verbose_name_plural = u"研究指标信息" + ordering = ['blog__id', 'indicator__id'] + + def __unicode__(self): + # cut down the length of title + blog_title = self.blog.title[:10] + return u"< ResearchIndicator: %s -> %s (type %s) >" % ( + blog_title, self.indicator.name, self.dataType) + + def save(self, **kwargs): + if self.is_valid(): + super(ResearchIndicator, self).save(**kwargs) + else: + return self + + def is_valid(self, **kwargs): # {{{ + # check dataType, which should be consistent with the + # dataType of the related indicator + ind_obj = self.indicator + ind_dataType = ind_obj.dataType + if self.dataType == self.MEAN_TYPE: + if ind_dataType not in [ind_obj.FLOAT_TYPE, ind_obj.FLOAT_RANGE_TYPE]: + raise ValueError(u"Error: dataType与Indicator不符") + return False + elif self.dataType == self.SCOPE_TYPE: + if ind_dataType not in [ind_obj.RANGE_TYPE, ind_obj.FLOAT_RANGE_TYPE]: + raise ValueError(u"Error: dataType与Indicator不符") + return False + elif self.dataType == self.KIND_TYPE: + if ind_dataType in [ind_obj.FLOAT_TYPE, + ind_obj.RANGE_TYPE, ind_obj.FLOAT_RANGE_TYPE]: + raise ValueError(u"Error: dataType与Indicator不符") + return False + else: + raise ValueError(u"Error: unknown dataType") + return False + # + return True + # }}} + + def dump(self, **kwargs): + dump_data = { + 'id': self.id, + 'blog_id': self.blog.id, + 'indicator_id': self.indicator.id, + 'indicator_name': self.indicator.name, + 'dataType': self.dataType, + } + return dump_data +# }}} + + +# class ResearchCombination(models.Model): # {{{ +# """ +# 记录文章研究的有效的指标组合 +# """ +# blog = models.ForeignKey("sciblog.SciBlog", +# related_name="research_combinations", +# verbose_name=u"待关联文章") +# combination = models.ManyToManyField("indicator.Indicator", +# related_name="research_combinations", +# verbose_name=u"研究指标组合") +# # datetime +# created_at = models.DateTimeField(u"创建时间", auto_now_add=True) +# updated_at = models.DateTimeField(u"更新时间", +# auto_now_add=True, auto_now=True) +# +# class Meta: +# verbose_name_plural = u"研究指标有效组合" +# ordering = ['blog__id'] +# +# def __unicode__(self): +# # cut down the length of title +# blog_title = self.blog.title[:10] +# combination = u'' +# for ind in self.combination: +# combination = combination + ind.name + ", " +# combination = re.sub(r',\ $', '', combination) +# return u"< ResearchCombination: %s -> (%s) >" % ( +# blog_title, combination) +# +# def save(self, **kwargs): +# if self.is_valid(): +# super(ResearchCombination, self).save(**kwargs) +# else: +# return self +# +# def is_valid(self): +# """ +# These M2M indicators must have related to the blog +# """ +# related_indicators = [ri.indicator +# for ri in self.blog.research_indicators.all()] +# for ind in self.combination: +# if ind not in related_indicators: +# raise ValueError(u"Error: 选择了未关联到该文章的指标") +# return False +# return True +# # }}} + + +# ResearchAtom {{{ +class ResearchAtom(models.Model): + """ + ???any good name??? + 用于记录某篇文章中对某个指标所分的每一个小类的具体信息 + """ + blog = models.ForeignKey("sciblog.SciBlog", + related_name="research_atoms", + verbose_name=u"待关联文章") + researchIndicator = models.ForeignKey("ResearchIndicator", + related_name="research_atoms", + verbose_name=u"文章研究指标") + ## value + # unit (XXX: only standard unit supported at the moment) + unit = models.ForeignKey("indicator.Unit", + null=True, blank=True, + related_name="research_atoms", verbose_name=u"单位") + # dataType: MEAN + mean = models.FloatField(u"平均值", null=True, blank=True) + sd = models.FloatField(u"标准值", null=True, blank=True) + # dataType: SCOP + scope_min = models.FloatField(u"范围下限", null=True, blank=True) + scope_max = models.FloatField(u"范围上限", null=True, blank=True) + # dataType: KIND + kind = models.ForeignKey("ValueKind", + null=True, blank=True, + related_name="research_atoms", verbose_name=u"种类") + ## datetime + created_at = models.DateTimeField(u"创建时间", auto_now_add=True) + updated_at = models.DateTimeField(u"更新时间", + auto_now_add=True, auto_now=True) + + class Meta: + verbose_name_plural = u"研究指标原子类" + ordering = ['blog__id', 'researchIndicator__id'] + + def __unicode__(self): + return u'< ResearchAtom: #%s %s >' % (self.id, self.display()) + + def save(self, **kwargs): + if self.is_valid(): + super(ResearchAtom, self).save(**kwargs) + else: + return self + + def is_valid(self, **kwargs): # {{{ + """ + The blog here must be consistent with the blog related to + the researchIndicator + """ + ri_obj = self.researchIndicator + dataType = ri_obj.dataType + # confine + ind_obj = ri_obj.indicator + if ind_obj.check_confine(): + confine = ind_obj.get_confine() + else: + raise ValueError(u"Error: 该指标未指定InnateConfine") + return False + # check blog id first + if self.blog.id != ri_obj.blog.id: + raise ValueError(u"Error: 关联blog错误") + return False + # check dataType and confine + if dataType == ri_obj.MEAN_TYPE: + if not (self.unit and self.unit.standard): + raise ValueError(u"Error: 单位未填写/不是标准单位") + return False + if ((self.mean is None) or (self.sd is None)): + raise ValueError(u"Error: 平均值/标准差未填写") + return False + # check with confine + # XXX: only check 'mean' at the moment; 'sd' may also needed + if (self.mean < confine['math_min']) or ( + self.mean > confine['math_max']): + raise ValueError(u"Error: 平均值超出允许范围") + return False + elif dataType == ri_obj.SCOPE_TYPE: + if not (self.unit and self.unit.standard): + raise ValueError(u"Error: 单位未填写/不是标准单位") + return False + if ((self.scope_min is None) or (self.scope_max is None)): + raise ValueError(u"Error: 范围下限/上限未填写") + return False + if (self.scope_min >= self.scope_max): + raise ValueError(u"Error: scope_min>=scope_max") + return False + # check confine + if (self.scope_min < confine['math_min']) or ( + self.scope_max > confine['math_max']): + raise ValueError(u"Error: scope_min/scope_max 超出允许范围") + return False + elif dataType == ri_obj.KIND_TYPE: + if not self.kind: + raise ValueError(u"Error: 未选择种类") + return False + else: + raise ValueError(u"Error: unknown dataType") + return False + # + return True + # }}} + + def get_value(self, **kwargs): # {{{ + ri_obj = self.researchIndicator + dataType = ri_obj.dataType + value = { + 'id': self.id, + 'dataType': dataType, + 'blog_id': self.blog.id, + } + if dataType == ri_obj.MEAN_TYPE: + value['mean'] = self.mean + value['sd'] = self.sd + value['unit'] = self.unit.dump() + elif dataType == ri_obj.SCOPE_TYPE: + value['scope_min'] = self.scope_min + value['scope_max'] = self.scope_max + value['unit'] = self.unit.dump() + elif dataType == ri_obj.KIND_TYPE: + value['kind'] = self.kind.dump() + else: + value['error'] = True + return value + # }}} + + def display(self, **kwargs): + """ + generate the display string for front page + """ + ri_obj = self.researchIndicator + ind_name = ri_obj.indicator.name + dataType = ri_obj.dataType + if dataType == ri_obj.MEAN_TYPE: + value = '%sSD%s' % (format_float(self.mean), self.sd) + elif dataType == ri_obj.SCOPE_TYPE: + value = '%s~%s' % (format_float(self.scope_min), + format_float(self.scope_max)) + elif dataType == ri_obj.KIND_TYPE: + value = '%s' % (self.kind.name) + else: + value = 'UNKNOWN' + disp_str = u'%s(%s|%s)' % (ind_name, dataType, value) + return disp_str +# }}} + + +# ResearchConfig {{{ +class ResearchConfig(models.Model): + """ + 记录某篇文章所研究的某一个具体的组合(哪几个指标的具体值) + 的治疗结果等信息 + """ + blog = models.ForeignKey("sciblog.SciBlog", + related_name="research_configs", + verbose_name=u"待关联文章") + researchAtoms = models.ManyToManyField("ResearchAtom", + related_name="research_configs", + verbose_name=u"研究指标值组合") + treatResponse = models.ForeignKey("TreatResponse", + related_name="research_configs", + verbose_name=u"治疗反应") + weight = models.FloatField(u"权重", help_text=u"范围:0-10") + # datetime + created_at = models.DateTimeField(u"创建时间", auto_now_add=True) + updated_at = models.DateTimeField(u"更新时间", + auto_now_add=True, auto_now=True) + + class Meta: + verbose_name_plural = u"研究指标值组合信息" + ordering = ['blog__id'] + + def __unicode__(self): + # XXX + info = '' + for atom in self.researchAtoms.all(): + info = '%s%s,' % (info, atom.id) + info = re.sub(r',\s*$', '', info) + return u'< ResearchConfig: #%s (Atoms: %s) -> %s >' % ( + self.id, info, self.blog.title[:10]) + + def save(self, **kwargs): + if self.is_valid(): + super(ResearchConfig, self).save(**kwargs) + else: + return self + + def is_valid(self): + """ + The blog here must be consistent with the blog related to + the researchAtoms + """ + # check weight range + if (self.weight < 0.0) or (self.weight > 10.0): + raise ValueError(u"Error: weight<0.0 / weight>10.0") + return False + #### check blog #### + ## Error: needs to have a value for field "researchconfig" before this many-to-many relationship can be used. + #for atom in self.researchAtoms.all(): + # if atom.blog.id != self.blog.id: + # raise ValueError(u"Error: 关联blog错误") + # return False + #### end #### + return True + + def is_ok(self, **kwargs): + """ + check this config whether ok or not? + i.e.: whether the data fields are valid? + """ + # check atoms + if not self.researchAtoms.all(): + return False + # check blog id + for atom in self.researchAtoms.all(): + if atom.blog.id != self.blog.id: + raise ValueError(u"Error: 关联blog错误") + return False + # + return True + + def dump(self, **kwargs): + dump_data = { + 'id': self.id, + 'blog_id': self.blog.id, + 'researchAtoms_id': [atom.id + for atom in self.researchAtoms.all()], + 'treatResponse_id': self.treatResponse.id, + 'weight': self.weight, + } + return dump_data +# }}} + + +class ValueKind(models.Model): # {{{ + """ + 记录系统所有可能使用到的"种类"值 + 并为需要使用"种类"值的地方提供可选范围(e.g.: recommend.ResearchAtom) + 使用统一的符号(symbol)来寻找及匹配 + """ + name = models.CharField(u"名称", max_length=30) + symbol = models.CharField(u"符号", max_length=30, + help_text=u"仅能使用字母、数字和下划线,最长30字符") + description = models.TextField(u"描述", blank=True) + + class Meta: + verbose_name_plural = u"可选种类" + + def __unicode__(self): + return u'< ValueKind: %s (%s) >' % (self.name, self.symbol) + + def save(self, **kwargs): + if self.is_valid(): + super(ValueKind, self).save(**kwargs) + else: + return self + + def is_valid(self): + # check symbol + sym_regex = re.compile(r'^[_0-9a-zA-Z]+$') + if sym_regex.search(self.symbol): + return True + else: + raise ValueError(u"仅能使用字母、数字和下划线,最长30字符") + return False + + def dump(self, **kwargs): + dump_data = { + 'id': self.id, + 'name': self.name, + 'symbol': self.symbol, + 'description': self.description, + } + return dump_data # }}} # admin admin.site.register([ - TreatRespnse, + TreatResponse, + ResearchIndicator, + #ResearchCombination, + ResearchAtom, + ResearchConfig, + ValueKind, ]) diff --git a/97suifangqa/apps/recommend/static/css/add_edit_blog_info.css b/97suifangqa/apps/recommend/static/css/add_edit_blog_info.css new file mode 100644 index 0000000..6d2137e --- /dev/null +++ b/97suifangqa/apps/recommend/static/css/add_edit_blog_info.css @@ -0,0 +1,107 @@ +/* + * css for 'add_edit_blog_info' page + * + * 2013/10/07 + */ + +section { + margin: 0.8em; +} + +h4 { + margin: 0.5em; +} + +p { + margin: 0.3em; +} + +section.categories { + border: 1px solid #4A4A4A; + border-radius: 2px; + padding: 0.8em; +} + +section.combinations { + border: 1px solid #4A4A4A; + border-radius: 2px; + padding: 0.8em; +} + +section.configs { + border: 1px solid #99CC33; + border-radius: 2px; + padding: 0.8em; +} + +input[type="button"], input[type="submit"] { + background-color: #99CC33; + border: 1px solid #F3F2F0; + border-radius: 3px; + box-shadow: 0 0 2px #FFFFFF; + color: #FFFFFF; + cursor: pointer; + height: 2.1em; + padding-left: 0.8em; + padding-right: 0.8em; + vertical-align: middle; +} + +input.selected[type="button"] { + background-color: #99CC33; + color: #FFFFFF; + border-color: #F3F2F0; +} + +input.unselected[type="button"] { + background-color: #FFFFFF; + color: #7E7E7E; + border-color: #4A4A4A; +} + +input[type="text"] { + width: 50px; + border-radius: 1px; + border: 1px solid #9C9C9C; +} +input.weight.valid { + border: 2px solid green; +} +input.weight.invalid { + border: 2px solid red; +} + +table { + width: 100%; +} +table th.name { + width: 70%; +} +table th.response { + width: 20%; +} +table th.weight { + width: 10%; +} +th, td { + /* border: 1px solid black; */ + padding: 0.2em; +} +table tr.odd { + background-color: #E0F0C2; +} +table tr.invalid { + background-color: #FFCCCC; +} + +select.treat_response { + border: 1px solid #9C9C9C; + background-color: #FFFFFF; + color: #000000; +} + +span.error { + background-color: #FFB3B3; + padding: 2px; +} + diff --git a/97suifangqa/apps/recommend/static/css/recommend_index.css b/97suifangqa/apps/recommend/static/css/recommend_index.css new file mode 100644 index 0000000..0039a2c --- /dev/null +++ b/97suifangqa/apps/recommend/static/css/recommend_index.css @@ -0,0 +1,21 @@ +/* + * css for 'recommend_index' page + * + * 2013/10/07 + */ + + +ul li { + padding: 4px +} + +span.has_info, .has_info a { + background-color: #B8DB70; + padding: 2px; +} + +span.error { + background-color: #FFB3B3; + padding: 2px; +} + diff --git a/97suifangqa/apps/recommend/static/javascripts/add_edit_blog_info.js b/97suifangqa/apps/recommend/static/javascripts/add_edit_blog_info.js new file mode 100644 index 0000000..1d994e5 --- /dev/null +++ b/97suifangqa/apps/recommend/static/javascripts/add_edit_blog_info.js @@ -0,0 +1,330 @@ +// +// js for 'add_edit_blog_info' page +// +// 2013/10/08 +// + +// a list contains all the configs data objects +var research_configs_list = new Array(); + +$(document).ready(function() { + // make a configs list from 'research_configs {{{ + var obj_keys = Object.keys(research_configs); + for (var i=0; i<obj_keys.length; i++) { + var key = obj_keys[i]; + var configs = research_configs[key].configs; + research_configs_list = research_configs_list.concat(configs); + } + // }}} + + // categories buttons {{{ + var cate_btns_html = ''; + for (var i=1; i<=rind_num; i++) { + var btn_id = 'btn_cate_'+i; + var btn_value = i+'个指标'; + cate_btns_html += '<input type="button" class="unselected" id="'+btn_id + '" value="'+btn_value + '" />   '; + }; + $("#cate_btns").html(cate_btns_html); + // button actions + $('#cate_btns input[type="button"]').on("click", document, function() { + // unselect all buttons + //console.log(this); + $('#cate_btns input[type="button"]').removeClass("selected"); + $('#cate_btns input[type="button"]').addClass("unselected"); + $(this).removeClass("unselected"); + $(this).addClass("selected"); + // unselect buttons of combinations + $('#comb_divs input[type="button"]').removeClass("selected"); + $('#comb_divs input[type="button"]').addClass("unselected"); + // hide configs div's + $('#conf_divs .conf_comb').hide(); + // display category of combinations + var cate_id = $(this).attr('id').replace('btn_cate_', ''); + $('#comb_divs .comb').hide(); + $('#div_comb_'+cate_id).show(); + }); + // }}} + + // indicator combinations div's and buttons {{{ + var comb_divs_html = ''; + for (var i=1; i<=rind_num; i++) { + var id = 'div_comb_'+i; + comb_divs_html += '<div class="comb" id="'+id + '" style="display: none;">\n'; + // combinations buttons + var btn_comb_html = ''; + var combs = rind_categories['N'+i]; + for (var j=0; j<combs.length; j++) { + var btn_id = combs[j].tag; + // get value for button + var btn_value = get_comb_btn_value(combs[j].data); + btn_comb_html += '<input type="button" class="unselected" id="'+btn_id + '" value="'+btn_value + '" />   '; + }; + comb_divs_html += btn_comb_html + '\n</div>\n'; + }; + $("#comb_divs").html(comb_divs_html); + // button actions + $('#comb_divs input[type="button"]').on("click", document, function() { + // unselect all buttons + $('#comb_divs input[type="button"]').removeClass("selected"); + $('#comb_divs input[type="button"]').addClass("unselected"); + $(this).removeClass("unselected"); + $(this).addClass("selected"); + // display configs of the combination + var comb_id = $(this).attr('id'); + $('#conf_divs .conf_comb').hide(); + $('#div_conf_'+comb_id).show(); + }); + // }}} + + // config div's & info input {{{ + var conf_divs_html = ''; + for (var i=0; i<rind_combs.length; i++) { + var comb = rind_combs[i]; + var id = 'div_conf_'+comb.tag; + conf_divs_html += '<div class="conf_comb" id="'+id + '" style="display: none;">\n'; + // configs input + var conf_input_html = '<table class="conf">\n'; + // table head + conf_input_html += '<thead style="display: none;">\n<tr class="head">'; + conf_input_html += '<th class="name"></th> <th class="response"></th> <th class="weight"></th>'; + conf_input_html += '</tr></thead>\n'; + // table body + conf_input_html += '<tbody>\n'; + var configs = research_configs[comb.tag].configs; + //console.log(configs); + for (var j=0; j<configs.length; j++) { + // odd or even (for table tr style) + if (j%2 == 0) { + var odd_even = 'odd'; + } + else { + var odd_even = 'even'; + } + var conf = configs[j]; + var conf_id = conf.tag; + var conf_tr_html = '<tr class="conf '+odd_even + '" id="'+conf_id + '">'; + // display name column + conf_tr_html += '<td class="name">' + conf.display + '</td>'; + // treat response column (prompt & select input) + conf_tr_html += '<td class="response">' + treat_responses_objs.name + ': <select class="treat_response"></select></td>'; + // weight column + conf_tr_html += '<td class="weight">权重: <input type="text" class="weight" value="" /></td>'; + // + conf_tr_html += '</tr>\n'; + conf_input_html += conf_tr_html; + }; + conf_input_html += '</tbody>\n</table>'; + //console.log(conf_input_html); + conf_divs_html += conf_input_html + '\n</div>\n'; + }; + $("#conf_divs").html(conf_divs_html); + // }}} + + // treat response select {{{ + $("select.treat_response").each(function() { + // add options for 'select' + var select_html = ''; + // add empty value + select_html += '<option value="" selected="selected">----</option>\n'; + for (var i=0; i<treat_responses_list.length; i++) { + var tr = treat_responses_list[i]; + select_html += '<option class="response tr'+tr.id+'" value="id'+tr.id + '">' + tr.name + '</option>\n'; + } + $(this).html(select_html); + }); + // }}} + + // validate weight {{{ + $("input.weight").on("validate", null, function(e) { + e.stopPropagation(); + var value = $(this).val(); + var number = parseFloat(value); + if (value == "") { + $(this).removeClass("valid invalid"); + } + else if (isNaN(number)) { + $(this).removeClass("valid"); + $(this).addClass("invalid"); + } + else if (number<0.0 || number>10.0) { + $(this).removeClass("valid"); + $(this).addClass("invalid"); + } + else { + $(this).removeClass("invalid"); + $(this).addClass("valid"); + } + }); + $("input.weight").on("change", null, function() { + $(this).trigger("validate"); + }); + // }}} + + // validate tr conf {{{ + // if only one of the 'treat_response' or 'weight' has data + // then 'invalid' + $("tr.conf").on("validate", null, function(e) { + e.stopPropagation(); + var weight_jq = $(this).find("input.weight"); + var response_jq = $(this).find("select.treat_response"); + //console.log(weight_jq); + // NOTES: + // only trigger 'validate' of '.weight' element, when + // the event originated on any element apart from '.weight' + // REF: http://stackoverflow.com/questions/5967923/jquery-trigger-click-gives-too-much-recursion + if (! $(e.target).is(".weight")) { + weight_jq.trigger("validate"); + } + if (weight_jq.hasClass("invalid")) { + $(this).addClass("invalid"); + } + // only one of the 'treat_response' or 'weight' has data + if (weight_jq.val() !== '' && response_jq.val() === '') { + $(this).addClass("invalid"); + } + else if (weight_jq.val() === '' && response_jq.val() !== '') { + $(this).addClass("invalid"); + } + }); + // }}} + + // fill provided configs data {{{ + $("tr.conf").each(function() { + var conf_id = $(this).attr('id'); + var conf_obj = get_config_obj(conf_id); + if (conf_obj.hasOwnProperty('id')) { + // this config already in database + var tr_id = conf_obj.treatResponse_id; + $(this).find('select>option.tr'+tr_id).prop('selected', true); + $(this).find('.weight').val(conf_obj.weight); + // validate tr config + $(this).trigger('validate'); + } + }); + // }}} + + + // back_to_list button {{{ + $("#back_to_list").bind("click", function() { + var msg = '注意:当前页面未保存的信息将会丢失。是否继续?'; + var ans = confirm(msg); + if (ans) { + window.location.href = recommend_index_url; + } + else { + return false; + } + }); + // }}} + + // submit info button {{{ + $("#submit_info").on("click", null, function() { + // validate tr conf data + $("tr.conf").trigger("validate"); + if ($("tr.conf.invalid").length) { + alert('存在有错误数据的行,请更正后再提交'); + return false; + } + // collect conf data + var configs_list = new Array(); + $("tr.conf").each(function() { + var conf_id = $(this).attr('id'); + var conf_obj = get_config_obj(conf_id); + // get config data + var weight_jq = $(this).find("input.weight"); + var response_jq = $(this).find("select.treat_response"); + var weight = weight_jq.val(); + var tr_id = response_jq.val().replace('id', ''); + // + if (conf_obj.hasOwnProperty('id')) { + if (weight === '' && tr_id === '') { + // delete config + conf_obj['action'] = 'delete'; + conf_obj['weight'] = null; + conf_obj['treatResponse_id'] = null; + } + else { + // edit config + conf_obj['action'] = 'edit'; + conf_obj['weight'] = parseFloat(weight); + conf_obj['treatResponse_id'] = parseInt(tr_id); + } + } + else { + if (weight !== '' && tr_id !== '') { + // add config + conf_obj['action'] = 'add'; + conf_obj['weight'] = parseFloat(weight); + conf_obj['treatResponse_id'] = parseInt(tr_id); + } + else { + // null config + conf_obj['action'] = null; + conf_obj['weight'] = null; + conf_obj['treatResponse_id'] = null; + } + } + // push config data + configs_list.push(conf_obj); + }); + //console.log(configs_list); + + // ajax post configs data {{{ + var time = moment().valueOf(); + $.ajax({ + type: 'post', + url: recommend_url + 'ajax/add_edit_configs/', + data: { + csrfmiddlewaretoken: document.getElementsByName('csrfmiddlewaretoken')[0].value, + configs_list: JSON.stringify(configs_list), + blog_id: blog_id, + time: time + }, + dataType: 'json', + success: function(dataJson) { + if (dataJson.failed == true) { + // submit failed + alert('Error: submit failed'); + return false; + } + else { + alert('submit successful'); + // reload page (do not use cache) + location.reload(true); + } + } + }); + // }}} + }); + // }}} +}); + + +// generate value for combination button +// sort id list by magnitude +function get_comb_btn_value(rid_list) { + // sort id list numerically (smallest first) + rid_list.sort(function(a, b) { return a-b }); + var value = ''; + for (var i=0; i<rid_list.length; i++) { + var key = 'id'+rid_list[i]; + value += rind_objs[key].indicator_name + ' | '; + } + value = value.replace(/\s*\|\s*$/, ''); + return value; +}; + +// return the config js obj (copy) +// by searching the given 'tag' in 'research_configs_list' +function get_config_obj(tag) { + var result = $.grep(research_configs_list, + function(e) { return e.tag == tag }); + if (result.length == 1) { + return $.extend({}, result[0]); + } + else { + return null; + } +}; + +// vim: set ts=8 sw=4 tw=0 fenc= ft=javascript: // diff --git a/97suifangqa/apps/recommend/templates/recommend/add_edit_blog_info.html b/97suifangqa/apps/recommend/templates/recommend/add_edit_blog_info.html new file mode 100644 index 0000000..e646abf --- /dev/null +++ b/97suifangqa/apps/recommend/templates/recommend/add_edit_blog_info.html @@ -0,0 +1,87 @@ +{% extends "base.html" %} +{% load static from staticfiles %} +{% load dict_get %} + +{% block title %} +添加文章信息 | admin | 97 随访 +{% endblock %} + +{% block css %} + <link rel="stylesheet" type="text/css" href="{% static "css/add_edit_blog_info.css" %}"/> +{% endblock %} + +{% block scripts %} + <script type="text/javascript" src="{% static "plugins/moment/moment.min.js" %}"></script> + <script type="text/javascript" src="{% static "plugins/moment/lang/zh-cn.js" %}"></script> + <script type="text/javascript" src="{% static "javascripts/add_edit_blog_info.js" %}"></script> + + <script type="text/javascript"> + // urls + var static_url = "{{ STATIC_URL }}"; + var recommend_url = "/recommend/"; + + // moment.js: default the language to English + moment.lang('en'); + // default date format + var mm_date_fmt = "YYYY-MM-DD"; + + // index + var recommend_index_url = '{% url recommend_index %}'; + // blog id + var blog_id = {{ blog.id }}; + // number of research indicators + var rind_num = {{ rind_num }}; + // all combinations of research indicators + var rind_combs = $.parseJSON('{{ rind_combs_json|safe }}'); + // research indicator categories (by number) + var rind_categories = $.parseJSON('{{ rind_categories_json|safe }}'); + // research indicator objs dump + var rind_objs = $.parseJSON('{{ rind_objs_json|safe }}'); + // research configs + var research_configs = $.parseJSON('{{ research_configs_json|safe }}'); + // treat response data + var treat_responses_list = $.parseJSON('{{ treat_responses_list_json|safe }}'); + var treat_responses_objs = $.parseJSON('{{ treat_responses_objs_json|safe }}'); + + </script> +{% endblock %} + +{% block page %} + {% csrf_token %} + + <h2>添加文章信息</h2> + + <section class="blog"> + id: {{ blog.id }} + <br /> + title: {{ blog.title }} + </section> + + <section class="buttons"> + <input type="button" id="submit_info" value="提交信息" /> +   |   + <input type="button" id="back_to_list" value="返回文章列表" /> + </section> + + <section class="categories"> + <!-- buttons --> + <div id="cate_btns"></div> + </section> + + <section class="combinations"> + <!-- div's & buttons --> + <div id="comb_divs"></div> + </section> + + <section class="configs"> + <!-- div's & info input --> + <div id="conf_divs"></div> + </section> + + <section class="notes"> + <h4>说明</h4> + <p>权重: 浮点数,范围 0-10</p> + </section> +{% endblock page %} + +{# vim: set ts=2 sw=2 tw=0 fenc=utf-8 ft=htmldjango.html: #} diff --git a/97suifangqa/apps/recommend/templates/recommend/add_edit_blog_info_error.html b/97suifangqa/apps/recommend/templates/recommend/add_edit_blog_info_error.html new file mode 100644 index 0000000..e1a6434 --- /dev/null +++ b/97suifangqa/apps/recommend/templates/recommend/add_edit_blog_info_error.html @@ -0,0 +1,66 @@ +{% extends "base.html" %} +{% load static from staticfiles %} +{% load dict_get %} + +{% block title %} +添加文章信息 | admin | 97 随访 +{% endblock %} + +{% block css %} + <link rel="stylesheet" type="text/css" href="{% static "css/add_edit_blog_info.css" %}"/> +{% endblock %} + +{% block scripts %} + <script type="text/javascript"> + // index + var recommend_index_url = '{% url recommend_index %}'; + // back_to_list button + $("#back_to_list").bind("click", function() { + window.location.href = recommend_index_url; + }); + </script> +{% endblock %} + +{% block page %} + <h2>错误 | 添加文章信息</h2> + + <section class="blog"> + id: {{ blog.id }} + <br /> + title: {{ blog.title }} + </section> + + <section class="no_indicator" style="display: {% if no_indicator %}block{% else %}none{% endif %};"> + <span class="error"> + 该文章未添加关联的研究指标"ResearchIndicator" + ({{ ResearchIndicatorName }}) + </span> + </section> + + <section class="error no_indicator_atom" style="display: {% if no_indicator_atom %}block{% else %}none{% endif %};"> + <span class="error"> + 该文章关联的以下研究指标"ResearchIndicator" + 没有添加原子分类"ResearchAtom"({{ ResearchAtomName }}) + </span> + {% if no_indicator_atom %} + <ul class="research_indicator"> + {% for ri in no_atom_ri %} + <li>{{ ri|dict_get:"display" }} (id={{ ri|dict_get:"id" }})</li> + {% endfor %} + </ul> + {% endif %} + </section> + + <section class="error no_treat_response" style="display: {% if no_treat_response %}block{% else %}none{% endif %};"> + <span class="error"> + 未添加全局的治疗反应"TreatResponse"({{ TreatResponseName }}) + </span> + </section> + + <section class="button"> + <input type="button" id="back_to_list" value="返回文章列表" /> + </section> + +{% endblock page %} + +{# vim: set ts=2 sw=2 tw=0 fenc=utf-8 ft=htmldjango.html: #} diff --git a/97suifangqa/apps/recommend/templates/recommend/recommend_index.html b/97suifangqa/apps/recommend/templates/recommend/recommend_index.html new file mode 100644 index 0000000..fd05bf3 --- /dev/null +++ b/97suifangqa/apps/recommend/templates/recommend/recommend_index.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} +{% load static from staticfiles %} +{% load dict_get %} + +{% block title %} +文章列表 | admin | 97 随访 +{% endblock %} + +{% block css %} + <link rel="stylesheet" type="text/css" href="{% static "css/recommend_index.css" %}"/> +{% endblock %} + +{% block page %} + <h2>文章列表 | 添加文章信息</h2> + + <p>图例:<span class="has_info">已添加信息</span></p> + + <ul class="blogs"> + {% for blog in blog_list %} + <li class="{% if blog|dict_get:"has_info" %}has_info{% endif %}"> + <a href="{% url add_edit_blog_info blog|dict_get:"id" %}">{{ blog|dict_get:"title" }}</a> + </li> + {% endfor %} + </ul> +{% endblock page %} + +{# vim: set ts=2 sw=2 tw=0 fenc=utf-8 ft=htmldjango.html: #} diff --git a/97suifangqa/apps/recommend/tools.py b/97suifangqa/apps/recommend/tools.py new file mode 100644 index 0000000..3b98c1f --- /dev/null +++ b/97suifangqa/apps/recommend/tools.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# +# tools for apps/recommend +# + +from django.db.models import Count + +from recommend import models as rm + +import re + + +def make_tag(ids=[], tag='tag', sep='_'): # {{{ + """ + make tag by using given list of ids + + if 'ids' is a list of integers, then sort them by magnitude + """ + if isinstance(ids, tuple): + ids = list(ids) + # check list element type + all_ints = all(isinstance(item, int) for item in ids) + if all_ints: + # sort ints by magnitude + ids.sort() + # + tag_name = tag + for id in ids: + tag_name = '%s%s%s' % (tag_name, sep, id) + return tag_name +# }}} + + +def get_research_config(atoms=[]): # {{{ + """ + return the found ResearchConfig object + by filtering on the given atoms list + """ + if not atoms: + return False + # convert id list to obj list + if isinstance(atoms[0], int): + atoms = [rm.ResearchAtom.objects.get(id=id) for id in atoms] + qs = rm.ResearchConfig.objects.annotate(c=Count('researchAtoms'))\ + .filter(c=len(atoms)) + for atom in atoms: + qs = qs.filter(researchAtoms=atom) + if not qs: + return None + elif len(qs) == 1: + return qs[0] + else: + return False +# }}} + + +def make_config_display(atoms=[]): # {{{ + """ + make a display string for the given config + """ + disp_str = u'' + if not atoms: + return disp_str + # convert to list + if isinstance(atoms, tuple): + atoms = list(atoms) + # check list element type + all_ints = all(isinstance(item, int) for item in atoms) + if all_ints: + # sort ints by magnitude and convert to objects list + atoms.sort() + atoms = [rm.ResearchAtom.objects.get(id=id) for id in atoms] + # + for atom in atoms: + disp_str = '%s%s | ' % (disp_str, atom.display()) + disp_str = re.sub(r'(^\s*\|\s*)|(\s*\|\s*$)', '', disp_str) + # + return disp_str +# }}} + + diff --git a/97suifangqa/apps/recommend/urls.py b/97suifangqa/apps/recommend/urls.py index 09dfed4..ad540d8 100644 --- a/97suifangqa/apps/recommend/urls.py +++ b/97suifangqa/apps/recommend/urls.py @@ -14,5 +14,14 @@ urlpatterns = patterns('recommend.views', url(r'^$', 'recommend_index', name='recommend_index'), + # add/edit blog info + url(r'add_edit/blog/(?P<blog_id>[1-9][0-9]*)/$', + 'add_edit_blog_info', + name='add_edit_blog_info'), + ## ajax + # add/edit configs + url(r'ajax/add_edit_configs/', + 'ajax_add_edit_configs', + name='ajax_add_edit_configs'), ) diff --git a/97suifangqa/apps/recommend/views.py b/97suifangqa/apps/recommend/views.py index 02d5616..d3294a2 100644 --- a/97suifangqa/apps/recommend/views.py +++ b/97suifangqa/apps/recommend/views.py @@ -4,14 +4,237 @@ views for apps/recommend """ -from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, Http404 +import itertools +try: + import json +except ImportError: + from django.utils import simplejson as json + +from django.http import ( + HttpResponse, HttpResponseRedirect, + HttpResponseForbidden, Http404 +) +from django.views.defaults import permission_denied from django.shortcuts import render, get_object_or_404 +from django.contrib.auth.decorators import login_required + +from recommend import models as rm +from sciblog.models import SciBlog +from tools import make_tag, get_research_config, make_config_display -# index +# index {{{ def recommend_index(request): """ index view for apps/recommend """ - return HttpResponse("recommend index") + template_name = 'recommend/recommend_index.html' + blogs = SciBlog.objects.all().order_by('id') + blog_list = [] + for blog in blogs: + if blog.research_configs.all(): + has_info = True + else: + has_info = False + b = { + 'id': blog.id, + 'title': blog.title, + 'has_info': has_info, + } + blog_list.append(b) + # + context = { + 'blog_list': blog_list, + } + return render(request, template_name, context) +# }}} + + +# add_edit_blog_info {{{ +@login_required +def add_edit_blog_info(request, blog_id=None): + """ + add/edit the infomation (used to recommend blog for user) + of the given blog + + ONLY *STAFF* ALLOWED + """ + template_name = 'recommend/add_edit_blog_info.html' + template_name_error = 'recommend/add_edit_blog_info_error.html' + context_error = { + 'no_indicator': False, + 'no_indicator_atom': False, + 'no_treat_response': False, + 'ResearchIndicatorName': rm.ResearchIndicator._meta.verbose_name_plural, + 'ResearchAtomName': rm.ResearchAtom._meta.verbose_name_plural, + 'TreatResponseName': rm.TreatResponse._meta.verbose_name_plural, + } + # check user type + if not request.user.is_staff: + #return permission_denied(request) + html = """ + <h1>403 Forbidden</h1> + <h2>ONLY *STAFF* ALLOWED</h2> + """ + return HttpResponseForbidden(html) + ## blog object + try: + blog_id = int(blog_id) + blog_obj = get_object_or_404(SciBlog, id=blog_id) + except ValueError: + raise ValueError(u"Error: blog_id='%s'错误" % blog_id) + except SciBlog.DoesNotExist: + raise ValueError(u"Error: SciBlog id='%s'不存在" % blog_id) + context_error['blog'] = blog_obj + ## research indicators & check + r_indicators = blog_obj.research_indicators.all() + r_indicators_id = [ri.id for ri in r_indicators] + rind_num = len(r_indicators) + # check indicator + if rind_num == 0: + context_error['no_indicator'] = True + return render(request, template_name_error, context_error) + # check indicator research atoms + no_atom_ri = [] + for ri in r_indicators: + if not ri.research_atoms.all(): + context_error['no_indicator_atom'] = True + no_atom_ri.append({ + 'id': ri.id, + 'display': ri.__unicode__(), + }) + # + if context_error['no_indicator_atom']: + context_error['no_atom_ri'] = no_atom_ri + return render(request, template_name_error, context_error) + # treat response & check + treat_responses = rm.TreatResponse.objects.all() + if not treat_responses: + context_error['no_treat_response'] = True + return render(request, template_name_error, context_error) + else: + treat_responses_list = [tr.dump() for tr in treat_responses] + treat_responses_objs = {'name': rm.TreatResponse._meta.verbose_name_plural} + for tr in treat_responses: + id = tr.id + treat_responses_objs['id%s'%id] = tr.dump() + ## research indicator numbers (categories by number) + rind_categories = {} + for i in range(1, rind_num+1): + comb = list(itertools.combinations(r_indicators_id, i)) + # tag all combinations + comb_tagged = [] + for c in comb: + tag = make_tag(ids=c, tag='comb') + comb_tagged.append({'tag': tag, 'data': c}) + rind_categories['N%s'%i] = comb_tagged + # dump used research indicators + rind_objs = {} + for id in r_indicators_id: + ri_obj = get_object_or_404(rm.ResearchIndicator, id=id) + ri_data = ri_obj.dump() + # atoms + ri_data['atoms_id'] = [atom.id + for atom in ri_obj.research_atoms.all()] + rind_objs['id%s'%id] = ri_data + ## research configs + rind_combs = [] + for ric in rind_categories.values(): + rind_combs = rind_combs + ric + # + research_configs = {} + for ric in rind_combs: + key = ric['tag'] + atoms_id_list = [] + for id in ric['data']: + atoms_id_list.append(rind_objs['id%s'%id]['atoms_id']) + # itertools to generate combinations + configs = list(itertools.product(*atoms_id_list)) + configs_tagged = [] + # generate config data for front page + for conf in configs: + config_obj = get_research_config(atoms=conf) + if config_obj: + config_data = config_obj.dump() + else: + config_data = {} + tag = make_tag(ids=conf, tag='conf') + display = make_config_display(atoms=conf) + config_data.update({ + 'tag': tag, + 'data': conf, + 'display': display, + }) + configs_tagged.append(config_data) + # TODO + data = { + 'rind_ids': list(ric['data']), + 'configs': configs_tagged, + } + research_configs[key] = data + ## context + context = { + 'blog': blog_obj, + 'rind_num': rind_num, + 'rind_objs_json': json.dumps(rind_objs), + 'rind_combs_json': json.dumps(rind_combs), + 'rind_categories_json': json.dumps(rind_categories), + 'research_configs_json': json.dumps(research_configs), + 'treat_responses_list_json': json.dumps(treat_responses_list), + 'treat_responses_objs_json': json.dumps(treat_responses_objs), + } + return render(request, template_name, context) +# }}} + + +# ajax add_edit_configs {{{ +@login_required +def ajax_add_edit_configs(request): + """ + response to the ajax post configs data + """ + data = {'failed': True} + if request.is_ajax() and request.method == 'POST': + #print request.POST.dict() + configs_list = json.loads(request.POST.get('configs_list')) + blog_id = int(request.POST.get('blog_id')) + blog_obj = get_object_or_404(SciBlog, id=blog_id) + #print configs_list + for conf in configs_list: + if conf['action'] == 'add': + # add config + tr_obj = get_object_or_404(rm.TreatResponse, + id=conf['treatResponse_id']) + new_conf_obj = rm.ResearchConfig.objects.create( + blog=blog_obj, treatResponse=tr_obj, + weight=conf['weight']) + new_conf_obj.save() + # add m2m researchAtoms + for atom_id in conf['data']: + atom_obj = get_object_or_404(rm.ResearchAtom, + id=atom_id) + new_conf_obj.researchAtoms.add(atom_obj) + new_conf_obj.save() + elif conf['action'] == 'delete': + # delete config + conf_obj = get_object_or_404(rm.ResearchConfig, + id=conf['id']) + conf_obj.delete() + elif conf['action'] == 'edit': + # edit config + conf_obj = get_object_or_404(rm.ResearchConfig, + id=conf['id']) + conf_obj.weight = conf['weight'] + tr_obj = get_object_or_404(rm.TreatResponse, + id=conf['treatResponse_id']) + conf_obj.treatResponse = tr_obj + conf_obj.save() + else: + # action==None / unknown action + pass + data = {'failed': False} + # + return HttpResponse(json.dumps(data), mimetype='application/json') +# }}} + |