from django.core import meta
from django.core.template.defaultfilters import slugify
from django.core.validators import *
import re

############################################################################
####
#### Constants
####

# nice massively large number close to half of maxint
DEFAULT_RANK = 10000

BOOLEAN_STYLE_CHOICES = (
    ('R', 'Radio'),
    ('C', 'Check'),
    ('L', 'List'),
  )
BOOLEAN_DEFAULT_CHOICES = (
    ('N', 'N/A'),
    ('F', 'False'),
    ('T', 'True'),
  )

TEXTAREA_WRAP_CHOICES = (
    ('SOFT', 'Soft (strip newlines)'),
    ('HARD', 'Hard (wrap and keep newlines)'),
    ('OFF',  'Off  (keep user formating)'),
  )

LISTTHREE_STYLE_CHOICES = (
    ('D', 'Digits'),
    ('A', 'Alpha'),
    ('N', 'None'),
  )

SINGLESELECTION_STYLE_CHOICES = (
    ('R',  'Radio'),            # radiolist with radio after? before?
    ('I',  'Inlined'),          # inlined radiolist
    ('L',  'Choice List'),      # puldown list
    ('RS', 'Radio-Set Table'),  # table used for multiple questions of a type
    ('RL', 'Unordered List'),   # expanded list of radio butons (on new lines)
  )

MULTISELECTION_STYLE_CHOICES = (
    ('C',  'Check'),            # checklist
    ('I',  'Inlined'),          # inlined checklist
    ('L',  'List Box'),         # area listing for multi-select
    ('CL', 'Unorderd List'),    # expanded list of check boxes (on new lines)
  )

split_re = re.compile("[^\w]")
ignore_set = set("""
    0 000 1 10 100 2 20 2006 25 3 30 4 40 5 50 6 60 7 8 9 a ability about
    above actually advance after ag again all almost already also amount
    amusing an and another any anything app appealed approach approaching
    aps are area around as aside ask at attend attended attention
    available aware bar based basics be because been before being belonged
    best better birds bit boring both brief built but by c can car cases
    channel clear closely comfy compared content could couldn couple
    course created d data date day days design detail did didn discuss do
    dodging doing done during e each early easier easily effective either
    elements else end ended enforced enjoy enjoyed enough essentially etc
    even example examples experienced extra far feel felt few fewer figure
    fill fine first fitting food for free frequently from full gaps gave
    general get getting good got great ground group guage guess had hadn
    happened hard has have having held help helped hours how however howto i
    idea if imo in inclined info intended interest interested into intro
    is isn it just keep kept kind know lame last later learn leave leaving
    left less level light like limit line little live lively ll longer
    looking lot lots loud m made main making manage many may maybe me
    means mention miss more most much must my name names naming need
    needed needs new next no non none not now obviously of often okay on
    one ones only open or order original other otherwise our out over
    overall overlapped page part pass past pay people perhaps plan power
    practiced prepared present pretty price product put rate rates rather
    rating re read real really reason reasonable received reduced regular
    remedy return right s said same say scores see seemed seems speech
    series service session set several short should show showed since single
    site situation situations size sizers slightly slots small so solve
    solved some something soup space speaker specific spend spent start state
    still strips stuff style subject such suggest sure t take talk teh
    tell tends terrible test text than thanks that the them then
    there these they thing things think third this thoroughly those though
    thought three throughout tied time to ton tons too topic topped twice
    two um under until up us use used useful uses usually various ve venue
    very wah walking want wanted was wasn way we well went were what when
    where which while who whole why will willing window wish with without
    work would year years you your zero pycon python make going nice don every
    conference
    """.split())
ignore_set.add("")
common_set = set("""
    interview speech keynote tutorial keynotes tutorials sprint sprints
    implementation implementations talk talks language development
    """.split())
ignore_common_set = common_set | ignore_set

############################################################################
####
#### Utility functions
####
def load_model(model):
    package = "django.models"
    if "." in model:
        res = model.split(".")
        package += "." + model
        model = res[-1]
    return __import__(package, {}, {}, model)

def get_model_object_list(parent, model_name, where_data):
    model = load_model(model_name)
    if where_data:
        return model.get_list(where=[where_data,])
    return model.get_list()

def text2tags(text, filter=ignore_set):
    return tuple(sorted(set(split_re.split(text.lower())) - filter))

