summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEvgeny Fadeev <evgeny.fadeev@gmail.com>2010-02-17 16:03:29 -0500
committerEvgeny Fadeev <evgeny.fadeev@gmail.com>2010-02-17 16:03:29 -0500
commit473bdf5773aed8bdd91a16e371f75e628d20511e (patch)
treec94ab39fef99bd9f89a1a33295dfc1344acaf489
parenta84d9d5713b66fdd0f948a5227ade56cb1559816 (diff)
parentc95b9cbb2c181248663e0584b4fca752f32dcec7 (diff)
downloadaskbot-473bdf5773aed8bdd91a16e371f75e628d20511e.tar.gz
askbot-473bdf5773aed8bdd91a16e371f75e628d20511e.tar.bz2
askbot-473bdf5773aed8bdd91a16e371f75e628d20511e.zip
merged with Hernanis model refactoring, ran out of time to fix an issue
-rwxr-xr-x.idea/compiler.xml24
-rwxr-xr-x.idea/copyright/profiles_settings.xml5
-rwxr-xr-x.idea/encodings.xml5
-rwxr-xr-x.idea/misc.xml127
-rwxr-xr-x.idea/modules.xml9
-rwxr-xr-x.idea/uiDesigner.xml125
-rwxr-xr-x.idea/vcs.xml7
-rwxr-xr-x.idea/workspace.xml683
-rw-r--r--forum/admin.py18
-rw-r--r--forum/forms.py1
-rw-r--r--forum/managers.py241
-rw-r--r--forum/models.py955
-rwxr-xr-xforum/models/__init__.py329
-rwxr-xr-xforum/models/answer.py134
-rwxr-xr-xforum/models/base.py140
-rwxr-xr-xforum/models/meta.py89
-rwxr-xr-xforum/models/question.py335
-rwxr-xr-xforum/models/repute.py107
-rwxr-xr-xforum/models/tag.py85
-rwxr-xr-xforum/models/user.py67
-rw-r--r--forum/urls.py6
-rw-r--r--forum/views/__init__.py2
-rw-r--r--forum/views/content.py1394
-rw-r--r--forum/views/users.py1
-rw-r--r--forum/views/writers.py98
-rw-r--r--pgfulltext/management.py2
-rwxr-xr-xsettings.py2
-rwxr-xr-xtester/__init__.py0
-rwxr-xr-xtester/models.py8
29 files changed, 3692 insertions, 1307 deletions
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100755
index 00000000..b9a1798a
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="CompilerConfiguration">
+ <option name="DEFAULT_COMPILER" value="Javac" />
+ <resourceExtensions>
+ <entry name=".+\.(properties|xml|html|dtd|tld)" />
+ <entry name=".+\.(gif|png|jpeg|jpg)" />
+ </resourceExtensions>
+ <wildcardResourcePatterns>
+ <entry name="?*.properties" />
+ <entry name="?*.xml" />
+ <entry name="?*.gif" />
+ <entry name="?*.png" />
+ <entry name="?*.jpeg" />
+ <entry name="?*.jpg" />
+ <entry name="?*.html" />
+ <entry name="?*.dtd" />
+ <entry name="?*.tld" />
+ <entry name="?*.ftl" />
+ </wildcardResourcePatterns>
+ <annotationProcessing enabled="false" useClasspath="true" />
+ </component>
+</project>
+
diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml
new file mode 100755
index 00000000..b385f01f
--- /dev/null
+++ b/.idea/copyright/profiles_settings.xml
@@ -0,0 +1,5 @@
+<component name="CopyrightManager">
+ <settings default="">
+ <module2copyright />
+ </settings>
+</component> \ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100755
index 00000000..7c62b52a
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false" />
+</project>
+
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100755
index 00000000..5253b461
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="AnalysisProjectProfileManager">
+ <option name="PROJECT_PROFILE" />
+ <option name="USE_PROJECT_LEVEL_SETTINGS" value="false" />
+ <list size="0" />
+ </component>
+ <component name="Archetypes.Configuration">
+ <option name="archetypesFileLocation" value="archetypes.xml" />
+ <option name="workingDirectory" value="" />
+ </component>
+ <component name="DependencyValidationManager">
+ <option name="SKIP_IMPORT_STATEMENTS" value="false" />
+ </component>
+ <component name="HandyEdit.TapestrySupport" tapestryProject="false" askEnableTapestry="true" componentSpec="true">
+ <locations pageRoot="" componentRoot="" applicationFile="" pagePropertiesRoot="" />
+ <packages page="" component="" />
+ <completion properties="true" methods="false" htmlAttributes="true" />
+ <toolWindows htmlAttributes="true" />
+ <ext extensionComponentSpec="jwc" extensionPageSpec="page" extensionScript="script" />
+ <create />
+ </component>
+ <component name="IdProvider" IDEtalkID="119765BF5908F4A0676504FC73A9C8F4" />
+ <component name="JavadocGenerationManager">
+ <option name="OUTPUT_DIRECTORY" />
+ <option name="OPTION_SCOPE" value="protected" />
+ <option name="OPTION_HIERARCHY" value="true" />
+ <option name="OPTION_NAVIGATOR" value="true" />
+ <option name="OPTION_INDEX" value="true" />
+ <option name="OPTION_SEPARATE_INDEX" value="true" />
+ <option name="OPTION_DOCUMENT_TAG_USE" value="false" />
+ <option name="OPTION_DOCUMENT_TAG_AUTHOR" value="false" />
+ <option name="OPTION_DOCUMENT_TAG_VERSION" value="false" />
+ <option name="OPTION_DOCUMENT_TAG_DEPRECATED" value="true" />
+ <option name="OPTION_DEPRECATED_LIST" value="true" />
+ <option name="OTHER_OPTIONS" value="" />
+ <option name="HEAP_SIZE" />
+ <option name="LOCALE" />
+ <option name="OPEN_IN_BROWSER" value="true" />
+ </component>
+ <component name="ProjectDetails">
+ <option name="projectName" value="osqa" />
+ </component>
+ <component name="ProjectRootManager" version="2" languageLevel="JDK_1_5" assert-keyword="true" jdk-15="true" project-jdk-name="1.6" project-jdk-type="JavaSDK">
+ <output url="file://$PROJECT_DIR$/out" />
+ </component>
+ <component name="SvnBranchConfigurationManager">
+ <option name="mySupportsUserInfoFilter" value="true" />
+ </component>
+ <component name="VssConfiguration">
+ <option name="CLIENT_PATH" value="" />
+ <option name="SRCSAFEINI_PATH" value="" />
+ <option name="USER_NAME" value="" />
+ <option name="PWD" value="" />
+ <CheckoutOptions>
+ <option name="COMMENT" value="" />
+ <option name="DO_NOT_GET_LATEST_VERSION" value="false" />
+ <option name="REPLACE_WRITABLE" value="false" />
+ <option name="RECURSIVE" value="false" />
+ </CheckoutOptions>
+ <CheckinOptions>
+ <option name="COMMENT" value="" />
+ <option name="KEEP_CHECKED_OUT" value="false" />
+ <option name="RECURSIVE" value="false" />
+ </CheckinOptions>
+ <AddOptions>
+ <option name="STORE_ONLY_LATEST_VERSION" value="false" />
+ <option name="CHECK_OUT_IMMEDIATELY" value="false" />
+ </AddOptions>
+ <UndocheckoutOptions>
+ <option name="MAKE_WRITABLE" value="false" />
+ <option name="REPLACE_LOCAL_COPY" value="2" />
+ <option name="RECURSIVE" value="false" />
+ </UndocheckoutOptions>
+ <GetOptions>
+ <option name="REPLACE_WRITABLE" value="0" />
+ <option name="MAKE_WRITABLE" value="false" />
+ <option name="ANSWER_NEGATIVELY" value="false" />
+ <option name="ANSWER_POSITIVELY" value="false" />
+ <option name="RECURSIVE" value="false" />
+ <option name="VERSION" />
+ </GetOptions>
+ </component>
+ <component name="WebServicesPlugin" addRequiredLibraries="true" />
+ <component name="masterDetails">
+ <states>
+ <state key="ArtifactsStructureConfigurable.UI">
+ <UIState>
+ <splitter-proportions>
+ <SplitterProportionsDataImpl />
+ </splitter-proportions>
+ <settings />
+ </UIState>
+ </state>
+ <state key="Copyright.UI">
+ <UIState>
+ <splitter-proportions>
+ <SplitterProportionsDataImpl />
+ </splitter-proportions>
+ </UIState>
+ </state>
+ <state key="ProjectJDKs.UI">
+ <UIState>
+ <splitter-proportions>
+ <SplitterProportionsDataImpl>
+ <option name="proportions">
+ <list>
+ <option value="0.2" />
+ </list>
+ </option>
+ </SplitterProportionsDataImpl>
+ </splitter-proportions>
+ <last-edited>1.6</last-edited>
+ </UIState>
+ </state>
+ <state key="ScopeChooserConfigurable.UI">
+ <UIState>
+ <splitter-proportions>
+ <SplitterProportionsDataImpl />
+ </splitter-proportions>
+ <settings />
+ </UIState>
+ </state>
+ </states>
+ </component>
+</project>
+
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100755
index 00000000..9a64d92a
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="ProjectModuleManager">
+ <modules>
+ <module fileurl="file://$PROJECT_DIR$/osqa.iml" filepath="$PROJECT_DIR$/osqa.iml" />
+ </modules>
+ </component>
+</project>
+
diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml
new file mode 100755
index 00000000..1e7cce4b
--- /dev/null
+++ b/.idea/uiDesigner.xml
@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="Palette2">
+ <group name="Swing">
+ <item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.png" removable="false" auto-create-binding="false" can-attach-label="false">
+ <default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
+ </item>
+ <item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.png" removable="false" auto-create-binding="false" can-attach-label="false">
+ <default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
+ </item>
+ <item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.png" removable="false" auto-create-binding="false" can-attach-label="false">
+ <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
+ </item>
+ <item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.png" removable="false" auto-create-binding="false" can-attach-label="true">
+ <default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
+ </item>
+ <item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.png" removable="false" auto-create-binding="true" can-attach-label="false">
+ <default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
+ <initial-values>
+ <property name="text" value="Button" />
+ </initial-values>
+ </item>
+ <item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.png" removable="false" auto-create-binding="true" can-attach-label="false">
+ <default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
+ <initial-values>
+ <property name="text" value="RadioButton" />
+ </initial-values>
+ </item>
+ <item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.png" removable="false" auto-create-binding="true" can-attach-label="false">
+ <default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
+ <initial-values>
+ <property name="text" value="CheckBox" />
+ </initial-values>
+ </item>
+ <item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.png" removable="false" auto-create-binding="false" can-attach-label="false">
+ <default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
+ <initial-values>
+ <property name="text" value="Label" />
+ </initial-values>
+ </item>
+ <item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.png" removable="false" auto-create-binding="true" can-attach-label="true">
+ <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
+ <preferred-size width="150" height="-1" />
+ </default-constraints>
+ </item>
+ <item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.png" removable="false" auto-create-binding="true" can-attach-label="true">
+ <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
+ <preferred-size width="150" height="-1" />
+ </default-constraints>
+ </item>
+ <item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.png" removable="false" auto-create-binding="true" can-attach-label="true">
+ <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
+ <preferred-size width="150" height="-1" />
+ </default-constraints>
+ </item>
+ <item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.png" removable="false" auto-create-binding="true" can-attach-label="true">
+ <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
+ <preferred-size width="150" height="50" />
+ </default-constraints>
+ </item>
+ <item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.png" removable="false" auto-create-binding="true" can-attach-label="true">
+ <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
+ <preferred-size width="150" height="50" />
+ </default-constraints>
+ </item>
+ <item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.png" removable="false" auto-create-binding="true" can-attach-label="true">
+ <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
+ <preferred-size width="150" height="50" />
+ </default-constraints>
+ </item>
+ <item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.png" removable="false" auto-create-binding="true" can-attach-label="true">
+ <default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
+ </item>
+ <item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.png" removable="false" auto-create-binding="true" can-attach-label="false">
+ <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
+ <preferred-size width="150" height="50" />
+ </default-constraints>
+ </item>
+ <item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.png" removable="false" auto-create-binding="true" can-attach-label="false">
+ <default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
+ <preferred-size width="150" height="50" />
+ </default-constraints>
+ </item>
+ <item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.png" removable="false" auto-create-binding="true" can-attach-label="false">
+ <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
+ <preferred-size width="150" height="50" />
+ </default-constraints>
+ </item>
+ <item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.png" removable="false" auto-create-binding="true" can-attach-label="false">
+ <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
+ <preferred-size width="200" height="200" />
+ </default-constraints>
+ </item>
+ <item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.png" removable="false" auto-create-binding="false" can-attach-label="false">
+ <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
+ <preferred-size width="200" height="200" />
+ </default-constraints>
+ </item>
+ <item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.png" removable="false" auto-create-binding="true" can-attach-label="true">
+ <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
+ </item>
+ <item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.png" removable="false" auto-create-binding="true" can-attach-label="false">
+ <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
+ </item>
+ <item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.png" removable="false" auto-create-binding="false" can-attach-label="false">
+ <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
+ </item>
+ <item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.png" removable="false" auto-create-binding="true" can-attach-label="false">
+ <default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
+ </item>
+ <item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.png" removable="false" auto-create-binding="false" can-attach-label="false">
+ <default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
+ <preferred-size width="-1" height="20" />
+ </default-constraints>
+ </item>
+ <item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.png" removable="false" auto-create-binding="false" can-attach-label="false">
+ <default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
+ </item>
+ <item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.png" removable="false" auto-create-binding="true" can-attach-label="false">
+ <default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
+ </item>
+ </group>
+ </component>
+</project>
+
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100755
index 00000000..7e76f0fd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="VcsDirectoryMappings">
+ <mapping directory="" vcs="Git" />
+ </component>
+</project>
+
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
new file mode 100755
index 00000000..69374f95
--- /dev/null
+++ b/.idea/workspace.xml
@@ -0,0 +1,683 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="CCaseConfig">
+ <option name="checkoutReserved" value="false" />
+ <option name="markExternalChangeAsUpToDate" value="true" />
+ <option name="checkInUseHijack" value="true" />
+ <option name="useUcmModel" value="true" />
+ <option name="isOffline" value="false" />
+ <option name="synchOutside" value="false" />
+ <option name="isHistoryResticted" value="true" />
+ <option name="useIdenticalSwitch" value="true" />
+ <option name="synchActivitiesOnRefresh" value="true" />
+ <option name="lastScr" value="" />
+ <option name="scrTextFileName" value="" />
+ <option name="historyRevisionsNumber" value="4" />
+ </component>
+ <component name="ChangeListManager" verified="true">
+ <list default="true" readonly="true" id="378a32c6-5795-4404-9430-96837181eba2" name="Default" comment="">
+ <change type="MODIFICATION" beforePath="$PROJECT_DIR$/pgfulltext/management.py" afterPath="$PROJECT_DIR$/pgfulltext/management.py" />
+ <change type="MODIFICATION" beforePath="$PROJECT_DIR$/forum/skins/default/templates/user_edit.html" afterPath="$PROJECT_DIR$/forum/skins/default/templates/user_edit.html" />
+ <change type="MODIFICATION" beforePath="$PROJECT_DIR$/fbconnect/fb.py" afterPath="$PROJECT_DIR$/fbconnect/fb.py" />
+ <change type="DELETED" beforePath="C:\osqadev\newmaster\osqa\forum\models.py" afterPath="" />
+ <change type="MODIFICATION" beforePath="$PROJECT_DIR$/osqa.iml" afterPath="$PROJECT_DIR$/osqa.iml" />
+ <change type="MODIFICATION" beforePath="$PROJECT_DIR$/settings.py" afterPath="$PROJECT_DIR$/settings.py" />
+ <change type="DELETED" beforePath="C:\osqadev\newmaster\osqa\forum\models\models.py" afterPath="" />
+ <change type="MODIFICATION" beforePath="$PROJECT_DIR$/forum/forms.py" afterPath="$PROJECT_DIR$/forum/forms.py" />
+ <change type="MODIFICATION" beforePath="$PROJECT_DIR$/forum/views/content.py" afterPath="$PROJECT_DIR$/forum/views/content.py" />
+ <change type="MODIFICATION" beforePath="$PROJECT_DIR$/fbconnect/urls.py" afterPath="$PROJECT_DIR$/fbconnect/urls.py" />
+ <change type="MODIFICATION" beforePath="$PROJECT_DIR$/TODO" afterPath="$PROJECT_DIR$/TODO" />
+ <change type="MODIFICATION" beforePath="$PROJECT_DIR$/fbconnect/tests.py" afterPath="$PROJECT_DIR$/fbconnect/tests.py" />
+ <change type="MODIFICATION" beforePath="$PROJECT_DIR$/forum/skins/default/templates/base.html" afterPath="$PROJECT_DIR$/forum/skins/default/templates/base.html" />
+ <change type="MODIFICATION" beforePath="$PROJECT_DIR$/forum/admin.py" afterPath="$PROJECT_DIR$/forum/admin.py" />
+ <change type="MODIFICATION" beforePath="$PROJECT_DIR$/forum/skins/default/templates/authopenid/signin.html" afterPath="$PROJECT_DIR$/forum/skins/default/templates/authopenid/signin.html" />
+ <change type="MODIFICATION" beforePath="$PROJECT_DIR$/fbconnect/views.py" afterPath="$PROJECT_DIR$/fbconnect/views.py" />
+ <change type="MODIFICATION" beforePath="$PROJECT_DIR$/sql_scripts/update_2010_01_23.sql" afterPath="$PROJECT_DIR$/sql_scripts/update_2010_01_23.sql" />
+ <change type="MODIFICATION" beforePath="$PROJECT_DIR$/forum/skins/default/templates/fbconnect/xd_receiver.html" afterPath="$PROJECT_DIR$/forum/skins/default/templates/fbconnect/xd_receiver.html" />
+ <change type="MODIFICATION" beforePath="$PROJECT_DIR$/forum/views/users.py" afterPath="$PROJECT_DIR$/forum/views/users.py" />
+ <change type="MODIFICATION" beforePath="$PROJECT_DIR$/forum/views/__init__.py" afterPath="$PROJECT_DIR$/forum/views/__init__.py" />
+ <change type="MODIFICATION" beforePath="$PROJECT_DIR$/fbconnect/models.py" afterPath="$PROJECT_DIR$/fbconnect/models.py" />
+ <change type="MODIFICATION" beforePath="$PROJECT_DIR$/fbconnect/pjson.py" afterPath="$PROJECT_DIR$/fbconnect/pjson.py" />
+ <change type="MODIFICATION" beforePath="$PROJECT_DIR$/forum/skins/default/templates/question.html" afterPath="$PROJECT_DIR$/forum/skins/default/templates/question.html" />
+ <change type="MODIFICATION" beforePath="$PROJECT_DIR$/rmpyc" afterPath="$PROJECT_DIR$/rmpyc" />
+ <change type="DELETED" beforePath="C:\osqadev\newmaster\osqa\forum\managers.py" afterPath="" />
+ <change type="MODIFICATION" beforePath="$PROJECT_DIR$/django_authopenid/urls.py" afterPath="$PROJECT_DIR$/django_authopenid/urls.py" />
+ <change type="MODIFICATION" beforePath="$PROJECT_DIR$/fbconnect/forms.py" afterPath="$PROJECT_DIR$/fbconnect/forms.py" />
+ <change type="MODIFICATION" beforePath="$PROJECT_DIR$/django_authopenid/views.py" afterPath="$PROJECT_DIR$/django_authopenid/views.py" />
+ <change type="MODIFICATION" beforePath="$PROJECT_DIR$/fbconnect/__init__.py" afterPath="$PROJECT_DIR$/fbconnect/__init__.py" />
+ <change type="MODIFICATION" beforePath="$PROJECT_DIR$/forum/skins/default/templates/questions.html" afterPath="$PROJECT_DIR$/forum/skins/default/templates/questions.html" />
+ <change type="MODIFICATION" beforePath="$PROJECT_DIR$/forum/urls.py" afterPath="$PROJECT_DIR$/forum/urls.py" />
+ </list>
+ <ignored path=".idea/workspace.xml" />
+ <ignored path="osqa.iws" />
+ <option name="TRACKING_ENABLED" value="true" />
+ <option name="SHOW_DIALOG" value="false" />
+ <option name="HIGHLIGHT_CONFLICTS" value="true" />
+ <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
+ <option name="LAST_RESOLUTION" value="IGNORE" />
+ </component>
+ <component name="ChangesViewManager" flattened_view="true" show_ignored="false" />
+ <component name="CreatePatchCommitExecutor">
+ <option name="PATCH_PATH" value="" />
+ <option name="REVERSE_PATCH" value="false" />
+ </component>
+ <component name="DaemonCodeAnalyzer">
+ <disable_hints />
+ </component>
+ <component name="DebuggerManager">
+ <breakpoint_any>
+ <breakpoint>
+ <option name="NOTIFY_CAUGHT" value="true" />
+ <option name="NOTIFY_UNCAUGHT" value="true" />
+ <option name="ENABLED" value="false" />
+ <option name="LOG_ENABLED" value="false" />
+ <option name="LOG_EXPRESSION_ENABLED" value="false" />
+ <option name="SUSPEND_POLICY" value="SuspendAll" />
+ <option name="COUNT_FILTER_ENABLED" value="false" />
+ <option name="COUNT_FILTER" value="0" />
+ <option name="CONDITION_ENABLED" value="false" />
+ <option name="CLASS_FILTERS_ENABLED" value="false" />
+ <option name="INSTANCE_FILTERS_ENABLED" value="false" />
+ <option name="CONDITION" value="" />
+ <option name="LOG_MESSAGE" value="" />
+ </breakpoint>
+ <breakpoint>
+ <option name="NOTIFY_CAUGHT" value="true" />
+ <option name="NOTIFY_UNCAUGHT" value="true" />
+ <option name="ENABLED" value="false" />
+ <option name="LOG_ENABLED" value="false" />
+ <option name="LOG_EXPRESSION_ENABLED" value="false" />
+ <option name="SUSPEND_POLICY" value="SuspendAll" />
+ <option name="COUNT_FILTER_ENABLED" value="false" />
+ <option name="COUNT_FILTER" value="0" />
+ <option name="CONDITION_ENABLED" value="false" />
+ <option name="CLASS_FILTERS_ENABLED" value="false" />
+ <option name="INSTANCE_FILTERS_ENABLED" value="false" />
+ <option name="CONDITION" value="" />
+ <option name="LOG_MESSAGE" value="" />
+ </breakpoint>
+ </breakpoint_any>
+ <breakpoint_rules />
+ <ui_properties />
+ </component>
+ <component name="FavoritesManager">
+ <favorites_list name="osqa" />
+ </component>
+ <component name="FileColors" enabled="true" enabledForTabs="true" />
+ <component name="FileEditorManager">
+ <leaf>
+ <file leaf-file-name="question.py" pinned="false" current="false" current-in-tab="false">
+ <entry file="file://$PROJECT_DIR$/forum/models/question.py">
+ <provider selected="true" editor-type-id="text-editor">
+ <state line="18" column="68" selection-start="710" selection-end="710" vertical-scroll-proportion="0.0">
+ <folding />
+ </state>
+ </provider>
+ </entry>
+ </file>
+ <file leaf-file-name="answer.py" pinned="false" current="false" current-in-tab="false">
+ <entry file="file://$PROJECT_DIR$/forum/models/answer.py">
+ <provider selected="true" editor-type-id="text-editor">
+ <state line="24" column="42" selection-start="746" selection-end="746" vertical-scroll-proportion="0.0">
+ <folding />
+ </state>
+ </provider>
+ </entry>
+ </file>
+ <file leaf-file-name="meta.py" pinned="false" current="false" current-in-tab="false">
+ <entry file="file://$PROJECT_DIR$/forum/models/meta.py">
+ <provider selected="true" editor-type-id="text-editor">
+ <state line="17" column="12" selection-start="496" selection-end="525" vertical-scroll-proportion="0.0">
+ <folding />
+ </state>
+ </provider>
+ </entry>
+ </file>
+ <file leaf-file-name="q_and_a.py" pinned="false" current="false" current-in-tab="false">
+ <entry file="file://$PROJECT_DIR$/forum/models/q_and_a.py">
+ <provider selected="true" editor-type-id="text-editor">
+ <state line="58" column="23" selection-start="2054" selection-end="2054" vertical-scroll-proportion="0.0">
+ <folding />
+ </state>
+ </provider>
+ </entry>
+ </file>
+ <file leaf-file-name="repute.py" pinned="false" current="true" current-in-tab="true">
+ <entry file="file://$PROJECT_DIR$/forum/models/repute.py">
+ <provider selected="true" editor-type-id="text-editor">
+ <state line="83" column="87" selection-start="3337" selection-end="3337" vertical-scroll-proportion="0.69195753">
+ <folding />
+ </state>
+ </provider>
+ </entry>
+ </file>
+ <file leaf-file-name="tag.py" pinned="false" current="false" current-in-tab="false">
+ <entry file="file://$PROJECT_DIR$/forum/models/tag.py">
+ <provider selected="true" editor-type-id="text-editor">
+ <state line="64" column="74" selection-start="2412" selection-end="2412" vertical-scroll-proportion="0.0">
+ <folding />
+ </state>
+ </provider>
+ </entry>
+ </file>
+ <file leaf-file-name="base.py" pinned="false" current="false" current-in-tab="false">
+ <entry file="file://$PROJECT_DIR$/forum/models/base.py">
+ <provider selected="true" editor-type-id="text-editor">
+ <state line="111" column="59" selection-start="4050" selection-end="4050" vertical-scroll-proportion="0.0">
+ <folding />
+ </state>
+ </provider>
+ </entry>
+ </file>
+ </leaf>
+ </component>
+ <component name="FindManager">
+ <FindUsagesManager>
+ <setting name="OPEN_NEW_TAB" value="false" />
+ </FindUsagesManager>
+ </component>
+ <component name="IdeDocumentHistory">
+ <option name="changedFiles">
+ <list>
+ <option value="$PROJECT_DIR$/settings.py" />
+ <option value="$PROJECT_DIR$/settings_local.py" />
+ <option value="$PROJECT_DIR$/forum/models/tag.py" />
+ <option value="$PROJECT_DIR$/forum/models/user.py" />
+ <option value="$PROJECT_DIR$/forum/models/__init__.py" />
+ <option value="$PROJECT_DIR$/forum/admin.py" />
+ <option value="$PROJECT_DIR$/forum/views/__init__.py" />
+ <option value="$PROJECT_DIR$/forum/urls.py" />
+ <option value="$PROJECT_DIR$/forum/views/users.py" />
+ <option value="$PROJECT_DIR$/forum/views/content.py" />
+ <option value="$PROJECT_DIR$/forum/models/base.py" />
+ <option value="$PROJECT_DIR$/forum/models/meta.py" />
+ <option value="$PROJECT_DIR$/forum/models/answer.py" />
+ <option value="$PROJECT_DIR$/forum/models/question.py" />
+ <option value="$PROJECT_DIR$/forum/models/q_and_a.py" />
+ <option value="$PROJECT_DIR$/forum/models/repute.py" />
+ </list>
+ </option>
+ </component>
+ <component name="ModuleEditorState">
+ <option name="LAST_EDITED_MODULE_NAME" />
+ <option name="LAST_EDITED_TAB_NAME" />
+ </component>
+ <component name="ProjectInspectionProfilesVisibleTreeState">
+ <entry key="Project Default">
+ <profile-state />
+ </entry>
+ </component>
+ <component name="ProjectLevelVcsManager">
+ <OptionsSetting value="true" id="Add" />
+ <OptionsSetting value="true" id="Remove" />
+ <OptionsSetting value="true" id="Checkout" />
+ <OptionsSetting value="true" id="Update" />
+ <OptionsSetting value="true" id="Status" />
+ <OptionsSetting value="true" id="Edit" />
+ <OptionsSetting value="true" id="Undo Check Out" />
+ <OptionsSetting value="true" id="Get Latest Version" />
+ <ConfirmationsSetting value="1" id="Add" />
+ <ConfirmationsSetting value="0" id="Remove" />
+ </component>
+ <component name="ProjectReloadState">
+ <option name="STATE" value="0" />
+ </component>
+ <component name="ProjectView">
+ <navigator currentView="ProjectPane" proportions="" version="1" splitterProportion="0.5">
+ <flattenPackages />
+ <showMembers />
+ <showModules />
+ <showLibraryContents />
+ <hideEmptyPackages />
+ <abbreviatePackageNames />
+ <autoscrollToSource />
+ <autoscrollFromSource />
+ <sortByType />
+ </navigator>
+ <panes>
+ <pane id="Scope" />
+ <pane id="ProjectPane">
+ <subPane>
+ <PATH>
+ <PATH_ELEMENT>
+ <option name="myItemId" value="osqa" />
+ <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.ProjectViewProjectNode" />
+ </PATH_ELEMENT>
+ </PATH>
+ <PATH>
+ <PATH_ELEMENT>
+ <option name="myItemId" value="osqa" />
+ <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.ProjectViewProjectNode" />
+ </PATH_ELEMENT>
+ <PATH_ELEMENT>
+ <option name="myItemId" value="osqa" />
+ <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
+ </PATH_ELEMENT>
+ </PATH>
+ <PATH>
+ <PATH_ELEMENT>
+ <option name="myItemId" value="osqa" />
+ <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.ProjectViewProjectNode" />
+ </PATH_ELEMENT>
+ <PATH_ELEMENT>
+ <option name="myItemId" value="osqa" />
+ <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
+ </PATH_ELEMENT>
+ <PATH_ELEMENT>
+ <option name="myItemId" value="tester" />
+ <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
+ </PATH_ELEMENT>
+ </PATH>
+ <PATH>
+ <PATH_ELEMENT>
+ <option name="myItemId" value="osqa" />
+ <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.ProjectViewProjectNode" />
+ </PATH_ELEMENT>
+ <PATH_ELEMENT>
+ <option name="myItemId" value="osqa" />
+ <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
+ </PATH_ELEMENT>
+ <PATH_ELEMENT>
+ <option name="myItemId" value="log" />
+ <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
+ </PATH_ELEMENT>
+ </PATH>
+ <PATH>
+ <PATH_ELEMENT>
+ <option name="myItemId" value="osqa" />
+ <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.ProjectViewProjectNode" />
+ </PATH_ELEMENT>
+ <PATH_ELEMENT>
+ <option name="myItemId" value="osqa" />
+ <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
+ </PATH_ELEMENT>
+ <PATH_ELEMENT>
+ <option name="myItemId" value="forum" />
+ <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
+ </PATH_ELEMENT>
+ </PATH>
+ <PATH>
+ <PATH_ELEMENT>
+ <option name="myItemId" value="osqa" />
+ <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.ProjectViewProjectNode" />
+ </PATH_ELEMENT>
+ <PATH_ELEMENT>
+ <option name="myItemId" value="osqa" />
+ <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
+ </PATH_ELEMENT>
+ <PATH_ELEMENT>
+ <option name="myItemId" value="forum" />
+ <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
+ </PATH_ELEMENT>
+ <PATH_ELEMENT>
+ <option name="myItemId" value="models" />
+ <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
+ </PATH_ELEMENT>
+ </PATH>
+ </subPane>
+ </pane>
+ <pane id="Favorites" />
+ <pane id="PackagesPane" />
+ </panes>
+ </component>
+ <component name="PropertiesComponent">
+ <property name="project.structure.last.edited" value="Libraries" />
+ <property name="GoToFile.includeJavaFiles" value="false" />
+ <property name="project.structure.proportion" value="0.0" />
+ <property name="options.splitter.main.proportions" value="0.3" />
+ <property name="MemberChooser.sorted" value="false" />
+ <property name="options.lastSelected" value="preferences.editor" />
+ <property name="project.structure.side.proportion" value="0.2" />
+ <property name="MemberChooser.copyJavadoc" value="false" />
+ <property name="GoToClass.toSaveIncludeLibraries" value="false" />
+ <property name="WebServerToolWindowFactoryState" value="false" />
+ <property name="MemberChooser.showClasses" value="true" />
+ <property name="GoToClass.includeLibraries" value="false" />
+ <property name="options.splitter.details.proportions" value="0.2" />
+ <property name="options.searchVisible" value="true" />
+ </component>
+ <component name="Regex">
+ <option name="pos1" value="218" />
+ <option name="pos2" value="218" />
+ <option name="pos3" value="162" />
+ <option name="pos4" value="444" />
+ <option name="pos5" value="162" />
+ <option name="autoUpdate" value="true" />
+ <option name="referenceOn" value="false" />
+ <option name="referencePos" value="0" />
+ <option name="showLabels" value="true" />
+ </component>
+ <component name="RunManager">
+ <configuration default="true" type="PythonUnitTestConfigurationType" factoryName="Python's unittest">
+ <option name="INTERPRETER_OPTIONS" value="" />
+ <option name="PARENT_ENVS" value="true" />
+ <option name="SDK_HOME" value="" />
+ <option name="WORKING_DIRECTORY" value="" />
+ <option name="IS_MODULE_SDK" value="false" />
+ <envs />
+ <module name="osqa" />
+ <option name="SCRIPT_NAME" value="" />
+ <option name="CLASS_NAME" value="" />
+ <option name="METHOD_NAME" value="" />
+ <option name="FOLDER_NAME" value="" />
+ <option name="TEST_TYPE" value="TEST_SCRIPT" />
+ <method>
+ <option name="AntTarget" enabled="false" />
+ <option name="BuildArtifacts" enabled="false" />
+ <option name="Make" enabled="true" />
+ <option name="Maven.BeforeRunTask" enabled="false" />
+ </method>
+ </configuration>
+ <configuration default="true" type="PythonConfigurationType" factoryName="Python">
+ <option name="INTERPRETER_OPTIONS" value="" />
+ <option name="PARENT_ENVS" value="true" />
+ <option name="SDK_HOME" value="" />
+ <option name="WORKING_DIRECTORY" value="" />
+ <option name="IS_MODULE_SDK" value="false" />
+ <envs>
+ <env name="PYTHONUNBUFFERED" value="1" />
+ </envs>
+ <module name="osqa" />
+ <option name="SCRIPT_NAME" value="" />
+ <option name="PARAMETERS" value="" />
+ <method>
+ <option name="AntTarget" enabled="false" />
+ <option name="BuildArtifacts" enabled="false" />
+ <option name="Make" enabled="true" />
+ <option name="Maven.BeforeRunTask" enabled="false" />
+ </method>
+ </configuration>
+ <configuration default="true" type="Remote" factoryName="Remote">
+ <option name="USE_SOCKET_TRANSPORT" value="true" />
+ <option name="SERVER_MODE" value="false" />
+ <option name="SHMEM_ADDRESS" value="javadebug" />
+ <option name="HOST" value="localhost" />
+ <option name="PORT" value="5005" />
+ <method>
+ <option name="AntTarget" enabled="false" />
+ <option name="BuildArtifacts" enabled="false" />
+ <option name="Maven.BeforeRunTask" enabled="false" />
+ </method>
+ </configuration>
+ <configuration default="true" type="py.test" factoryName="py.test">
+ <option name="INTERPRETER_OPTIONS" value="" />
+ <option name="PARENT_ENVS" value="true" />
+ <option name="SDK_HOME" value="" />
+ <option name="WORKING_DIRECTORY" value="" />
+ <option name="IS_MODULE_SDK" value="false" />
+ <envs />
+ <module name="osqa" />
+ <option name="testToRun" value="" />
+ <option name="keywords" value="" />
+ <method>
+ <option name="AntTarget" enabled="false" />
+ <option name="BuildArtifacts" enabled="false" />
+ <option name="Make" enabled="true" />
+ <option name="Maven.BeforeRunTask" enabled="false" />
+ </method>
+ </configuration>
+ <configuration default="true" type="Applet" factoryName="Applet">
+ <module name="" />
+ <option name="MAIN_CLASS_NAME" />
+ <option name="HTML_FILE_NAME" />
+ <option name="HTML_USED" value="false" />
+ <option name="WIDTH" value="400" />
+ <option name="HEIGHT" value="300" />
+ <option name="POLICY_FILE" value="$APPLICATION_HOME_DIR$/bin/appletviewer.policy" />
+ <option name="VM_PARAMETERS" />
+ <option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
+ <option name="ALTERNATIVE_JRE_PATH" />
+ <method>
+ <option name="AntTarget" enabled="false" />
+ <option name="BuildArtifacts" enabled="false" />
+ <option name="Make" enabled="true" />
+ <option name="Maven.BeforeRunTask" enabled="false" />
+ </method>
+ </configuration>
+ <configuration default="true" type="Application" factoryName="Application">
+ <extension name="coverage" enabled="false" merge="false" />
+ <extension name="snapshooter" />
+ <option name="MAIN_CLASS_NAME" />
+ <option name="VM_PARAMETERS" />
+ <option name="PROGRAM_PARAMETERS" />
+ <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
+ <option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
+ <option name="ALTERNATIVE_JRE_PATH" />
+ <option name="ENABLE_SWING_INSPECTOR" value="false" />
+ <option name="ENV_VARIABLES" />
+ <option name="PASS_PARENT_ENVS" value="true" />
+ <module name="" />
+ <envs />
+ <method>
+ <option name="AntTarget" enabled="false" />
+ <option name="BuildArtifacts" enabled="false" />
+ <option name="Make" enabled="true" />
+ <option name="Maven.BeforeRunTask" enabled="false" />
+ </method>
+ </configuration>
+ <configuration default="true" type="JUnit" factoryName="JUnit">
+ <extension name="coverage" enabled="false" merge="false" />
+ <extension name="snapshooter" />
+ <module name="" />
+ <option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
+ <option name="ALTERNATIVE_JRE_PATH" />
+ <option name="PACKAGE_NAME" />
+ <option name="MAIN_CLASS_NAME" />
+ <option name="METHOD_NAME" />
+ <option name="TEST_OBJECT" value="class" />
+ <option name="VM_PARAMETERS" />
+ <option name="PARAMETERS" />
+ <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
+ <option name="ENV_VARIABLES" />
+ <option name="PASS_PARENT_ENVS" value="true" />
+ <option name="TEST_SEARCH_SCOPE">
+ <value defaultName="moduleWithDependencies" />
+ </option>
+ <envs />
+ <method>
+ <option name="AntTarget" enabled="false" />
+ <option name="BuildArtifacts" enabled="false" />
+ <option name="Make" enabled="true" />
+ <option name="Maven.BeforeRunTask" enabled="false" />
+ </method>
+ </configuration>
+ <list size="0" />
+ <configuration name="&lt;template&gt;" type="WebApp" default="true" selected="false">
+ <Host>localhost</Host>
+ <Port>5050</Port>
+ </configuration>
+ </component>
+ <component name="ShelveChangesManager" show_recycled="false" />
+ <component name="StarteamConfiguration">
+ <option name="SERVER" value="" />
+ <option name="PORT" value="49201" />
+ <option name="USER" value="" />
+ <option name="PASSWORD" value="" />
+ <option name="PROJECT" value="" />
+ <option name="VIEW" value="" />
+ <option name="ALTERNATIVE_WORKING_PATH" value="" />
+ <option name="LOCK_ON_CHECKOUT" value="false" />
+ <option name="UNLOCK_ON_CHECKIN" value="false" />
+ </component>
+ <component name="StructureViewFactory">
+ <option name="ACTIVE_ACTIONS" value="ALPHA_COMPARATOR" />
+ </component>
+ <component name="SvnConfiguration">
+ <option name="USER" value="" />
+ <option name="PASSWORD" value="" />
+ <option name="LAST_MERGED_REVISION" />
+ <option name="UPDATE_RUN_STATUS" value="false" />
+ <option name="MERGE_DRY_RUN" value="false" />
+ <option name="MERGE_DIFF_USE_ANCESTRY" value="true" />
+ <option name="UPDATE_LOCK_ON_DEMAND" value="false" />
+ <option name="IGNORE_SPACES_IN_MERGE" value="false" />
+ <option name="DETECT_NESTED_COPIES" value="false" />
+ <option name="IGNORE_SPACES_IN_ANNOTATE" value="true" />
+ <option name="SHOW_MERGE_SOURCES_IN_ANNOTATE" value="true" />
+ <configuration useDefault="true">F:\Users\COOL\AppData\Roaming\Subversion</configuration>
+ <myIsUseDefaultProxy>false</myIsUseDefaultProxy>
+ <supportedVersion>125</supportedVersion>
+ </component>
+ <component name="TaskManager">
+ <task active="true" id="Default" summary="Default task">
+ <changelist id="378a32c6-5795-4404-9430-96837181eba2" name="Default" comment="" />
+ <created>1266231540266</created>
+ <updated>1266231540266</updated>
+ </task>
+ <servers />
+ </component>
+ <component name="ToolWindowManager">
+ <frame x="1277" y="-3" width="1286" height="806" extended-state="6" />
+ <editor active="false" />
+ <layout>
+ <window_info id="Archetypes" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="2" side_tool="false" content_ui="tabs" />
+ <window_info id="Changes" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="7" side_tool="false" content_ui="tabs" />
+ <window_info id="Palette" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="3" side_tool="false" content_ui="tabs" />
+ <window_info id="Find" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.329927" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" />
+ <window_info id="Ant Build" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.25" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" />
+ <window_info id="Debug" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.4" sideWeight="0.5" order="3" side_tool="false" content_ui="tabs" />
+ <window_info id="IDEtalk Messages" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="11" side_tool="false" content_ui="tabs" />
+ <window_info id="IDEtalk" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="5" side_tool="false" content_ui="tabs" />
+ <window_info id="Version Control" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="8" side_tool="false" content_ui="tabs" />
+ <window_info id="TODO" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="6" side_tool="false" content_ui="tabs" />
+ <window_info id="Structure" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.24959481" sideWeight="0.7080182" order="1" side_tool="true" content_ui="tabs" />
+ <window_info id="Maven Projects" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="4" side_tool="false" content_ui="tabs" />
+ <window_info id="Commander" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.4" sideWeight="0.5" order="0" side_tool="false" content_ui="tabs" />
+ <window_info id="Dependency Viewer" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="9" side_tool="false" content_ui="tabs" />
+ <window_info id="Project" active="true" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="true" weight="0.24939074" sideWeight="0.659854" order="0" side_tool="false" content_ui="tabs" />
+ <window_info id="Run" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="2" side_tool="false" content_ui="tabs" />
+ <window_info id="Regex" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="10" side_tool="false" content_ui="tabs" />
+ <window_info id="Cvs" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.25" sideWeight="0.5" order="4" side_tool="false" content_ui="tabs" />
+ <window_info id="Message" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="0" side_tool="false" content_ui="tabs" />
+ <window_info id="Hierarchy" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.25" sideWeight="0.5" order="2" side_tool="false" content_ui="combo" />
+ <window_info id="Inspection" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.4" sideWeight="0.5" order="5" side_tool="false" content_ui="tabs" />
+ </layout>
+ </component>
+ <component name="VcsManagerConfiguration">
+ <option name="OFFER_MOVE_TO_ANOTHER_CHANGELIST_ON_PARTIAL_COMMIT" value="true" />
+ <option name="CHECK_CODE_SMELLS_BEFORE_PROJECT_COMMIT" value="true" />
+ <option name="PERFORM_UPDATE_IN_BACKGROUND" value="true" />
+ <option name="PERFORM_COMMIT_IN_BACKGROUND" value="true" />
+ <option name="PERFORM_EDIT_IN_BACKGROUND" value="true" />
+ <option name="PERFORM_CHECKOUT_IN_BACKGROUND" value="true" />
+ <option name="PERFORM_ADD_REMOVE_IN_BACKGROUND" value="true" />
+ <option name="PERFORM_ROLLBACK_IN_BACKGROUND" value="false" />
+ <option name="CHECK_LOCALLY_CHANGED_CONFLICTS_IN_BACKGROUND" value="true" />
+ <option name="FORCE_NON_EMPTY_COMMENT" value="false" />
+ <option name="LAST_COMMIT_MESSAGE" />
+ <option name="MAKE_NEW_CHANGELIST_ACTIVE" value="true" />
+ <option name="OPTIMIZE_IMPORTS_BEFORE_PROJECT_COMMIT" value="false" />
+ <option name="CHECK_FILES_UP_TO_DATE_BEFORE_COMMIT" value="false" />
+ <option name="REFORMAT_BEFORE_PROJECT_COMMIT" value="false" />
+ <option name="REFORMAT_BEFORE_FILE_COMMIT" value="false" />
+ <option name="FILE_HISTORY_DIALOG_COMMENTS_SPLITTER_PROPORTION" value="0.8" />
+ <option name="FILE_HISTORY_DIALOG_SPLITTER_PROPORTION" value="0.5" />
+ <option name="ACTIVE_VCS_NAME" />
+ <option name="UPDATE_GROUP_BY_PACKAGES" value="false" />
+ <option name="UPDATE_GROUP_BY_CHANGELIST" value="false" />
+ <option name="SHOW_FILE_HISTORY_AS_TREE" value="false" />
+ <option name="FILE_HISTORY_SPLITTER_PROPORTION" value="0.6" />
+ </component>
+ <component name="XDebuggerManager">
+ <breakpoint-manager />
+ </component>
+ <component name="editorHistoryManager">
+ <entry file="file://$PROJECT_DIR$/forum/admin.py">
+ <provider selected="true" editor-type-id="text-editor">
+ <state line="73" column="1" selection-start="1937" selection-end="1937" vertical-scroll-proportion="0.0" />
+ </provider>
+ </entry>
+ <entry file="file://$PROJECT_DIR$/forum/views/__init__.py">
+ <provider selected="true" editor-type-id="text-editor">
+ <state line="3" column="1" selection-start="41" selection-end="41" vertical-scroll-proportion="0.0" />
+ </provider>
+ </entry>
+ <entry file="file://$PROJECT_DIR$/forum/urls.py">
+ <provider selected="true" editor-type-id="text-editor">
+ <state line="86" column="5" selection-start="5472" selection-end="5472" vertical-scroll-proportion="0.0" />
+ </provider>
+ </entry>
+ <entry file="file://$PROJECT_DIR$/forum/models/__init__.py">
+ <provider selected="true" editor-type-id="text-editor">
+ <state line="17" column="11" selection-start="491" selection-end="491" vertical-scroll-proportion="0.44875777" />
+ </provider>
+ </entry>
+ <entry file="file://$PROJECT_DIR$/log/django.osqa.log">
+ <provider selected="true" editor-type-id="text-editor">
+ <state line="196" column="47" selection-start="27912" selection-end="27913" vertical-scroll-proportion="0.42326334" />
+ </provider>
+ </entry>
+ <entry file="file://$PROJECT_DIR$/forum/views/users.py">
+ <provider selected="true" editor-type-id="text-editor">
+ <state line="278" column="28" selection-start="11173" selection-end="11173" vertical-scroll-proportion="0.0" />
+ </provider>
+ </entry>
+ <entry file="file://$PROJECT_DIR$/forum/skins/default/templates/index.html">
+ <provider selected="true" editor-type-id="text-editor">
+ <state line="156" column="15" selection-start="7611" selection-end="7611" vertical-scroll-proportion="-10.755102">
+ <folding />
+ </state>
+ </provider>
+ </entry>
+ <entry file="file://$PROJECT_DIR$/forum/models/user.py">
+ <provider selected="true" editor-type-id="text-editor">
+ <state line="2" column="0" selection-start="20" selection-end="70" vertical-scroll-proportion="0.0" />
+ </provider>
+ </entry>
+ <entry file="file://$PROJECT_DIR$/forum/views/content.py">
+ <provider selected="true" editor-type-id="text-editor">
+ <state line="1042" column="23" selection-start="43546" selection-end="43546" vertical-scroll-proportion="0.0">
+ <folding />
+ </state>
+ </provider>
+ </entry>
+ <entry file="file://$PROJECT_DIR$/forum/models/tag.py">
+ <provider selected="true" editor-type-id="text-editor">
+ <state line="64" column="74" selection-start="2412" selection-end="2412" vertical-scroll-proportion="0.0">
+ <folding />
+ </state>
+ </provider>
+ </entry>
+ <entry file="file://$PROJECT_DIR$/forum/models/base.py">
+ <provider selected="true" editor-type-id="text-editor">
+ <state line="111" column="59" selection-start="4050" selection-end="4050" vertical-scroll-proportion="0.0" />
+ </provider>
+ </entry>
+ <entry file="file://$PROJECT_DIR$/forum/models/question.py">
+ <provider selected="true" editor-type-id="text-editor">
+ <state line="18" column="68" selection-start="710" selection-end="710" vertical-scroll-proportion="0.0">
+ <folding />
+ </state>
+ </provider>
+ </entry>
+ <entry file="file://$PROJECT_DIR$/forum/models/meta.py">
+ <provider selected="true" editor-type-id="text-editor">
+ <state line="17" column="12" selection-start="496" selection-end="525" vertical-scroll-proportion="0.0">
+ <folding />
+ </state>
+ </provider>
+ </entry>
+ <entry file="file://$PROJECT_DIR$/forum/models/answer.py">
+ <provider selected="true" editor-type-id="text-editor">
+ <state line="24" column="42" selection-start="746" selection-end="746" vertical-scroll-proportion="0.0">
+ <folding />
+ </state>
+ </provider>
+ </entry>
+ <entry file="file://$PROJECT_DIR$/forum/models/q_and_a.py">
+ <provider selected="true" editor-type-id="text-editor">
+ <state line="58" column="23" selection-start="2054" selection-end="2054" vertical-scroll-proportion="0.0">
+ <folding />
+ </state>
+ </provider>
+ </entry>
+ <entry file="file://$PROJECT_DIR$/forum/models/repute.py">
+ <provider selected="true" editor-type-id="text-editor">
+ <state line="83" column="87" selection-start="3337" selection-end="3337" vertical-scroll-proportion="0.69195753">
+ <folding />
+ </state>
+ </provider>
+ </entry>
+ </component>
+</project>
+
diff --git a/forum/admin.py b/forum/admin.py
index 482da048..88643b92 100644
--- a/forum/admin.py
+++ b/forum/admin.py
@@ -46,14 +46,14 @@ class ReputeAdmin(admin.ModelAdmin):
class ActivityAdmin(admin.ModelAdmin):
""" admin class"""
-class BookAdmin(admin.ModelAdmin):
- """ admin class"""
+#class BookAdmin(admin.ModelAdmin):
+# """ admin class"""
-class BookAuthorInfoAdmin(admin.ModelAdmin):
- """ admin class"""
+#class BookAuthorInfoAdmin(admin.ModelAdmin):
+# """ admin class"""
-class BookAuthorRssAdmin(admin.ModelAdmin):
- """ admin class"""
+#class BookAuthorRssAdmin(admin.ModelAdmin):
+# """ admin class"""
admin.site.register(Question, QuestionAdmin)
@@ -69,6 +69,6 @@ admin.site.register(Badge, BadgeAdmin)
admin.site.register(Award, AwardAdmin)
admin.site.register(Repute, ReputeAdmin)
admin.site.register(Activity, ActivityAdmin)
-admin.site.register(Book, BookAdmin)
-admin.site.register(BookAuthorInfo, BookAuthorInfoAdmin)
-admin.site.register(BookAuthorRss, BookAuthorRssAdmin)
+#admin.site.register(Book, BookAdmin)
+#admin.site.register(BookAuthorInfo, BookAuthorInfoAdmin)
+#admin.site.register(BookAuthorRss, BookAuthorRssAdmin)
diff --git a/forum/forms.py b/forum/forms.py
index 308f853b..5796e2c1 100644
--- a/forum/forms.py
+++ b/forum/forms.py
@@ -4,6 +4,7 @@ from django import forms
from models import *
from const import *
from django.utils.translation import ugettext as _
+from django.contrib.auth.models import User
from utils.forms import NextUrlField, UserNameField
from recaptcha_django import ReCaptchaField
from django.conf import settings
diff --git a/forum/managers.py b/forum/managers.py
deleted file mode 100644
index 3f580bd3..00000000
--- a/forum/managers.py
+++ /dev/null
@@ -1,241 +0,0 @@
-import datetime
-import time
-import logging
-from django.contrib.auth.models import User, UserManager
-from django.db import connection, models, transaction
-from django.db.models import Q
-from forum.models import *
-from urllib import quote, unquote
-
-class QuestionManager(models.Manager):
-
- def update_tags(self, question, tagnames, user):
- """
- Updates Tag associations for a question to match the given
- tagname string.
-
- Returns ``True`` if tag usage counts were updated as a result,
- ``False`` otherwise.
- """
- from forum.models import Tag
- current_tags = list(question.tags.all())
- current_tagnames = set(t.name for t in current_tags)
- updated_tagnames = set(t for t in tagnames.split(' ') if t)
- modified_tags = []
-
- removed_tags = [t for t in current_tags
- if t.name not in updated_tagnames]
- if removed_tags:
- modified_tags.extend(removed_tags)
- question.tags.remove(*removed_tags)
-
- added_tagnames = updated_tagnames - current_tagnames
- if added_tagnames:
- added_tags = Tag.objects.get_or_create_multiple(added_tagnames,
- user)
- modified_tags.extend(added_tags)
- question.tags.add(*added_tags)
-
- if modified_tags:
- Tag.objects.update_use_counts(modified_tags)
- return True
-
- return False
-
- def update_answer_count(self, question):
- """
- Executes an UPDATE query to update denormalised data with the
- number of answers the given question has.
- """
-
- # for some reasons, this Answer class failed to be imported,
- # although we have imported all classes from models on top.
- from forum.models import Answer
- self.filter(id=question.id).update(
- answer_count=Answer.objects.get_answers_from_question(question).filter(deleted=False).count())
-
- def update_view_count(self, question):
- """
- update counter+1 when user browse question page
- """
- self.filter(id=question.id).update(view_count = question.view_count + 1)
-
- def update_favorite_count(self, question):
- """
- update favourite_count for given question
- """
- from forum.models import FavoriteQuestion
- self.filter(id=question.id).update(favourite_count = FavoriteQuestion.objects.filter(question=question).count())
-
- def get_similar_questions(self, question):
- """
- Get 10 similar questions for given one.
- This will search the same tag list for give question(by exactly same string) first.
- Questions with the individual tags will be added to list if above questions are not full.
- """
- #print datetime.datetime.now()
- questions = list(self.filter(tagnames = question.tagnames, deleted=False).all())
-
- tags_list = question.tags.all()
- for tag in tags_list:
- extend_questions = self.filter(tags__id = tag.id, deleted=False)[:50]
- for item in extend_questions:
- if item not in questions and len(questions) < 10:
- questions.append(item)
-
- #print datetime.datetime.now()
- return questions
-
-class TagManager(models.Manager):
- UPDATE_USED_COUNTS_QUERY = (
- 'UPDATE tag '
- 'SET used_count = ('
- 'SELECT COUNT(*) FROM question_tags '
- 'INNER JOIN question ON question_id=question.id '
- 'WHERE tag_id = tag.id AND question.deleted=False'
- ') '
- 'WHERE id IN (%s)')
-
- def get_valid_tags(self, page_size):
- from forum.models import Tag
- tags = Tag.objects.all().filter(deleted=False).exclude(used_count=0).order_by("-id")[:page_size]
- return tags
-
- def get_or_create_multiple(self, names, user):
- """
- Fetches a list of Tags with the given names, creating any Tags
- which don't exist when necesssary.
- """
- tags = list(self.filter(name__in=names))
- #Set all these tag visible
- for tag in tags:
- if tag.deleted:
- tag.deleted = False
- tag.deleted_by = None
- tag.deleted_at = None
- tag.save()
-
- if len(tags) < len(names):
- existing_names = set(tag.name for tag in tags)
- new_names = [name for name in names if name not in existing_names]
- tags.extend([self.create(name=name, created_by=user)
- for name in new_names if self.filter(name=name).count() == 0 and len(name.strip()) > 0])
-
- return tags
-
- def update_use_counts(self, tags):
- """Updates the given Tags with their current use counts."""
- if not tags:
- return
- cursor = connection.cursor()
- query = self.UPDATE_USED_COUNTS_QUERY % ','.join(['%s'] * len(tags))
- cursor.execute(query, [tag.id for tag in tags])
- transaction.commit_unless_managed()
-
- def get_tags_by_questions(self, questions):
- question_ids = []
- for question in questions:
- question_ids.append(question.id)
-
- question_ids_str = ','.join([str(id) for id in question_ids])
- related_tags = self.extra(
- tables=['tag', 'question_tags'],
- where=["tag.id = question_tags.tag_id AND question_tags.question_id IN (" + question_ids_str + ")"]
- ).distinct()
-
- return related_tags
-
-class AnswerManager(models.Manager):
- GET_ANSWERS_FROM_USER_QUESTIONS = u'SELECT answer.* FROM answer INNER JOIN question ON answer.question_id = question.id WHERE question.author_id =%s AND answer.author_id <> %s'
- def get_answers_from_question(self, question, user=None):
- """
- Retrieves visibile answers for the given question. Delete answers
- are only visibile to the person who deleted them.
- """
-
- if user is None or not user.is_authenticated():
- return self.filter(question=question, deleted=False)
- else:
- return self.filter(Q(question=question),
- Q(deleted=False) | Q(deleted_by=user))
-
- def get_answers_from_questions(self, user_id):
- """
- Retrieves visibile answers for the given question. Which are not included own answers
- """
- cursor = connection.cursor()
- cursor.execute(self.GET_ANSWERS_FROM_USER_QUESTIONS, [user_id, user_id])
- return cursor.fetchall()
-
-class VoteManager(models.Manager):
- COUNT_UP_VOTE_BY_USER = "SELECT count(*) FROM vote WHERE user_id = %s AND vote = 1"
- COUNT_DOWN_VOTE_BY_USER = "SELECT count(*) FROM vote WHERE user_id = %s AND vote = -1"
- COUNT_VOTES_PER_DAY_BY_USER = "SELECT COUNT(*) FROM vote WHERE user_id = %s AND DATE(voted_at) = %s"
- def get_up_vote_count_from_user(self, user):
- if user is not None:
- cursor = connection.cursor()
- cursor.execute(self.COUNT_UP_VOTE_BY_USER, [user.id])
- row = cursor.fetchone()
- return row[0]
- else:
- return 0
-
- def get_down_vote_count_from_user(self, user):
- if user is not None:
- cursor = connection.cursor()
- cursor.execute(self.COUNT_DOWN_VOTE_BY_USER, [user.id])
- row = cursor.fetchone()
- return row[0]
- else:
- return 0
-
- def get_votes_count_today_from_user(self, user):
- if user is not None:
- cursor = connection.cursor()
- cursor.execute(self.COUNT_VOTES_PER_DAY_BY_USER, [user.id, time.strftime("%Y-%m-%d", datetime.datetime.now().timetuple())])
- row = cursor.fetchone()
- return row[0]
-
- else:
- return 0
-
-class FlaggedItemManager(models.Manager):
- COUNT_FLAGS_PER_DAY_BY_USER = "SELECT COUNT(*) FROM flagged_item WHERE user_id = %s AND DATE(flagged_at) = %s"
- def get_flagged_items_count_today(self, user):
- if user is not None:
- cursor = connection.cursor()
- cursor.execute(self.COUNT_FLAGS_PER_DAY_BY_USER, [user.id, time.strftime("%Y-%m-%d", datetime.datetime.now().timetuple())])
- row = cursor.fetchone()
- return row[0]
-
- else:
- return 0
-
-class ReputeManager(models.Manager):
- COUNT_REPUTATION_PER_DAY_BY_USER = "SELECT SUM(positive)+SUM(negative) FROM repute WHERE user_id = %s AND (reputation_type=1 OR reputation_type=-8) AND DATE(reputed_at) = %s"
- def get_reputation_by_upvoted_today(self, user):
- """
- For one user in one day, he can only earn rep till certain score (ep. +200)
- by upvoted(also substracted from upvoted canceled). This is because we need
- to prohibit gaming system by upvoting/cancel again and again.
- """
- if user is not None:
- cursor = connection.cursor()
- cursor.execute(self.COUNT_REPUTATION_PER_DAY_BY_USER, [user.id, time.strftime("%Y-%m-%d", datetime.datetime.now().timetuple())])
- row = cursor.fetchone()
- return row[0]
-
- else:
- return 0
-class AwardManager(models.Manager):
- def get_recent_awards(self):
- awards = super(AwardManager, self).extra(
- select={'badge_id': 'badge.id', 'badge_name':'badge.name',
- 'badge_description': 'badge.description', 'badge_type': 'badge.type',
- 'user_id': 'auth_user.id', 'user_name': 'auth_user.username'
- },
- tables=['award', 'badge', 'auth_user'],
- order_by=['-awarded_at'],
- where=['auth_user.id=award.user_id AND badge_id=badge.id'],
- ).values('badge_id', 'badge_name', 'badge_description', 'badge_type', 'user_id', 'user_name')
- return awards
diff --git a/forum/models.py b/forum/models.py
deleted file mode 100644
index 2da8ef7b..00000000
--- a/forum/models.py
+++ /dev/null
@@ -1,955 +0,0 @@
-# encoding:utf-8
-import datetime
-import hashlib
-from urllib import quote_plus, urlencode
-from django.db import models, IntegrityError
-from django.utils.http import urlquote as django_urlquote
-from django.utils.html import strip_tags
-from django.core.urlresolvers import reverse
-from django.contrib.auth.models import User
-from django.contrib.contenttypes import generic
-from django.contrib.contenttypes.models import ContentType
-from django.template.defaultfilters import slugify
-from django.db.models.signals import post_delete, post_save, pre_save
-from django.utils.translation import ugettext as _
-from django.utils.safestring import mark_safe
-from django.contrib.sitemaps import ping_google
-import django.dispatch
-from django.conf import settings
-import logging
-
-if settings.USE_SPHINX_SEARCH == True:
- from djangosphinx.models import SphinxSearch
-
-from forum.managers import *
-from forum.const import *
-
-def get_object_comments(self):
- comments = self.comments.all().order_by('id')
- return comments
-
-def post_get_last_update_info(self):
- when = self.added_at
- who = self.author
- if self.last_edited_at and self.last_edited_at > when:
- when = self.last_edited_at
- who = self.last_edited_by
- comments = self.comments.all()
- if len(comments) > 0:
- for c in comments:
- if c.added_at > when:
- when = c.added_at
- who = c.user
- return when, who
-
-class EmailFeedSetting(models.Model):
- DELTA_TABLE = {
- 'w':datetime.timedelta(7),
- 'd':datetime.timedelta(1),
- 'n':datetime.timedelta(-1),
- }
- FEED_TYPES = (
- ('q_all',_('Entire forum')),
- ('q_ask',_('Questions that I asked')),
- ('q_ans',_('Questions that I answered')),
- ('q_sel',_('Individually selected questions')),
- )
- UPDATE_FREQUENCY = (
- ('w',_('Weekly')),
- ('d',_('Daily')),
- ('n',_('No email')),
- )
- subscriber = models.ForeignKey(User)
- feed_type = models.CharField(max_length=16,choices=FEED_TYPES)
- frequency = models.CharField(max_length=8,choices=UPDATE_FREQUENCY,default='n')
- added_at = models.DateTimeField(auto_now_add=True)
- reported_at = models.DateTimeField(null=True)
-
- def save(self,*args,**kwargs):
- type = self.feed_type
- subscriber = self.subscriber
- similar = self.__class__.objects.filter(feed_type=type,subscriber=subscriber).exclude(pk=self.id)
- if len(similar) > 0:
- raise IntegrityError('email feed setting already exists')
- super(EmailFeedSetting,self).save(*args,**kwargs)
-
-class Tag(models.Model):
- name = models.CharField(max_length=255, unique=True)
- created_by = models.ForeignKey(User, related_name='created_tags')
- deleted = models.BooleanField(default=False)
- deleted_at = models.DateTimeField(null=True, blank=True)
- deleted_by = models.ForeignKey(User, null=True, blank=True, related_name='deleted_tags')
- # Denormalised data
- used_count = models.PositiveIntegerField(default=0)
-
- objects = TagManager()
-
- class Meta:
- db_table = u'tag'
- ordering = ('-used_count', 'name')
-
- def __unicode__(self):
- return self.name
-
-class Comment(models.Model):
- content_type = models.ForeignKey(ContentType)
- object_id = models.PositiveIntegerField()
- content_object = generic.GenericForeignKey('content_type', 'object_id')
- user = models.ForeignKey(User, related_name='comments')
- comment = models.CharField(max_length=300)
- added_at = models.DateTimeField(default=datetime.datetime.now)
-
- class Meta:
- ordering = ('-added_at',)
- db_table = u'comment'
-
- def save(self,**kwargs):
- super(Comment,self).save(**kwargs)
- try:
- ping_google()
- except Exception:
- logging.debug('problem pinging google did you register you sitemap with google?')
-
- def __unicode__(self):
- return self.comment
-
-class Vote(models.Model):
- VOTE_UP = +1
- VOTE_DOWN = -1
- VOTE_CHOICES = (
- (VOTE_UP, u'Up'),
- (VOTE_DOWN, u'Down'),
- )
-
- content_type = models.ForeignKey(ContentType)
- object_id = models.PositiveIntegerField()
- content_object = generic.GenericForeignKey('content_type', 'object_id')
- user = models.ForeignKey(User, related_name='votes')
- vote = models.SmallIntegerField(choices=VOTE_CHOICES)
- voted_at = models.DateTimeField(default=datetime.datetime.now)
-
- objects = VoteManager()
-
- class Meta:
- unique_together = ('content_type', 'object_id', 'user')
- db_table = u'vote'
- def __unicode__(self):
- return '[%s] voted at %s: %s' %(self.user, self.voted_at, self.vote)
-
- def is_upvote(self):
- return self.vote == self.VOTE_UP
-
- def is_downvote(self):
- return self.vote == self.VOTE_DOWN
-
-class FlaggedItem(models.Model):
- """A flag on a Question or Answer indicating offensive content."""
- content_type = models.ForeignKey(ContentType)
- object_id = models.PositiveIntegerField()
- content_object = generic.GenericForeignKey('content_type', 'object_id')
- user = models.ForeignKey(User, related_name='flagged_items')
- flagged_at = models.DateTimeField(default=datetime.datetime.now)
-
- objects = FlaggedItemManager()
-
- class Meta:
- unique_together = ('content_type', 'object_id', 'user')
- db_table = u'flagged_item'
- def __unicode__(self):
- return '[%s] flagged at %s' %(self.user, self.flagged_at)
-
-class Question(models.Model):
- title = models.CharField(max_length=300)
- author = models.ForeignKey(User, related_name='questions')
- added_at = models.DateTimeField(default=datetime.datetime.now)
- tags = models.ManyToManyField(Tag, related_name='questions')
- # Status
- wiki = models.BooleanField(default=False)
- wikified_at = models.DateTimeField(null=True, blank=True)
- answer_accepted = models.BooleanField(default=False)
- closed = models.BooleanField(default=False)
- closed_by = models.ForeignKey(User, null=True, blank=True, related_name='closed_questions')
- closed_at = models.DateTimeField(null=True, blank=True)
- close_reason = models.SmallIntegerField(choices=CLOSE_REASONS, null=True, blank=True)
- deleted = models.BooleanField(default=False)
- deleted_at = models.DateTimeField(null=True, blank=True)
- deleted_by = models.ForeignKey(User, null=True, blank=True, related_name='deleted_questions')
- locked = models.BooleanField(default=False)
- locked_by = models.ForeignKey(User, null=True, blank=True, related_name='locked_questions')
- locked_at = models.DateTimeField(null=True, blank=True)
- followed_by = models.ManyToManyField(User, related_name='followed_questions')
- # Denormalised data
- score = models.IntegerField(default=0)
- vote_up_count = models.IntegerField(default=0)
- vote_down_count = models.IntegerField(default=0)
- answer_count = models.PositiveIntegerField(default=0)
- comment_count = models.PositiveIntegerField(default=0)
- view_count = models.PositiveIntegerField(default=0)
- offensive_flag_count = models.SmallIntegerField(default=0)
- favourite_count = models.PositiveIntegerField(default=0)
- last_edited_at = models.DateTimeField(null=True, blank=True)
- last_edited_by = models.ForeignKey(User, null=True, blank=True, related_name='last_edited_questions')
- last_activity_at = models.DateTimeField(default=datetime.datetime.now)
- last_activity_by = models.ForeignKey(User, related_name='last_active_in_questions')
- tagnames = models.CharField(max_length=125)
- summary = models.CharField(max_length=180)
- html = models.TextField()
- comments = generic.GenericRelation(Comment)
- votes = generic.GenericRelation(Vote)
- flagged_items = generic.GenericRelation(FlaggedItem)
-
- if settings.USE_SPHINX_SEARCH == True:
- search = SphinxSearch(
- index=' '.join(settings.SPHINX_SEARCH_INDICES),
- mode='SPH_MATCH_ALL',
- )
- logging.debug('have sphinx search')
-
- objects = QuestionManager()
-
- def delete(self):
- super(Question, self).delete()
- try:
- ping_google()
- except Exception:
- logging.debug('problem pinging google did you register you sitemap with google?')
-
- def save(self, **kwargs):
- """
- Overridden to manually manage addition of tags when the object
- is first saved.
-
- This is required as we're using ``tagnames`` as the sole means of
- adding and editing tags.
- """
- initial_addition = (self.id is None)
- super(Question, self).save(**kwargs)
- try:
- ping_google()
- except Exception:
- logging.debug('problem pinging google did you register you sitemap with google?')
- if initial_addition:
- tags = Tag.objects.get_or_create_multiple(self.tagname_list(),
- self.author)
- self.tags.add(*tags)
- Tag.objects.update_use_counts(tags)
-
- def tagname_list(self):
- """Creates a list of Tag names from the ``tagnames`` attribute."""
- return [name for name in self.tagnames.split(u' ')]
-
- def tagname_meta_generator(self):
- return u','.join([unicode(tag) for tag in self.tagname_list()])
-
- def get_absolute_url(self):
- return '%s%s' % (reverse('question', args=[self.id]), django_urlquote(slugify(self.title)))
-
- def has_favorite_by_user(self, user):
- if not user.is_authenticated():
- return False
- return FavoriteQuestion.objects.filter(question=self, user=user).count() > 0
-
- def get_answer_count_by_user(self, user_id):
- query_set = Answer.objects.filter(author__id=user_id)
- return query_set.filter(question=self).count()
-
- def get_question_title(self):
- if self.closed:
- attr = CONST['closed']
- elif self.deleted:
- attr = CONST['deleted']
- else:
- attr = None
- if attr is not None:
- return u'%s %s' % (self.title, attr)
- else:
- return self.title
-
- def get_revision_url(self):
- return reverse('question_revisions', args=[self.id])
-
- def get_latest_revision(self):
- return self.revisions.all()[0]
-
- get_comments = get_object_comments
-
- def get_last_update_info(self):
-
- when, who = post_get_last_update_info(self)
-
- answers = self.answers.all()
- if len(answers) > 0:
- for a in answers:
- a_when, a_who = a.get_last_update_info()
- if a_when > when:
- when = a_when
- who = a_who
-
- return when, who
-
- def get_update_summary(self,last_reported_at=None,recipient_email=''):
- edited = False
- if self.last_edited_at and self.last_edited_at > last_reported_at:
- if self.last_edited_by.email != recipient_email:
- edited = True
- comments = []
- for comment in self.comments.all():
- if comment.added_at > last_reported_at and comment.user.email != recipient_email:
- comments.append(comment)
- new_answers = []
- answer_comments = []
- modified_answers = []
- commented_answers = []
- import sets
- commented_answers = sets.Set([])
- for answer in self.answers.all():
- if (answer.added_at > last_reported_at and answer.author.email != recipient_email):
- new_answers.append(answer)
- if (answer.last_edited_at
- and answer.last_edited_at > last_reported_at
- and answer.last_edited_by.email != recipient_email):
- modified_answers.append(answer)
- for comment in answer.comments.all():
- if comment.added_at > last_reported_at and comment.user.email != recipient_email:
- commented_answers.add(answer)
- answer_comments.append(comment)
-
- #create the report
- if edited or new_answers or modified_answers or answer_comments:
- out = []
- if edited:
- out.append(_('%(author)s modified the question') % {'author':self.last_edited_by.username})
- if new_answers:
- names = sets.Set(map(lambda x: x.author.username,new_answers))
- people = ', '.join(names)
- out.append(_('%(people)s posted %(new_answer_count)s new answers') \
- % {'new_answer_count':len(new_answers),'people':people})
- if comments:
- names = sets.Set(map(lambda x: x.user.username,comments))
- people = ', '.join(names)
- out.append(_('%(people)s commented the question') % {'people':people})
- if answer_comments:
- names = sets.Set(map(lambda x: x.user.username,answer_comments))
- people = ', '.join(names)
- if len(commented_answers) > 1:
- out.append(_('%(people)s commented answers') % {'people':people})
- else:
- out.append(_('%(people)s commented an answer') % {'people':people})
- url = settings.APP_URL + self.get_absolute_url()
- retval = '<a href="%s">%s</a>:<br>\n' % (url,self.title)
- out = map(lambda x: '<li>' + x + '</li>',out)
- retval += '<ul>' + '\n'.join(out) + '</ul><br>\n'
- return retval
- else:
- return None
-
- def __unicode__(self):
- return self.title
-
- class Meta:
- db_table = u'question'
-
-class QuestionView(models.Model):
- question = models.ForeignKey(Question, related_name='viewed')
- who = models.ForeignKey(User, related_name='question_views')
- when = models.DateTimeField()
-
-class FavoriteQuestion(models.Model):
- """A favorite Question of a User."""
- question = models.ForeignKey(Question)
- user = models.ForeignKey(User, related_name='user_favorite_questions')
- added_at = models.DateTimeField(default=datetime.datetime.now)
- class Meta:
- db_table = u'favorite_question'
- def __unicode__(self):
- return '[%s] favorited at %s' %(self.user, self.added_at)
-
-class MarkedTag(models.Model):
- TAG_MARK_REASONS = (('good',_('interesting')),('bad',_('ignored')))
- tag = models.ForeignKey(Tag, related_name='user_selections')
- user = models.ForeignKey(User, related_name='tag_selections')
- reason = models.CharField(max_length=16, choices=TAG_MARK_REASONS)
-
-class QuestionRevision(models.Model):
- """A revision of a Question."""
- question = models.ForeignKey(Question, related_name='revisions')
- revision = models.PositiveIntegerField(blank=True)
- title = models.CharField(max_length=300)
- author = models.ForeignKey(User, related_name='question_revisions')
- revised_at = models.DateTimeField()
- tagnames = models.CharField(max_length=125)
- summary = models.CharField(max_length=300, blank=True)
- text = models.TextField()
-
- class Meta:
- db_table = u'question_revision'
- ordering = ('-revision',)
-
- def get_question_title(self):
- return self.question.title
-
- def get_absolute_url(self):
- #print 'in QuestionRevision.get_absolute_url()'
- return reverse('question_revisions', args=[self.question.id])
-
- def save(self, **kwargs):
- """Looks up the next available revision number."""
- if not self.revision:
- self.revision = QuestionRevision.objects.filter(
- question=self.question).values_list('revision',
- flat=True)[0] + 1
- super(QuestionRevision, self).save(**kwargs)
-
- def __unicode__(self):
- return u'revision %s of %s' % (self.revision, self.title)
-
-class AnonymousAnswer(models.Model):
- question = models.ForeignKey(Question, related_name='anonymous_answers')
- session_key = models.CharField(max_length=40) #session id for anonymous questions
- wiki = models.BooleanField(default=False)
- added_at = models.DateTimeField(default=datetime.datetime.now)
- ip_addr = models.IPAddressField(max_length=21) #allow high port numbers
- author = models.ForeignKey(User,null=True)
- text = models.TextField()
- summary = models.CharField(max_length=180)
-
- def publish(self,user):
- from forum.views import create_new_answer
- added_at = datetime.datetime.now()
- #print user.id
- create_new_answer(question=self.question,wiki=self.wiki,
- added_at=added_at,text=self.text,
- author=user)
- self.delete()
-
-class AnonymousQuestion(models.Model):
- title = models.CharField(max_length=300)
- session_key = models.CharField(max_length=40) #session id for anonymous questions
- text = models.TextField()
- summary = models.CharField(max_length=180)
- tagnames = models.CharField(max_length=125)
- wiki = models.BooleanField(default=False)
- added_at = models.DateTimeField(default=datetime.datetime.now)
- ip_addr = models.IPAddressField(max_length=21) #allow high port numbers
- author = models.ForeignKey(User,null=True)
-
- def publish(self,user):
- from forum.views import create_new_question
- added_at = datetime.datetime.now()
- create_new_question(title=self.title, author=user, added_at=added_at,
- wiki=self.wiki, tagnames=self.tagnames,
- summary=self.summary, text=self.text)
- self.delete()
-
-class Answer(models.Model):
- question = models.ForeignKey(Question, related_name='answers')
- author = models.ForeignKey(User, related_name='answers')
- added_at = models.DateTimeField(default=datetime.datetime.now)
- # Status
- wiki = models.BooleanField(default=False)
- wikified_at = models.DateTimeField(null=True, blank=True)
- accepted = models.BooleanField(default=False)
- accepted_at = models.DateTimeField(null=True, blank=True)
- deleted = models.BooleanField(default=False)
- deleted_by = models.ForeignKey(User, null=True, blank=True, related_name='deleted_answers')
- locked = models.BooleanField(default=False)
- locked_by = models.ForeignKey(User, null=True, blank=True, related_name='locked_answers')
- locked_at = models.DateTimeField(null=True, blank=True)
- # Denormalised data
- score = models.IntegerField(default=0)
- vote_up_count = models.IntegerField(default=0)
- vote_down_count = models.IntegerField(default=0)
- comment_count = models.PositiveIntegerField(default=0)
- offensive_flag_count = models.SmallIntegerField(default=0)
- last_edited_at = models.DateTimeField(null=True, blank=True)
- last_edited_by = models.ForeignKey(User, null=True, blank=True, related_name='last_edited_answers')
- html = models.TextField()
- comments = generic.GenericRelation(Comment)
- votes = generic.GenericRelation(Vote)
- flagged_items = generic.GenericRelation(FlaggedItem)
-
- objects = AnswerManager()
-
- get_comments = get_object_comments
- get_last_update_info = post_get_last_update_info
-
- def save(self,**kwargs):
- super(Answer,self).save(**kwargs)
- try:
- ping_google()
- except Exception:
- logging.debug('problem pinging google did you register you sitemap with google?')
-
- def get_user_vote(self, user):
- if user.__class__.__name__ == "AnonymousUser":
- return None
-
- votes = self.votes.filter(user=user)
- if votes and votes.count() > 0:
- return votes[0]
- else:
- return None
-
- def get_latest_revision(self):
- return self.revisions.all()[0]
-
- def get_question_title(self):
- return self.question.title
-
- def get_absolute_url(self):
- return '%s%s#%s' % (reverse('question', args=[self.question.id]), django_urlquote(slugify(self.question.title)), self.id)
-
- class Meta:
- db_table = u'answer'
-
- def __unicode__(self):
- return self.html
-
-class AnswerRevision(models.Model):
- """A revision of an Answer."""
- answer = models.ForeignKey(Answer, related_name='revisions')
- revision = models.PositiveIntegerField()
- author = models.ForeignKey(User, related_name='answer_revisions')
- revised_at = models.DateTimeField()
- summary = models.CharField(max_length=300, blank=True)
- text = models.TextField()
-
- def get_absolute_url(self):
- return reverse('answer_revisions', kwargs={'id':self.answer.id})
-
- def get_question_title(self):
- return self.answer.question.title
-
- class Meta:
- db_table = u'answer_revision'
- ordering = ('-revision',)
-
- def save(self, **kwargs):
- """Looks up the next available revision number if not set."""
- if not self.revision:
- self.revision = AnswerRevision.objects.filter(
- answer=self.answer).values_list('revision',
- flat=True)[0] + 1
- super(AnswerRevision, self).save(**kwargs)
-
-class Badge(models.Model):
- """Awarded for notable actions performed on the site by Users."""
- GOLD = 1
- SILVER = 2
- BRONZE = 3
- TYPE_CHOICES = (
- (GOLD, _('gold')),
- (SILVER, _('silver')),
- (BRONZE, _('bronze')),
- )
-
- name = models.CharField(max_length=50)
- type = models.SmallIntegerField(choices=TYPE_CHOICES)
- slug = models.SlugField(max_length=50, blank=True)
- description = models.CharField(max_length=300)
- multiple = models.BooleanField(default=False)
- # Denormalised data
- awarded_count = models.PositiveIntegerField(default=0)
-
- class Meta:
- db_table = u'badge'
- ordering = ('name',)
- unique_together = ('name', 'type')
-
- def __unicode__(self):
- return u'%s: %s' % (self.get_type_display(), self.name)
-
- def save(self, **kwargs):
- if not self.slug:
- self.slug = self.name#slugify(self.name)
- super(Badge, self).save(**kwargs)
-
- def get_absolute_url(self):
- return '%s%s/' % (reverse('badge', args=[self.id]), self.slug)
-
-class Award(models.Model):
- """The awarding of a Badge to a User."""
- user = models.ForeignKey(User, related_name='award_user')
- badge = models.ForeignKey(Badge, related_name='award_badge')
- content_type = models.ForeignKey(ContentType)
- object_id = models.PositiveIntegerField()
- content_object = generic.GenericForeignKey('content_type', 'object_id')
- awarded_at = models.DateTimeField(default=datetime.datetime.now)
- notified = models.BooleanField(default=False)
- objects = AwardManager()
-
- def __unicode__(self):
- return u'[%s] is awarded a badge [%s] at %s' % (self.user.username, self.badge.name, self.awarded_at)
-
- class Meta:
- db_table = u'award'
-
-class Repute(models.Model):
- """The reputation histories for user"""
- user = models.ForeignKey(User)
- positive = models.SmallIntegerField(default=0)
- negative = models.SmallIntegerField(default=0)
- question = models.ForeignKey(Question)
- reputed_at = models.DateTimeField(default=datetime.datetime.now)
- reputation_type = models.SmallIntegerField(choices=TYPE_REPUTATION)
- reputation = models.IntegerField(default=1)
- objects = ReputeManager()
-
- def __unicode__(self):
- return u'[%s]\' reputation changed at %s' % (self.user.username, self.reputed_at)
-
- class Meta:
- db_table = u'repute'
-
-class Activity(models.Model):
- """
- We keep some history data for user activities
- """
- user = models.ForeignKey(User)
- activity_type = models.SmallIntegerField(choices=TYPE_ACTIVITY)
- active_at = models.DateTimeField(default=datetime.datetime.now)
- content_type = models.ForeignKey(ContentType)
- object_id = models.PositiveIntegerField()
- content_object = generic.GenericForeignKey('content_type', 'object_id')
- is_auditted = models.BooleanField(default=False)
-
- def __unicode__(self):
- return u'[%s] was active at %s' % (self.user.username, self.active_at)
-
- class Meta:
- db_table = u'activity'
-
-
-class AnonymousEmail(models.Model):
- #validation key, if used
- key = models.CharField(max_length=32)
- email = models.EmailField(null=False,unique=True)
- isvalid = models.BooleanField(default=False)
-
-# User extend properties
-QUESTIONS_PER_PAGE_CHOICES = (
- (10, u'10'),
- (30, u'30'),
- (50, u'50'),
-)
-
-def user_is_username_taken(cls,username):
- try:
- cls.objects.get(username=username)
- return True
- except cls.MultipleObjectsReturned:
- return True
- except cls.DoesNotExist:
- return False
-
-def user_get_q_sel_email_feed_frequency(self):
- #print 'looking for frequency for user %s' % self
- try:
- feed_setting = EmailFeedSetting.objects.get(subscriber=self,feed_type='q_sel')
- except Exception, e:
- #print 'have error %s' % e.message
- raise e
- #print 'have freq=%s' % feed_setting.frequency
- return feed_setting.frequency
-
-User.add_to_class('is_approved', models.BooleanField(default=False))
-User.add_to_class('email_isvalid', models.BooleanField(default=False))
-User.add_to_class('email_key', models.CharField(max_length=32, null=True))
-User.add_to_class('reputation', models.PositiveIntegerField(default=1))
-User.add_to_class('gravatar', models.CharField(max_length=32))
-User.add_to_class('favorite_questions',
- models.ManyToManyField(Question, through=FavoriteQuestion,
- related_name='favorited_by'))
-User.add_to_class('badges', models.ManyToManyField(Badge, through=Award,
- related_name='awarded_to'))
-User.add_to_class('gold', models.SmallIntegerField(default=0))
-User.add_to_class('silver', models.SmallIntegerField(default=0))
-User.add_to_class('bronze', models.SmallIntegerField(default=0))
-User.add_to_class('questions_per_page',
- models.SmallIntegerField(choices=QUESTIONS_PER_PAGE_CHOICES, default=10))
-User.add_to_class('last_seen',
- models.DateTimeField(default=datetime.datetime.now))
-User.add_to_class('real_name', models.CharField(max_length=100, blank=True))
-User.add_to_class('website', models.URLField(max_length=200, blank=True))
-User.add_to_class('location', models.CharField(max_length=100, blank=True))
-User.add_to_class('date_of_birth', models.DateField(null=True, blank=True))
-User.add_to_class('about', models.TextField(blank=True))
-User.add_to_class('is_username_taken',classmethod(user_is_username_taken))
-User.add_to_class('get_q_sel_email_feed_frequency',user_get_q_sel_email_feed_frequency)
-User.add_to_class('hide_ignored_questions', models.BooleanField(default=False))
-User.add_to_class('tag_filter_setting',
- models.CharField(
- max_length=16,
- choices=TAG_EMAIL_FILTER_CHOICES,
- default='ignored'
- )
- )
-
-# custom signal
-tags_updated = django.dispatch.Signal(providing_args=["question"])
-edit_question_or_answer = django.dispatch.Signal(providing_args=["instance", "modified_by"])
-delete_post_or_answer = django.dispatch.Signal(providing_args=["instance", "deleted_by"])
-mark_offensive = django.dispatch.Signal(providing_args=["instance", "mark_by"])
-user_updated = django.dispatch.Signal(providing_args=["instance", "updated_by"])
-user_logged_in = django.dispatch.Signal(providing_args=["session"])
-
-
-def get_messages(self):
- messages = []
- for m in self.message_set.all():
- messages.append(m.message)
- return messages
-
-def delete_messages(self):
- self.message_set.all().delete()
-
-def get_profile_url(self):
- """Returns the URL for this User's profile."""
- return '%s%s/' % (reverse('user', args=[self.id]), slugify(self.username))
-
-def get_profile_link(self):
- profile_link = u'<a href="%s">%s</a>' % (self.get_profile_url(),self.username)
- logging.debug('in get profile link %s' % profile_link)
- return mark_safe(profile_link)
-
-User.add_to_class('get_profile_url', get_profile_url)
-User.add_to_class('get_profile_link', get_profile_link)
-User.add_to_class('get_messages', get_messages)
-User.add_to_class('delete_messages', delete_messages)
-
-def calculate_gravatar_hash(instance, **kwargs):
- """Calculates a User's gravatar hash from their email address."""
- if kwargs.get('raw', False):
- return
- instance.gravatar = hashlib.md5(instance.email).hexdigest()
-
-def record_ask_event(instance, created, **kwargs):
- if created:
- activity = Activity(user=instance.author, active_at=instance.added_at, content_object=instance, activity_type=TYPE_ACTIVITY_ASK_QUESTION)
- activity.save()
-
-def record_answer_event(instance, created, **kwargs):
- if created:
- activity = Activity(user=instance.author, active_at=instance.added_at, content_object=instance, activity_type=TYPE_ACTIVITY_ANSWER)
- activity.save()
-
-def record_comment_event(instance, created, **kwargs):
- if created:
- from django.contrib.contenttypes.models import ContentType
- question_type = ContentType.objects.get_for_model(Question)
- question_type_id = question_type.id
- if (instance.content_type_id == question_type_id):
- type = TYPE_ACTIVITY_COMMENT_QUESTION
- else:
- type = TYPE_ACTIVITY_COMMENT_ANSWER
- activity = Activity(user=instance.user, active_at=instance.added_at, content_object=instance, activity_type=type)
- activity.save()
-
-def record_revision_question_event(instance, created, **kwargs):
- if created and instance.revision <> 1:
- activity = Activity(user=instance.author, active_at=instance.revised_at, content_object=instance, activity_type=TYPE_ACTIVITY_UPDATE_QUESTION)
- activity.save()
-
-def record_revision_answer_event(instance, created, **kwargs):
- if created and instance.revision <> 1:
- activity = Activity(user=instance.author, active_at=instance.revised_at, content_object=instance, activity_type=TYPE_ACTIVITY_UPDATE_ANSWER)
- activity.save()
-
-def record_award_event(instance, created, **kwargs):
- """
- After we awarded a badge to user, we need to record this activity and notify user.
- We also recaculate awarded_count of this badge and user information.
- """
- if created:
- activity = Activity(user=instance.user, active_at=instance.awarded_at, content_object=instance,
- activity_type=TYPE_ACTIVITY_PRIZE)
- activity.save()
-
- instance.badge.awarded_count += 1
- instance.badge.save()
-
- if instance.badge.type == Badge.GOLD:
- instance.user.gold += 1
- if instance.badge.type == Badge.SILVER:
- instance.user.silver += 1
- if instance.badge.type == Badge.BRONZE:
- instance.user.bronze += 1
- instance.user.save()
-
-def notify_award_message(instance, created, **kwargs):
- """
- Notify users when they have been awarded badges by using Django message.
- """
- if created:
- user = instance.user
- user.message_set.create(message=u"Congratulations, you have received a badge '%s'" % instance.badge.name)
-
-def record_answer_accepted(instance, created, **kwargs):
- """
- when answer is accepted, we record this for question author - who accepted it.
- """
- if not created and instance.accepted:
- activity = Activity(user=instance.question.author, active_at=datetime.datetime.now(), \
- content_object=instance, activity_type=TYPE_ACTIVITY_MARK_ANSWER)
- activity.save()
-
-def update_last_seen(instance, created, **kwargs):
- """
- when user has activities, we update 'last_seen' time stamp for him
- """
- user = instance.user
- user.last_seen = datetime.datetime.now()
- user.save()
-
-def record_vote(instance, created, **kwargs):
- """
- when user have voted
- """
- if created:
- if instance.vote == 1:
- vote_type = TYPE_ACTIVITY_VOTE_UP
- else:
- vote_type = TYPE_ACTIVITY_VOTE_DOWN
-
- activity = Activity(user=instance.user, active_at=instance.voted_at, content_object=instance, activity_type=vote_type)
- activity.save()
-
-def record_cancel_vote(instance, **kwargs):
- """
- when user canceled vote, the vote will be deleted.
- """
- activity = Activity(user=instance.user, active_at=datetime.datetime.now(), content_object=instance, activity_type=TYPE_ACTIVITY_CANCEL_VOTE)
- activity.save()
-
-def record_delete_question(instance, delete_by, **kwargs):
- """
- when user deleted the question
- """
- if instance.__class__ == "Question":
- activity_type = TYPE_ACTIVITY_DELETE_QUESTION
- else:
- activity_type = TYPE_ACTIVITY_DELETE_ANSWER
-
- activity = Activity(user=delete_by, active_at=datetime.datetime.now(), content_object=instance, activity_type=activity_type)
- activity.save()
-
-def record_mark_offensive(instance, mark_by, **kwargs):
- activity = Activity(user=mark_by, active_at=datetime.datetime.now(), content_object=instance, activity_type=TYPE_ACTIVITY_MARK_OFFENSIVE)
- activity.save()
-
-def record_update_tags(question, **kwargs):
- """
- when user updated tags of the question
- """
- activity = Activity(user=question.author, active_at=datetime.datetime.now(), content_object=question, activity_type=TYPE_ACTIVITY_UPDATE_TAGS)
- activity.save()
-
-def record_favorite_question(instance, created, **kwargs):
- """
- when user add the question in him favorite questions list.
- """
- if created:
- activity = Activity(user=instance.user, active_at=datetime.datetime.now(), content_object=instance, activity_type=TYPE_ACTIVITY_FAVORITE)
- activity.save()
-
-def record_user_full_updated(instance, **kwargs):
- activity = Activity(user=instance, active_at=datetime.datetime.now(), content_object=instance, activity_type=TYPE_ACTIVITY_USER_FULL_UPDATED)
- activity.save()
-
-def post_stored_anonymous_content(sender,user,session_key,signal,*args,**kwargs):
- aq_list = AnonymousQuestion.objects.filter(session_key = session_key)
- aa_list = AnonymousAnswer.objects.filter(session_key = session_key)
- import settings
- if settings.EMAIL_VALIDATION == 'on':#add user to the record
- for aq in aq_list:
- aq.author = user
- aq.save()
- for aa in aa_list:
- aa.author = user
- aa.save()
- #maybe add pending posts message?
- else: #just publish the questions
- for aq in aq_list:
- aq.publish(user)
- for aa in aa_list:
- aa.publish(user)
-
-#signal for User modle save changes
-pre_save.connect(calculate_gravatar_hash, sender=User)
-post_save.connect(record_ask_event, sender=Question)
-post_save.connect(record_answer_event, sender=Answer)
-post_save.connect(record_comment_event, sender=Comment)
-post_save.connect(record_revision_question_event, sender=QuestionRevision)
-post_save.connect(record_revision_answer_event, sender=AnswerRevision)
-post_save.connect(record_award_event, sender=Award)
-post_save.connect(notify_award_message, sender=Award)
-post_save.connect(record_answer_accepted, sender=Answer)
-post_save.connect(update_last_seen, sender=Activity)
-post_save.connect(record_vote, sender=Vote)
-post_delete.connect(record_cancel_vote, sender=Vote)
-delete_post_or_answer.connect(record_delete_question, sender=Question)
-delete_post_or_answer.connect(record_delete_question, sender=Answer)
-mark_offensive.connect(record_mark_offensive, sender=Question)
-mark_offensive.connect(record_mark_offensive, sender=Answer)
-tags_updated.connect(record_update_tags, sender=Question)
-post_save.connect(record_favorite_question, sender=FavoriteQuestion)
-user_updated.connect(record_user_full_updated, sender=User)
-user_logged_in.connect(post_stored_anonymous_content)
-
-#todo later split this out to the books extension models
-#from django.db import models
-#from django.contrib.auth.models import User
-#from forum.models import Question
-
-class Book(models.Model):
- """
- Model for book info
- """
- user = models.ForeignKey(User)
- title = models.CharField(max_length=255)
- short_name = models.CharField(max_length=255)
- author = models.CharField(max_length=255)
- price = models.DecimalField(max_digits=6, decimal_places=2)
- pages = models.SmallIntegerField()
- published_at = models.DateTimeField()
- publication = models.CharField(max_length=255)
- cover_img = models.CharField(max_length=255)
- tagnames = models.CharField(max_length=125)
- added_at = models.DateTimeField()
- last_edited_at = models.DateTimeField()
- questions = models.ManyToManyField(Question, related_name='book', db_table='book_question')
-
- def get_absolute_url(self):
- return reverse('book', args=[django_urlquote(slugify(self.short_name))])
-
- def __unicode__(self):
- return self.title
- class Meta:
- db_table = u'book'
-
-class BookAuthorInfo(models.Model):
- """
- Model for book author info
- """
- user = models.ForeignKey(User)
- book = models.ForeignKey(Book)
- blog_url = models.CharField(max_length=255)
- added_at = models.DateTimeField()
- last_edited_at = models.DateTimeField()
-
- class Meta:
- db_table = u'book_author_info'
-
-class BookAuthorRss(models.Model):
- """
- Model for book author blog rss
- """
- user = models.ForeignKey(User)
- book = models.ForeignKey(Book)
- title = models.CharField(max_length=255)
- url = models.CharField(max_length=255)
- rss_created_at = models.DateTimeField()
- added_at = models.DateTimeField()
-
- class Meta:
- db_table = u'book_author_rss'
diff --git a/forum/models/__init__.py b/forum/models/__init__.py
new file mode 100755
index 00000000..2ab7b263
--- /dev/null
+++ b/forum/models/__init__.py
@@ -0,0 +1,329 @@
+from question import Question ,QuestionRevision, QuestionView, AnonymousQuestion, FavoriteQuestion
+from answer import Answer, AnonymousAnswer, AnswerRevision
+from tag import Tag, MarkedTag
+from meta import Vote, Comment, FlaggedItem
+from user import Activity, AnonymousEmail, EmailFeedSetting
+from repute import Badge, Award, Repute
+
+from base import *
+
+# User extend properties
+QUESTIONS_PER_PAGE_CHOICES = (
+ (10, u'10'),
+ (30, u'30'),
+ (50, u'50'),
+)
+
+def user_is_username_taken(cls,username):
+ try:
+ cls.objects.get(username=username)
+ return True
+ except cls.MultipleObjectsReturned:
+ return True
+ except cls.DoesNotExist:
+ return False
+
+def user_get_q_sel_email_feed_frequency(self):
+ #print 'looking for frequency for user %s' % self
+ try:
+ feed_setting = EmailFeedSetting.objects.get(subscriber=self,feed_type='q_sel')
+ except Exception, e:
+ #print 'have error %s' % e.message
+ raise e
+ #print 'have freq=%s' % feed_setting.frequency
+ return feed_setting.frequency
+
+User.add_to_class('is_approved', models.BooleanField(default=False))
+User.add_to_class('email_isvalid', models.BooleanField(default=False))
+User.add_to_class('email_key', models.CharField(max_length=32, null=True))
+User.add_to_class('reputation', models.PositiveIntegerField(default=1))
+User.add_to_class('gravatar', models.CharField(max_length=32))
+User.add_to_class('favorite_questions',
+ models.ManyToManyField(Question, through=FavoriteQuestion,
+ related_name='favorited_by'))
+User.add_to_class('badges', models.ManyToManyField(Badge, through=Award,
+ related_name='awarded_to'))
+User.add_to_class('gold', models.SmallIntegerField(default=0))
+User.add_to_class('silver', models.SmallIntegerField(default=0))
+User.add_to_class('bronze', models.SmallIntegerField(default=0))
+User.add_to_class('questions_per_page',
+ models.SmallIntegerField(choices=QUESTIONS_PER_PAGE_CHOICES, default=10))
+User.add_to_class('last_seen',
+ models.DateTimeField(default=datetime.datetime.now))
+User.add_to_class('real_name', models.CharField(max_length=100, blank=True))
+User.add_to_class('website', models.URLField(max_length=200, blank=True))
+User.add_to_class('location', models.CharField(max_length=100, blank=True))
+User.add_to_class('date_of_birth', models.DateField(null=True, blank=True))
+User.add_to_class('about', models.TextField(blank=True))
+User.add_to_class('is_username_taken',classmethod(user_is_username_taken))
+User.add_to_class('get_q_sel_email_feed_frequency',user_get_q_sel_email_feed_frequency)
+User.add_to_class('hide_ignored_questions', models.BooleanField(default=False))
+User.add_to_class('tag_filter_setting',
+ models.CharField(
+ max_length=16,
+ choices=TAG_EMAIL_FILTER_CHOICES,
+ default='ignored'
+ )
+ )
+
+# custom signal
+tags_updated = django.dispatch.Signal(providing_args=["question"])
+edit_question_or_answer = django.dispatch.Signal(providing_args=["instance", "modified_by"])
+delete_post_or_answer = django.dispatch.Signal(providing_args=["instance", "deleted_by"])
+mark_offensive = django.dispatch.Signal(providing_args=["instance", "mark_by"])
+user_updated = django.dispatch.Signal(providing_args=["instance", "updated_by"])
+user_logged_in = django.dispatch.Signal(providing_args=["session"])
+
+
+def get_messages(self):
+ messages = []
+ for m in self.message_set.all():
+ messages.append(m.message)
+ return messages
+
+def delete_messages(self):
+ self.message_set.all().delete()
+
+def get_profile_url(self):
+ """Returns the URL for this User's profile."""
+ return '%s%s/' % (reverse('user', args=[self.id]), slugify(self.username))
+
+def get_profile_link(self):
+ profile_link = u'<a href="%s">%s</a>' % (self.get_profile_url(),self.username)
+ logging.debug('in get profile link %s' % profile_link)
+ return mark_safe(profile_link)
+
+User.add_to_class('get_profile_url', get_profile_url)
+User.add_to_class('get_profile_link', get_profile_link)
+User.add_to_class('get_messages', get_messages)
+User.add_to_class('delete_messages', delete_messages)
+
+def calculate_gravatar_hash(instance, **kwargs):
+ """Calculates a User's gravatar hash from their email address."""
+ if kwargs.get('raw', False):
+ return
+ instance.gravatar = hashlib.md5(instance.email).hexdigest()
+
+def record_ask_event(instance, created, **kwargs):
+ if created:
+ activity = Activity(user=instance.author, active_at=instance.added_at, content_object=instance, activity_type=TYPE_ACTIVITY_ASK_QUESTION)
+ activity.save()
+
+def record_answer_event(instance, created, **kwargs):
+ if created:
+ activity = Activity(user=instance.author, active_at=instance.added_at, content_object=instance, activity_type=TYPE_ACTIVITY_ANSWER)
+ activity.save()
+
+def record_comment_event(instance, created, **kwargs):
+ if created:
+ from django.contrib.contenttypes.models import ContentType
+ question_type = ContentType.objects.get_for_model(Question)
+ question_type_id = question_type.id
+ if (instance.content_type_id == question_type_id):
+ type = TYPE_ACTIVITY_COMMENT_QUESTION
+ else:
+ type = TYPE_ACTIVITY_COMMENT_ANSWER
+ activity = Activity(user=instance.user, active_at=instance.added_at, content_object=instance, activity_type=type)
+ activity.save()
+
+def record_revision_question_event(instance, created, **kwargs):
+ if created and instance.revision <> 1:
+ activity = Activity(user=instance.author, active_at=instance.revised_at, content_object=instance, activity_type=TYPE_ACTIVITY_UPDATE_QUESTION)
+ activity.save()
+
+def record_revision_answer_event(instance, created, **kwargs):
+ if created and instance.revision <> 1:
+ activity = Activity(user=instance.author, active_at=instance.revised_at, content_object=instance, activity_type=TYPE_ACTIVITY_UPDATE_ANSWER)
+ activity.save()
+
+def record_award_event(instance, created, **kwargs):
+ """
+ After we awarded a badge to user, we need to record this activity and notify user.
+ We also recaculate awarded_count of this badge and user information.
+ """
+ if created:
+ activity = Activity(user=instance.user, active_at=instance.awarded_at, content_object=instance,
+ activity_type=TYPE_ACTIVITY_PRIZE)
+ activity.save()
+
+ instance.badge.awarded_count += 1
+ instance.badge.save()
+
+ if instance.badge.type == Badge.GOLD:
+ instance.user.gold += 1
+ if instance.badge.type == Badge.SILVER:
+ instance.user.silver += 1
+ if instance.badge.type == Badge.BRONZE:
+ instance.user.bronze += 1
+ instance.user.save()
+
+def notify_award_message(instance, created, **kwargs):
+ """
+ Notify users when they have been awarded badges by using Django message.
+ """
+ if created:
+ user = instance.user
+ user.message_set.create(message=u"Congratulations, you have received a badge '%s'" % instance.badge.name)
+
+def record_answer_accepted(instance, created, **kwargs):
+ """
+ when answer is accepted, we record this for question author - who accepted it.
+ """
+ if not created and instance.accepted:
+ activity = Activity(user=instance.question.author, active_at=datetime.datetime.now(), \
+ content_object=instance, activity_type=TYPE_ACTIVITY_MARK_ANSWER)
+ activity.save()
+
+def update_last_seen(instance, created, **kwargs):
+ """
+ when user has activities, we update 'last_seen' time stamp for him
+ """
+ user = instance.user
+ user.last_seen = datetime.datetime.now()
+ user.save()
+
+def record_vote(instance, created, **kwargs):
+ """
+ when user have voted
+ """
+ if created:
+ if instance.vote == 1:
+ vote_type = TYPE_ACTIVITY_VOTE_UP
+ else:
+ vote_type = TYPE_ACTIVITY_VOTE_DOWN
+
+ activity = Activity(user=instance.user, active_at=instance.voted_at, content_object=instance, activity_type=vote_type)
+ activity.save()
+
+def record_cancel_vote(instance, **kwargs):
+ """
+ when user canceled vote, the vote will be deleted.
+ """
+ activity = Activity(user=instance.user, active_at=datetime.datetime.now(), content_object=instance, activity_type=TYPE_ACTIVITY_CANCEL_VOTE)
+ activity.save()
+
+def record_delete_question(instance, delete_by, **kwargs):
+ """
+ when user deleted the question
+ """
+ if instance.__class__ == "Question":
+ activity_type = TYPE_ACTIVITY_DELETE_QUESTION
+ else:
+ activity_type = TYPE_ACTIVITY_DELETE_ANSWER
+
+ activity = Activity(user=delete_by, active_at=datetime.datetime.now(), content_object=instance, activity_type=activity_type)
+ activity.save()
+
+def record_mark_offensive(instance, mark_by, **kwargs):
+ activity = Activity(user=mark_by, active_at=datetime.datetime.now(), content_object=instance, activity_type=TYPE_ACTIVITY_MARK_OFFENSIVE)
+ activity.save()
+
+def record_update_tags(question, **kwargs):
+ """
+ when user updated tags of the question
+ """
+ activity = Activity(user=question.author, active_at=datetime.datetime.now(), content_object=question, activity_type=TYPE_ACTIVITY_UPDATE_TAGS)
+ activity.save()
+
+def record_favorite_question(instance, created, **kwargs):
+ """
+ when user add the question in him favorite questions list.
+ """
+ if created:
+ activity = Activity(user=instance.user, active_at=datetime.datetime.now(), content_object=instance, activity_type=TYPE_ACTIVITY_FAVORITE)
+ activity.save()
+
+def record_user_full_updated(instance, **kwargs):
+ activity = Activity(user=instance, active_at=datetime.datetime.now(), content_object=instance, activity_type=TYPE_ACTIVITY_USER_FULL_UPDATED)
+ activity.save()
+
+def post_stored_anonymous_content(sender,user,session_key,signal,*args,**kwargs):
+ aq_list = AnonymousQuestion.objects.filter(session_key = session_key)
+ aa_list = AnonymousAnswer.objects.filter(session_key = session_key)
+ import settings
+ if settings.EMAIL_VALIDATION == 'on':#add user to the record
+ for aq in aq_list:
+ aq.author = user
+ aq.save()
+ for aa in aa_list:
+ aa.author = user
+ aa.save()
+ #maybe add pending posts message?
+ else: #just publish the questions
+ for aq in aq_list:
+ aq.publish(user)
+ for aa in aa_list:
+ aa.publish(user)
+
+#signal for User modle save changes
+
+pre_save.connect(calculate_gravatar_hash, sender=User)
+post_save.connect(record_ask_event, sender=Question)
+post_save.connect(record_answer_event, sender=Answer)
+post_save.connect(record_comment_event, sender=Comment)
+post_save.connect(record_revision_question_event, sender=QuestionRevision)
+post_save.connect(record_revision_answer_event, sender=AnswerRevision)
+post_save.connect(record_award_event, sender=Award)
+post_save.connect(notify_award_message, sender=Award)
+post_save.connect(record_answer_accepted, sender=Answer)
+post_save.connect(update_last_seen, sender=Activity)
+post_save.connect(record_vote, sender=Vote)
+post_delete.connect(record_cancel_vote, sender=Vote)
+delete_post_or_answer.connect(record_delete_question, sender=Question)
+delete_post_or_answer.connect(record_delete_question, sender=Answer)
+mark_offensive.connect(record_mark_offensive, sender=Question)
+mark_offensive.connect(record_mark_offensive, sender=Answer)
+tags_updated.connect(record_update_tags, sender=Question)
+post_save.connect(record_favorite_question, sender=FavoriteQuestion)
+user_updated.connect(record_user_full_updated, sender=User)
+user_logged_in.connect(post_stored_anonymous_content)
+
+Question = Question
+QuestionRevision = QuestionRevision
+QuestionView = QuestionView
+FavoriteQuestion = FavoriteQuestion
+AnonymousQuestion = AnonymousQuestion
+
+Answer = Answer
+AnswerRevision = AnswerRevision
+AnonymousAnswer = AnonymousAnswer
+
+Tag = Tag
+Comment = Comment
+Vote = Vote
+FlaggedItem = FlaggedItem
+MarkedTag = MarkedTag
+
+Badge = Badge
+Award = Award
+Repute = Repute
+
+Activity = Activity
+EmailFeedSetting = EmailFeedSetting
+AnonymousEmail = AnonymousEmail
+
+__all__ = [
+ 'Question',
+ 'QuestionRevision',
+ 'QuestionView',
+ 'FavoriteQuestion',
+ 'AnonymousQuestion',
+
+ 'Answer',
+ 'AnswerRevision',
+ 'AnonymousAnswer',
+
+ 'Tag',
+ 'Comment',
+ 'Vote',
+ 'FlaggedItem',
+ 'MarkedTag',
+
+ 'Badge',
+ 'Award',
+ 'Repute',
+
+ 'Activity',
+ 'EmailFeedSetting',
+ 'AnonymousEmail',
+ ]
diff --git a/forum/models/answer.py b/forum/models/answer.py
new file mode 100755
index 00000000..4a44bd49
--- /dev/null
+++ b/forum/models/answer.py
@@ -0,0 +1,134 @@
+from base import *
+
+from question import Question
+
+class AnswerManager(models.Manager):
+ def create_new(self, question=None, author=None, added_at=None, wiki=False, text='', email_notify=False):
+ answer = Answer(
+ question = question,
+ author = author,
+ added_at = added_at,
+ wiki = wiki,
+ html = text
+ )
+ if answer.wiki:
+ answer.last_edited_by = answer.author
+ answer.last_edited_at = added_at
+ answer.wikified_at = added_at
+
+ answer.save()
+
+ #update question data
+ question.last_activity_at = added_at
+ question.last_activity_by = author
+ question.save()
+ Question.objects.update_answer_count(question)
+
+ #update revision
+ from models import AnswerRevision
+
+ AnswerRevision.objects.create(
+ answer = answer,
+ revision = 1,
+ author = author,
+ revised_at = added_at,
+ summary = CONST['default_version'],
+ text = text
+ )
+
+ #set notification/delete
+ if email_notify:
+ if author not in question.followed_by.all():
+ question.followed_by.add(author)
+ else:
+ #not sure if this is necessary. ajax should take care of this...
+ try:
+ question.followed_by.remove(author)
+ except:
+ pass
+
+ GET_ANSWERS_FROM_USER_QUESTIONS = u'SELECT answer.* FROM answer INNER JOIN question ON answer.question_id = question.id WHERE question.author_id =%s AND answer.author_id <> %s'
+ def get_answers_from_question(self, question, user=None):
+ """
+ Retrieves visibile answers for the given question. Delete answers
+ are only visibile to the person who deleted them.
+ """
+
+ if user is None or not user.is_authenticated():
+ return self.filter(question=question, deleted=False)
+ else:
+ return self.filter(Q(question=question),
+ Q(deleted=False) | Q(deleted_by=user))
+
+ def get_answers_from_questions(self, user_id):
+ """
+ Retrieves visibile answers for the given question. Which are not included own answers
+ """
+ cursor = connection.cursor()
+ cursor.execute(self.GET_ANSWERS_FROM_USER_QUESTIONS, [user_id, user_id])
+ return cursor.fetchall()
+
+class Answer(Content, DeletableContent):
+ question = models.ForeignKey('Question', related_name='answers')
+ accepted = models.BooleanField(default=False)
+ accepted_at = models.DateTimeField(null=True, blank=True)
+
+ objects = AnswerManager()
+
+ class Meta(Content.Meta):
+ db_table = u'answer'
+
+ def get_user_vote(self, user):
+ if user.__class__.__name__ == "AnonymousUser":
+ return None
+
+ votes = self.votes.filter(user=user)
+ if votes and votes.count() > 0:
+ return votes[0]
+ else:
+ return None
+
+ def get_latest_revision(self):
+ return self.revisions.all()[0]
+
+ def get_question_title(self):
+ return self.question.title
+
+ def get_absolute_url(self):
+ return '%s%s#%s' % (reverse('question', args=[self.question.id]), django_urlquote(slugify(self.question.title)), self.id)
+
+ def __unicode__(self):
+ return self.html
+
+class AnswerRevision(ContentRevision):
+ """A revision of an Answer."""
+ answer = models.ForeignKey('Answer', related_name='revisions')
+
+ def get_absolute_url(self):
+ return reverse('answer_revisions', kwargs={'id':self.answer.id})
+
+ def get_question_title(self):
+ return self.answer.question.title
+
+ class Meta(ContentRevision.Meta):
+ db_table = u'answer_revision'
+ ordering = ('-revision',)
+
+ def save(self, **kwargs):
+ """Looks up the next available revision number if not set."""
+ if not self.revision:
+ self.revision = AnswerRevision.objects.filter(
+ answer=self.answer).values_list('revision',
+ flat=True)[0] + 1
+ super(AnswerRevision, self).save(**kwargs)
+
+class AnonymousAnswer(AnonymousContent):
+ question = models.ForeignKey('Question', related_name='anonymous_answers')
+
+ def publish(self,user):
+ added_at = datetime.datetime.now()
+ #print user.id
+ AnswerManager.create_new(question=self.question,wiki=self.wiki,
+ added_at=added_at,text=self.text,
+ author=user)
+ self.delete()
diff --git a/forum/models/base.py b/forum/models/base.py
new file mode 100755
index 00000000..24fe2b0d
--- /dev/null
+++ b/forum/models/base.py
@@ -0,0 +1,140 @@
+import datetime
+import hashlib
+from urllib import quote_plus, urlencode
+from django.db import models, IntegrityError
+from django.utils.http import urlquote as django_urlquote
+from django.utils.html import strip_tags
+from django.core.urlresolvers import reverse
+from django.contrib.auth.models import User
+from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.models import ContentType
+from django.template.defaultfilters import slugify
+from django.db.models.signals import post_delete, post_save, pre_save
+from django.utils.translation import ugettext as _
+from django.utils.safestring import mark_safe
+from django.contrib.sitemaps import ping_google
+import django.dispatch
+from django.conf import settings
+import logging
+
+if settings.USE_SPHINX_SEARCH == True:
+ from djangosphinx.models import SphinxSearch
+
+from forum.managers import *
+from forum.const import *
+
+class MetaContent(models.Model):
+ """
+ Base class for Vote, Comment and FlaggedItem
+ """
+ content_type = models.ForeignKey(ContentType)
+ object_id = models.PositiveIntegerField()
+ content_object = generic.GenericForeignKey('content_type', 'object_id')
+ user = models.ForeignKey(User, related_name='%(class)ss')
+
+ class Meta:
+ abstract = True
+ app_label = 'forum'
+
+
+class DeletableContent(models.Model):
+ deleted = models.BooleanField(default=False)
+ deleted_at = models.DateTimeField(null=True, blank=True)
+ deleted_by = models.ForeignKey(User, null=True, blank=True, related_name='deleted_%(class)ss')
+
+ class Meta:
+ abstract = True
+ app_label = 'forum'
+
+
+class ContentRevision(models.Model):
+ """
+ Base class for QuestionRevision and AnswerRevision
+ """
+ revision = models.PositiveIntegerField()
+ author = models.ForeignKey(User, related_name='%(class)ss')
+ revised_at = models.DateTimeField()
+ summary = models.CharField(max_length=300, blank=True)
+ text = models.TextField()
+
+ class Meta:
+ abstract = True
+ app_label = 'forum'
+
+
+class AnonymousContent(models.Model):
+ """
+ Base class for AnonymousQuestion and AnonymousAnswer
+ """
+ session_key = models.CharField(max_length=40) #session id for anonymous questions
+ wiki = models.BooleanField(default=False)
+ added_at = models.DateTimeField(default=datetime.datetime.now)
+ ip_addr = models.IPAddressField(max_length=21) #allow high port numbers
+ author = models.ForeignKey(User,null=True)
+ text = models.TextField()
+ summary = models.CharField(max_length=180)
+
+ class Meta:
+ abstract = True
+ app_label = 'forum'
+
+
+from meta import Comment, Vote, FlaggedItem
+
+class Content(models.Model):
+ """
+ Base class for Question and Answer
+ """
+ author = models.ForeignKey(User, related_name='%(class)ss')
+ added_at = models.DateTimeField(default=datetime.datetime.now)
+
+ wiki = models.BooleanField(default=False)
+ wikified_at = models.DateTimeField(null=True, blank=True)
+
+ locked = models.BooleanField(default=False)
+ locked_by = models.ForeignKey(User, null=True, blank=True, related_name='locked_%(class)ss')
+ locked_at = models.DateTimeField(null=True, blank=True)
+
+ score = models.IntegerField(default=0)
+ vote_up_count = models.IntegerField(default=0)
+ vote_down_count = models.IntegerField(default=0)
+
+ comment_count = models.PositiveIntegerField(default=0)
+ offensive_flag_count = models.SmallIntegerField(default=0)
+
+ last_edited_at = models.DateTimeField(null=True, blank=True)
+ last_edited_by = models.ForeignKey(User, null=True, blank=True, related_name='last_edited_%(class)ss')
+
+ html = models.TextField()
+ comments = generic.GenericRelation(Comment)
+ votes = generic.GenericRelation(Vote)
+ flagged_items = generic.GenericRelation(FlaggedItem)
+
+ class Meta:
+ abstract = True
+ app_label = 'forum'
+
+ def save(self,**kwargs):
+ super(Content,self).save(**kwargs)
+ try:
+ ping_google()
+ except Exception:
+ logging.debug('problem pinging google did you register you sitemap with google?')
+
+ def get_object_comments(self):
+ comments = self.comments.all().order_by('id')
+ return comments
+
+ def post_get_last_update_info(self):
+ when = self.added_at
+ who = self.author
+ if self.last_edited_at and self.last_edited_at > when:
+ when = self.last_edited_at
+ who = self.last_edited_by
+ comments = self.comments.all()
+ if len(comments) > 0:
+ for c in comments:
+ if c.added_at > when:
+ when = c.added_at
+ who = c.user
+ return when, who \ No newline at end of file
diff --git a/forum/models/meta.py b/forum/models/meta.py
new file mode 100755
index 00000000..3dfd3e86
--- /dev/null
+++ b/forum/models/meta.py
@@ -0,0 +1,89 @@
+from base import *
+
+class VoteManager(models.Manager):
+ def get_up_vote_count_from_user(self, user):
+ if user is not None:
+ return self.filter(user=user, vote=1).count()
+ else:
+ return 0
+
+ def get_down_vote_count_from_user(self, user):
+ if user is not None:
+ return self.filter(user=user, vote=-1).count()
+ else:
+ return 0
+
+ def get_votes_count_today_from_user(self, user):
+ if user is not None:
+ today = datetime.date.today()
+ return self.filter(user=user, voted_at__range=(today, today + datetime.timedelta(1))).count()
+
+ else:
+ return 0
+
+
+class Vote(MetaContent):
+ VOTE_UP = +1
+ VOTE_DOWN = -1
+ VOTE_CHOICES = (
+ (VOTE_UP, u'Up'),
+ (VOTE_DOWN, u'Down'),
+ )
+
+ vote = models.SmallIntegerField(choices=VOTE_CHOICES)
+ voted_at = models.DateTimeField(default=datetime.datetime.now)
+
+ objects = VoteManager()
+
+ class Meta(MetaContent.Meta):
+ unique_together = ('content_type', 'object_id', 'user')
+ db_table = u'vote'
+
+ def __unicode__(self):
+ return '[%s] voted at %s: %s' %(self.user, self.voted_at, self.vote)
+
+ def is_upvote(self):
+ return self.vote == self.VOTE_UP
+
+ def is_downvote(self):
+ return self.vote == self.VOTE_DOWN
+
+
+class FlaggedItemManager(models.Manager):
+ def get_flagged_items_count_today(self, user):
+ if user is not None:
+ today = datetime.date.today()
+ return self.filter(user=user, flagged_at__range=(today, today + datetime.timedelta(1))).count()
+ else:
+ return 0
+
+class FlaggedItem(MetaContent):
+ """A flag on a Question or Answer indicating offensive content."""
+ flagged_at = models.DateTimeField(default=datetime.datetime.now)
+
+ objects = FlaggedItemManager()
+
+ class Meta(MetaContent.Meta):
+ unique_together = ('content_type', 'object_id', 'user')
+ db_table = u'flagged_item'
+
+ def __unicode__(self):
+ return '[%s] flagged at %s' %(self.user, self.flagged_at)
+
+class Comment(MetaContent):
+ comment = models.CharField(max_length=300)
+ added_at = models.DateTimeField(default=datetime.datetime.now)
+
+ class Meta(MetaContent.Meta):
+ ordering = ('-added_at',)
+ db_table = u'comment'
+
+ def save(self,**kwargs):
+ super(Comment,self).save(**kwargs)
+ try:
+ ping_google()
+ except Exception:
+ logging.debug('problem pinging google did you register you sitemap with google?')
+
+ def __unicode__(self):
+ return self.comment \ No newline at end of file
diff --git a/forum/models/question.py b/forum/models/question.py
new file mode 100755
index 00000000..cfa2f6be
--- /dev/null
+++ b/forum/models/question.py
@@ -0,0 +1,335 @@
+from base import *
+from tag import Tag
+
+class QuestionManager(models.Manager):
+ def create_new(self, title=None,author=None,added_at=None, wiki=False,tagnames=None,summary=None, text=None):
+ question = Question(
+ title = title,
+ author = author,
+ added_at = added_at,
+ last_activity_at = added_at,
+ last_activity_by = author,
+ wiki = wiki,
+ tagnames = tagnames,
+ html = text,
+ summary = summary
+ )
+ if question.wiki:
+ question.last_edited_by = question.author
+ question.last_edited_at = added_at
+ question.wikified_at = added_at
+
+ question.save()
+
+ from models import QuestionRevision
+
+ # create the first revision
+ QuestionRevision.objects.create(
+ question = question,
+ revision = 1,
+ title = question.title,
+ author = author,
+ revised_at = added_at,
+ tagnames = question.tagnames,
+ summary = CONST['default_version'],
+ text = text
+ )
+ return question
+
+ def update_tags(self, question, tagnames, user):
+ """
+ Updates Tag associations for a question to match the given
+ tagname string.
+
+ Returns ``True`` if tag usage counts were updated as a result,
+ ``False`` otherwise.
+ """
+
+ current_tags = list(question.tags.all())
+ current_tagnames = set(t.name for t in current_tags)
+ updated_tagnames = set(t for t in tagnames.split(' ') if t)
+ modified_tags = []
+
+ removed_tags = [t for t in current_tags
+ if t.name not in updated_tagnames]
+ if removed_tags:
+ modified_tags.extend(removed_tags)
+ question.tags.remove(*removed_tags)
+
+ added_tagnames = updated_tagnames - current_tagnames
+ if added_tagnames:
+ added_tags = Tag.objects.get_or_create_multiple(added_tagnames,
+ user)
+ modified_tags.extend(added_tags)
+ question.tags.add(*added_tags)
+
+ if modified_tags:
+ Tag.objects.update_use_counts(modified_tags)
+ return True
+
+ return False
+
+ def update_answer_count(self, question):
+ """
+ Executes an UPDATE query to update denormalised data with the
+ number of answers the given question has.
+ """
+
+ # for some reasons, this Answer class failed to be imported,
+ # although we have imported all classes from models on top.
+ from models import Answer
+ self.filter(id=question.id).update(
+ answer_count=Answer.objects.get_answers_from_question(question).filter(deleted=False).count())
+
+ def update_view_count(self, question):
+ """
+ update counter+1 when user browse question page
+ """
+ self.filter(id=question.id).update(view_count = question.view_count + 1)
+
+ def update_favorite_count(self, question):
+ """
+ update favourite_count for given question
+ """
+ from models import FavoriteQuestion
+ self.filter(id=question.id).update(favourite_count = FavoriteQuestion.objects.filter(question=question).count())
+
+ def get_similar_questions(self, question):
+ """
+ Get 10 similar questions for given one.
+ This will search the same tag list for give question(by exactly same string) first.
+ Questions with the individual tags will be added to list if above questions are not full.
+ """
+ #print datetime.datetime.now()
+ questions = list(self.filter(tagnames = question.tagnames, deleted=False).all())
+
+ tags_list = question.tags.all()
+ for tag in tags_list:
+ extend_questions = self.filter(tags__id = tag.id, deleted=False)[:50]
+ for item in extend_questions:
+ if item not in questions and len(questions) < 10:
+ questions.append(item)
+
+ #print datetime.datetime.now()
+ return questions
+
+class Question(Content, DeletableContent):
+ title = models.CharField(max_length=300)
+ tags = models.ManyToManyField('Tag', related_name='questions')
+ answer_accepted = models.BooleanField(default=False)
+ closed = models.BooleanField(default=False)
+ closed_by = models.ForeignKey(User, null=True, blank=True, related_name='closed_questions')
+ closed_at = models.DateTimeField(null=True, blank=True)
+ close_reason = models.SmallIntegerField(choices=CLOSE_REASONS, null=True, blank=True)
+ followed_by = models.ManyToManyField(User, related_name='followed_questions')
+
+ # Denormalised data
+ answer_count = models.PositiveIntegerField(default=0)
+ view_count = models.PositiveIntegerField(default=0)
+ favourite_count = models.PositiveIntegerField(default=0)
+ last_activity_at = models.DateTimeField(default=datetime.datetime.now)
+ last_activity_by = models.ForeignKey(User, related_name='last_active_in_questions')
+ tagnames = models.CharField(max_length=125)
+ summary = models.CharField(max_length=180)
+
+ objects = QuestionManager()
+
+ class Meta(Content.Meta):
+ db_table = u'question'
+
+ def delete(self):
+ super(Question, self).delete()
+ try:
+ ping_google()
+ except Exception:
+ logging.debug('problem pinging google did you register you sitemap with google?')
+
+ def save(self, **kwargs):
+ """
+ Overridden to manually manage addition of tags when the object
+ is first saved.
+
+ This is required as we're using ``tagnames`` as the sole means of
+ adding and editing tags.
+ """
+ initial_addition = (self.id is None)
+
+ super(Question, self).save(**kwargs)
+
+ if initial_addition:
+ tags = Tag.objects.get_or_create_multiple(self.tagname_list(),
+ self.author)
+ self.tags.add(*tags)
+ Tag.objects.update_use_counts(tags)
+
+ def tagname_list(self):
+ """Creates a list of Tag names from the ``tagnames`` attribute."""
+ return [name for name in self.tagnames.split(u' ')]
+
+ def tagname_meta_generator(self):
+ return u','.join([unicode(tag) for tag in self.tagname_list()])
+
+ def get_absolute_url(self):
+ return '%s%s' % (reverse('question', args=[self.id]), django_urlquote(slugify(self.title)))
+
+ def has_favorite_by_user(self, user):
+ if not user.is_authenticated():
+ return False
+
+ from models import FavoriteQuestion
+ return FavoriteQuestion.objects.filter(question=self, user=user).count() > 0
+
+ def get_answer_count_by_user(self, user_id):
+ from models import Answer
+ query_set = Answer.objects.filter(author__id=user_id)
+ return query_set.filter(question=self).count()
+
+ def get_question_title(self):
+ if self.closed:
+ attr = CONST['closed']
+ elif self.deleted:
+ attr = CONST['deleted']
+ else:
+ attr = None
+ if attr is not None:
+ return u'%s %s' % (self.title, attr)
+ else:
+ return self.title
+
+ def get_revision_url(self):
+ return reverse('question_revisions', args=[self.id])
+
+ def get_latest_revision(self):
+ return self.revisions.all()[0]
+
+ def get_last_update_info(self):
+ when, who = self.post_get_last_update_info()
+
+ answers = self.answers.all()
+ if len(answers) > 0:
+ for a in answers:
+ a_when, a_who = a.post_get_last_update_info()
+ if a_when > when:
+ when = a_when
+ who = a_who
+
+ return when, who
+
+ def get_update_summary(self,last_reported_at=None,recipient_email=''):
+ edited = False
+ if self.last_edited_at and self.last_edited_at > last_reported_at:
+ if self.last_edited_by.email != recipient_email:
+ edited = True
+ comments = []
+ for comment in self.comments.all():
+ if comment.added_at > last_reported_at and comment.user.email != recipient_email:
+ comments.append(comment)
+ new_answers = []
+ answer_comments = []
+ modified_answers = []
+ commented_answers = []
+ import sets
+ commented_answers = sets.Set([])
+ for answer in self.answers.all():
+ if (answer.added_at > last_reported_at and answer.author.email != recipient_email):
+ new_answers.append(answer)
+ if (answer.last_edited_at
+ and answer.last_edited_at > last_reported_at
+ and answer.last_edited_by.email != recipient_email):
+ modified_answers.append(answer)
+ for comment in answer.comments.all():
+ if comment.added_at > last_reported_at and comment.user.email != recipient_email:
+ commented_answers.add(answer)
+ answer_comments.append(comment)
+
+ #create the report
+ if edited or new_answers or modified_answers or answer_comments:
+ out = []
+ if edited:
+ out.append(_('%(author)s modified the question') % {'author':self.last_edited_by.username})
+ if new_answers:
+ names = sets.Set(map(lambda x: x.author.username,new_answers))
+ people = ', '.join(names)
+ out.append(_('%(people)s posted %(new_answer_count)s new answers') \
+ % {'new_answer_count':len(new_answers),'people':people})
+ if comments:
+ names = sets.Set(map(lambda x: x.user.username,comments))
+ people = ', '.join(names)
+ out.append(_('%(people)s commented the question') % {'people':people})
+ if answer_comments:
+ names = sets.Set(map(lambda x: x.user.username,answer_comments))
+ people = ', '.join(names)
+ if len(commented_answers) > 1:
+ out.append(_('%(people)s commented answers') % {'people':people})
+ else:
+ out.append(_('%(people)s commented an answer') % {'people':people})
+ url = settings.APP_URL + self.get_absolute_url()
+ retval = '<a href="%s">%s</a>:<br>\n' % (url,self.title)
+ out = map(lambda x: '<li>' + x + '</li>',out)
+ retval += '<ul>' + '\n'.join(out) + '</ul><br>\n'
+ return retval
+ else:
+ return None
+
+ def __unicode__(self):
+ return self.title
+class QuestionView(models.Model):
+ question = models.ForeignKey(Question, related_name='viewed')
+ who = models.ForeignKey(User, related_name='question_views')
+ when = models.DateTimeField()
+
+ class Meta:
+ app_label = 'forum'
+
+class FavoriteQuestion(models.Model):
+ """A favorite Question of a User."""
+ question = models.ForeignKey(Question)
+ user = models.ForeignKey(User, related_name='user_favorite_questions')
+ added_at = models.DateTimeField(default=datetime.datetime.now)
+
+ class Meta:
+ app_label = 'forum'
+ db_table = u'favorite_question'
+ def __unicode__(self):
+ return '[%s] favorited at %s' %(self.user, self.added_at)
+
+class QuestionRevision(ContentRevision):
+ """A revision of a Question."""
+ question = models.ForeignKey(Question, related_name='revisions')
+ title = models.CharField(max_length=300)
+ tagnames = models.CharField(max_length=125)
+
+ class Meta(ContentRevision.Meta):
+ db_table = u'question_revision'
+ ordering = ('-revision',)
+
+ def get_question_title(self):
+ return self.question.title
+
+ def get_absolute_url(self):
+ #print 'in QuestionRevision.get_absolute_url()'
+ return reverse('question_revisions', args=[self.question.id])
+
+ def save(self, **kwargs):
+ """Looks up the next available revision number."""
+ if not self.revision:
+ self.revision = QuestionRevision.objects.filter(
+ question=self.question).values_list('revision',
+ flat=True)[0] + 1
+ super(QuestionRevision, self).save(**kwargs)
+
+ def __unicode__(self):
+ return u'revision %s of %s' % (self.revision, self.title)
+
+class AnonymousQuestion(AnonymousContent):
+ title = models.CharField(max_length=300)
+ tagnames = models.CharField(max_length=125)
+
+ def publish(self,user):
+ added_at = datetime.datetime.now()
+ QuestionManager.create_new(title=self.title, author=user, added_at=added_at,
+ wiki=self.wiki, tagnames=self.tagnames,
+ summary=self.summary, text=self.text)
+ self.delete()
+
+from answer import Answer, AnswerManager
diff --git a/forum/models/repute.py b/forum/models/repute.py
new file mode 100755
index 00000000..768dbee6
--- /dev/null
+++ b/forum/models/repute.py
@@ -0,0 +1,107 @@
+from base import *
+
+from django.utils.translation import ugettext as _
+
+class Badge(models.Model):
+ """Awarded for notable actions performed on the site by Users."""
+ GOLD = 1
+ SILVER = 2
+ BRONZE = 3
+ TYPE_CHOICES = (
+ (GOLD, _('gold')),
+ (SILVER, _('silver')),
+ (BRONZE, _('bronze')),
+ )
+
+ name = models.CharField(max_length=50)
+ type = models.SmallIntegerField(choices=TYPE_CHOICES)
+ slug = models.SlugField(max_length=50, blank=True)
+ description = models.CharField(max_length=300)
+ multiple = models.BooleanField(default=False)
+ # Denormalised data
+ awarded_count = models.PositiveIntegerField(default=0)
+
+ class Meta:
+ app_label = 'forum'
+ db_table = u'badge'
+ ordering = ('name',)
+ unique_together = ('name', 'type')
+
+ def __unicode__(self):
+ return u'%s: %s' % (self.get_type_display(), self.name)
+
+ def save(self, **kwargs):
+ if not self.slug:
+ self.slug = self.name#slugify(self.name)
+ super(Badge, self).save(**kwargs)
+
+ def get_absolute_url(self):
+ return '%s%s/' % (reverse('badge', args=[self.id]), self.slug)
+
+class AwardManager(models.Manager):
+ def get_recent_awards(self):
+ awards = super(AwardManager, self).extra(
+ select={'badge_id': 'badge.id', 'badge_name':'badge.name',
+ 'badge_description': 'badge.description', 'badge_type': 'badge.type',
+ 'user_id': 'auth_user.id', 'user_name': 'auth_user.username'
+ },
+ tables=['award', 'badge', 'auth_user'],
+ order_by=['-awarded_at'],
+ where=['auth_user.id=award.user_id AND badge_id=badge.id'],
+ ).values('badge_id', 'badge_name', 'badge_description', 'badge_type', 'user_id', 'user_name')
+ return awards
+
+class Award(models.Model):
+ """The awarding of a Badge to a User."""
+ user = models.ForeignKey(User, related_name='award_user')
+ badge = models.ForeignKey('Badge', related_name='award_badge')
+ content_type = models.ForeignKey(ContentType)
+ object_id = models.PositiveIntegerField()
+ content_object = generic.GenericForeignKey('content_type', 'object_id')
+ awarded_at = models.DateTimeField(default=datetime.datetime.now)
+ notified = models.BooleanField(default=False)
+
+ objects = AwardManager()
+
+ def __unicode__(self):
+ return u'[%s] is awarded a badge [%s] at %s' % (self.user.username, self.badge.name, self.awarded_at)
+
+ class Meta:
+ app_label = 'forum'
+ db_table = u'award'
+
+class ReputeManager(models.Manager):
+ def get_reputation_by_upvoted_today(self, user):
+ """
+ For one user in one day, he can only earn rep till certain score (ep. +200)
+ by upvoted(also substracted from upvoted canceled). This is because we need
+ to prohibit gaming system by upvoting/cancel again and again.
+ """
+ if user is not None:
+ today = datetime.date.today()
+ sums = self.filter(models.Q(reputation_type=1) | models.Q(reputation_type=-8),
+ user=user, reputed_at__range=(today, today + datetime.timedelta(1))). \
+ agregate(models.Sum('positive'), models.SUM('negative'))
+
+ return sums['positive__sum'] + sums['negative__sum']
+ else:
+ return 0
+
+class Repute(models.Model):
+ """The reputation histories for user"""
+ user = models.ForeignKey(User)
+ positive = models.SmallIntegerField(default=0)
+ negative = models.SmallIntegerField(default=0)
+ question = models.ForeignKey('Question')
+ reputed_at = models.DateTimeField(default=datetime.datetime.now)
+ reputation_type = models.SmallIntegerField(choices=TYPE_REPUTATION)
+ reputation = models.IntegerField(default=1)
+
+ objects = ReputeManager()
+
+ def __unicode__(self):
+ return u'[%s]\' reputation changed at %s' % (self.user.username, self.reputed_at)
+
+ class Meta:
+ app_label = 'forum'
+ db_table = u'repute' \ No newline at end of file
diff --git a/forum/models/tag.py b/forum/models/tag.py
new file mode 100755
index 00000000..28b9e572
--- /dev/null
+++ b/forum/models/tag.py
@@ -0,0 +1,85 @@
+from base import *
+
+from django.utils.translation import ugettext as _
+
+class TagManager(models.Manager):
+ UPDATE_USED_COUNTS_QUERY = (
+ 'UPDATE tag '
+ 'SET used_count = ('
+ 'SELECT COUNT(*) FROM question_tags '
+ 'INNER JOIN question ON question_id=question.id '
+ 'WHERE tag_id = tag.id AND question.deleted=False'
+ ') '
+ 'WHERE id IN (%s)')
+
+ def get_valid_tags(self, page_size):
+ tags = self.all().filter(deleted=False).exclude(used_count=0).order_by("-id")[:page_size]
+ return tags
+
+ def get_or_create_multiple(self, names, user):
+ """
+ Fetches a list of Tags with the given names, creating any Tags
+ which don't exist when necesssary.
+ """
+ tags = list(self.filter(name__in=names))
+ #Set all these tag visible
+ for tag in tags:
+ if tag.deleted:
+ tag.deleted = False
+ tag.deleted_by = None
+ tag.deleted_at = None
+ tag.save()
+
+ if len(tags) < len(names):
+ existing_names = set(tag.name for tag in tags)
+ new_names = [name for name in names if name not in existing_names]
+ tags.extend([self.create(name=name, created_by=user)
+ for name in new_names if self.filter(name=name).count() == 0 and len(name.strip()) > 0])
+
+ return tags
+
+ def update_use_counts(self, tags):
+ """Updates the given Tags with their current use counts."""
+ if not tags:
+ return
+ cursor = connection.cursor()
+ query = self.UPDATE_USED_COUNTS_QUERY % ','.join(['%s'] * len(tags))
+ cursor.execute(query, [tag.id for tag in tags])
+ transaction.commit_unless_managed()
+
+ def get_tags_by_questions(self, questions):
+ question_ids = []
+ for question in questions:
+ question_ids.append(question.id)
+
+ question_ids_str = ','.join([str(id) for id in question_ids])
+ related_tags = self.extra(
+ tables=['tag', 'question_tags'],
+ where=["tag.id = question_tags.tag_id AND question_tags.question_id IN (" + question_ids_str + ")"]
+ ).distinct()
+
+ return related_tags
+
+class Tag(DeletableContent):
+ name = models.CharField(max_length=255, unique=True)
+ created_by = models.ForeignKey(User, related_name='created_tags')
+ # Denormalised data
+ used_count = models.PositiveIntegerField(default=0)
+
+ objects = TagManager()
+
+ class Meta(DeletableContent.Meta):
+ db_table = u'tag'
+ ordering = ('-used_count', 'name')
+
+ def __unicode__(self):
+ return self.name
+
+class MarkedTag(models.Model):
+ TAG_MARK_REASONS = (('good',_('interesting')),('bad',_('ignored')))
+ tag = models.ForeignKey('Tag', related_name='user_selections')
+ user = models.ForeignKey(User, related_name='tag_selections')
+ reason = models.CharField(max_length=16, choices=TAG_MARK_REASONS)
+
+ class Meta:
+ app_label = 'forum' \ No newline at end of file
diff --git a/forum/models/user.py b/forum/models/user.py
new file mode 100755
index 00000000..4e1a376d
--- /dev/null
+++ b/forum/models/user.py
@@ -0,0 +1,67 @@
+from base import *
+
+from django.utils.translation import ugettext as _
+
+class Activity(models.Model):
+ """
+ We keep some history data for user activities
+ """
+ user = models.ForeignKey(User)
+ activity_type = models.SmallIntegerField(choices=TYPE_ACTIVITY)
+ active_at = models.DateTimeField(default=datetime.datetime.now)
+ content_type = models.ForeignKey(ContentType)
+ object_id = models.PositiveIntegerField()
+ content_object = generic.GenericForeignKey('content_type', 'object_id')
+ is_auditted = models.BooleanField(default=False)
+
+ def __unicode__(self):
+ return u'[%s] was active at %s' % (self.user.username, self.active_at)
+
+ class Meta:
+ app_label = 'forum'
+ db_table = u'activity'
+
+class EmailFeedSetting(models.Model):
+ DELTA_TABLE = {
+ 'w':datetime.timedelta(7),
+ 'd':datetime.timedelta(1),
+ 'n':datetime.timedelta(-1),
+ }
+ FEED_TYPES = (
+ ('q_all',_('Entire forum')),
+ ('q_ask',_('Questions that I asked')),
+ ('q_ans',_('Questions that I answered')),
+ ('q_sel',_('Individually selected questions')),
+ )
+ UPDATE_FREQUENCY = (
+ ('w',_('Weekly')),
+ ('d',_('Daily')),
+ ('n',_('No email')),
+ )
+ subscriber = models.ForeignKey(User)
+ feed_type = models.CharField(max_length=16,choices=FEED_TYPES)
+ frequency = models.CharField(max_length=8,choices=UPDATE_FREQUENCY,default='n')
+ added_at = models.DateTimeField(auto_now_add=True)
+ reported_at = models.DateTimeField(null=True)
+
+ def save(self,*args,**kwargs):
+ type = self.feed_type
+ subscriber = self.subscriber
+ similar = self.__class__.objects.filter(feed_type=type,subscriber=subscriber).exclude(pk=self.id)
+ if len(similar) > 0:
+ raise IntegrityError('email feed setting already exists')
+ super(EmailFeedSetting,self).save(*args,**kwargs)
+
+ class Meta:
+ app_label = 'forum'
+
+class AnonymousEmail(models.Model):
+ #validation key, if used
+ key = models.CharField(max_length=32)
+ email = models.EmailField(null=False,unique=True)
+ isvalid = models.BooleanField(default=False)
+
+ class Meta:
+ app_label = 'forum'
+
+
diff --git a/forum/urls.py b/forum/urls.py
index a045b027..219364cb 100644
--- a/forum/urls.py
+++ b/forum/urls.py
@@ -84,9 +84,9 @@ urlpatterns = patterns('',
url(r'^%s(.*)' % _('nimda/'), admin.site.root, name='osqa_admin'),
url(r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed', {'feed_dict': feeds}, name='feeds'),
url(r'^%s$' % _('upload/'), app.writers.upload, name='upload'),
- url(r'^%s$' % _('books/'), app.books.books, name='books'),
- url(r'^%s%s(?P<short_name>[^/]+)/$' % (_('books/'), _('ask/')), app.books.ask_book, name='ask_book'),
- url(r'^%s(?P<short_name>[^/]+)/$' % _('books/'), app.books.book, name='book'),
+ #url(r'^%s$' % _('books/'), app.books.books, name='books'),
+ #url(r'^%s%s(?P<short_name>[^/]+)/$' % (_('books/'), _('ask/')), app.books.ask_book, name='ask_book'),
+ #url(r'^%s(?P<short_name>[^/]+)/$' % _('books/'), app.books.book, name='book'),
url(r'^%s$' % _('search/'), app.readers.search, name='search'),
url(r'^%s$' % _('feedback/'), app.meta.feedback, name='feedback'),
(r'^%sfb/' % _('account/'), include('fbconnect.urls')),
diff --git a/forum/views/__init__.py b/forum/views/__init__.py
index 8b82d161..faea1179 100644
--- a/forum/views/__init__.py
+++ b/forum/views/__init__.py
@@ -3,4 +3,4 @@ import writers
import commands
import users
import meta
-import books
+#import books
diff --git a/forum/views/content.py b/forum/views/content.py
new file mode 100644
index 00000000..9506fe3a
--- /dev/null
+++ b/forum/views/content.py
@@ -0,0 +1,1394 @@
+# encoding:utf-8
+import os.path
+import time, datetime, calendar, random
+import logging
+from urllib import quote, unquote
+from django.conf import settings
+from django.core.files.storage import default_storage
+from django.shortcuts import render_to_response, get_object_or_404
+from django.contrib.auth.decorators import login_required
+from django.http import HttpResponseRedirect, HttpResponse, HttpResponseForbidden, Http404
+from django.core.paginator import Paginator, EmptyPage, InvalidPage
+from django.template import RequestContext, loader
+from django.utils.html import *
+from django.utils import simplejson
+from django.core import serializers
+from django.db import transaction
+from django.db.models import Count, Q
+from django.contrib.contenttypes.models import ContentType
+from django.utils.translation import ugettext as _
+from django.utils.datastructures import SortedDict
+from django.template.defaultfilters import slugify
+from django.core.exceptions import PermissionDenied
+from django.core.urlresolvers import reverse
+
+from utils.html import sanitize_html
+from utils.decorators import ajax_method, ajax_login_required
+from markdown2 import Markdown
+#from lxml.html.diff import htmldiff
+from forum.diff import textDiff as htmldiff
+from forum.forms import *
+from forum.models import *
+from forum.auth import *
+from forum.const import *
+from forum import auth
+from utils.forms import get_next_url
+
+# used in index page
+INDEX_PAGE_SIZE = 20
+INDEX_AWARD_SIZE = 15
+INDEX_TAGS_SIZE = 100
+# used in tags list
+DEFAULT_PAGE_SIZE = 60
+# used in questions
+QUESTIONS_PAGE_SIZE = 10
+# used in answers
+ANSWERS_PAGE_SIZE = 10
+
+markdowner = Markdown(html4tags=True)
+
+#system to display main content
+def _get_tags_cache_json():#service routine
+ """returns list of all tags in json format
+ no caching yet, actually
+ """
+ tags = Tag.objects.filter(deleted=False).all()
+ tags_list = []
+ for tag in tags:
+ dic = {'n': tag.name, 'c': tag.used_count}
+ tags_list.append(dic)
+ tags = simplejson.dumps(tags_list)
+ return tags
+
+def _get_and_remember_questions_sort_method(request, view_dic, default):#service routine
+ """manages persistence of post sort order
+ it is assumed that when user wants newest question -
+ then he/she wants newest answers as well, etc.
+ how far should this assumption actually go - may be a good question
+ """
+ if default not in view_dic:
+ raise Exception('default value must be in view_dic')
+
+ q_sort_method = request.REQUEST.get('sort', None)
+ if q_sort_method == None:
+ q_sort_method = request.session.get('questions_sort_method', default)
+
+ if q_sort_method not in view_dic:
+ q_sort_method = default
+ request.session['questions_sort_method'] = q_sort_method
+ return q_sort_method, view_dic[q_sort_method]
+
+#refactor? - we have these
+#views that generate a listing of questions in one way or another:
+#index, unanswered, questions, search, tag
+#should we dry them up?
+#related topics - information drill-down, search refinement
+
+def index(request):#generates front page - shows listing of questions sorted in various ways
+ """index view mapped to the root url of the Q&A site
+ """
+ view_dic = {
+ "latest":"-last_activity_at",
+ "hottest":"-answer_count",
+ "mostvoted":"-score",
+ }
+ view_id, orderby = _get_and_remember_questions_sort_method(request, view_dic, 'latest')
+
+ page_size = request.session.get('pagesize', QUESTIONS_PAGE_SIZE)
+ questions = Question.objects.exclude(deleted=True).order_by(orderby)[:page_size]
+ # RISK - inner join queries
+ questions = questions.select_related()
+ tags = Tag.objects.get_valid_tags(INDEX_TAGS_SIZE)
+
+ awards = Award.objects.get_recent_awards()
+
+ (interesting_tag_names, ignored_tag_names) = (None, None)
+ if request.user.is_authenticated():
+ pt = MarkedTag.objects.filter(user=request.user)
+ interesting_tag_names = pt.filter(reason='good').values_list('tag__name', flat=True)
+ ignored_tag_names = pt.filter(reason='bad').values_list('tag__name', flat=True)
+
+ tags_autocomplete = _get_tags_cache_json()
+
+ return render_to_response('index.html', {
+ 'interesting_tag_names': interesting_tag_names,
+ 'tags_autocomplete': tags_autocomplete,
+ 'ignored_tag_names': ignored_tag_names,
+ "questions" : questions,
+ "tab_id" : view_id,
+ "tags" : tags,
+ "awards" : awards[:INDEX_AWARD_SIZE],
+ }, context_instance=RequestContext(request))
+
+def unanswered(request):#generates listing of unanswered questions
+ return questions(request, unanswered=True)
+
+def questions(request, tagname=None, unanswered=False):#a view generating listing of questions, used by 'unanswered' too
+ """
+ List of Questions, Tagged questions, and Unanswered questions.
+ """
+ # template file
+ # "questions.html" or maybe index.html in the future
+ template_file = "questions.html"
+ # Set flag to False by default. If it is equal to True, then need to be saved.
+ pagesize_changed = False
+ # get pagesize from session, if failed then get default value
+ pagesize = request.session.get("pagesize",10)
+ try:
+ page = int(request.GET.get('page', '1'))
+ except ValueError:
+ page = 1
+
+ view_dic = {"latest":"-added_at", "active":"-last_activity_at", "hottest":"-answer_count", "mostvoted":"-score" }
+ view_id, orderby = _get_and_remember_questions_sort_method(request,view_dic,'latest')
+
+ # check if request is from tagged questions
+ qs = Question.objects.exclude(deleted=True)
+
+ if tagname is not None:
+ qs = qs.filter(tags__name = unquote(tagname))
+
+ if unanswered:
+ qs = qs.exclude(answer_accepted=True)
+
+ author_name = None
+ #user contributed questions & answers
+ if 'user' in request.GET:
+ try:
+ author_name = request.GET['user']
+ u = User.objects.get(username=author_name)
+ qs = qs.filter(Q(author=u) | Q(answers__author=u))
+ except User.DoesNotExist:
+ author_name = None
+
+ if request.user.is_authenticated():
+ uid_str = str(request.user.id)
+ qs = qs.extra(
+ select = SortedDict([
+ (
+ 'interesting_score',
+ 'SELECT COUNT(1) FROM forum_markedtag, question_tags '
+ + 'WHERE forum_markedtag.user_id = %s '
+ + 'AND forum_markedtag.tag_id = question_tags.tag_id '
+ + 'AND forum_markedtag.reason = \'good\' '
+ + 'AND question_tags.question_id = question.id'
+ ),
+ ]),
+ select_params = (uid_str,),
+ )
+ if request.user.hide_ignored_questions:
+ ignored_tags = Tag.objects.filter(user_selections__reason='bad',
+ user_selections__user = request.user)
+ qs = qs.exclude(tags__in=ignored_tags)
+ else:
+ qs = qs.extra(
+ select = SortedDict([
+ (
+ 'ignored_score',
+ 'SELECT COUNT(1) FROM forum_markedtag, question_tags '
+ + 'WHERE forum_markedtag.user_id = %s '
+ + 'AND forum_markedtag.tag_id = question_tags.tag_id '
+ + 'AND forum_markedtag.reason = \'bad\' '
+ + 'AND question_tags.question_id = question.id'
+ )
+ ]),
+ select_params = (uid_str, )
+ )
+
+ qs = qs.select_related(depth=1).order_by(orderby)
+
+ objects_list = Paginator(qs, pagesize)
+ questions = objects_list.page(page)
+
+ # Get related tags from this page objects
+ if questions.object_list.count() > 0:
+ related_tags = Tag.objects.get_tags_by_questions(questions.object_list)
+ else:
+ related_tags = None
+ tags_autocomplete = _get_tags_cache_json()
+
+ # get the list of interesting and ignored tags
+ (interesting_tag_names, ignored_tag_names) = (None, None)
+ if request.user.is_authenticated():
+ pt = MarkedTag.objects.filter(user=request.user)
+ interesting_tag_names = pt.filter(reason='good').values_list('tag__name', flat=True)
+ ignored_tag_names = pt.filter(reason='bad').values_list('tag__name', flat=True)
+
+ return render_to_response(template_file, {
+ "questions" : questions,
+ "author_name" : author_name,
+ "tab_id" : view_id,
+ "questions_count" : objects_list.count,
+ "tags" : related_tags,
+ "tags_autocomplete" : tags_autocomplete,
+ "searchtag" : tagname,
+ "is_unanswered" : unanswered,
+ "interesting_tag_names": interesting_tag_names,
+ 'ignored_tag_names': ignored_tag_names,
+ "context" : {
+ 'is_paginated' : True,
+ 'pages': objects_list.num_pages,
+ 'page': page,
+ 'has_previous': questions.has_previous(),
+ 'has_next': questions.has_next(),
+ 'previous': questions.previous_page_number(),
+ 'next': questions.next_page_number(),
+ 'base_url' : request.path + '?sort=%s&' % view_id,
+ 'pagesize' : pagesize
+ }}, context_instance=RequestContext(request))
+
+def search(request): #generates listing of questions matching a search query - including tags and just words
+ """generates listing of questions matching a search query
+ supports full text search in mysql db using sphinx and internally in postgresql
+ falls back on simple partial string matching approach if
+ full text search function is not available
+ """
+ if request.method == "GET":
+ keywords = request.GET.get("q")
+ search_type = request.GET.get("t")
+ try:
+ page = int(request.GET.get('page', '1'))
+ except ValueError:
+ page = 1
+ if keywords is None:
+ return HttpResponseRedirect(reverse(index))
+ if search_type == 'tag':
+ return HttpResponseRedirect(reverse('tags') + '?q=%s&page=%s' % (keywords.strip(), page))
+ elif search_type == "user":
+ return HttpResponseRedirect(reverse('users') + '?q=%s&page=%s' % (keywords.strip(), page))
+ elif search_type == "question":
+
+ template_file = "questions.html"
+ # Set flag to False by default. If it is equal to True, then need to be saved.
+ pagesize_changed = False
+ # get pagesize from session, if failed then get default value
+ user_page_size = request.session.get("pagesize", QUESTIONS_PAGE_SIZE)
+ # set pagesize equal to logon user specified value in database
+ if request.user.is_authenticated() and request.user.questions_per_page > 0:
+ user_page_size = request.user.questions_per_page
+
+ try:
+ page = int(request.GET.get('page', '1'))
+ # get new pagesize from UI selection
+ pagesize = int(request.GET.get('pagesize', user_page_size))
+ if pagesize <> user_page_size:
+ pagesize_changed = True
+
+ except ValueError:
+ page = 1
+ pagesize = user_page_size
+
+ # save this pagesize to user database
+ if pagesize_changed:
+ request.session["pagesize"] = pagesize
+ if request.user.is_authenticated():
+ user = request.user
+ user.questions_per_page = pagesize
+ user.save()
+
+ view_id = request.GET.get('sort', None)
+ view_dic = {"latest":"-added_at", "active":"-last_activity_at", "hottest":"-answer_count", "mostvoted":"-score" }
+ try:
+ orderby = view_dic[view_id]
+ except KeyError:
+ view_id = "latest"
+ orderby = "-added_at"
+
+ if settings.USE_PG_FTS:
+ objects = Question.objects.filter(deleted=False).extra(
+ select={
+ 'ranking': "ts_rank_cd(tsv, plainto_tsquery(%s), 32)",
+ },
+ where=["tsv @@ plainto_tsquery(%s)"],
+ params=[keywords],
+ select_params=[keywords]
+ ).order_by('-ranking')
+
+ elif settings.USE_SPHINX_SEARCH == True:
+ #search index is now free of delete questions and answers
+ #so there is not "antideleted" filtering here
+ objects = Question.search.query(keywords)
+ #no related selection either because we're relying on full text search here
+ else:
+ objects = Question.objects.filter(deleted=False).extra(where=['title like %s'], params=['%' + keywords + '%']).order_by(orderby)
+ # RISK - inner join queries
+ objects = objects.select_related();
+
+ objects_list = Paginator(objects, pagesize)
+ questions = objects_list.page(page)
+
+ # Get related tags from this page objects
+ related_tags = []
+ for question in questions.object_list:
+ tags = list(question.tags.all())
+ for tag in tags:
+ if tag not in related_tags:
+ related_tags.append(tag)
+
+ #if is_search is true in the context, prepend this string to soting tabs urls
+ search_uri = "?q=%s&page=%d&t=question" % ("+".join(keywords.split()), page)
+
+ return render_to_response(template_file, {
+ "questions" : questions,
+ "tab_id" : view_id,
+ "questions_count" : objects_list.count,
+ "tags" : related_tags,
+ "searchtag" : None,
+ "searchtitle" : keywords,
+ "keywords" : keywords,
+ "is_unanswered" : False,
+ "is_search": True,
+ "search_uri": search_uri,
+ "context" : {
+ 'is_paginated' : True,
+ 'pages': objects_list.num_pages,
+ 'page': page,
+ 'has_previous': questions.has_previous(),
+ 'has_next': questions.has_next(),
+ 'previous': questions.previous_page_number(),
+ 'next': questions.next_page_number(),
+ 'base_url' : request.path + '?t=question&q=%s&sort=%s&' % (keywords, view_id),
+ 'pagesize' : pagesize
+ }}, context_instance=RequestContext(request))
+
+ else:
+ raise Http404
+
+def tag(request, tag):#generates listing of questions tagged with a single tag
+ return questions(request, tagname=tag)
+
+def tags(request):#view showing a listing of available tags - plain list
+ stag = ""
+ is_paginated = True
+ sortby = request.GET.get('sort', 'used')
+ try:
+ page = int(request.GET.get('page', '1'))
+ except ValueError:
+ page = 1
+
+ if request.method == "GET":
+ stag = request.GET.get("q", "").strip()
+ if stag != '':
+ objects_list = Paginator(Tag.objects.filter(deleted=False).exclude(used_count=0).extra(where=['name like %s'], params=['%' + stag + '%']), DEFAULT_PAGE_SIZE)
+ else:
+ if sortby == "name":
+ objects_list = Paginator(Tag.objects.all().filter(deleted=False).exclude(used_count=0).order_by("name"), DEFAULT_PAGE_SIZE)
+ else:
+ objects_list = Paginator(Tag.objects.all().filter(deleted=False).exclude(used_count=0).order_by("-used_count"), DEFAULT_PAGE_SIZE)
+
+ try:
+ tags = objects_list.page(page)
+ except (EmptyPage, InvalidPage):
+ tags = objects_list.page(objects_list.num_pages)
+
+ return render_to_response('tags.html', {
+ "tags" : tags,
+ "stag" : stag,
+ "tab_id" : sortby,
+ "keywords" : stag,
+ "context" : {
+ 'is_paginated' : is_paginated,
+ 'pages': objects_list.num_pages,
+ 'page': page,
+ 'has_previous': tags.has_previous(),
+ 'has_next': tags.has_next(),
+ 'previous': tags.previous_page_number(),
+ 'next': tags.next_page_number(),
+ 'base_url' : reverse('tags') + '?sort=%s&' % sortby
+ }
+ }, context_instance=RequestContext(request))
+
+def question(request, id):#refactor - long subroutine. display question body, answers and comments
+ """view that displays body of the question and
+ all answers to it
+ """
+ try:
+ page = int(request.GET.get('page', '1'))
+ except ValueError:
+ page = 1
+
+ view_id = request.GET.get('sort', None)
+ view_dic = {"latest":"-added_at", "oldest":"added_at", "votes":"-score" }
+ try:
+ orderby = view_dic[view_id]
+ except KeyError:
+ qsm = request.session.get('questions_sort_method',None)
+ if qsm in ('mostvoted','latest'):
+ logging.debug('loaded from session ' + qsm)
+ if qsm == 'mostvoted':
+ view_id = 'votes'
+ orderby = '-score'
+ else:
+ view_id = 'latest'
+ orderby = '-added_at'
+ else:
+ view_id = "votes"
+ orderby = "-score"
+
+ logging.debug('view_id=' + str(view_id))
+
+ question = get_object_or_404(Question, id=id)
+ try:
+ pattern = r'/%s%s%d/([\w-]+)' % (settings.FORUM_SCRIPT_ALIAS,_('question/'), question.id)
+ path_re = re.compile(pattern)
+ logging.debug(pattern)
+ logging.debug(request.path)
+ m = path_re.match(request.path)
+ if m:
+ slug = m.group(1)
+ logging.debug('have slug %s' % slug)
+ assert(slug == slugify(question.title))
+ else:
+ logging.debug('no match!')
+ except:
+ return HttpResponseRedirect(question.get_absolute_url())
+
+ if question.deleted and not auth.can_view_deleted_post(request.user, question):
+ raise Http404
+ answer_form = AnswerForm(question,request.user)
+ answers = Answer.objects.get_answers_from_question(question, request.user)
+ answers = answers.select_related(depth=1)
+
+ favorited = question.has_favorite_by_user(request.user)
+ if request.user.is_authenticated():
+ question_vote = question.votes.select_related().filter(user=request.user)
+ else:
+ question_vote = None #is this correct?
+ if question_vote is not None and question_vote.count() > 0:
+ question_vote = question_vote[0]
+
+ user_answer_votes = {}
+ for answer in answers:
+ vote = answer.get_user_vote(request.user)
+ if vote is not None and not user_answer_votes.has_key(answer.id):
+ vote_value = -1
+ if vote.is_upvote():
+ vote_value = 1
+ user_answer_votes[answer.id] = vote_value
+
+ if answers is not None:
+ answers = answers.order_by("-accepted", orderby)
+
+ filtered_answers = []
+ for answer in answers:
+ if answer.deleted == True:
+ if answer.author_id == request.user.id:
+ filtered_answers.append(answer)
+ else:
+ filtered_answers.append(answer)
+
+ objects_list = Paginator(filtered_answers, ANSWERS_PAGE_SIZE)
+ page_objects = objects_list.page(page)
+
+ #todo: merge view counts per user and per session
+ #1) view count per session
+ update_view_count = False
+ if 'question_view_times' not in request.session:
+ request.session['question_view_times'] = {}
+
+ last_seen = request.session['question_view_times'].get(question.id,None)
+ updated_when, updated_who = question.get_last_update_info()
+
+ if updated_who != request.user:
+ if last_seen:
+ if last_seen < updated_when:
+ update_view_count = True
+ else:
+ update_view_count = True
+
+ request.session['question_view_times'][question.id] = datetime.datetime.now()
+
+ if update_view_count:
+ question.view_count += 1
+ question.save()
+
+ #2) question view count per user
+ if request.user.is_authenticated():
+ try:
+ question_view = QuestionView.objects.get(who=request.user, question=question)
+ except QuestionView.DoesNotExist:
+ question_view = QuestionView(who=request.user, question=question)
+ question_view.when = datetime.datetime.now()
+ question_view.save()
+
+ return render_to_response('question.html', {
+ "question" : question,
+ "question_vote" : question_vote,
+ "question_comment_count":question.comments.count(),
+ "answer" : answer_form,
+ "answers" : page_objects.object_list,
+ "user_answer_votes": user_answer_votes,
+ "tags" : question.tags.all(),
+ "tab_id" : view_id,
+ "favorited" : favorited,
+ "similar_questions" : Question.objects.get_similar_questions(question),
+ "context" : {
+ 'is_paginated' : True,
+ 'pages': objects_list.num_pages,
+ 'page': page,
+ 'has_previous': page_objects.has_previous(),
+ 'has_next': page_objects.has_next(),
+ 'previous': page_objects.previous_page_number(),
+ 'next': page_objects.next_page_number(),
+ 'base_url' : request.path + '?sort=%s&' % view_id,
+ 'extend_url' : "#sort-top"
+ }
+ }, context_instance=RequestContext(request))
+
+QUESTION_REVISION_TEMPLATE = ('<h1>%(title)s</h1>\n'
+ '<div class="text">%(html)s</div>\n'
+ '<div class="tags">%(tags)s</div>')
+def question_revisions(request, id):
+ post = get_object_or_404(Question, id=id)
+ revisions = list(post.revisions.all())
+ revisions.reverse()
+ for i, revision in enumerate(revisions):
+ revision.html = QUESTION_REVISION_TEMPLATE % {
+ 'title': revision.title,
+ 'html': sanitize_html(markdowner.convert(revision.text)),
+ 'tags': ' '.join(['<a class="post-tag">%s</a>' % tag
+ for tag in revision.tagnames.split(' ')]),
+ }
+ if i > 0:
+ revisions[i].diff = htmldiff(revisions[i-1].html, revision.html)
+ else:
+ revisions[i].diff = QUESTION_REVISION_TEMPLATE % {
+ 'title': revisions[0].title,
+ 'html': sanitize_html(markdowner.convert(revisions[0].text)),
+ 'tags': ' '.join(['<a class="post-tag">%s</a>' % tag
+ for tag in revisions[0].tagnames.split(' ')]),
+ }
+ revisions[i].summary = _('initial version')
+ return render_to_response('revisions_question.html', {
+ 'post': post,
+ 'revisions': revisions,
+ }, context_instance=RequestContext(request))
+
+ANSWER_REVISION_TEMPLATE = ('<div class="text">%(html)s</div>')
+def answer_revisions(request, id):
+ post = get_object_or_404(Answer, id=id)
+ revisions = list(post.revisions.all())
+ revisions.reverse()
+ for i, revision in enumerate(revisions):
+ revision.html = ANSWER_REVISION_TEMPLATE % {
+ 'html': sanitize_html(markdowner.convert(revision.text))
+ }
+ if i > 0:
+ revisions[i].diff = htmldiff(revisions[i-1].html, revision.html)
+ else:
+ revisions[i].diff = revisions[i].text
+ revisions[i].summary = _('initial version')
+ return render_to_response('revisions_answer.html', {
+ 'post': post,
+ 'revisions': revisions,
+ }, context_instance=RequestContext(request))
+#system to collect user actions and change content and store in the database
+def create_new_answer( question=None, author=None, #service subroutine - refactor
+ added_at=None, wiki=False,\
+ text='', email_notify=False):
+ """refactor
+ non-view subroutine
+ initializes the answer and revision
+ and updates stuff in the corresponding question
+ probably there is more Django-ish way to do it
+ """
+
+ html = sanitize_html(markdowner.convert(text))
+
+ #create answer
+ answer = Answer(
+ question = question,
+ author = author,
+ added_at = added_at,
+ wiki = wiki,
+ html = html
+ )
+ if answer.wiki:
+ answer.last_edited_by = answer.author
+ answer.last_edited_at = added_at
+ answer.wikified_at = added_at
+
+ answer.save()
+
+ #update question data
+ question.last_activity_at = added_at
+ question.last_activity_by = author
+ question.save()
+ Question.objects.update_answer_count(question)
+
+ #update revision
+ AnswerRevision.objects.create(
+ answer = answer,
+ revision = 1,
+ author = author,
+ revised_at = added_at,
+ summary = CONST['default_version'],
+ text = text
+ )
+
+ #set notification/delete
+ if email_notify:
+ if author not in question.followed_by.all():
+ question.followed_by.add(author)
+ else:
+ #not sure if this is necessary. ajax should take care of this...
+ try:
+ question.followed_by.remove(author)
+ except:
+ pass
+
+def create_new_question(title=None,author=None,added_at=None, #service subroutine - refactor
+ wiki=False,tagnames=None,summary=None,
+ text=None):
+ """refactor
+ this is not a view saves new question and revision
+ and maybe should become one of the methods on Question object?
+ """
+ html = sanitize_html(markdowner.convert(text))
+ question = Question(
+ title = title,
+ author = author,
+ added_at = added_at,
+ last_activity_at = added_at,
+ last_activity_by = author,
+ wiki = wiki,
+ tagnames = tagnames,
+ html = html,
+ summary = summary
+ )
+ if question.wiki:
+ question.last_edited_by = question.author
+ question.last_edited_at = added_at
+ question.wikified_at = added_at
+
+ question.save()
+
+ # create the first revision
+ QuestionRevision.objects.create(
+ question = question,
+ revision = 1,
+ title = question.title,
+ author = author,
+ revised_at = added_at,
+ tagnames = question.tagnames,
+ summary = CONST['default_version'],
+ text = text
+ )
+ return question
+
+def upload(request):#ajax upload file to a question or answer
+ class FileTypeNotAllow(Exception):
+ pass
+ class FileSizeNotAllow(Exception):
+ pass
+ class UploadPermissionNotAuthorized(Exception):
+ pass
+
+ #<result><msg><![CDATA[%s]]></msg><error><![CDATA[%s]]></error><file_url>%s</file_url></result>
+ xml_template = "<result><msg><![CDATA[%s]]></msg><error><![CDATA[%s]]></error><file_url>%s</file_url></result>"
+
+ try:
+ f = request.FILES['file-upload']
+ # check upload permission
+ if not auth.can_upload_files(request.user):
+ raise UploadPermissionNotAuthorized
+
+ # check file type
+ file_name_suffix = os.path.splitext(f.name)[1].lower()
+ if not file_name_suffix in settings.ALLOW_FILE_TYPES:
+ raise FileTypeNotAllow
+
+ # generate new file name
+ new_file_name = str(time.time()).replace('.', str(random.randint(0,100000))) + file_name_suffix
+ # use default storage to store file
+ default_storage.save(new_file_name, f)
+ # check file size
+ # byte
+ size = default_storage.size(new_file_name)
+ if size > settings.ALLOW_MAX_FILE_SIZE:
+ default_storage.delete(new_file_name)
+ raise FileSizeNotAllow
+
+ result = xml_template % ('Good', '', default_storage.url(new_file_name))
+ except UploadPermissionNotAuthorized:
+ result = xml_template % ('', _('uploading images is limited to users with >60 reputation points'), '')
+ except FileTypeNotAllow:
+ result = xml_template % ('', _("allowed file types are 'jpg', 'jpeg', 'gif', 'bmp', 'png', 'tiff'"), '')
+ except FileSizeNotAllow:
+ result = xml_template % ('', _("maximum upload file size is %sK") % settings.ALLOW_MAX_FILE_SIZE / 1024, '')
+ except Exception:
+ result = xml_template % ('', _('Error uploading file. Please contact the site administrator. Thank you. %s' % Exception), '')
+
+ return HttpResponse(result, mimetype="application/xml")
+
+#@login_required #actually you can post anonymously, but then must register
+def ask(request):#view used to ask a new question
+ """a view to ask a new question
+ gives space for q title, body, tags and checkbox for to post as wiki
+
+ user can start posting a question anonymously but then
+ must login/register in order for the question go be shown
+ """
+ if request.method == "POST":
+ form = AskForm(request.POST)
+ if form.is_valid():
+
+ added_at = datetime.datetime.now()
+ title = strip_tags(form.cleaned_data['title'].strip())
+ wiki = form.cleaned_data['wiki']
+ tagnames = form.cleaned_data['tags'].strip()
+ text = form.cleaned_data['text']
+ html = sanitize_html(markdowner.convert(text))
+ summary = strip_tags(html)[:120]
+
+ if request.user.is_authenticated():
+ author = request.user
+
+ question = create_new_question(
+ title = title,
+ author = author,
+ added_at = added_at,
+ wiki = wiki,
+ tagnames = tagnames,
+ summary = summary,
+ text = text
+ )
+
+ return HttpResponseRedirect(question.get_absolute_url())
+ else:
+ request.session.flush()
+ session_key = request.session.session_key
+ question = AnonymousQuestion(
+ session_key = session_key,
+ title = title,
+ tagnames = tagnames,
+ wiki = wiki,
+ text = text,
+ summary = summary,
+ added_at = added_at,
+ ip_addr = request.META['REMOTE_ADDR'],
+ )
+ question.save()
+ return HttpResponseRedirect(reverse('user_signin_new_question'))
+ else:
+ form = AskForm()
+
+ tags = _get_tags_cache_json()
+ return render_to_response('ask.html', {
+ 'form' : form,
+ 'tags' : tags,
+ 'email_validation_faq_url':reverse('faq') + '#validate',
+ }, context_instance=RequestContext(request))
+
+@login_required
+def close(request, id):#close question
+ """view to initiate and process
+ question close
+ """
+ question = get_object_or_404(Question, id=id)
+ if not auth.can_close_question(request.user, question):
+ return HttpResponse('Permission denied.')
+ if request.method == 'POST':
+ form = CloseForm(request.POST)
+ if form.is_valid():
+ reason = form.cleaned_data['reason']
+ question.closed = True
+ question.closed_by = request.user
+ question.closed_at = datetime.datetime.now()
+ question.close_reason = reason
+ question.save()
+ return HttpResponseRedirect(question.get_absolute_url())
+ else:
+ form = CloseForm()
+ return render_to_response('close.html', {
+ 'form' : form,
+ 'question' : question,
+ }, context_instance=RequestContext(request))
+
+@login_required
+def reopen(request, id):#re-open question
+ """view to initiate and process
+ question close
+ """
+ question = get_object_or_404(Question, id=id)
+ # open question
+ if not auth.can_reopen_question(request.user, question):
+ return HttpResponse('Permission denied.')
+ if request.method == 'POST' :
+ Question.objects.filter(id=question.id).update(closed=False,
+ closed_by=None, closed_at=None, close_reason=None)
+ return HttpResponseRedirect(question.get_absolute_url())
+ else:
+ return render_to_response('reopen.html', {
+ 'question' : question,
+ }, context_instance=RequestContext(request))
+
+@login_required
+def edit_question(request, id):#edit or retag a question
+ """view to edit question
+ """
+ question = get_object_or_404(Question, id=id)
+ if question.deleted and not auth.can_view_deleted_post(request.user, question):
+ raise Http404
+ if auth.can_edit_post(request.user, question):
+ return _edit_question(request, question)
+ elif auth.can_retag_questions(request.user):
+ return _retag_question(request, question)
+ else:
+ raise Http404
+
+def _retag_question(request, question):#non-url subview of edit question - just retag
+ """retag question sub-view used by
+ view "edit_question"
+ """
+ if request.method == 'POST':
+ form = RetagQuestionForm(question, request.POST)
+ if form.is_valid():
+ if form.has_changed():
+ latest_revision = question.get_latest_revision()
+ retagged_at = datetime.datetime.now()
+ # Update the Question itself
+ Question.objects.filter(id=question.id).update(
+ tagnames = form.cleaned_data['tags'],
+ last_edited_at = retagged_at,
+ last_edited_by = request.user,
+ last_activity_at = retagged_at,
+ last_activity_by = request.user
+ )
+ # Update the Question's tag associations
+ tags_updated = Question.objects.update_tags(question,
+ form.cleaned_data['tags'], request.user)
+ # Create a new revision
+ QuestionRevision.objects.create(
+ question = question,
+ title = latest_revision.title,
+ author = request.user,
+ revised_at = retagged_at,
+ tagnames = form.cleaned_data['tags'],
+ summary = CONST['retagged'],
+ text = latest_revision.text
+ )
+ # send tags updated singal
+ tags_updated.send(sender=question.__class__, question=question)
+
+ return HttpResponseRedirect(question.get_absolute_url())
+ else:
+ form = RetagQuestionForm(question)
+ return render_to_response('question_retag.html', {
+ 'question': question,
+ 'form' : form,
+ 'tags' : _get_tags_cache_json(),
+ }, context_instance=RequestContext(request))
+
+def _edit_question(request, question):#non-url subview of edit_question - just edit the body/title
+ latest_revision = question.get_latest_revision()
+ revision_form = None
+ if request.method == 'POST':
+ if 'select_revision' in request.POST:
+ # user has changed revistion number
+ revision_form = RevisionForm(question, latest_revision, request.POST)
+ if revision_form.is_valid():
+ # Replace with those from the selected revision
+ form = EditQuestionForm(question,
+ QuestionRevision.objects.get(question=question,
+ revision=revision_form.cleaned_data['revision']))
+ else:
+ form = EditQuestionForm(question, latest_revision, request.POST)
+ else:
+ # Always check modifications against the latest revision
+ form = EditQuestionForm(question, latest_revision, request.POST)
+ if form.is_valid():
+ html = sanitize_html(markdowner.convert(form.cleaned_data['text']))
+ if form.has_changed():
+ edited_at = datetime.datetime.now()
+ tags_changed = (latest_revision.tagnames !=
+ form.cleaned_data['tags'])
+ tags_updated = False
+ # Update the Question itself
+ updated_fields = {
+ 'title': form.cleaned_data['title'],
+ 'last_edited_at': edited_at,
+ 'last_edited_by': request.user,
+ 'last_activity_at': edited_at,
+ 'last_activity_by': request.user,
+ 'tagnames': form.cleaned_data['tags'],
+ 'summary': strip_tags(html)[:120],
+ 'html': html,
+ }
+
+ # only save when it's checked
+ # because wiki doesn't allow to be edited if last version has been enabled already
+ # and we make sure this in forms.
+ if ('wiki' in form.cleaned_data and
+ form.cleaned_data['wiki']):
+ updated_fields['wiki'] = True
+ updated_fields['wikified_at'] = edited_at
+
+ Question.objects.filter(
+ id=question.id).update(**updated_fields)
+ # Update the Question's tag associations
+ if tags_changed:
+ tags_updated = Question.objects.update_tags(
+ question, form.cleaned_data['tags'], request.user)
+ # Create a new revision
+ revision = QuestionRevision(
+ question = question,
+ title = form.cleaned_data['title'],
+ author = request.user,
+ revised_at = edited_at,
+ tagnames = form.cleaned_data['tags'],
+ text = form.cleaned_data['text'],
+ )
+ if form.cleaned_data['summary']:
+ revision.summary = form.cleaned_data['summary']
+ else:
+ revision.summary = 'No.%s Revision' % latest_revision.revision
+ revision.save()
+
+ return HttpResponseRedirect(question.get_absolute_url())
+ else:
+
+ revision_form = RevisionForm(question, latest_revision)
+ form = EditQuestionForm(question, latest_revision)
+ return render_to_response('question_edit.html', {
+ 'question': question,
+ 'revision_form': revision_form,
+ 'form' : form,
+ 'tags' : _get_tags_cache_json()
+ }, context_instance=RequestContext(request))
+
+@login_required
+def edit_answer(request, id):
+ answer = get_object_or_404(Answer, id=id)
+ if answer.deleted and not auth.can_view_deleted_post(request.user, answer):
+ raise Http404
+ elif not auth.can_edit_post(request.user, answer):
+ raise Http404
+ else:
+ latest_revision = answer.get_latest_revision()
+ if request.method == "POST":
+ if 'select_revision' in request.POST:
+ # user has changed revistion number
+ revision_form = RevisionForm(answer, latest_revision, request.POST)
+ if revision_form.is_valid():
+ # Replace with those from the selected revision
+ form = EditAnswerForm(answer,
+ AnswerRevision.objects.get(answer=answer,
+ revision=revision_form.cleaned_data['revision']))
+ else:
+ form = EditAnswerForm(answer, latest_revision, request.POST)
+ else:
+ form = EditAnswerForm(answer, latest_revision, request.POST)
+ if form.is_valid():
+ html = sanitize_html(markdowner.convert(form.cleaned_data['text']))
+ if form.has_changed():
+ edited_at = datetime.datetime.now()
+ updated_fields = {
+ 'last_edited_at': edited_at,
+ 'last_edited_by': request.user,
+ 'html': html,
+ }
+ Answer.objects.filter(id=answer.id).update(**updated_fields)
+
+ revision = AnswerRevision(
+ answer=answer,
+ author=request.user,
+ revised_at=edited_at,
+ text=form.cleaned_data['text']
+ )
+
+ if form.cleaned_data['summary']:
+ revision.summary = form.cleaned_data['summary']
+ else:
+ revision.summary = 'No.%s Revision' % latest_revision.revision
+ revision.save()
+
+ answer.question.last_activity_at = edited_at
+ answer.question.last_activity_by = request.user
+ answer.question.save()
+
+ return HttpResponseRedirect(answer.get_absolute_url())
+ else:
+ revision_form = RevisionForm(answer, latest_revision)
+ form = EditAnswerForm(answer, latest_revision)
+ return render_to_response('answer_edit.html', {
+ 'answer': answer,
+ 'revision_form': revision_form,
+ 'form': form,
+ }, context_instance=RequestContext(request))
+
+
+def answer(request, id):#process a new answer
+ question = get_object_or_404(Question, id=id)
+ if request.method == "POST":
+ form = AnswerForm(question, request.user, request.POST)
+ if form.is_valid():
+ wiki = form.cleaned_data['wiki']
+ text = form.cleaned_data['text']
+ update_time = datetime.datetime.now()
+
+ if request.user.is_authenticated():
+ create_new_answer(
+ question=question,
+ author=request.user,
+ added_at=update_time,
+ wiki=wiki,
+ text=text,
+ email_notify=form.cleaned_data['email_notify']
+ )
+ else:
+ request.session.flush()
+ html = sanitize_html(markdowner.convert(text))
+ summary = strip_tags(html)[:120]
+ anon = AnonymousAnswer(
+ question=question,
+ wiki=wiki,
+ text=text,
+ summary=summary,
+ session_key=request.session.session_key,
+ ip_addr=request.META['REMOTE_ADDR'],
+ )
+ anon.save()
+ return HttpResponseRedirect(reverse('user_signin_new_answer'))
+
+ return HttpResponseRedirect(question.get_absolute_url())
+
+def vote(request, id):#refactor - pretty incomprehensible view used by various ajax calls
+#issues: this subroutine is too long, contains many magic numbers and other issues
+#it's called "vote" but many actions processed here have nothing to do with voting
+ """
+ vote_type:
+ acceptAnswer : 0,
+ questionUpVote : 1,
+ questionDownVote : 2,
+ favorite : 4,
+ answerUpVote: 5,
+ answerDownVote:6,
+ offensiveQuestion : 7,
+ offensiveAnswer:8,
+ removeQuestion: 9,
+ removeAnswer:10
+ questionSubscribeUpdates:11
+
+ accept answer code:
+ response_data['allowed'] = -1, Accept his own answer 0, no allowed - Anonymous 1, Allowed - by default
+ response_data['success'] = 0, failed 1, Success - by default
+ response_data['status'] = 0, By default 1, Answer has been accepted already(Cancel)
+
+ vote code:
+ allowed = -3, Don't have enough votes left
+ -2, Don't have enough reputation score
+ -1, Vote his own post
+ 0, no allowed - Anonymous
+ 1, Allowed - by default
+ status = 0, By default
+ 1, Cancel
+ 2, Vote is too old to be canceled
+
+ offensive code:
+ allowed = -3, Don't have enough flags left
+ -2, Don't have enough reputation score to do this
+ 0, not allowed
+ 1, allowed
+ status = 0, by default
+ 1, can't do it again
+ """
+ response_data = {
+ "allowed": 1,
+ "success": 1,
+ "status" : 0,
+ "count" : 0,
+ "message" : ''
+ }
+
+ def __can_vote(vote_score, user):#refactor - belongs to auth.py
+ if vote_score == 1:#refactor magic number
+ return auth.can_vote_up(request.user)
+ else:
+ return auth.can_vote_down(request.user)
+
+ try:
+ if not request.user.is_authenticated():
+ response_data['allowed'] = 0
+ response_data['success'] = 0
+
+ elif request.is_ajax():
+ question = get_object_or_404(Question, id=id)
+ vote_type = request.POST.get('type')
+
+ #accept answer
+ if vote_type == '0':
+ answer_id = request.POST.get('postId')
+ answer = get_object_or_404(Answer, id=answer_id)
+ # make sure question author is current user
+ if question.author == request.user:
+ # answer user who is also question author is not allow to accept answer
+ if answer.author == question.author:
+ response_data['success'] = 0
+ response_data['allowed'] = -1
+ # check if answer has been accepted already
+ elif answer.accepted:
+ onAnswerAcceptCanceled(answer, request.user)
+ response_data['status'] = 1
+ else:
+ # set other answers in this question not accepted first
+ for answer_of_question in Answer.objects.get_answers_from_question(question, request.user):
+ if answer_of_question != answer and answer_of_question.accepted:
+ onAnswerAcceptCanceled(answer_of_question, request.user)
+
+ #make sure retrieve data again after above author changes, they may have related data
+ answer = get_object_or_404(Answer, id=answer_id)
+ onAnswerAccept(answer, request.user)
+ else:
+ response_data['allowed'] = 0
+ response_data['success'] = 0
+ # favorite
+ elif vote_type == '4':
+ has_favorited = False
+ fav_questions = FavoriteQuestion.objects.filter(question=question)
+ # if the same question has been favorited before, then delete it
+ if fav_questions is not None:
+ for item in fav_questions:
+ if item.user == request.user:
+ item.delete()
+ response_data['status'] = 1
+ response_data['count'] = len(fav_questions) - 1
+ if response_data['count'] < 0:
+ response_data['count'] = 0
+ has_favorited = True
+ # if above deletion has not been executed, just insert a new favorite question
+ if not has_favorited:
+ new_item = FavoriteQuestion(question=question, user=request.user)
+ new_item.save()
+ response_data['count'] = FavoriteQuestion.objects.filter(question=question).count()
+ Question.objects.update_favorite_count(question)
+
+ elif vote_type in ['1', '2', '5', '6']:
+ post_id = id
+ post = question
+ vote_score = 1
+ if vote_type in ['5', '6']:
+ answer_id = request.POST.get('postId')
+ answer = get_object_or_404(Answer, id=answer_id)
+ post_id = answer_id
+ post = answer
+ if vote_type in ['2', '6']:
+ vote_score = -1
+
+ if post.author == request.user:
+ response_data['allowed'] = -1
+ elif not __can_vote(vote_score, request.user):
+ response_data['allowed'] = -2
+ elif post.votes.filter(user=request.user).count() > 0:
+ vote = post.votes.filter(user=request.user)[0]
+ # unvote should be less than certain time
+ if (datetime.datetime.now().day - vote.voted_at.day) >= VOTE_RULES['scope_deny_unvote_days']:
+ response_data['status'] = 2
+ else:
+ voted = vote.vote
+ if voted > 0:
+ # cancel upvote
+ onUpVotedCanceled(vote, post, request.user)
+
+ else:
+ # cancel downvote
+ onDownVotedCanceled(vote, post, request.user)
+
+ response_data['status'] = 1
+ response_data['count'] = post.score
+ elif Vote.objects.get_votes_count_today_from_user(request.user) >= VOTE_RULES['scope_votes_per_user_per_day']:
+ response_data['allowed'] = -3
+ else:
+ vote = Vote(user=request.user, content_object=post, vote=vote_score, voted_at=datetime.datetime.now())
+ if vote_score > 0:
+ # upvote
+ onUpVoted(vote, post, request.user)
+ else:
+ # downvote
+ onDownVoted(vote, post, request.user)
+
+ votes_left = VOTE_RULES['scope_votes_per_user_per_day'] - Vote.objects.get_votes_count_today_from_user(request.user)
+ if votes_left <= VOTE_RULES['scope_warn_votes_left']:
+ response_data['message'] = u'%s votes left' % votes_left
+ response_data['count'] = post.score
+ elif vote_type in ['7', '8']:
+ post = question
+ post_id = id
+ if vote_type == '8':
+ post_id = request.POST.get('postId')
+ post = get_object_or_404(Answer, id=post_id)
+
+ if FlaggedItem.objects.get_flagged_items_count_today(request.user) >= VOTE_RULES['scope_flags_per_user_per_day']:
+ response_data['allowed'] = -3
+ elif not auth.can_flag_offensive(request.user):
+ response_data['allowed'] = -2
+ elif post.flagged_items.filter(user=request.user).count() > 0:
+ response_data['status'] = 1
+ else:
+ item = FlaggedItem(user=request.user, content_object=post, flagged_at=datetime.datetime.now())
+ onFlaggedItem(item, post, request.user)
+ response_data['count'] = post.offensive_flag_count
+ # send signal when question or answer be marked offensive
+ mark_offensive.send(sender=post.__class__, instance=post, mark_by=request.user)
+ elif vote_type in ['9', '10']:
+ post = question
+ post_id = id
+ if vote_type == '10':
+ post_id = request.POST.get('postId')
+ post = get_object_or_404(Answer, id=post_id)
+
+ if not auth.can_delete_post(request.user, post):
+ response_data['allowed'] = -2
+ elif post.deleted == True:
+ logging.debug('debug restoring post in view')
+ onDeleteCanceled(post, request.user)
+ response_data['status'] = 1
+ else:
+ onDeleted(post, request.user)
+ delete_post_or_answer.send(sender=post.__class__, instance=post, delete_by=request.user)
+ elif vote_type == '11':#subscribe q updates
+ user = request.user
+ if user.is_authenticated():
+ if user not in question.followed_by.all():
+ question.followed_by.add(user)
+ if settings.EMAIL_VALIDATION == 'on' and user.email_isvalid == False:
+ response_data['message'] = \
+ _('subscription saved, %(email)s needs validation, see %(details_url)s') \
+ % {'email':user.email,'details_url':reverse('faq') + '#validate'}
+ feed_setting = EmailFeedSetting.objects.get(subscriber=user,feed_type='q_sel')
+ if feed_setting.frequency == 'n':
+ feed_setting.frequency = 'd'
+ feed_setting.save()
+ if 'message' in response_data:
+ response_data['message'] += '<br/>'
+ response_data['message'] = _('email update frequency has been set to daily')
+ #response_data['status'] = 1
+ #responst_data['allowed'] = 1
+ else:
+ pass
+ #response_data['status'] = 0
+ #response_data['allowed'] = 0
+ elif vote_type == '12':#unsubscribe q updates
+ user = request.user
+ if user.is_authenticated():
+ if user in question.followed_by.all():
+ question.followed_by.remove(user)
+ else:
+ response_data['success'] = 0
+ response_data['message'] = u'Request mode is not supported. Please try again.'
+
+ data = simplejson.dumps(response_data)
+
+ except Exception, e:
+ response_data['message'] = str(e)
+ data = simplejson.dumps(response_data)
+ return HttpResponse(data, mimetype="application/json")
+
+#internally grouped views - used by the tagging system
+@ajax_login_required
+def mark_tag(request, tag=None, **kwargs):#tagging system
+ action = kwargs['action']
+ ts = MarkedTag.objects.filter(user=request.user, tag__name=tag)
+ if action == 'remove':
+ logging.debug('deleting tag %s' % tag)
+ ts.delete()
+ else:
+ reason = kwargs['reason']
+ if len(ts) == 0:
+ try:
+ t = Tag.objects.get(name=tag)
+ mt = MarkedTag(user=request.user, reason=reason, tag=t)
+ mt.save()
+ except:
+ pass
+ else:
+ ts.update(reason=reason)
+ return HttpResponse(simplejson.dumps(''), mimetype="application/json")
+
+@ajax_login_required
+def ajax_toggle_ignored_questions(request):#ajax tagging and tag-filtering system
+ if request.user.hide_ignored_questions:
+ new_hide_setting = False
+ else:
+ new_hide_setting = True
+ request.user.hide_ignored_questions = new_hide_setting
+ request.user.save()
+
+@ajax_method
+def ajax_command(request):#refactor? view processing ajax commands - note "vote" and view others do it too
+ if 'command' not in request.POST:
+ return HttpResponseForbidden(mimetype="application/json")
+ if request.POST['command'] == 'toggle-ignored-questions':
+ return ajax_toggle_ignored_questions(request)
+
+def question_comments(request, id):#ajax handler for loading comments to question
+ question = get_object_or_404(Question, id=id)
+ user = request.user
+ return __comments(request, question, 'question')
+
+def answer_comments(request, id):#ajax handler for loading comments on answer
+ answer = get_object_or_404(Answer, id=id)
+ user = request.user
+ return __comments(request, answer, 'answer')
+
+def __comments(request, obj, type):#non-view generic ajax handler to load comments to an object
+ # only support get post comments by ajax now
+ user = request.user
+ if request.is_ajax():
+ if request.method == "GET":
+ response = __generate_comments_json(obj, type, user)
+ elif request.method == "POST":
+ if auth.can_add_comments(user,obj):
+ comment_data = request.POST.get('comment')
+ comment = Comment(content_object=obj, comment=comment_data, user=request.user)
+ comment.save()
+ obj.comment_count = obj.comment_count + 1
+ obj.save()
+ response = __generate_comments_json(obj, type, user)
+ else:
+ response = HttpResponseForbidden(mimetype="application/json")
+ return response
+
+def __generate_comments_json(obj, type, user):#non-view generates json data for the post comments
+ comments = obj.comments.all().order_by('id')
+ # {"Id":6,"PostId":38589,"CreationDate":"an hour ago","Text":"hello there!","UserDisplayName":"Jarrod Dixon","UserUrl":"/users/3/jarrod-dixon","DeleteUrl":null}
+ json_comments = []
+ from forum.templatetags.extra_tags import diff_date
+ for comment in comments:
+ comment_user = comment.user
+ delete_url = ""
+ if user != None and auth.can_delete_comment(user, comment):
+ #/posts/392845/comments/219852/delete
+ #todo translate this url
+ delete_url = reverse(index) + type + "s/%s/comments/%s/delete/" % (obj.id, comment.id)
+ json_comments.append({"id" : comment.id,
+ "object_id" : obj.id,
+ "comment_age" : diff_date(comment.added_at),
+ "text" : comment.comment,
+ "user_display_name" : comment_user.username,
+ "user_url" : comment_user.get_profile_url(),
+ "delete_url" : delete_url
+ })
+
+ data = simplejson.dumps(json_comments)
+ return HttpResponse(data, mimetype="application/json")
+
+def delete_comment(request, object_id='', comment_id='', commented_object_type=None):#ajax handler to delete comment
+ response = None
+ commented_object = None
+ if commented_object_type == 'question':
+ commented_object = Question
+ elif commented_object_type == 'answer':
+ commented_object = Answer
+
+ if request.is_ajax():
+ comment = get_object_or_404(Comment, id=comment_id)
+ if auth.can_delete_comment(request.user, comment):
+ obj = get_object_or_404(commented_object, id=object_id)
+ obj.comments.remove(comment)
+ obj.comment_count = obj.comment_count - 1
+ obj.save()
+ user = request.user
+ return __generate_comments_json(obj, commented_object_type, user)
+ raise PermissionDenied()
+
diff --git a/forum/views/users.py b/forum/views/users.py
index 7567ccd4..d988a195 100644
--- a/forum/views/users.py
+++ b/forum/views/users.py
@@ -9,6 +9,7 @@ from django.http import HttpResponse, HttpResponseForbidden
from django.utils.translation import ugettext as _
from forum.forms import *#incomplete list is EditUserForm, ModerateUserForm, TagFilterSelectionForm,
from forum import auth
+from django.contrib.contenttypes.models import ContentType
question_type = ContentType.objects.get_for_model(Question)
answer_type = ContentType.objects.get_for_model(Answer)
diff --git a/forum/views/writers.py b/forum/views/writers.py
index 33b49423..bb966b9f 100644
--- a/forum/views/writers.py
+++ b/forum/views/writers.py
@@ -35,100 +35,6 @@ ANSWERS_PAGE_SIZE = 10
markdowner = Markdown(html4tags=True)
-#system to collect user actions and change content and store in the database
-def create_new_answer( question=None, author=None, #service subroutine - refactor
- added_at=None, wiki=False,\
- text='', email_notify=False):
- """refactor
- non-view subroutine
- initializes the answer and revision
- and updates stuff in the corresponding question
- probably there is more Django-ish way to do it
- """
-
- html = sanitize_html(markdowner.convert(text))
-
- #create answer
- answer = Answer(
- question = question,
- author = author,
- added_at = added_at,
- wiki = wiki,
- html = html
- )
- if answer.wiki:
- answer.last_edited_by = answer.author
- answer.last_edited_at = added_at
- answer.wikified_at = added_at
-
- answer.save()
-
- #update question data
- question.last_activity_at = added_at
- question.last_activity_by = author
- question.save()
- Question.objects.update_answer_count(question)
-
- #update revision
- AnswerRevision.objects.create(
- answer = answer,
- revision = 1,
- author = author,
- revised_at = added_at,
- summary = CONST['default_version'],
- text = text
- )
-
- #set notification/delete
- if email_notify:
- if author not in question.followed_by.all():
- question.followed_by.add(author)
- else:
- #not sure if this is necessary. ajax should take care of this...
- try:
- question.followed_by.remove(author)
- except:
- pass
-
-def create_new_question(title=None,author=None,added_at=None, #service subroutine - refactor
- wiki=False,tagnames=None,summary=None,
- text=None):
- """refactor
- this is not a view saves new question and revision
- and maybe should become one of the methods on Question object?
- """
- html = sanitize_html(markdowner.convert(text))
- question = Question(
- title = title,
- author = author,
- added_at = added_at,
- last_activity_at = added_at,
- last_activity_by = author,
- wiki = wiki,
- tagnames = tagnames,
- html = html,
- summary = summary
- )
- if question.wiki:
- question.last_edited_by = question.author
- question.last_edited_at = added_at
- question.wikified_at = added_at
-
- question.save()
-
- # create the first revision
- QuestionRevision.objects.create(
- question = question,
- revision = 1,
- title = question.title,
- author = author,
- revised_at = added_at,
- tagnames = question.tagnames,
- summary = CONST['default_version'],
- text = text
- )
- return question
-
def upload(request):#ajax upload file to a question or answer
class FileTypeNotAllow(Exception):
pass
@@ -197,7 +103,7 @@ def ask(request):#view used to ask a new question
if request.user.is_authenticated():
author = request.user
- question = create_new_question(
+ Question.objects.create_new(
title = title,
author = author,
added_at = added_at,
@@ -437,7 +343,7 @@ def answer(request, id):#process a new answer
update_time = datetime.datetime.now()
if request.user.is_authenticated():
- create_new_answer(
+ Answer.objects.create_new(
question=question,
author=request.user,
added_at=update_time,
diff --git a/pgfulltext/management.py b/pgfulltext/management.py
index 04303092..46f51edc 100644
--- a/pgfulltext/management.py
+++ b/pgfulltext/management.py
@@ -9,7 +9,7 @@ if settings.USE_PG_FTS:
from django.db.models.signals import post_syncdb
def setup_pgfulltext(sender, **kwargs):
- if sender == forum.models:
+ if sender == forum.models_:
install_pg_fts()
post_syncdb.connect(setup_pgfulltext)
diff --git a/settings.py b/settings.py
index 3ffc6d4f..b384e3fa 100755
--- a/settings.py
+++ b/settings.py
@@ -81,7 +81,7 @@ if USE_SPHINX_SEARCH:
if USE_FB_CONNECT:
INSTALLED_APPS.append('fbconnect')
-if DATABASE_ENGINE in ('postgresql_psycopg2', 'postgresql', ):
+if DATABASE_ENGINE in ('postgresql_psycopg2', 'postgresql', ) and False:
USE_PG_FTS = True
INSTALLED_APPS.append('pgfulltext')
else:
diff --git a/tester/__init__.py b/tester/__init__.py
new file mode 100755
index 00000000..e69de29b
--- /dev/null
+++ b/tester/__init__.py
diff --git a/tester/models.py b/tester/models.py
new file mode 100755
index 00000000..2029cc56
--- /dev/null
+++ b/tester/models.py
@@ -0,0 +1,8 @@
+from django.db import models
+
+class Tag(models.Model):
+ name = models.CharField(max_length=255, unique=True)
+
+class Question(models.Model):
+ title = models.CharField(max_length=300)
+ tags = models.ManyToManyField(Tag, related_name='questions') \ No newline at end of file