├── .travis.yml ├── CHANGELOG ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs └── index.txt ├── flag ├── __init__.py ├── admin.py ├── models.py ├── signals.py ├── templates │ └── flag │ │ ├── flag_form.html │ │ └── thank_you.html ├── templatetags │ ├── __init__.py │ └── flag_tags.py ├── urls.py └── views.py └── setup.py /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - 2.7 5 | 6 | install: 7 | - pip install flake8 8 | - pip install -e . 9 | 10 | script: 11 | - flake8 --max-line-length=100 --max-complexity=10 --statistics --benchmark flag 12 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | ===================== 2 | django-flag CHANGELOG 3 | ===================== 4 | 5 | 0.2 6 | === 7 | 8 | * BI: renamed {% load flagtags %} to {% load flag_tags %} 9 | * requires Django 1.1.2+ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | There are many ways you can help contribute to django-flag and the 4 | various apps, themes, and starter projects that it is made up of. Contributing 5 | code, writing documentation, reporting bugs, as well as reading and providing 6 | feedback on issues and pull requests, all are valid and necessary ways to 7 | help. 8 | 9 | ## Committing Code 10 | 11 | The great thing about using a distributed versioning control system like git 12 | is that everyone becomes a committer. When other people write good patches 13 | it makes it very easy to include their fixes/features and give them proper 14 | credit for the work. 15 | 16 | We recommend that you do all your work in a separate branch. When you 17 | are ready to work on a bug or a new feature create yourself a new branch. The 18 | reason why this is important is you can commit as often you like. When you are 19 | ready you can merge in the change. Let's take a look at a common workflow: 20 | 21 | git checkout -b task-566 22 | ... fix and git commit often ... 23 | git push origin task-566 24 | 25 | The reason we have created two new branches is to stay off of `master`. 26 | Keeping master clean of only upstream changes makes yours and ours lives 27 | easier. You can then send us a pull request for the fix/feature. Then we can 28 | easily review it and merge it when ready. 29 | 30 | 31 | ### Writing Commit Messages 32 | 33 | Writing a good commit message makes it simple for us to identify what your 34 | commit does from a high-level. There are some basic guidelines we'd like to 35 | ask you to follow. 36 | 37 | A critical part is that you keep the **first** line as short and sweet 38 | as possible. This line is important because when git shows commits and it has 39 | limited space or a different formatting option is used the first line becomes 40 | all someone might see. If your change isn't something non-trivial or there 41 | reasoning behind the change is not obvious, then please write up an extended 42 | message explaining the fix, your rationale, and anything else relevant for 43 | someone else that might be reviewing the change. Lastly, if there is a 44 | corresponding issue in Github issues for it, use the final line to provide 45 | a message that will link the commit message to the issue and auto-close it 46 | if appropriate. 47 | 48 | Add ability to travel back in time 49 | 50 | You need to be driving 88 miles per hour to generate 1.21 gigawatts of 51 | power to properly use this feature. 52 | 53 | Fixes #88 54 | 55 | 56 | ## Coding style 57 | 58 | When writing code to be included in django-flag keep our style in mind: 59 | 60 | * Follow [PEP8](http://www.python.org/dev/peps/pep-0008/) there are some 61 | cases where we do not follow PEP8. It is an excellent starting point. 62 | * Follow [Django's coding style](http://docs.djangoproject.com/en/dev/internals/contributing/#coding-style) 63 | we're pretty much in agreement on Django style outlined there. 64 | 65 | We would like to enforce a few more strict guides not outlined by PEP8 or 66 | Django's coding style: 67 | 68 | * PEP8 tries to keep line length at 80 characters. We follow it when we can, 69 | but not when it makes a line harder to read. It is okay to go a little bit 70 | over 80 characters if not breaking the line improves readability. 71 | * Use double quotes not single quotes. Single quotes are allowed in cases 72 | where a double quote is needed in the string. This makes the code read 73 | cleaner in those cases. 74 | * Blank lines are indented to the appropriate level for the block they are in. 75 | * Docstrings always use three double quotes on a line of their own, so, for 76 | example, a single line docstring should take up three lines not one. 77 | * Imports are grouped specifically and ordered alphabetically. This is shown 78 | in the example below. 79 | * Always use `reverse` and never `@models.permalink`. 80 | * Tuples should be reserved for positional data structures and not used 81 | where a list is more appropriate. 82 | * URL patterns should use the `url()` function rather than a tuple. 83 | 84 | Here is an example of these rules applied: 85 | 86 | # first set of imports are stdlib imports 87 | # non-from imports go first then from style import in their own group 88 | import csv 89 | 90 | # second set of imports are Django imports with contrib in their own 91 | # group. 92 | from django.core.urlresolvers import reverse 93 | from django.db import models 94 | from django.utils import timezone 95 | from django.utils.translation import ugettext_lazy as _ 96 | 97 | from django.contrib.auth.models import User 98 | 99 | # third set of imports are external apps (if applicable) 100 | from tagging.fields import TagField 101 | 102 | # fourth set of imports are local apps 103 | from .fields import MarkupField 104 | 105 | 106 | class Task(models.Model): 107 | """ 108 | A model for storing a task. 109 | """ 110 | 111 | creator = models.ForeignKey(User) 112 | created = models.DateTimeField(default=timezone.now) 113 | modified = models.DateTimeField(default=timezone.now) 114 | 115 | objects = models.Manager() 116 | 117 | class Meta: 118 | verbose_name = _("task") 119 | verbose_name_plural = _("tasks") 120 | 121 | def __unicode__(self): 122 | return self.summary 123 | 124 | def save(self, **kwargs): 125 | self.modified = datetime.now() 126 | super(Task, self).save(**kwargs) 127 | 128 | def get_absolute_url(self): 129 | return reverse("task_detail", kwargs={"task_id": self.pk}) 130 | 131 | # custom methods 132 | 133 | 134 | class TaskComment(models.Model): 135 | # ... you get the point ... 136 | pass 137 | 138 | 139 | ## Pull Requests 140 | 141 | Please keep your pull requests focused on one specific thing only. If you 142 | have a number of contributions to make, then please send seperate pull 143 | requests. It is much easier on maintainers to receive small, well defined, 144 | pull requests, than it is to have a single large one that batches up a 145 | lot of unrelated commits. 146 | 147 | If you ended up making multiple commits for one logical change, please 148 | rebase into a single commit. 149 | 150 | git rebase -i HEAD~10 # where 10 is the number of commits back you need 151 | 152 | This will pop up an editor with your commits and some instructions you want 153 | to squash commits down by replacing 'pick' with 's' to have it combined with 154 | the commit before it. You can squash multiple ones at the same time. 155 | 156 | When you save and exit the text editor where you were squashing commits, git 157 | will squash them down and then present you with another editor with commit 158 | messages. Choose the one to apply to the squashed commit (or write a new 159 | one entirely.) Save and exit will complete the rebase. Use a forced push to 160 | your fork. 161 | 162 | git push -f 163 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012-2019 James Tauber and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include docs *.txt 2 | recursive-include flag/templates *.html 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django Flag 2 | ----------- 3 | .. image:: http://slack.pinaxproject.com/badge.svg 4 | :target: http://slack.pinaxproject.com/ 5 | 6 | .. image:: https://img.shields.io/travis/pinax/django-flag.svg 7 | :target: https://travis-ci.org/pinax/django-flag 8 | 9 | .. image:: https://img.shields.io/coveralls/pinax/django-flag.svg 10 | :target: https://coveralls.io/r/pinax/django-flag 11 | 12 | .. image:: https://img.shields.io/pypi/dm/django-flag.svg 13 | :target: https://pypi.python.org/pypi/django-flag/ 14 | 15 | .. image:: https://img.shields.io/pypi/v/django-flag.svg 16 | :target: https://pypi.python.org/pypi/django-flag/ 17 | 18 | .. image:: https://img.shields.io/badge/license-MIT-blue.svg 19 | :target: https://pypi.python.org/pypi/django-flag/ 20 | 21 | 22 | Pinax 23 | ------ 24 | 25 | Pinax is an open-source platform built on the Django Web Framework. It is an ecosystem of reusable Django apps, themes, and starter project templates. 26 | This collection can be found at http://pinaxproject.com. 27 | 28 | This app was developed as part of the Pinax ecosystem but is just a Django app and can be used independently of other Pinax apps. 29 | 30 | 31 | django-flag 32 | ------------ 33 | 34 | ``django-flag`` provides flagging of inapproprite spam/content. 35 | 36 | 37 | Documentation 38 | --------------- 39 | 40 | The ``django-flag`` documentation is currently under construction. If you would like to help us write documentation, please join our Pinax Project Slack team and let us know! The Pinax documentation is available at http://pinaxproject.com/pinax/. 41 | 42 | 43 | Contribute 44 | ---------------- 45 | 46 | See this blog post http://blog.pinaxproject.com/2016/02/26/recap-february-pinax-hangout/ including a video, or our How to Contribute (http://pinaxproject.com/pinax/how_to_contribute/) section for an overview on how contributing to Pinax works. For concrete contribution ideas, please see our Ways to Contribute/What We Need Help With (http://pinaxproject.com/pinax/ways_to_contribute/) section. 47 | 48 | In case of any questions we recommend you join our Pinax Slack team (http://slack.pinaxproject.com) and ping us there instead of creating an issue on GitHub. Creating issues on GitHub is of course also valid but we are usually able to help you faster if you ping us in Slack. 49 | 50 | We also highly recommend reading our Open Source and Self-Care blog post (http://blog.pinaxproject.com/2016/01/19/open-source-and-self-care/). 51 | 52 | 53 | Code of Conduct 54 | ---------------- 55 | 56 | In order to foster a kind, inclusive, and harassment-free community, the Pinax Project has a code of conduct, which can be found here http://pinaxproject.com/pinax/code_of_conduct/. We ask you to treat everyone as a smart human programmer that shares an interest in Python, Django, and Pinax with you. 57 | 58 | 59 | Pinax Project Blog and Twitter 60 | ------------------------------- 61 | 62 | For updates and news regarding the Pinax Project, please follow us on Twitter at @pinaxproject and check out our blog http://blog.pinaxproject.com. 63 | -------------------------------------------------------------------------------- /docs/index.txt: -------------------------------------------------------------------------------- 1 | django-flag 2 | =========== 3 | 4 | This app lets users of your site flag content as inappropriate or spam. 5 | 6 | By default some choices for status are available, however if you'd like to 7 | customize them you can provide a ``FLAG_STATUSES`` setting which is a list of 8 | two tuples where the first item is the value (a one character string) and the 9 | second is the readable value. The default choice should have a key of ``"1"``. 10 | 11 | (more here soon) 12 | -------------------------------------------------------------------------------- /flag/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinax/django-flag/76c776a961e7fdcaa1475902c8a4f4486d902b4d/flag/__init__.py -------------------------------------------------------------------------------- /flag/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from flag.models import FlaggedContent, FlagInstance 4 | 5 | 6 | class InlineFlagInstance(admin.TabularInline): 7 | model = FlagInstance 8 | extra = 0 9 | 10 | 11 | class FlaggedContentAdmin(admin.ModelAdmin): 12 | inlines = [InlineFlagInstance] 13 | 14 | 15 | admin.site.register(FlaggedContent, FlaggedContentAdmin) 16 | -------------------------------------------------------------------------------- /flag/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from django.conf import settings 4 | from django.db import models 5 | 6 | from django.contrib.contenttypes.models import ContentType 7 | from django.contrib.contenttypes import generic 8 | from django.utils.translation import ugettext_lazy as _ 9 | 10 | from flag import signals 11 | 12 | 13 | STATUS = getattr(settings, "FLAG_STATUSES", [ 14 | ("1", _("flagged")), 15 | ("2", _("flag rejected by moderator")), 16 | ("3", _("creator notified")), 17 | ("4", _("content removed by creator")), 18 | ("5", _("content removed by moderator")), 19 | ]) 20 | 21 | 22 | class FlaggedContent(models.Model): 23 | 24 | content_type = models.ForeignKey(ContentType) 25 | object_id = models.PositiveIntegerField() 26 | content_object = generic.GenericForeignKey("content_type", "object_id") 27 | # user who created flagged content -- this is kept in model so it outlives content 28 | creator = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="flagged_content") 29 | status = models.CharField(max_length=1, choices=STATUS, default="1") 30 | # moderator responsible for last status change 31 | moderator = models.ForeignKey( 32 | settings.AUTH_USER_MODEL, 33 | null=True, 34 | related_name="moderated_content" 35 | ) 36 | count = models.PositiveIntegerField(default=1) 37 | 38 | class Meta: 39 | unique_together = [("content_type", "object_id")] 40 | 41 | 42 | class FlagInstance(models.Model): 43 | 44 | flagged_content = models.ForeignKey(FlaggedContent) 45 | user = models.ForeignKey(settings.AUTH_USER_MODEL) # user flagging the content 46 | when_added = models.DateTimeField(default=datetime.now) 47 | when_recalled = models.DateTimeField(null=True) # if recalled at all 48 | comment = models.TextField() # comment by the flagger 49 | 50 | 51 | def add_flag(flagger, content_type, object_id, content_creator, comment, status=None): 52 | 53 | # check if it's already been flagged 54 | defaults = dict(creator=content_creator) 55 | if status is not None: 56 | defaults["status"] = status 57 | flagged_content, created = FlaggedContent.objects.get_or_create( 58 | content_type=content_type, 59 | object_id=object_id, 60 | defaults=defaults 61 | ) 62 | if not created: 63 | flagged_content.count = models.F("count") + 1 64 | flagged_content.save() 65 | # pull flagged_content from database to get count attribute filled 66 | # properly (not the best way, but works) 67 | flagged_content = FlaggedContent.objects.get(pk=flagged_content.pk) 68 | 69 | flag_instance = FlagInstance( 70 | flagged_content=flagged_content, 71 | user=flagger, 72 | comment=comment 73 | ) 74 | flag_instance.save() 75 | 76 | signals.content_flagged.send( 77 | sender=FlaggedContent, 78 | flagged_content=flagged_content, 79 | flagged_instance=flag_instance, 80 | ) 81 | 82 | return flag_instance 83 | -------------------------------------------------------------------------------- /flag/signals.py: -------------------------------------------------------------------------------- 1 | from django.dispatch import Signal 2 | 3 | 4 | content_flagged = Signal(providing_args=["flagged_content", "flagged_instance"]) 5 | -------------------------------------------------------------------------------- /flag/templates/flag/flag_form.html: -------------------------------------------------------------------------------- 1 |
2 | {% csrf_token %} 3 | 4 | 5 | 6 | 11 | 12 |
7 | 8 | 9 | 10 |
13 |
-------------------------------------------------------------------------------- /flag/templates/flag/thank_you.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 |

