From 02afd8a32edb13ea7fc2266ac80092ea15c0930c Mon Sep 17 00:00:00 2001 From: Alvin Li Date: Wed, 9 Oct 2013 15:52:53 +0800 Subject: * 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'; --- 97suifangqa/apps/recommend/models.py | 431 ++++++++++++++++++++- .../recommend/static/css/add_edit_blog_info.css | 107 +++++ .../apps/recommend/static/css/recommend_index.css | 21 + .../static/javascripts/add_edit_blog_info.js | 330 ++++++++++++++++ .../templates/recommend/add_edit_blog_info.html | 87 +++++ .../recommend/add_edit_blog_info_error.html | 66 ++++ .../templates/recommend/recommend_index.html | 27 ++ 97suifangqa/apps/recommend/tools.py | 81 ++++ 97suifangqa/apps/recommend/urls.py | 9 + 97suifangqa/apps/recommend/views.py | 229 ++++++++++- 10 files changed, 1381 insertions(+), 7 deletions(-) create mode 100644 97suifangqa/apps/recommend/static/css/add_edit_blog_info.css create mode 100644 97suifangqa/apps/recommend/static/css/recommend_index.css create mode 100644 97suifangqa/apps/recommend/static/javascripts/add_edit_blog_info.js create mode 100644 97suifangqa/apps/recommend/templates/recommend/add_edit_blog_info.html create mode 100644 97suifangqa/apps/recommend/templates/recommend/add_edit_blog_info_error.html create mode 100644 97suifangqa/apps/recommend/templates/recommend/recommend_index.html create mode 100644 97suifangqa/apps/recommend/tools.py (limited to '97suifangqa/apps/recommend') 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   '; + }; + $("#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 += '\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