############################################################################
####
#### Validators
####
class RequiredValueIfOtherFieldEquals:
    def __init__(self, field_value, other_field, other_value,
                 error_message=None):
        self.field_value = field_value
        self.other_field = other_field
        self.other_value = other_value
        self.error_message = error_message or lazy_inter(gettext_lazy(
            "This field must be %(required)s if %(field)s is %(value)s"), {
                'required': field_value,
                'field':    other_field,
                'value':    other_value})
        self.always_test = True

    def __call__(self, field_data, all_data):
        if (all_data.has_key(self.other_field) and
            all_data[self.other_field][0] == self.other_value and
            self.field_value != field_data):
            raise ValidationError(self.error_message)

class InvalidValueIfOtherFieldEquals:
    def __init__(self, field_value, other_field, other_value,
                 error_message=None):
        self.field_value = field_value
        self.other_field = other_field
        self.other_value = other_value
        self.error_message = error_message or lazy_inter(gettext_lazy(
            "This field can not be %(invalid)s if %(field)s is %(value)s"), {
                'invalid': field_value,
                'field':   other_field,
                'value':   other_value})
        self.always_test = True

    def __call__(self, field_data, all_data):
        if (self.other_field in all_data and
            all_data[self.other_field] == self.other_value and
            self.field_value == field_data):
            raise ValidationError(self.error_message)

class InvalidValueIfNotSet:
    def __init__(self, field_value, other_field, error_message=None):
        self.field_value = field_value
        self.other_field = other_field
        self.error_message = error_message or lazy_inter(gettext_lazy(
            "This field can not be %(invalid)s if %(field)s is not set."), {
                'invalid': field_value,
                'field':   other_field})
        self.always_test = True

    def __call__(self, field_data, all_data):
        if (self.other_field not in all_data and
            self.field_value == field_data):
            raise ValidationError(self.error_message)

def validateUniqSurveyQuestion(field_data, all_data):
    from django.models.survey import questions, categories
    catid = all_data['category']
    surid = catagories.get_object(pk=catid).survey_id
    import sys
    sys.stderr.write(str(field_data) + " " + str(all_data) + "\n")

def validateManipulatedQuestion(manipulator, field_data, all_data):
    from django.models.survey import questions
    questions.validate_question_id(manipulator, field_data, all_data)

def validateModuleLoad(module_name):
    from django.core.validators import ValidationError
    if not module_name: return
    try:
        model = load_model(module_name)
    except Exception, e:
        raise ValidationError, (
            "Failed to load model: " + str(e) )
    if not hasattr(model, 'get_list'):
        raise ValidationError, "Invalid model: No get_list method."

def validateModuleLoadWhere(module_name, where_data):
    from django.core.validators import ValidationError
    if not where_data: return
    if not module_name:
        raise ValidationError, 'Only valid if a Choice DB is set.'
    model, listing = None, None
    try:
        model = load_model(module_name)
    except Exception, e:
        raise ValidationError, "Failed to load model: " + str(e)
    try:
        model.get_list(where=[where_data,])
    except Exception, e:
        raise ValidationError, "Invalid where clause: " + str(e)

class ValidateUniqueChoiceLabel:
    def __init__(self, classname):
        self.startswith = classname.lower() + '.'
    def __call__(self, field_data, all_data):
        from django.core.validators import ValidationError
        labels = [ all_data[name] for name in all_data
                    if (name.startswith(self.startswith) and
                        name.endswith('label'))]
        if labels.count(field_data) > 1:
            raise ValidationError, "Choice Elements must be unique."

class ValidateChoiceStyle:
    def __init__(self, choice_class_name, styles, invalid_with_other):
        self.startswith = choice_class_name.lower() + '.'
        self.styles = dict(styles)
        self.invalid = invalid_with_other
    def __call__(self, field_data, all_data):
        from django.core.validators import ValidationError
        labels = [ all_data[name] for name in all_data
                    if (name.startswith(self.startswith) and
                        name.endswith('label') and
                        all_data[name])]
        if not labels:
            ## ok ALL styles require this.. .but its the only place to make the
            ## error and the text should make sense.
            if 'remote' in all_data:
                if not all_data['remote']:
                    raise ValidationError, ("At least one Choice Element or a "
                        "remote choice DB must be set below for this Style.")
            else:
                raise ValidationError, ("At least one Choice Element must be "
                                        "set below for this Style.")
        if self.invalid and 'other' in all_data:
            for key in self.invalid:
                if field_data == key:
                    raise ValidationError, (
                        "%s is not valid when 'Other' choice is enabled." % (
                            repr(self.styles[field_data])))