{% trans 'Thank you' %}

4 | {% if messages %} 5 |
6 | {% for message in messages %} 7 |

{{ message }}

8 | {% endfor %} 9 |
10 | {% endif %} -------------------------------------------------------------------------------- /flag/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinax/django-flag/76c776a961e7fdcaa1475902c8a4f4486d902b4d/flag/templatetags/__init__.py -------------------------------------------------------------------------------- /flag/templatetags/flag_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | from django.contrib.contenttypes.models import ContentType 4 | 5 | 6 | register = template.Library() 7 | 8 | 9 | @register.inclusion_tag("flag/flag_form.html", takes_context=True) 10 | def flag(context, content_object, creator_field): 11 | content_type = ContentType.objects.get( 12 | app_label=content_object._meta.app_label, 13 | model=content_object._meta.module_name 14 | ) 15 | request = context["request"] 16 | return { 17 | "content_type": content_type.id, 18 | "object_id": content_object.id, 19 | "creator_field": creator_field, 20 | "request": request, 21 | "user": request.user, 22 | } 23 | -------------------------------------------------------------------------------- /flag/urls.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from django.conf.urls import patterns, url 3 | from django.views.generic import TemplateView 4 | 5 | 6 | urlpatterns = patterns("", 7 | url(r"^$", "flag.views.flag", name="flag"), 8 | url(r'^thank_you', TemplateView.as_view(template_name="flag/thank_you.html"), name='flag-reported'), 9 | ) 10 | -------------------------------------------------------------------------------- /flag/views.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | from django.http import HttpResponseRedirect 3 | from django.shortcuts import get_object_or_404 4 | 5 | from django.contrib.auth.decorators import login_required 6 | from django.contrib.contenttypes.models import ContentType 7 | from django.utils.translation import ugettext as _ 8 | from django.contrib import messages 9 | 10 | from flag.models import add_flag 11 | 12 | 13 | @login_required 14 | def flag(request): 15 | 16 | content_type = request.POST.get("content_type") 17 | object_id = request.POST.get("object_id") 18 | creator_field = request.POST.get("creator_field") 19 | comment = request.POST.get("comment") 20 | next = request.POST.get("next") 21 | 22 | content_type = get_object_or_404(ContentType, id=int(content_type)) 23 | object_id = int(object_id) 24 | 25 | content_object = content_type.get_object_for_this_type(id=object_id) 26 | 27 | if creator_field and hasattr(content_object, creator_field): 28 | creator = getattr(content_object, creator_field) 29 | else: 30 | creator = None 31 | 32 | add_flag(request.user, content_type, object_id, creator, comment) 33 | messages.success(request, _("You have added a flag. A moderator will review your submission " 34 | "shortly."), fail_silently=True) 35 | 36 | if next: 37 | return HttpResponseRedirect(next) 38 | else: 39 | return HttpResponseRedirect(reverse('flag-reported')) 40 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | 4 | setup( 5 | name = "django-flag", 6 | version = "0.2.dev10", 7 | description = "flagging of inapproriate/spam content", 8 | author = "Greg Newman", 9 | author_email = "greg@20seven.org", 10 | url = "http://code.google.com/p/django-flag/", 11 | packages = find_packages(), 12 | license = "MIT", 13 | classifiers = [ 14 | "Development Status :: 3 - Alpha", 15 | "Environment :: Web Environment", 16 | "Intended Audience :: Developers", 17 | "License :: OSI Approved :: MIT License", 18 | "Operating System :: OS Independent", 19 | "Programming Language :: Python", 20 | "Framework :: Django", 21 | ], 22 | include_package_data = True, 23 | zip_safe = False, 24 | ) 25 | --------------------------------------------------------------------------------