├── aiteo ├── templatetags │ ├── __init__.py │ └── aiteo_tags.py ├── __init__.py ├── workflow.py ├── signals.py ├── admin.py ├── forms.py ├── templates │ └── aiteo │ │ ├── _accepted.html │ │ ├── _question_vote_badge.html │ │ └── _response_vote_badge.html ├── urls.py ├── models.py └── views.py ├── tox.ini ├── MANIFEST.in ├── .travis.yml ├── runtests.py ├── README.rst ├── LICENSE └── setup.py /aiteo/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E265,E501 3 | -------------------------------------------------------------------------------- /aiteo/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.0b1.dev5" 2 | -------------------------------------------------------------------------------- /aiteo/workflow.py: -------------------------------------------------------------------------------- 1 | 2 | def can_mark_accepted(user, question): 3 | return question.user == user 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | 3 | recursive-include aiteo/templates * 4 | recursive-include aiteo/fixtures * 5 | recursive-include media * 6 | -------------------------------------------------------------------------------- /aiteo/signals.py: -------------------------------------------------------------------------------- 1 | import django.dispatch 2 | 3 | 4 | voted = django.dispatch.Signal(providing_args=["vote_obj", "upvote"]) 5 | vote_cleared = django.dispatch.Signal(providing_args=["vote_obj", "was_upvote"]) 6 | -------------------------------------------------------------------------------- /aiteo/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from aiteo.models import Question, Response 4 | 5 | 6 | class ResponseInline(admin.StackedInline): 7 | model = Response 8 | 9 | 10 | class QuestionAdmin(admin.ModelAdmin): 11 | inlines = [ 12 | ResponseInline, 13 | ] 14 | 15 | 16 | admin.site.register(Question, QuestionAdmin) 17 | -------------------------------------------------------------------------------- /aiteo/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from aiteo.models import Question, Response 4 | 5 | 6 | class AskQuestionForm(forms.ModelForm): 7 | 8 | class Meta: 9 | model = Question 10 | fields = ["question", "content"] 11 | 12 | 13 | class AddResponseForm(forms.ModelForm): 14 | 15 | class Meta: 16 | model = Response 17 | fields = ["content"] 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - 2.7 5 | 6 | env: 7 | - DJANGO=1.4 8 | - DJANGO=1.5 9 | - DJANGO=1.6 10 | 11 | install: 12 | - pip install flake8 13 | - pip install -q Django==$DJANGO 14 | - pip install -q django-nose 15 | - pip install -q coverage 16 | - pip install -q python-coveralls 17 | - pip install -e . 18 | 19 | script: 20 | - flake8 . 21 | - coverage run runtests.py 22 | 23 | after_script: 24 | - coveralls 25 | -------------------------------------------------------------------------------- /aiteo/templatetags/aiteo_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.conf import settings 3 | from django.utils.importlib import import_module 4 | 5 | 6 | workflow = import_module(getattr(settings, "AITEO_WORKFLOW_MODULE", "aiteo.workflow")) 7 | register = template.Library() 8 | 9 | 10 | @register.filter 11 | def can_accept(user, response): 12 | return workflow.can_mark_accepted(user, response.question) 13 | 14 | 15 | @register.filter 16 | def voted_up(user, obj): 17 | return obj.votes.filter(user=user, upvote=True).exists() 18 | 19 | 20 | @register.filter 21 | def voted_down(user, obj): 22 | return obj.votes.filter(user=user, upvote=False).exists() 23 | -------------------------------------------------------------------------------- /aiteo/templates/aiteo/_accepted.html: -------------------------------------------------------------------------------- 1 | {% load url from future %} 2 | {% load aiteo_tags %} 3 | {% if request.user|can_accept:response %} 4 |
5 | {% if response.accepted %} 6 | 7 | {% else %} 8 | 9 | 10 | 11 | {% endif %} 12 |
13 | {% else %} 14 | {% if response.accepted %} 15 |
16 | 17 |
18 | {% endif %} 19 | {% endif %} 20 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | 4 | from django.conf import settings 5 | 6 | 7 | if not settings.configured: 8 | settings.configure( 9 | DATABASES={ 10 | "default": { 11 | "ENGINE": "django.db.backends.sqlite3", 12 | } 13 | }, 14 | ROOT_URLCONF="dialogos.urls", 15 | INSTALLED_APPS=[ 16 | "django.contrib.auth", 17 | "django.contrib.contenttypes", 18 | "django.contrib.sessions", 19 | "aiteo", 20 | ], 21 | ) 22 | 23 | 24 | from django_nose import NoseTestSuiteRunner 25 | 26 | test_runner = NoseTestSuiteRunner(verbosity=1) 27 | failures = test_runner.run_tests(["aiteo"]) 28 | 29 | if failures: 30 | sys.exit(failures) 31 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | aiteo README 2 | ============ 3 | 4 | .. image:: https://img.shields.io/travis/eldarion/aiteo.svg 5 | :target: https://travis-ci.org/eldarion/aiteo 6 | 7 | .. image:: https://img.shields.io/coveralls/eldarion/aiteo.svg 8 | :target: https://coveralls.io/r/eldarion/aiteo 9 | 10 | .. image:: https://img.shields.io/pypi/dm/aiteo.svg 11 | :target: https://pypi.python.org/pypi/aiteo/ 12 | 13 | .. image:: https://img.shields.io/pypi/v/aiteo.svg 14 | :target: https://pypi.python.org/pypi/aiteo/ 15 | 16 | .. image:: https://img.shields.io/badge/license-BSD-blue.svg 17 | :target: https://pypi.python.org/pypi/aiteo/ 18 | 19 | 20 | aiteo is an app for asking questions. Similar to StackOverflow this 21 | app provides functionality for people to ask and answer questions. 22 | -------------------------------------------------------------------------------- /aiteo/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url 2 | 3 | 4 | urlpatterns = patterns( 5 | "aiteo.views", 6 | url(r"^$", "question_list", name="aiteo_question_list"), 7 | url(r"^ask/$", "question_create", name="aiteo_question_create"), 8 | url(r"^questions/(?P\d+)/$", "question_detail", name="aiteo_question_detail"), 9 | url(r"^questions/(?P\d+)/upvote/$", "question_upvote", name="aiteo_question_upvote"), 10 | url(r"^questions/(?P\d+)/downvote/$", "question_downvote", name="aiteo_question_downvote"), 11 | url(r"^responses/(?P\d+)/upvote/$", "response_upvote", name="aiteo_response_upvote"), 12 | url(r"^responses/(?P\d+)/downvote/$", "response_downvote", name="aiteo_response_downvote"), 13 | url(r"^responses/(?P\d+)/accept/$", "mark_accepted", name="aiteo_mark_accepted"), 14 | ) 15 | -------------------------------------------------------------------------------- /aiteo/templates/aiteo/_question_vote_badge.html: -------------------------------------------------------------------------------- 1 | {% load aiteo_tags %} 2 | {% load url from future %} 3 |
4 |
5 | {% if request.user.is_authenticated %} 6 | {% if request.user|voted_up:question or question.user == request.user %} 7 | 8 | {% else %} 9 | 10 | 11 | 12 | {% endif %} 13 | {% endif %} 14 |
15 |
{{ question.score|default:"0" }}
16 |
17 | {% if request.user.is_authenticated %} 18 | {% if user|voted_down:question or question.user == request.user %} 19 | 20 | {% else %} 21 | 22 | 23 | 24 | {% endif %} 25 | {% endif %} 26 |
27 |
28 | -------------------------------------------------------------------------------- /aiteo/templates/aiteo/_response_vote_badge.html: -------------------------------------------------------------------------------- 1 | {% load aiteo_tags %} 2 | {% load url from future %} 3 |
4 |
5 | {% if request.user.is_authenticated %} 6 | {% if request.user|voted_up:response or response.user == request.user %} 7 | 8 | {% else %} 9 | 10 | 11 | 12 | {% endif %} 13 | {% endif %} 14 |
15 |
{{ response.score|default:"0" }}
16 |
17 | {% if request.user.is_authenticated %} 18 | {% if user|voted_down:response or response.user == request.user %} 19 | 20 | {% else %} 21 | 22 | 23 | 24 | {% endif %} 25 | {% endif %} 26 |
27 |
28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2014, Eldarion, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of Eldarion, Inc. nor the names of its contributors may 15 | be used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /aiteo/models.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | from django.db import models 3 | from django.utils import timezone 4 | 5 | from django.contrib.auth.models import User 6 | 7 | from aiteo.signals import voted, vote_cleared 8 | 9 | 10 | class TimestampModel(models.Model): 11 | created = models.DateTimeField(default=timezone.now) 12 | modified = models.DateTimeField(default=timezone.now) 13 | 14 | class Meta: 15 | abstract = True 16 | 17 | def save(self, *args, **kwargs): 18 | self.modified = timezone.now() 19 | return super(TimestampModel, self).save(*args, **kwargs) 20 | 21 | 22 | class ScoringModel(TimestampModel): 23 | score = models.IntegerField(editable=False, default=0) 24 | vote_count = models.IntegerField(editable=False, default=0) 25 | 26 | class Meta: 27 | abstract = True 28 | 29 | def vote(self, user, upvote): 30 | vote, created = self.votes.get_or_create(user=user, defaults={"upvote": upvote}) 31 | if hasattr(vote, "response"): 32 | vote_obj = vote.response 33 | else: 34 | vote_obj = vote.question 35 | changed = not created and (vote.upvote != upvote) 36 | if changed: 37 | vote.delete() 38 | if changed or created: 39 | self.update_score() 40 | if changed: 41 | vote_cleared.send(sender=vote.__class__, vote_obj=vote_obj, was_upvote=upvote) 42 | if created: 43 | voted.send(sender=vote.__class__, vote_obj=vote_obj, upvote=upvote) 44 | 45 | def update_score(self): 46 | votes = self.votes.count() 47 | upvotes = self.votes.filter(upvote=True).count() 48 | downvotes = votes - upvotes 49 | self.score = upvotes - downvotes 50 | self.vote_count = votes 51 | self.save() 52 | 53 | 54 | class Question(ScoringModel): 55 | 56 | question = models.CharField(max_length=100) 57 | content = models.TextField() 58 | user = models.ForeignKey(User, related_name="questions") 59 | 60 | @property 61 | def accepted_response(self): 62 | try: 63 | response = self.responses.get(accepted=True) 64 | except Response.DoesNotExist: 65 | response = None 66 | return response 67 | 68 | def get_absolute_url(self): 69 | return reverse("aiteo_question_detail", args=[self.pk]) 70 | 71 | 72 | class Response(ScoringModel): 73 | 74 | question = models.ForeignKey(Question, related_name="responses") 75 | content = models.TextField() 76 | accepted = models.BooleanField(default=False) 77 | user = models.ForeignKey(User, related_name="responses") 78 | 79 | def accept(self): 80 | # check for another active one and mark it inactive 81 | try: 82 | response = Response.objects.get(question=self.question, accepted=True) 83 | except Response.DoesNotExist: 84 | pass 85 | else: 86 | if self != response: 87 | response.accepted = False 88 | response.save() 89 | self.accepted = True 90 | self.save() 91 | 92 | def get_absolute_url(self): 93 | return "%s#response-%d" % (self.question.get_absolute_url(), self.pk) 94 | 95 | 96 | class QuestionVote(TimestampModel): 97 | question = models.ForeignKey(Question, related_name="votes") 98 | upvote = models.BooleanField(default=True) 99 | user = models.ForeignKey(User, related_name="question_votes") 100 | 101 | class Meta: 102 | unique_together = [("question", "user")] 103 | 104 | 105 | class ResponseVote(TimestampModel): 106 | response = models.ForeignKey(Response, related_name="votes") 107 | upvote = models.BooleanField(default=True) 108 | user = models.ForeignKey(User, related_name="response_votes") 109 | 110 | class Meta: 111 | unique_together = [("response", "user")] 112 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import os 3 | import sys 4 | 5 | from distutils.util import convert_path 6 | from fnmatch import fnmatchcase 7 | from setuptools import setup, find_packages 8 | 9 | 10 | def read(fname): 11 | return codecs.open(os.path.join(os.path.dirname(__file__), fname)).read() 12 | 13 | 14 | # Provided as an attribute, so you can append to these instead 15 | # of replicating them: 16 | standard_exclude = ["*.py", "*.pyc", "*$py.class", "*~", ".*", "*.bak"] 17 | standard_exclude_directories = [ 18 | ".*", "CVS", "_darcs", "./build", "./dist", "EGG-INFO", "*.egg-info" 19 | ] 20 | 21 | 22 | # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) 23 | # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php 24 | # Note: you may want to copy this into your setup.py file verbatim, as 25 | # you can't import this from another package, when you don't know if 26 | # that package is installed yet. 27 | def find_package_data( 28 | where=".", 29 | package="", 30 | exclude=standard_exclude, 31 | exclude_directories=standard_exclude_directories, 32 | only_in_packages=True, 33 | show_ignored=False 34 | ): 35 | """ 36 | Return a dictionary suitable for use in ``package_data`` 37 | in a distutils ``setup.py`` file. 38 | 39 | The dictionary looks like:: 40 | 41 | {"package": [files]} 42 | 43 | Where ``files`` is a list of all the files in that package that 44 | don"t match anything in ``exclude``. 45 | 46 | If ``only_in_packages`` is true, then top-level directories that 47 | are not packages won"t be included (but directories under packages 48 | will). 49 | 50 | Directories matching any pattern in ``exclude_directories`` will 51 | be ignored; by default directories with leading ``.``, ``CVS``, 52 | and ``_darcs`` will be ignored. 53 | 54 | If ``show_ignored`` is true, then all the files that aren"t 55 | included in package data are shown on stderr (for debugging 56 | purposes). 57 | 58 | Note patterns use wildcards, or can be exact paths (including 59 | leading ``./``), and all searching is case-insensitive. 60 | """ 61 | out = {} 62 | stack = [(convert_path(where), "", package, only_in_packages)] 63 | while stack: 64 | where, prefix, package, only_in_packages = stack.pop(0) 65 | for name in os.listdir(where): 66 | fn = os.path.join(where, name) 67 | if os.path.isdir(fn): 68 | bad_name = False 69 | for pattern in exclude_directories: 70 | if fnmatchcase(name, pattern) or fn.lower() == pattern.lower(): 71 | bad_name = True 72 | if show_ignored: 73 | print >> sys.stderr, ( 74 | "Directory %s ignored by pattern %s" 75 | % (fn, pattern)) 76 | break 77 | if bad_name: 78 | continue 79 | if os.path.isfile(os.path.join(fn, "__init__.py")) and not prefix: 80 | if not package: 81 | new_package = name 82 | else: 83 | new_package = package + "." + name 84 | stack.append((fn, "", new_package, False)) 85 | else: 86 | stack.append((fn, prefix + name + "/", package, only_in_packages)) 87 | elif package or not only_in_packages: 88 | # is a file 89 | bad_name = False 90 | for pattern in exclude: 91 | if fnmatchcase(name, pattern) or fn.lower() == pattern.lower(): 92 | bad_name = True 93 | if show_ignored: 94 | print >> sys.stderr, ( 95 | "File %s ignored by pattern %s" 96 | % (fn, pattern)) 97 | break 98 | if bad_name: 99 | continue 100 | out.setdefault(package, []).append(prefix + name) 101 | return out 102 | 103 | 104 | PACKAGE = "aiteo" 105 | NAME = "aiteo" 106 | DESCRIPTION = "a Django app for question and answer functionality" 107 | AUTHOR = "Eldarion" 108 | AUTHOR_EMAIL = "development@eldarion.com" 109 | URL = "http://github.com/eldarion/aiteo" 110 | VERSION = __import__(PACKAGE).__version__ 111 | 112 | 113 | setup( 114 | name=NAME, 115 | version=VERSION, 116 | description=DESCRIPTION, 117 | long_description=read("README.rst"), 118 | author=AUTHOR, 119 | author_email=AUTHOR_EMAIL, 120 | license="BSD", 121 | url=URL, 122 | packages=find_packages(exclude=["tests.*", "tests"]), 123 | package_data=find_package_data(PACKAGE, only_in_packages=False), 124 | classifiers=[ 125 | "Development Status :: 3 - Alpha", 126 | "Environment :: Web Environment", 127 | "Intended Audience :: Developers", 128 | "License :: OSI Approved :: BSD License", 129 | "Operating System :: OS Independent", 130 | "Programming Language :: Python", 131 | "Framework :: Django", 132 | ], 133 | zip_safe=False, 134 | ) 135 | -------------------------------------------------------------------------------- /aiteo/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.conf import settings 4 | from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden 5 | from django.shortcuts import get_object_or_404, render 6 | from django.template import RequestContext 7 | from django.template.loader import render_to_string 8 | from django.utils.importlib import import_module 9 | from django.views.decorators.http import require_POST 10 | 11 | from account.decorators import login_required 12 | from aiteo.forms import AskQuestionForm, AddResponseForm 13 | from aiteo.models import Question, Response 14 | 15 | 16 | workflow = import_module(getattr(settings, "AITEO_WORKFLOW_MODULE", "aiteo.workflow")) 17 | 18 | 19 | def question_list(request): 20 | questions = Question.objects.all().order_by("-score", "created", "id") 21 | ctx = { 22 | "questions": questions, 23 | } 24 | return render(request, "aiteo/question_list.html", ctx) 25 | 26 | 27 | @login_required 28 | def question_create(request): 29 | if request.method == "POST": 30 | form = AskQuestionForm(request.POST) 31 | if form.is_valid(): 32 | question = form.save(commit=False) 33 | question.user = request.user 34 | question.save() 35 | return HttpResponseRedirect(question.get_absolute_url()) 36 | else: 37 | form = AskQuestionForm() 38 | ctx = { 39 | "form": form, 40 | } 41 | return render(request, "aiteo/question_create.html", ctx) 42 | 43 | 44 | def question_detail(request, pk): 45 | questions = Question.objects.all() 46 | question = get_object_or_404(questions, pk=pk) 47 | responses = question.responses.order_by("-score", "created", "id") 48 | is_me = question.user == request.user 49 | if request.method == "POST": 50 | add_response_form = AddResponseForm(request.POST) 51 | if add_response_form.is_valid(): 52 | response = add_response_form.save(commit=False) 53 | response.question = question 54 | response.user = request.user 55 | response.save() 56 | return HttpResponseRedirect(response.get_absolute_url()) 57 | else: 58 | if not is_me or request.user.is_staff: 59 | add_response_form = AddResponseForm() 60 | else: 61 | add_response_form = None 62 | ctx = { 63 | "can_mark_accepted": workflow.can_mark_accepted(request.user, question), 64 | "question": question, 65 | "responses": responses, 66 | "add_response_form": add_response_form, 67 | } 68 | return render(request, "aiteo/question_detail.html", ctx) 69 | 70 | 71 | @login_required 72 | @require_POST 73 | def mark_accepted(request, pk): 74 | response = get_object_or_404(Response, pk=pk) 75 | if not workflow.can_mark_accepted(request.user, response.question): 76 | return HttpResponseForbidden("You are not allowed to mark this question accepted.") 77 | 78 | response.accept() 79 | 80 | data = {"fragments": {}} 81 | for resp in response.question.responses.all(): 82 | data["fragments"]["#accepted-{}".format(resp.pk)] = render_to_string( 83 | "aiteo/_accepted.html", 84 | {"response": resp}, 85 | context_instance=RequestContext(request) 86 | ) 87 | return HttpResponse(json.dumps(data), mimetype="application/json") 88 | 89 | 90 | @login_required 91 | @require_POST 92 | def question_upvote(request, pk): 93 | question = get_object_or_404(Question, pk=pk) 94 | question.vote(user=request.user, upvote=True) 95 | data = { 96 | "html": render_to_string("aiteo/_question_vote_badge.html", { 97 | "question": question 98 | }, context_instance=RequestContext(request)) 99 | } 100 | return HttpResponse(json.dumps(data), mimetype="application/json") 101 | 102 | 103 | @login_required 104 | @require_POST 105 | def question_downvote(request, pk): 106 | question = get_object_or_404(Question, pk=pk) 107 | question.vote(user=request.user, upvote=False) 108 | data = { 109 | "html": render_to_string("aiteo/_question_vote_badge.html", { 110 | "question": question 111 | }, context_instance=RequestContext(request)) 112 | } 113 | return HttpResponse(json.dumps(data), mimetype="application/json") 114 | 115 | 116 | @login_required 117 | @require_POST 118 | def response_upvote(request, pk): 119 | response = get_object_or_404(Response, pk=pk) 120 | response.vote(user=request.user, upvote=True) 121 | data = { 122 | "html": render_to_string("aiteo/_response_vote_badge.html", { 123 | "response": response 124 | }, context_instance=RequestContext(request)) 125 | } 126 | return HttpResponse(json.dumps(data), mimetype="application/json") 127 | 128 | 129 | @login_required 130 | @require_POST 131 | def response_downvote(request, pk): 132 | response = get_object_or_404(Response, pk=pk) 133 | response.vote(user=request.user, upvote=False) 134 | data = { 135 | "html": render_to_string("aiteo/_response_vote_badge.html", { 136 | "response": response 137 | }, context_instance=RequestContext(request)) 138 | } 139 | return HttpResponse(json.dumps(data), mimetype="application/json") 140 | --------------------------------------------------------------------------------