############################################################################
####
#### Main Parent Models
####
class Survey(meta.Model):
    """
    """
    name  = meta.SlugField(blank=False, unique=True,
                           help_text=("Single word name used in the admin "
                                "and in the survey URL.<BR>\n"
                                "This value must contain only letters, "
                                "numbers, underscores or hyphens."))
    title = meta.CharField(maxlength=80,
                           help_text=("Title for the survey page"))
    live  = meta.BooleanField("Live", null=False, blank=False,
                              default=False,
                              help_text=("If enabled, the survey is made "
                                    "available and will start accepting "
                                    "submissions.<br>\nYou should not make "
                                    "modifications to a live survey. Older "
                                    "surveys should also be disabled to "
                                    "prevent clutter in the admin interface."))
    lock  = meta.BooleanField("Editable", null=False, blank=False,
                              default=True,
                              help_text=("If disabled, the survey questions "
                                    "are editable and filtered from some "
                                    "admin interfaces.<br>\nYou should disable "
                                    "editing on live surveys."))
    label = meta.CharField("Submit Label", maxlength=20,
                           null=False, blank=False, default="Submit",
                           help_text=("Text to use for the Submit button."))
    view  = meta.CharField("Custom View", maxlength=80,
                           null=False, blank=True, default="",
                           help_text=("Custom django view extension."))
    head  = meta.TextField("Header", null=False, blank=True,
                           help_text=("This optional header is displayed "
                                      "at the top of the survey."))
    foot  = meta.TextField("Footer", null=False, blank=True,
                           help_text=("This optional footer is displayed "
                                      "at the bottom of the survey."))
    class META:
        ordering = ['name',]
        permissions = ( ('can_view_results' , "Can View Results"), )
        admin    = meta.Admin(list_display=('name', 'title', 'live', 'lock'),
                              list_filter=['live', 'lock'],
                              fields=((None,
                                        {'fields': (
                                            'name', 'title', 'live', 'lock')},),
                                      ('Header|Footer|Submit',
                                        {'classes': 'collapse',
                                         'fields': (
                                            'label', 'view', 'head', 'foot'),
                                        },), ))
    def _manipulator_validate_view(manipulator, field_data, all_data):
        from django.core.validators import ValidationError
        package, view = "", field_data
        if "." in field_data:
            res = field_data.split(".")
            view = res.pop()
            package = ".".join(res)
        try:
            mod = __import__(package, {}, {}, view)
        except Exception, e:
            raise ValidationError, (
                "Failed to load view module: " + str(e))
        if not hasattr(mod, view):
            raise ValidationError, "Failed to find view in module."
        elif not callable(getattr(mod, view)):
            raise ValidationError, "View is not a callable."
    def get_custom_view(self):
        if not self.view: return None
        package, view = "", self.view
        if "." in view:
            res = view.split(".")
            view = res.pop()
            package = ".".join(res)
        mod = __import__(package, {}, {}, view)
        return getattr(mod, view)

    def __repr__(self):
        return self.name


class Category(meta.Model):
    """Questions are first grouped by Categories
    """
    name = meta.SlugField("Category", blank=False,
                          help_text=("Surveys have their questions grouped "
                                     "into Categories. This identifies the "
                                     "Category in the admin and in url's only "
                                     "\nMust be unique per Survey "
                                     "and contain only letters, "
                                     "numbers, underscores or hyphens."))
    title = meta.CharField(maxlength=80,
                           help_text=("Title for the Category Section."))
    survey = meta.ForeignKey(Survey)
    desc = meta.TextField("Description", null=False, blank=True,
                          help_text=("This optional description is displayed "
                                     "below the category heading."))
    rank = meta.PositiveIntegerField("Ordering Rank", null=False, blank=False,
                                     unique=False, default=DEFAULT_RANK,
                                     help_text=(
                                          "This value ranks the order in which"
                                          "categories will be displayed "
                                          "in the survey. Those entries with "
                                          "the same rank are ordered "
                                          "Alphabeticly by name. Higher the "
                                          "number, lower on the page."))
    contingent = meta.CharField(maxlength=80,
                                null=False, blank=True, default="",
                                help_text=("Make this category contingent "
                                           "on a question responce. format is "
                                           "'qid:answer'."))

    class META:
        module_name         = "categories"
        verbose_name_plural = "Categories"
        verbose_name        = "Category"
        unique_together     = (('name', 'survey'),)
        ordering            = ['survey', 'rank', 'name',]
        admin               = meta.Admin(list_display=('name', 'survey', 'rank'),
                                         list_filter=['survey'],
                                        )


    def __repr__(self):
        from django.models.survey import surveys
        return surveys.get_object(pk=self.survey_id).name + " - " + self.name

    def _post_save(self):
        from django.models.survey import questions, categories, surveys
        ## update all questions if the survey changed:
        for question in questions.get_list(where=[
            'category_id=%s' % self.id,
            'survey_id!=%s' % self.survey_id]):
                question.save()

    def get_contingency(self):
        from django.models.survey import questions
        if not self.contingent: return None
        try:
            class contingency(object): pass
            q, a = self.contingent.split(':', 1)
            q = questions.get_object(pk=q)
            res = contingency()
            res.question = q
            res.answer = a
            return res
        except:
            return None

    def _manipulator_validate_name(manipulator, field_data, all_data):
        if 'CategoryManipulatorAdd' not in repr(manipulator): return
        from django.models.survey import questions, categories, surveys
        from django.core.validators import ValidationError
        found = categories.get_list(where=["survey_id=%s" % all_data['survey'],
                                           "name='%s'" % field_data])
        surv = surveys.get_object(pk=all_data['survey'])
        if not surv.lock:
            raise ValidationError, (
                "The Survey associated with this Question has been locked to "
                "prevent edits." )
        if found:
            raise ValidationError, (
                "Category already exists in Survey '%s'." % surv.name)

    def _manipulator_validate_contingent(manipulator, field_data, all_data):
        from django.core.validators import ValidationError
        from django.models.survey import questions
        if ':' not in field_data or not field_data.split(':',1)[0].isdigit():
            raise ValidationError, "Malformed"
        qid = field_data.split(':',1)[0]
        try:
            qobj = questions.get_object(pk=qid)
        except QuestionDoesNotExist:
            raise ValidationError, "Question does not exist"
        if str(qobj.survey_id) != str(all_data['survey']):
            raise ValidationError, (
                "Question is from another survey: " + str(qobj.survey))


class Question(meta.Model):
    """All questions are listed here
    """
    question = meta.CharField(maxlength=150,
                              help_text=("Text for the question being asked. "
                                  "By default all questions are bound to the "
                                  "type 'None' and will not appear in the "
                                  "survey. Select one of the Question Types "
                                  "(e.g. Boolean questions), add one, and "
                                  "bind it to a question."))
    category = meta.ForeignKey(Category, verbose_name="Survey - Category",
                               help_text=("Which Survey Category section the "
                                          "Question will be displayed in."))
    rank     = meta.PositiveIntegerField("Ordering Rank",
                               null=False, blank=False,
                               unique=False, default=DEFAULT_RANK,
                               help_text=("Questions are ordered by: [Survey:"
                                    "Catagory-rank:Catagory-name:Question-rank:"
                                    "Question-name]. The 'Display Order' is "
                                    "to help the javascript sorting in the "
                                    "Questions listing."))

    ## internal type and order management
    survey = meta.ForeignKey(Survey, verbose_name="Survey", editable=False)
    qtype  = meta.CharField("Type", maxlength=50, default="None",
                            editable=False)
    hrank  = meta.CharField("Display Order", maxlength=80, default="",
                            editable=False)

    class META:
        module_name         = "questions"
        verbose_name_plural = "Questions"
        verbose_name        = "Question"
        unique_together     = (('question', 'survey'),)
        ordering = ['category', 'rank', 'question',]
        admin = meta.Admin(list_display = ['question',  'survey',
                                           ## to mask the default category repr
                                           '_get_category_name',
                                           'qtype', 'hrank',],
                           list_filter  = ['survey', 'category'],
                           search_fields = ['question'],
                           fields = ((None,
                                    ## Hide the dynamic OneToOne Order element
                                    {'fields': ('question', 'category', 'rank')
                                    },),),)

    def __repr__(self):
        return self.question

    def _get_category_name(self):
        from django.models.survey import categories
        return categories.get_object(pk=self.category_id).name
    _get_category_name.short_description = 'Category'
    def _get_hrank(self):
        from django.models.survey import categories
        from django.core.template.defaultfilters import slugify
        qn = slugify(self.question)
        qn = qn[:min(8, len(qn))]
        cat = categories.get_object(pk=self.category_id)
        return "%.2d:%.8d:%s:%.8d:%s" % (cat.survey_id, cat.rank,
                                         cat.name, self.rank, qn)
    _get_hrank.short_description = 'Display Order'

    def _pre_save(self):
        from django.models.survey import categories
        cat = categories.get_object(pk=self.category_id)
        self.survey_id = cat.survey_id
        self.hrank = self._get_hrank()

    def get_extension(self):
        return getattr(self, "get_" + self.qtype.lower()+"question")()

    def _manipulator_validate_question(manipulator, field_data, all_data):
        if 'QuestionManipulatorAdd' not in repr(manipulator): return
        from django.models.survey import questions, categories, surveys
        from django.core.validators import ValidationError
        surid = categories.get_object(pk=all_data['category']).survey_id
        found = questions.get_list(where=["survey_id=%d" % surid,
                                          "question='%s'" % field_data])
        surv = surveys.get_object(pk=surid)
        if not surv.lock:
            raise ValidationError, (
                "The Survey associated with this Question has been locked to "
                "prevend edits." )
        if found:
            raise ValidationError, (
                "Question already exists in Survey '%s'." % surv.name)

    def _module_validate_question_id(manipulator, field_data, all_data):
        from django.models.survey import questions, categories, surveys
        from django.core.validators import ValidationError
        cid = questions.get_object(pk=field_data).category_id
        sid = categories.get_object(pk=cid).survey_id
        if not surveys.get_object(pk=sid).lock:
            raise ValidationError, (
                "The Survey associated with this Question has been locked to "
                "prevend edits." )
        if not field_data:
            raise ValidationError, ("A non-'None' Question to be bound "
                                    "must be selected.")
        try:
            # there is a getmodule() somewhere... cant find it in the doc...
            # dont feel like doing live object introspection, so hack it:
            qtype = questions.get_object(pk=field_data).qtype
            qmod  = qtype.lower() + 'questions'
            emod  = manipulator.__module__.split('.')[-1]
            ## we don't mind edits, the OneToOne will deal with attempting to
            ## asign a new extension type to a question already bound to that
            ## same ype. This is only to stop cross-type extension bindings.
            #import sys
            #sys.stderr.write(qmod + " " + emod + "\n")
            if (qtype == 'None' or qmod == emod): return
        except Exception, e:
            raise ValidationError, str(e)

        raise ValidationError, (
            "This Question has already been assigned the type: '%s'. "
            "Delete that type assignment first, if you wish to re-assign." %
            qtype)

class _ExtendedQuestion(meta.Model):
    question = meta.OneToOneField(Question, verbose_name="Question",
                                  null=False, blank=False,
                                  help_text=("Select the Question to bind to "
                                             "this type. Only Questions "
                                             "currently of type 'None' can "
                                             "be bound to a type."),)
    ## Unfortunatly this breaks tooo much. The questions are limited,
    ## but other problems with listing and post add occur
    #                              limit_choices_to = { 'question__in' :
    #    '(SELECT "id" FROM "survey_questions" WHERE "qtype"=\'None\')' })
    # Need to add a way to say 'OR qtype='<ThisType>' where <ThisType> is
    # Dynamicly determined from the child class.
    # Could this be done by having a "qtype" here on extended which is set
    # durring a __new__ extension? needs work....
    # Would be nice to have a
    # lock filter as well based on survey.... something to think about...

    class META:
        ## This is a workaround for a admin list display bug.
        order_with_respect_to = 'question' # Ticket #930 (defect)

    def __repr__(self):
        return self.get_question().question

    def _post_save(self):
        quest = self.get_question()
        quest.qtype=type(self).__name__[:-len('Question')]
        quest.save()

    def _pre_delete(self):
        try:
            quest = self.get_question()
            quest.qtype='None'
            quest.save()
        except: pass

    def _get_question_category(self):
        return self.get_question().get_category().name
    _get_question_category.short_description = 'Category'

    def _get_question_survey(self):
        return self.get_question().get_survey().name
    _get_question_survey.short_description = 'Survey'

############################################################################
####
#### Question Type Extensions
####

class BooleanQuestion(_ExtendedQuestion):
    ## Ok, origionally I did this to play with the power of DJango....
    ## Think boolean questions are easy?
    truelabel  = meta.CharField("True Label", maxlength=50,
                                null=False, blank=False, default="Yes")
    falselabel = meta.CharField("False Label", maxlength=50,
                                null=False, blank=False, default="No")
    default    = meta.CharField("Default Value", maxlength=1,
                                choices=BOOLEAN_DEFAULT_CHOICES,
                                default=BOOLEAN_DEFAULT_CHOICES[0][0],
                                validator_list=[
                                    InvalidValueIfNotSet(
                                        'N', 'allowna',
                                        "'N/A' is only valid if the advanced "
                                        "option 'Allow N/A' is set to True.")])

    ## advanced
    style   = meta.CharField(maxlength=1,
                             choices=BOOLEAN_STYLE_CHOICES,
                             default=BOOLEAN_STYLE_CHOICES[0][0],
                             null=False, blank=False,
                             validator_list=[
                                InvalidValueIfOtherFieldEquals(
                                    'C', 'allowna', 'on',
                                    "'Check' is only valid if the advanced "
                                    "option 'Allow N/A' is set to False.")])
    allowna = meta.BooleanField("Allow N/A",
                                null=False, blank=False,
                                default=False)
    nalabel = meta.CharField("N/A Label", maxlength=50,
                             null=False, blank=True,
                             default="")

    class META:
      admin = meta.Admin(list_display=('question', 'style',
                                       '_get_question_survey',
                                       '_get_question_category',),
                         #list_filter  = ['survey', 'category'],
                         fields=((None,
                                    {'fields': (
                                        'question', 'truelabel', 'falselabel',
                                        'default')
                                    },),
                                 ('Advanced',
                                    {  #'classes': 'collapse',
                                     'fields': (
                                        'style', 'allowna', 'nalabel'),},), ))

    ## deal with a bug with manipulator validators and inheritance
    _manipulator_validate_question = validateManipulatedQuestion

class StringQuestion(_ExtendedQuestion):
    maxlen  = meta.PositiveIntegerField("Max Length",
                                        null=False, blank=False,
                                        default=50)
    size    = meta.PositiveIntegerField("Display Length",
                                        null=False, blank=False,
                                        default=30)
    default = meta.CharField("Default Value", maxlength=50,
                             null=False, blank=True)
    class META:
        admin = meta.Admin(list_display=[
            'question', '_get_question_survey','_get_question_category'],)
                           #list_filter  = ['survey', 'category'],)
    ## deal with a bug with manipulator validators and inheritance
    _manipulator_validate_question = validateManipulatedQuestion

class TextAreaQuestion(_ExtendedQuestion):
    rows    = meta.PositiveIntegerField("Number Rows",
                                        null=False, blank=False,
                                        default=10)
    cols    = meta.PositiveIntegerField("Number Columns",
                                        null=False, blank=False,
                                        default=40)
    wrap    = meta.CharField(maxlength=4,
                             choices=TEXTAREA_WRAP_CHOICES,
                             default=TEXTAREA_WRAP_CHOICES[0][0],
                             null=False, blank=False)
    maxlen  = meta.PositiveIntegerField("Max Length",
                                        null=False, blank=False,
                                        default=200)
    default = meta.TextField("Default Value", maxlength=500,
                             null=False, blank=True)
    class META:
        admin = meta.Admin(list_display=[
            'question', '_get_question_survey', '_get_question_category'],)
                           #list_filter  = ['survey', 'category'],)
    ## deal with a bug with manipulator validators and inheritance
    _manipulator_validate_question = validateManipulatedQuestion

class ListThreeQuestion(_ExtendedQuestion):
    maxlen  = meta.PositiveIntegerField("Max Length per Responce",
                                        null=False, blank=False,
                                        default=100)
    style   = meta.CharField(maxlength=1,
                             null=False, blank=False,
                             choices=LISTTHREE_STYLE_CHOICES,
                             default=LISTTHREE_STYLE_CHOICES[0][0])
    remote  = meta.CharField("DJango Choice DB", maxlength=50,
                             null=False, blank=True,)
    where   = meta.CharField("Where Filter", maxlength=50,
                             null=False, blank=True,)
    class META:
        admin = meta.Admin(list_display=[
            'question', 'style', '_get_question_survey',
            '_get_question_category'],)
                           #list_filter  = ['survey', 'category'],)
        module_constants = {
            '__import__': __import__,
            'load_model': load_model,
            'validateModuleLoad': validateModuleLoad,
            'validateModuleLoadWhere': validateModuleLoadWhere,
            'get_remote_objects': get_model_object_list
        }

    def get_remote_choice_list(self):
        if not self.remote: return []
        return get_remote_objects(self, self.remote, self.where)

    def _manipulator_validate_remote(manipulator, field_data, all_data):
        validateModuleLoad(field_data)

    def _manipulator_validate_where(manipulator, field_data, all_data):
        validateModuleLoadWhere(all_data['remote'], field_data)

    ## deal with a bug with manipulator validators and inheritance
    _manipulator_validate_question = validateManipulatedQuestion

class SingleSelectionQuestion(_ExtendedQuestion):
    style = meta.CharField(maxlength=2,
                           null=False, blank=False,
                           choices=SINGLESELECTION_STYLE_CHOICES,
                           default=SINGLESELECTION_STYLE_CHOICES[0][0],
                           validator_list = [
                                ValidateChoiceStyle('SingleChoice',
                                    SINGLESELECTION_STYLE_CHOICES, ('RS','L'))])
    na    = meta.BooleanField("Include N/A Option",
                              null=False, blank=False,
                              default=False,
                              help_text = ("If enabled a special 'N/A' "
                                           "selection will be included which "
                                           "if selected in the survey will not "
                                           "result in an answer. If you wish "
                                           "to see 'N/A' options in survey "
                                           "results, add it below instead."))
    other = meta.BooleanField("Include 'Other:' Choice",
                              null=False, blank=False,
                              default=False,
                              help_text = ('If enabled a special \'Other:\' '
                                'entry will be added as the last choice '
                                'with a string input field.\n'
                                'NOTE: Only  valid for %s and %s Styles.' % (
                        repr(dict(SINGLESELECTION_STYLE_CHOICES)['R']),
                        repr(dict(SINGLESELECTION_STYLE_CHOICES)['RL']))))
    remote  = meta.CharField("DJango Choice DB", maxlength=50,
                             null=False, blank=True,)
    where   = meta.CharField("Where Filter", maxlength=50,
                             null=False, blank=True,)

    class META:
        admin = meta.Admin(list_display=('question', 'style',
                                         '_get_number_of_choices',
                                         '_get_question_survey',
                                         '_get_question_category',), )
        module_constants = {
            '__import__': __import__,
            'load_model': load_model,
            'validateModuleLoad': validateModuleLoad,
            'validateModuleLoadWhere': validateModuleLoadWhere,
            'get_remote_objects': get_model_object_list
        }

    def get_choice_list(self):
        if not self.remote: return self.get_singlechoice_list()
        return get_remote_objects(self, self.remote, self.where)

    def _get_number_of_choices(self):
        annotations, num = [], 0
        if self.other:  annotations.append("'Other:'")
        if self.na:     annotations.append("'N/A'")
        num = len(self.get_choice_list()) + len(annotations)
        return str(num) + " (including " + ' and '.join(annotations) + ")"
    _get_number_of_choices.short_description="Number of choices"

    def _manipulator_validate_remote(manipulator, field_data, all_data):
        from django.core.validators import ValidationError
        labels = [ all_data[name] for name in all_data
                    if (name.startswith('singlechoice.') and
                        name.endswith('.label')) and all_data[name] ]
        if len(labels):
            raise ValidationError, (
                "Cannot pull choices from remote DB if choices are set.")
        validateModuleLoad(field_data)

    def _manipulator_validate_where(manipulator, field_data, all_data):
        validateModuleLoadWhere(all_data['remote'], field_data)

    ## deal with a bug with manipulator validators and inheritance
    _manipulator_validate_question = validateManipulatedQuestion

class SingleChoice(meta.Model):
    question = meta.ForeignKey(SingleSelectionQuestion,
                               edit_inline=meta.TABULAR,
                               #to_field='question_id', # workaround for bug
                               num_in_admin=5,
                               num_extra_on_change=3)
    label  = meta.CharField(maxlength=80, core=True,
                            validator_list = [
                                ValidateUniqueChoiceLabel('SingleChoice'),])
    rank   = meta.PositiveIntegerField("Ordering Rank", null=False, blank=False,
                                       unique=False, default=DEFAULT_RANK,
                                       help_text="Ranking for choices")

    class META:
        unique_together = (('question', 'label'),)
        ordering = [ 'rank', 'id', 'label' ]

    def __repr__(self):
        return self.label

    def _manipulator_validate_question(manipulator, field_data, all_data):
        from django.models.survey import singleselectionquestions
        from django.core.validators import ValidationError
        ssq = singleselectionquestions.get_object(pk=field_data)
        if ssq.remote:
            raise ValidationError, (
               "Cannot have selection choices and pull choices from remote DB.")

class MultiSelectionQuestion(_ExtendedQuestion):
    style = meta.CharField(maxlength=2,
                           null=False, blank=False,
                           choices=MULTISELECTION_STYLE_CHOICES,
                           default=MULTISELECTION_STYLE_CHOICES[0][0],
                            validator_list = [
                                ValidateChoiceStyle('MultiChoice',
                                    MULTISELECTION_STYLE_CHOICES, ('L',))])
    maxsize = meta.PositiveIntegerField("Max List Display Size", null=True,
                                        blank=True, default=5,
                                        help_text=("This only effects the "
                                "List Box style. Not setting this value will "
                                "cause the listbox to be the size of the "
                                "number of choices."))
    other = meta.BooleanField("Include 'Other:' Choice",
                              null=False, blank=False,
                              default=False,
                              help_text = ('If enabled a special \'Other:\' '
                                'entry will be added as the last choice '
                                'with a string input field.\n'
                                'NOTE: Only  valid for %s and %s Styles.' % (
                        repr(dict(MULTISELECTION_STYLE_CHOICES)['C']),
                        repr(dict(MULTISELECTION_STYLE_CHOICES)['CL']))))
    class META:
        admin = meta.Admin(list_display=('question', 'style',
                                         '_get_number_of_choices',
                                         '_get_question_survey',
                                         '_get_question_category',),
                           #list_filter  = ['survey', 'category'],
                           fields=((None,
                                    {'fields': (
                                        'question', 'style', 'maxsize', 'other',
                                        )},),),)

    def _get_number_of_choices(self):
        from django.models.survey import multichoices
        num = len(multichoices.get_list(
            where=["question_id=%d" % self.question_id] ))
        if self.other: return str(num+1) +  " (including 'Other:')"
        return num
    _get_number_of_choices.short_description="Number of choices"

    ## deal with a bug with manipulator validators and inheritance
    _manipulator_validate_question = validateManipulatedQuestion

class MultiChoice(meta.Model):
    question = meta.ForeignKey(MultiSelectionQuestion,
                               edit_inline=meta.TABULAR,
                               #to_field='question_id', # workaround for bug
                               num_in_admin=5,
                               num_extra_on_change=3)
    label  = meta.CharField(maxlength=50, core=True,
                            validator_list = [
                                ValidateUniqueChoiceLabel('MultiChoice'),])
    rank   = meta.PositiveIntegerField("Ordering Rank", null=False, blank=False,
                                       unique=False, default=DEFAULT_RANK,
                                       help_text="Ranking for choices")

    class META:
        unique_together = (('question', 'label'),)
        ordering = [ 'rank', 'id', 'label' ]

    def __repr__(self):
        return self.label

############################################################################
####
#### Survey Submissions and The Answers therein!
####    origionally part of another app....
####

class Submission(meta.Model):
    datetime = meta.DateTimeField("Date/Time", auto_now_add=True,
                                  null=False, blank=False, editable=False)
    ipaddr   = meta.IPAddressField("IP Address",
                                   help_text=("This is only editable to get "
                                        "around a bug in django admin. Without "
                                        "this, submissions would not be "
                                        "deletable."))
    survey   = meta.ForeignKey(Survey, editable=False)
    session  = meta.CharField("Session", maxlength=40, editable=False)

    class META:
        ordering = [ 'datetime' ]
        admin = meta.Admin(list_display = ['id', 'session', 'ipaddr', 'survey',
                                           'datetime',],
                           list_filter  = ['id', 'survey', 'datetime'])

    def __repr__(self):
        if not hasattr(self, 'id'): return "-1"
        return str(self.id)

class Answer(meta.Model):
    submission = meta.ForeignKey(Submission, editable=False,
                                 verbose_name="Submit #")
    survey     = meta.ForeignKey(Survey, editable=False)
    question   = meta.ForeignKey(Question, editable=False)
    qtype      = meta.CharField("Type", maxlength=50, default="None",
                                editable=False)
    rank       = meta.IntegerField("Rank", null=True, editable=False)
    answer     = meta.TextField("Answer")

    class META:
        ordering = [ 'survey', 'submission', 'question', 'rank' ]
        admin = meta.Admin(
            list_filter  = ['submission', 'survey', 'question'],
            search_fields = ['question', 'answer'],
            list_display=[
                'submission', 'survey', 'question', 'qtype', 'rank', 'answer'])
        module_constants = { 'text2tags': text2tags,
                             'ignore_set': ignore_set,
                             'common_set': common_set,
                             'ignore_common_set': ignore_common_set,}
    def gen_tags(self):
        if hasattr(self, 'cached_tags'):
            return self.cached_tags
        self.cached_tags = text2tags(self.answer, ignore_set)
        return self.cached_tags

    def gen_uncommon_tags(self):
        if hasattr(self, 'cached_uncommon_tags'):
            return self.cached_uncommon_tags
        self.cached_uncommon_tags = text2tags(self.answer, ignore_common_set)
        return self.cached_uncommon_tags

    def _pre_save(self):
        quest = self.get_question()
        self.survey_id = quest.survey_id
        self.qtype = quest.qtype

    def _module_createAnswer(submission, question, answer, rank=None):
        from django.models.survey import questions, answers
        try:
            quest = questions.get_object(pk=question)
        except:
            return None
        ans = answers.Answer(submission_id=submission.id,
                             question_id=quest.id,
                             answer=answer)
        if rank is not None: ans.rank = rank
        ans.save()
        return ans

    def __repr__(self):
        return self.answer
