diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b7b8ffd --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/dist +*.pyc +*.egg-info diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..84d23e9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2011, ramusus and contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of Django nor the names of its contributors may be used + to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..0df8c94 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include README.md +include LICENSE +include MANIFEST.in +recursive-include vkontakte_wall * diff --git a/README.md b/README.md new file mode 100644 index 0000000..c8602fc --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +#### Вступление + +Приложение позволяет взаимодействовать со стенами Вконтакте, сообщениями и комментариями на них через Вконтакте API и парсер используя стандартные модели Django diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ad09054 --- /dev/null +++ b/setup.py @@ -0,0 +1,30 @@ +from setuptools import setup, find_packages + +setup( + name='django-vkontakte-wall', + version=__import__('vkontakte_wall').__version__, + description='Django implementation for vkontakte API Wall', + long_description=open('README.md').read(), + author='ramusus', + author_email='ramusus@gmail.com', + url='https://github.com/ramusus/django-vkontakte-wall', + download_url='http://pypi.python.org/pypi/django-vkontakte-wall', + license='BSD', + packages=find_packages(), + include_package_data=True, + zip_safe=False, # because we're including media that Django needs + install_requires=[ + 'django-vkontakte-api==0.1.1', + 'factory_boy', +# 'django-ajax-selects', + ], + classifiers=[ + 'Development Status :: 4 - Beta', + 'Environment :: Web Environment', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], +) diff --git a/vkontakte_wall/__init__.py b/vkontakte_wall/__init__.py new file mode 100644 index 0000000..dc8206a --- /dev/null +++ b/vkontakte_wall/__init__.py @@ -0,0 +1,2 @@ +VERSION = (0, 1, 1) +__version__ = '.'.join(map(str, VERSION)) diff --git a/vkontakte_wall/admin.py b/vkontakte_wall/admin.py new file mode 100644 index 0000000..e5b2263 --- /dev/null +++ b/vkontakte_wall/admin.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from django.contrib import admin +from django.utils.translation import ugettext as _ +from vkontakte_api.admin import VkontakteModelAdmin +from models import Post, Comment + +class CommentInline(admin.TabularInline): + model = Comment + extra = 0 + can_delete = False + fields = ('author','text','date','likes') + readonly_fields = fields + +class PostAdmin(VkontakteModelAdmin): + list_display = ('wall_owner','text','author','vk_link','date','comments','likes','reposts') + list_display_links = ('text',) +# list_filter = ('wall_owner',) + search_fields = ('text','copy_text') + exclude = ('like_users','repost_users',) + inlines = [CommentInline] + +class CommentAdmin(VkontakteModelAdmin): + list_display = ('author','post','vk_link','date','likes') + +admin.site.register(Post, PostAdmin) +admin.site.register(Comment, CommentAdmin) \ No newline at end of file diff --git a/vkontakte_wall/factories.py b/vkontakte_wall/factories.py new file mode 100644 index 0000000..ba48f51 --- /dev/null +++ b/vkontakte_wall/factories.py @@ -0,0 +1,28 @@ +from vkontakte_users.factories import UserFactory +from vkontakte_groups.factories import GroupFactory +from models import Post, Comment +from datetime import datetime +import factory +import random + +class PostFactory(factory.Factory): + FACTORY_FOR = Post + + date = datetime.now() + + wall_owner = factory.SubFactory(UserFactory) + author = factory.SubFactory(UserFactory) + remote_id = factory.LazyAttributeSequence(lambda o, n: '%s_%s' % (o.wall_owner.remote_id, n)) + +class GroupPostFactory(PostFactory): + wall_owner = factory.SubFactory(GroupFactory) + remote_id = factory.LazyAttributeSequence(lambda o, n: '-%s_%s' % (o.wall_owner.remote_id, n)) + +class CommentFactory(factory.Factory): + FACTORY_FOR = Comment + + date = datetime.now() + + post = factory.SubFactory(PostFactory) + author = factory.SubFactory(UserFactory) + remote_id = factory.Sequence(lambda n: n) \ No newline at end of file diff --git a/vkontakte_wall/migrations/0001_initial.py b/vkontakte_wall/migrations/0001_initial.py new file mode 100644 index 0000000..877adad --- /dev/null +++ b/vkontakte_wall/migrations/0001_initial.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Post' + db.create_table('vkontakte_wall_post', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('fetched', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), + ('remote_id', self.gf('django.db.models.fields.CharField')(unique=True, max_length='20')), + ('owner', self.gf('django.db.models.fields.related.ForeignKey')(related_name='wall', to=orm['vkontakte_users.User'])), + ('author', self.gf('django.db.models.fields.related.ForeignKey')(related_name='posts', to=orm['vkontakte_users.User'])), + ('date', self.gf('django.db.models.fields.DateTimeField')()), + ('text', self.gf('django.db.models.fields.TextField')()), + ('comments', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)), + ('likes', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)), + ('reposts', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)), + ('attachments', self.gf('django.db.models.fields.TextField')()), + ('media', self.gf('django.db.models.fields.TextField')()), + ('geo', self.gf('django.db.models.fields.TextField')()), + ('signer_id', self.gf('django.db.models.fields.PositiveIntegerField')(null=True)), + ('copy_owner_id', self.gf('django.db.models.fields.IntegerField')(null=True)), + ('copy_post_id', self.gf('django.db.models.fields.PositiveIntegerField')(null=True)), + ('copy_text', self.gf('django.db.models.fields.TextField')()), + ('post_source', self.gf('django.db.models.fields.TextField')()), + ('online', self.gf('django.db.models.fields.PositiveSmallIntegerField')(null=True)), + ('reply_count', self.gf('django.db.models.fields.PositiveSmallIntegerField')(null=True)), + )) + db.send_create_signal('vkontakte_wall', ['Post']) + + # Adding model 'Comment' + db.create_table('vkontakte_wall_comment', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('fetched', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), + ('remote_id', self.gf('django.db.models.fields.CharField')(unique=True, max_length='20')), + ('post', self.gf('django.db.models.fields.related.ForeignKey')(related_name='wall_comments', to=orm['vkontakte_wall.Post'])), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='comments', to=orm['vkontakte_users.User'])), + ('from_id', self.gf('django.db.models.fields.IntegerField')(null=True)), + ('reply_to_uid', self.gf('django.db.models.fields.IntegerField')(null=True)), + ('reply_to_cid', self.gf('django.db.models.fields.IntegerField')(null=True)), + ('date', self.gf('django.db.models.fields.DateTimeField')()), + ('text', self.gf('django.db.models.fields.TextField')()), + ('likes', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)), + )) + db.send_create_signal('vkontakte_wall', ['Comment']) + + def backwards(self, orm): + # Deleting model 'Post' + db.delete_table('vkontakte_wall_post') + + # Deleting model 'Comment' + db.delete_table('vkontakte_wall_comment') + + models = { + 'vkontakte_places.city': { + 'Meta': {'ordering': "['name']", 'object_name': 'City'}, + 'area': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'country': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'cities'", 'null': 'True', 'to': "orm['vkontakte_places.Country']"}), + 'fetched': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'region': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'remote_id': ('django.db.models.fields.BigIntegerField', [], {'unique': 'True'}) + }, + 'vkontakte_places.country': { + 'Meta': {'ordering': "['name']", 'object_name': 'Country'}, + 'fetched': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'remote_id': ('django.db.models.fields.BigIntegerField', [], {'unique': 'True'}) + }, + 'vkontakte_users.user': { + 'Meta': {'ordering': "['remote_id']", 'object_name': 'User'}, + 'activity': ('django.db.models.fields.TextField', [], {}), + 'albums': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'audios': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'bdate': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'city': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['vkontakte_places.City']", 'null': 'True', 'on_delete': 'models.SET_NULL'}), + 'counters_updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'country': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['vkontakte_places.Country']", 'null': 'True', 'on_delete': 'models.SET_NULL'}), + 'faculty': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'faculty_name': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'fetched': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'followers': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'friends': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'graduation': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'has_mobile': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'home_phone': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'mobile_phone': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'mutual_friends': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'notes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'photo': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'photo_big': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'photo_medium': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'photo_medium_rec': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'photo_rec': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'rate': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'relation': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True'}), + 'remote_id': ('django.db.models.fields.BigIntegerField', [], {'unique': 'True'}), + 'screen_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'sex': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'subscriptions': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'sum_counters': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'timezone': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'university': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'university_name': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'user_photos': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'user_videos': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'videos': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'wall_comments': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'vkontakte_wall.comment': { + 'Meta': {'ordering': "['remote_id']", 'object_name': 'Comment'}, + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'fetched': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'from_id': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'likes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'wall_comments'", 'to': "orm['vkontakte_wall.Post']"}), + 'remote_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': "'20'"}), + 'reply_to_cid': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'reply_to_uid': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'comments'", 'to': "orm['vkontakte_users.User']"}) + }, + 'vkontakte_wall.post': { + 'Meta': {'ordering': "['remote_id']", 'object_name': 'Post'}, + 'attachments': ('django.db.models.fields.TextField', [], {}), + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'to': "orm['vkontakte_users.User']"}), + 'comments': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'copy_owner_id': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'copy_post_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'copy_text': ('django.db.models.fields.TextField', [], {}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'fetched': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'geo': ('django.db.models.fields.TextField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'likes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'media': ('django.db.models.fields.TextField', [], {}), + 'online': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'wall'", 'to': "orm['vkontakte_users.User']"}), + 'post_source': ('django.db.models.fields.TextField', [], {}), + 'remote_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': "'20'"}), + 'reply_count': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True'}), + 'reposts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'signer_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {}) + } + } + + complete_apps = ['vkontakte_wall'] \ No newline at end of file diff --git a/vkontakte_wall/migrations/0002_auto__del_field_comment_reply_to_uid__del_field_comment_reply_to_cid__.py b/vkontakte_wall/migrations/0002_auto__del_field_comment_reply_to_uid__del_field_comment_reply_to_cid__.py new file mode 100644 index 0000000..a7e1ea4 --- /dev/null +++ b/vkontakte_wall/migrations/0002_auto__del_field_comment_reply_to_uid__del_field_comment_reply_to_cid__.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Deleting field 'Comment.reply_to_uid' + db.delete_column('vkontakte_wall_comment', 'reply_to_uid') + + # Deleting field 'Comment.reply_to_cid' + db.delete_column('vkontakte_wall_comment', 'reply_to_cid') + + # Adding field 'Comment.reply_for' + db.add_column('vkontakte_wall_comment', 'reply_for', + self.gf('django.db.models.fields.related.ForeignKey')(to=orm['vkontakte_users.User'], null=True), + keep_default=False) + + # Adding field 'Comment.reply_to' + db.add_column('vkontakte_wall_comment', 'reply_to', + self.gf('django.db.models.fields.related.ForeignKey')(to=orm['vkontakte_wall.Comment'], null=True), + keep_default=False) + + # Adding field 'Post.group' + db.add_column('vkontakte_wall_post', 'group', + self.gf('django.db.models.fields.related.ForeignKey')(related_name='posts', null=True, to=orm['vkontakte_groups.Group']), + keep_default=False) + + # Adding M2M table for field like_users on 'Post' + db.create_table('vkontakte_wall_post_like_users', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('post', models.ForeignKey(orm['vkontakte_wall.post'], null=False)), + ('user', models.ForeignKey(orm['vkontakte_users.user'], null=False)) + )) + db.create_unique('vkontakte_wall_post_like_users', ['post_id', 'user_id']) + + # Adding M2M table for field repost_users on 'Post' + db.create_table('vkontakte_wall_post_repost_users', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('post', models.ForeignKey(orm['vkontakte_wall.post'], null=False)), + ('user', models.ForeignKey(orm['vkontakte_users.user'], null=False)) + )) + db.create_unique('vkontakte_wall_post_repost_users', ['post_id', 'user_id']) + + + # Changing field 'Post.author' + db.alter_column('vkontakte_wall_post', 'author_id', self.gf('django.db.models.fields.related.ForeignKey')(null=True, to=orm['vkontakte_users.User'])) + + # Changing field 'Post.owner' + db.alter_column('vkontakte_wall_post', 'owner_id', self.gf('django.db.models.fields.related.ForeignKey')(null=True, to=orm['vkontakte_users.User'])) + def backwards(self, orm): + # Adding field 'Comment.reply_to_uid' + db.add_column('vkontakte_wall_comment', 'reply_to_uid', + self.gf('django.db.models.fields.IntegerField')(null=True), + keep_default=False) + + # Adding field 'Comment.reply_to_cid' + db.add_column('vkontakte_wall_comment', 'reply_to_cid', + self.gf('django.db.models.fields.IntegerField')(null=True), + keep_default=False) + + # Deleting field 'Comment.reply_for' + db.delete_column('vkontakte_wall_comment', 'reply_for_id') + + # Deleting field 'Comment.reply_to' + db.delete_column('vkontakte_wall_comment', 'reply_to_id') + + # Deleting field 'Post.group' + db.delete_column('vkontakte_wall_post', 'group_id') + + # Removing M2M table for field like_users on 'Post' + db.delete_table('vkontakte_wall_post_like_users') + + # Removing M2M table for field repost_users on 'Post' + db.delete_table('vkontakte_wall_post_repost_users') + + + # Changing field 'Post.author' + db.alter_column('vkontakte_wall_post', 'author_id', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['vkontakte_users.User'])) + + # Changing field 'Post.owner' + db.alter_column('vkontakte_wall_post', 'owner_id', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['vkontakte_users.User'])) + models = { + 'vkontakte_groups.group': { + 'Meta': {'ordering': "['name']", 'object_name': 'Group'}, + 'fetched': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '800'}), + 'photo': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'photo_big': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'photo_medium': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'remote_id': ('django.db.models.fields.BigIntegerField', [], {'unique': 'True'}), + 'screen_name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '10'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['vkontakte_users.User']", 'symmetrical': 'False'}) + }, + 'vkontakte_places.city': { + 'Meta': {'ordering': "['name']", 'object_name': 'City'}, + 'area': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'country': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'cities'", 'null': 'True', 'to': "orm['vkontakte_places.Country']"}), + 'fetched': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'region': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'remote_id': ('django.db.models.fields.BigIntegerField', [], {'unique': 'True'}) + }, + 'vkontakte_places.country': { + 'Meta': {'ordering': "['name']", 'object_name': 'Country'}, + 'fetched': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'remote_id': ('django.db.models.fields.BigIntegerField', [], {'unique': 'True'}) + }, + 'vkontakte_users.user': { + 'Meta': {'ordering': "['remote_id']", 'object_name': 'User'}, + 'activity': ('django.db.models.fields.TextField', [], {}), + 'albums': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'audios': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'bdate': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'city': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['vkontakte_places.City']", 'null': 'True', 'on_delete': 'models.SET_NULL'}), + 'counters_updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'country': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['vkontakte_places.Country']", 'null': 'True', 'on_delete': 'models.SET_NULL'}), + 'faculty': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'faculty_name': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'fetched': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'followers': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'friends': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'graduation': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'has_mobile': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'home_phone': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'mobile_phone': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'mutual_friends': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'notes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'photo': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'photo_big': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'photo_medium': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'photo_medium_rec': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'photo_rec': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'rate': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'relation': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True'}), + 'remote_id': ('django.db.models.fields.BigIntegerField', [], {'unique': 'True'}), + 'screen_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'sex': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'subscriptions': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'sum_counters': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'timezone': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'university': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'university_name': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'user_photos': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'user_videos': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'videos': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'wall_comments': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'vkontakte_wall.comment': { + 'Meta': {'ordering': "['remote_id']", 'object_name': 'Comment'}, + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'fetched': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'from_id': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'likes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'wall_comments'", 'to': "orm['vkontakte_wall.Post']"}), + 'remote_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': "'20'"}), + 'reply_for': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['vkontakte_users.User']", 'null': 'True'}), + 'reply_to': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['vkontakte_wall.Comment']", 'null': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'comments'", 'to': "orm['vkontakte_users.User']"}) + }, + 'vkontakte_wall.post': { + 'Meta': {'ordering': "['remote_id']", 'object_name': 'Post'}, + 'attachments': ('django.db.models.fields.TextField', [], {}), + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'null': 'True', 'to': "orm['vkontakte_users.User']"}), + 'comments': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'copy_owner_id': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'copy_post_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'copy_text': ('django.db.models.fields.TextField', [], {}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'fetched': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'geo': ('django.db.models.fields.TextField', [], {}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'null': 'True', 'to': "orm['vkontakte_groups.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'like_users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'like_posts'", 'blank': 'True', 'to': "orm['vkontakte_users.User']"}), + 'likes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'media': ('django.db.models.fields.TextField', [], {}), + 'online': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'wall'", 'null': 'True', 'to': "orm['vkontakte_users.User']"}), + 'post_source': ('django.db.models.fields.TextField', [], {}), + 'remote_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': "'20'"}), + 'reply_count': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True'}), + 'repost_users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'repost_posts'", 'blank': 'True', 'to': "orm['vkontakte_users.User']"}), + 'reposts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'signer_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {}) + } + } + + complete_apps = ['vkontakte_wall'] \ No newline at end of file diff --git a/vkontakte_wall/migrations/0003_auto__del_field_comment_reply_for__del_field_comment_user__add_field_c.py b/vkontakte_wall/migrations/0003_auto__del_field_comment_reply_for__del_field_comment_user__add_field_c.py new file mode 100644 index 0000000..ccf918c --- /dev/null +++ b/vkontakte_wall/migrations/0003_auto__del_field_comment_reply_for__del_field_comment_user__add_field_c.py @@ -0,0 +1,229 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Deleting field 'Comment.reply_for' + db.delete_column('vkontakte_wall_comment', 'reply_for_id') + + # Deleting field 'Comment.user' + db.delete_column('vkontakte_wall_comment', 'user_id') + + # Adding field 'Comment.author_content_type' + db.add_column('vkontakte_wall_comment', 'author_content_type', + self.gf('django.db.models.fields.related.ForeignKey')(default=45, related_name='comments', to=orm['contenttypes.ContentType']), + keep_default=False) + + # Adding field 'Comment.author_id' + db.add_column('vkontakte_wall_comment', 'author_id', + self.gf('django.db.models.fields.PositiveIntegerField')(default=0), + keep_default=False) + + # Adding field 'Comment.reply_for_content_type' + db.add_column('vkontakte_wall_comment', 'reply_for_content_type', + self.gf('django.db.models.fields.related.ForeignKey')(related_name='replies', null=True, to=orm['contenttypes.ContentType']), + keep_default=False) + + # Adding field 'Comment.reply_for_id' + db.add_column('vkontakte_wall_comment', 'reply_for_id', + self.gf('django.db.models.fields.PositiveIntegerField')(null=True), + keep_default=False) + + # Deleting field 'Post.owner' + db.delete_column('vkontakte_wall_post', 'owner_id') + + # Deleting field 'Post.group' + db.delete_column('vkontakte_wall_post', 'group_id') + + # Deleting field 'Post.author' + db.delete_column('vkontakte_wall_post', 'author_id') + + # Adding field 'Post.wall_owner_content_type' + db.add_column('vkontakte_wall_post', 'wall_owner_content_type', + self.gf('django.db.models.fields.related.ForeignKey')(default=39, related_name='wall_posts', to=orm['contenttypes.ContentType']), + keep_default=False) + + # Adding field 'Post.wall_owner_id' + db.add_column('vkontakte_wall_post', 'wall_owner_id', + self.gf('django.db.models.fields.PositiveIntegerField')(default=0), + keep_default=False) + + # Adding field 'Post.author_content_type' + db.add_column('vkontakte_wall_post', 'author_content_type', + self.gf('django.db.models.fields.related.ForeignKey')(default=45, related_name='posts', to=orm['contenttypes.ContentType']), + keep_default=False) + + # Adding field 'Post.author_id' + db.add_column('vkontakte_wall_post', 'author_id', + self.gf('django.db.models.fields.PositiveIntegerField')(default=0), + keep_default=False) + + def backwards(self, orm): + # Adding field 'Comment.reply_for' + db.add_column('vkontakte_wall_comment', 'reply_for', + self.gf('django.db.models.fields.related.ForeignKey')(to=orm['vkontakte_users.User'], null=True), + keep_default=False) + + # Adding field 'Comment.user' + db.add_column('vkontakte_wall_comment', 'user', + self.gf('django.db.models.fields.related.ForeignKey')(default=0, related_name='comments', to=orm['vkontakte_users.User']), + keep_default=False) + + # Deleting field 'Comment.author_content_type' + db.delete_column('vkontakte_wall_comment', 'author_content_type_id') + + # Deleting field 'Comment.author_id' + db.delete_column('vkontakte_wall_comment', 'author_id') + + # Deleting field 'Comment.reply_for_content_type' + db.delete_column('vkontakte_wall_comment', 'reply_for_content_type_id') + + # Deleting field 'Comment.reply_for_id' + db.delete_column('vkontakte_wall_comment', 'reply_for_id') + + # Adding field 'Post.owner' + db.add_column('vkontakte_wall_post', 'owner', + self.gf('django.db.models.fields.related.ForeignKey')(related_name='wall', null=True, to=orm['vkontakte_users.User']), + keep_default=False) + + # Adding field 'Post.group' + db.add_column('vkontakte_wall_post', 'group', + self.gf('django.db.models.fields.related.ForeignKey')(related_name='posts', null=True, to=orm['vkontakte_groups.Group']), + keep_default=False) + + # Adding field 'Post.author' + db.add_column('vkontakte_wall_post', 'author', + self.gf('django.db.models.fields.related.ForeignKey')(related_name='posts', null=True, to=orm['vkontakte_users.User']), + keep_default=False) + + # Deleting field 'Post.wall_owner_content_type' + db.delete_column('vkontakte_wall_post', 'wall_owner_content_type_id') + + # Deleting field 'Post.wall_owner_id' + db.delete_column('vkontakte_wall_post', 'wall_owner_id') + + # Deleting field 'Post.author_content_type' + db.delete_column('vkontakte_wall_post', 'author_content_type_id') + + # Deleting field 'Post.author_id' + db.delete_column('vkontakte_wall_post', 'author_id') + + models = { + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'vkontakte_places.city': { + 'Meta': {'ordering': "['name']", 'object_name': 'City'}, + 'area': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'country': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'cities'", 'null': 'True', 'to': "orm['vkontakte_places.Country']"}), + 'fetched': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'region': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'remote_id': ('django.db.models.fields.BigIntegerField', [], {'unique': 'True'}) + }, + 'vkontakte_places.country': { + 'Meta': {'ordering': "['name']", 'object_name': 'Country'}, + 'fetched': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'remote_id': ('django.db.models.fields.BigIntegerField', [], {'unique': 'True'}) + }, + 'vkontakte_users.user': { + 'Meta': {'ordering': "['remote_id']", 'object_name': 'User'}, + 'activity': ('django.db.models.fields.TextField', [], {}), + 'albums': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'audios': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'bdate': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'city': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['vkontakte_places.City']", 'null': 'True', 'on_delete': 'models.SET_NULL'}), + 'counters_updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'country': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['vkontakte_places.Country']", 'null': 'True', 'on_delete': 'models.SET_NULL'}), + 'faculty': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'faculty_name': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'fetched': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'followers': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'friends': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'graduation': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'has_mobile': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'home_phone': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'mobile_phone': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'mutual_friends': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'notes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'photo': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'photo_big': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'photo_medium': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'photo_medium_rec': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'photo_rec': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'rate': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'relation': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True'}), + 'remote_id': ('django.db.models.fields.BigIntegerField', [], {'unique': 'True'}), + 'screen_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'sex': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'subscriptions': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'sum_counters': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'timezone': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'university': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'university_name': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'user_photos': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'user_videos': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'videos': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'wall_comments': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'vkontakte_wall.comment': { + 'Meta': {'ordering': "['post', 'date']", 'object_name': 'Comment'}, + 'author_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'comments'", 'to': "orm['contenttypes.ContentType']"}), + 'author_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'fetched': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'from_id': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'likes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'wall_comments'", 'to': "orm['vkontakte_wall.Post']"}), + 'remote_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': "'20'"}), + 'reply_for_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'replies'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}), + 'reply_for_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'reply_to': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['vkontakte_wall.Comment']", 'null': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {}) + }, + 'vkontakte_wall.post': { + 'Meta': {'ordering': "['wall_owner_id', 'date']", 'object_name': 'Post'}, + 'attachments': ('django.db.models.fields.TextField', [], {}), + 'author_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'to': "orm['contenttypes.ContentType']"}), + 'author_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'comments': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'copy_owner_id': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'copy_post_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'copy_text': ('django.db.models.fields.TextField', [], {}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'fetched': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'geo': ('django.db.models.fields.TextField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'like_users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'like_posts'", 'blank': 'True', 'to': "orm['vkontakte_users.User']"}), + 'likes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'media': ('django.db.models.fields.TextField', [], {}), + 'online': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True'}), + 'post_source': ('django.db.models.fields.TextField', [], {}), + 'remote_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': "'20'"}), + 'reply_count': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True'}), + 'repost_users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'repost_posts'", 'blank': 'True', 'to': "orm['vkontakte_users.User']"}), + 'reposts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'signer_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {}), + 'wall_owner_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'wall_posts'", 'to': "orm['contenttypes.ContentType']"}), + 'wall_owner_id': ('django.db.models.fields.PositiveIntegerField', [], {}) + } + } + + complete_apps = ['vkontakte_wall'] \ No newline at end of file diff --git a/vkontakte_wall/migrations/0004_auto__add_field_comment_raw_html__add_field_post_raw_html.py b/vkontakte_wall/migrations/0004_auto__add_field_comment_raw_html__add_field_post_raw_html.py new file mode 100644 index 0000000..0654338 --- /dev/null +++ b/vkontakte_wall/migrations/0004_auto__add_field_comment_raw_html__add_field_post_raw_html.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Comment.raw_html' + db.add_column('vkontakte_wall_comment', 'raw_html', + self.gf('django.db.models.fields.TextField')(default=''), + keep_default=False) + + # Adding field 'Post.raw_html' + db.add_column('vkontakte_wall_post', 'raw_html', + self.gf('django.db.models.fields.TextField')(default=''), + keep_default=False) + + def backwards(self, orm): + # Deleting field 'Comment.raw_html' + db.delete_column('vkontakte_wall_comment', 'raw_html') + + # Deleting field 'Post.raw_html' + db.delete_column('vkontakte_wall_post', 'raw_html') + + models = { + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'vkontakte_places.city': { + 'Meta': {'ordering': "['name']", 'object_name': 'City'}, + 'area': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'country': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'cities'", 'null': 'True', 'to': "orm['vkontakte_places.Country']"}), + 'fetched': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'region': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'remote_id': ('django.db.models.fields.BigIntegerField', [], {'unique': 'True'}) + }, + 'vkontakte_places.country': { + 'Meta': {'ordering': "['name']", 'object_name': 'Country'}, + 'fetched': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'remote_id': ('django.db.models.fields.BigIntegerField', [], {'unique': 'True'}) + }, + 'vkontakte_users.user': { + 'Meta': {'ordering': "['remote_id']", 'object_name': 'User'}, + 'activity': ('django.db.models.fields.TextField', [], {}), + 'albums': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'audios': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'bdate': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'city': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['vkontakte_places.City']", 'null': 'True', 'on_delete': 'models.SET_NULL'}), + 'counters_updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'country': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['vkontakte_places.Country']", 'null': 'True', 'on_delete': 'models.SET_NULL'}), + 'faculty': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'faculty_name': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'fetched': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'followers': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'friends': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'graduation': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'has_mobile': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'home_phone': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'mobile_phone': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'mutual_friends': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'notes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'photo': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'photo_big': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'photo_medium': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'photo_medium_rec': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'photo_rec': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'rate': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'relation': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True'}), + 'remote_id': ('django.db.models.fields.BigIntegerField', [], {'unique': 'True'}), + 'screen_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'sex': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'subscriptions': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'sum_counters': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'timezone': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'university': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'university_name': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'user_photos': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'user_videos': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'videos': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'wall_comments': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'vkontakte_wall.comment': { + 'Meta': {'ordering': "['post', '-date']", 'object_name': 'Comment'}, + 'author_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'comments'", 'to': "orm['contenttypes.ContentType']"}), + 'author_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'fetched': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'from_id': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'likes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'wall_comments'", 'to': "orm['vkontakte_wall.Post']"}), + 'raw_html': ('django.db.models.fields.TextField', [], {}), + 'remote_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': "'20'"}), + 'reply_for_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'replies'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}), + 'reply_for_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'reply_to': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['vkontakte_wall.Comment']", 'null': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {}) + }, + 'vkontakte_wall.post': { + 'Meta': {'ordering': "['wall_owner_id', '-date']", 'object_name': 'Post'}, + 'attachments': ('django.db.models.fields.TextField', [], {}), + 'author_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'to': "orm['contenttypes.ContentType']"}), + 'author_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'comments': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'copy_owner_id': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'copy_post_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'copy_text': ('django.db.models.fields.TextField', [], {}), + 'date': ('django.db.models.fields.DateTimeField', [], {}), + 'fetched': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'geo': ('django.db.models.fields.TextField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'like_users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'like_posts'", 'blank': 'True', 'to': "orm['vkontakte_users.User']"}), + 'likes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'media': ('django.db.models.fields.TextField', [], {}), + 'online': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True'}), + 'post_source': ('django.db.models.fields.TextField', [], {}), + 'raw_html': ('django.db.models.fields.TextField', [], {}), + 'remote_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': "'20'"}), + 'reply_count': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True'}), + 'repost_users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'repost_posts'", 'blank': 'True', 'to': "orm['vkontakte_users.User']"}), + 'reposts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'signer_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {}), + 'wall_owner_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'wall_posts'", 'to': "orm['contenttypes.ContentType']"}), + 'wall_owner_id': ('django.db.models.fields.PositiveIntegerField', [], {}) + } + } + + complete_apps = ['vkontakte_wall'] \ No newline at end of file diff --git a/vkontakte_wall/migrations/__init__.py b/vkontakte_wall/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vkontakte_wall/models.py b/vkontakte_wall/models.py new file mode 100644 index 0000000..c63ae32 --- /dev/null +++ b/vkontakte_wall/models.py @@ -0,0 +1,477 @@ +# -*- coding: utf-8 -*- +from django.db import models +from django.dispatch import Signal +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes import generic +from datetime import datetime +from vkontakte_api.utils import api_call +from vkontakte_api import fields +from vkontakte_api.models import VkontakteManager, VkontakteModel +from vkontakte_users.models import User +from vkontakte_groups.models import Group +from parser import VkontakteWallParser, VkontakteParseError +import logging +import re + +log = logging.getLogger('vkontakte_wall') + +parsed = Signal(providing_args=['sender', 'instance', 'container']) + +class VkontaktePostsRemoteManager(VkontakteManager): + + def fetch_user_wall(self, user, offset=0, count=None, filter='all', extended=False): + + if filter not in ['owner','others','all']: + raise ValueError("Attribute 'fiter' has illegal value '%s'" % filter) + if count > 100: + raise ValueError("Attribute 'count' can not be more than 100") + + kwargs = { + 'owner_id': user.remote_id, + 'filter': filter, + } + + if offset: + kwargs.update({'offset': offset}) + if count: + kwargs.update({'count': count}) + if extended: + kwargs.update({'extended': 1}) + return self.fetch(**kwargs) + + def fetch_group_wall(self, group, offset=0, count=None, own=False, after=None): + post_data = { + 'al':1, + 'offset': offset, + 'own': int(own), # posts by only group or any users + 'part': 1, # without header, footer + } + parser = VkontakteWallParser().request('/wall-%s' % group.remote_id, data=post_data) + + items = parser.content_bs.findAll('div', {'class': re.compile('^post'), 'id': re.compile('^post-%d' % group.remote_id)}) + + current_count = offset + len(items) + need_cut = count and count < current_count + if need_cut: + items = items[:count-offset] + + for item in items: + + try: + post = parser.parse_post(item, group) + except VkontakteParseError, e: + log.error(e) + continue + + post.raw_html = unicode(item) + post.save() + parsed.send(sender=Post, instance=post, container=item) + + if after and post.date < after: + need_cut = True + break + + if len(items) == 20 and not need_cut: + return self.fetch_group_wall(group, offset=current_count, count=count, own=own, after=after) + elif after and need_cut: + return group.wall_posts.filter(date__gte=after) + else: + return group.wall_posts.all() + +class VkontakteCommentsRemoteManager(VkontakteManager): + + def fetch_user_post(self, post, offset=0, count=None): + if count > 100: + raise ValueError("Attribute 'count' can not be more than 100") + + kwargs = { + 'owner_id': post.wall_owner.remote_id, + 'post_id': post.remote_id.split('_')[1], + 'preview_length': 0, + 'sort': 'asc', + } + + if offset: + kwargs.update({'offset': offset}) + if count: + kwargs.update({'count': count}) + + instances = self.get(**kwargs) + instances_saved = [] + + for instance in instances: + instance.post = post + instance.fetched = datetime.now() + instances_saved += [self.get_or_create_from_instance(instance)] + + return instances_saved + + def fetch_group_post(self, post, offset=0, count=None):#, after=None, only_new=False): + post_data = { + 'al':1, + 'offset': offset, + 'part': 1, + } + parser = VkontakteWallParser().request('/wall%s' % (post.remote_id), data=post_data) + + items = parser.content_bs.findAll('div', {'class': 'fw_reply'}) + + current_count = offset + len(items) + need_cut = count and count < current_count + if need_cut: + items = items[:count-offset] + +# # get date of last comment and set after attribute +# if only_new: +# comments = post.wall_comments.order_by('-date') +# if comments: +# after = comments[0].date + + for item in items: + + try: + comment = parser.parse_comment(item, post.wall_owner) + except VkontakteParseError, e: + log.error(e) + continue + + comment.post = post + comment.raw_html = unicode(item) + comment.save() + parsed.send(sender=Comment, instance=comment, container=item) + +# if after and comment.date < after: +# need_cut = True +# break + + if len(items) == 20 and not need_cut: + return self.fetch_group_post(post, offset=current_count, count=count)#, after=after, only_new=only_new) +# elif after and need_cut: +# return post.wall_comments.filter(date__gte=after) + else: + if not count: + post.comments = post.wall_comments.count() + post.save() + return post.wall_comments.all() + +class VkontakteWallModel(VkontakteModel): + class Meta: + abstract = True + + methods_namespace = 'wall' + + # only for posts/comments from parser + raw_html = models.TextField() + + @property + def slug(self): + return 'wall%s' % self.remote_id + +class Post(VkontakteWallModel): + class Meta: + db_table = 'vkontakte_wall_post' + verbose_name = u'Сообщение Вконтакте' + verbose_name_plural = u'Сообщения Вконтакте' + ordering = ['wall_owner_id','-date'] + + remote_id = models.CharField(u'ID', max_length='20', help_text=u'Уникальный идентификатор', unique=True) + + # Владелец стены сообщения User or Group + wall_owner_content_type = models.ForeignKey(ContentType, related_name='vkontakte_wall_posts') + wall_owner_id = models.PositiveIntegerField() + wall_owner = generic.GenericForeignKey('wall_owner_content_type', 'wall_owner_id') + + # Создатель/автор сообщения + author_content_type = models.ForeignKey(ContentType, related_name='vkontakte_posts') + author_id = models.PositiveIntegerField() + author = generic.GenericForeignKey('author_content_type', 'author_id') + + # abstract field for correct deleting group and user models in admin + group_wall = generic.GenericForeignKey('wall_owner_content_type', 'wall_owner_id') + user_wall = generic.GenericForeignKey('wall_owner_content_type', 'wall_owner_id') + group = generic.GenericForeignKey('author_content_type', 'author_id') + user = generic.GenericForeignKey('author_content_type', 'author_id') + + date = models.DateTimeField(u'Время сообщения') + text = models.TextField(u'Текст записи') + + comments = models.PositiveIntegerField(u'Кол-во комментариев', default=0) + likes = models.PositiveIntegerField(u'Кол-во лайков', default=0) + reposts = models.PositiveIntegerField(u'Кол-во репостов', default=0) + + like_users = models.ManyToManyField(User, blank=True, related_name='like_posts') + repost_users = models.ManyToManyField(User, blank=True, related_name='repost_posts') + + #{u'photo': {u'access_key': u'5f19dfdc36a1852824', + #u'aid': -7, + #u'created': 1333664090, + #u'height': 960, + #u'owner_id': 2462759, + #u'pid': 281543621, + #u'src': u'http://cs9733.userapi.com/u2462759/-14/m_fdad45ec.jpg', + #u'src_big': u'http://cs9733.userapi.com/u2462759/-14/x_60b1aed1.jpg', + #u'src_small': u'http://cs9733.userapi.com/u2462759/-14/s_d457021e.jpg', + #u'src_xbig': u'http://cs9733.userapi.com/u2462759/-14/y_b5a67b8d.jpg', + #u'src_xxbig': u'http://cs9733.userapi.com/u2462759/-14/z_5a64a153.jpg', + #u'text': u'', + #u'width': 1280}, + #u'type': u'photo'} + + #u'attachments': [{u'link': {u'description': u'', + #u'image_src': u'http://cs6030.userapi.com/u2462759/-2/x_cb9c00f8.jpg', + #u'title': u'SAAB_9000_CD_2_0_Turbo_190_k.jpg', + #u'url': u'http://www.yauto.cz/includes/img/inzerce/SAAB_9000_CD_2_0_Turbo_190_k.jpg'}, + #u'type': u'link'}], + #attachments - содержит массив объектов, которые присоединены к текущей записи (фотографии, ссылки и т.п.). Более подробная информация представлена на странице Описание поля attachments + attachments = models.TextField() + media = models.TextField() + + #{u'coordinates': u'55.6745689498 37.8724562529', + #u'place': {u'city': u'Moskovskaya oblast', + #u'country': u'Russian Federation', + #u'title': u'Shosseynaya ulitsa, Moskovskaya oblast'}, + #u'type': u'point'} + #geo - если в записи содержится информация о местоположении, то она будет представлена в данном поле. Более подробная информация представлена на странице Описание поля geo + geo = models.TextField() + + signer_id = models.PositiveIntegerField(null=True, help_text=u'Eсли запись была опубликована от имени группы и подписана пользователем, то в поле содержится идентификатор её автора') + # могут быть негативные id, это группы или страницы + copy_owner_id = models.IntegerField(null=True, help_text=u'Eсли запись является копией записи с чужой стены, то в поле содержится идентификатор владельца стены у которого была скопирована запись') + copy_post_id = models.PositiveIntegerField(null=True, help_text=u'Если запись является копией записи с чужой стены, то в поле содержится идентфикатор скопированной записи на стене ее владельца') + copy_text = models.TextField(u'Комментарий при репосте', help_text=u'Если запись является копией записи с чужой стены и при её копировании был добавлен комментарий, его текст содержится в данном поле') + + # not in API + post_source = models.TextField() + online = models.PositiveSmallIntegerField(null=True) + reply_count = models.PositiveSmallIntegerField(null=True) + + objects = models.Manager() + remote = VkontaktePostsRemoteManager(remote_pk=('remote_id',), methods={ + 'get': 'get', + }) + + @property + def on_group_wall(self): + return self.wall_owner_content_type == ContentType.objects.get_for_model(Group) + @property + def on_user_wall(self): + return self.wall_owner_content_type == ContentType.objects.get_for_model(User) + + def __unicode__(self): + return '%s: %s' % (unicode(self.wall_owner), self.text) + + def save(self, *args, **kwargs): + # check strings for good encoding + # there is problems to save users with bad encoded activity strings like user ID=88798245 +# try: +# self.text.encode('utf-16').decode('utf-16') +# except UnicodeDecodeError: +# self.text = '' + + # TODO: move this checking and other one to universal place + # set exactly right Group or User contentTypes, not a child + for field_name in ['wall_owner', 'author']: + for allowed_model in [Group, User]: + if isinstance(getattr(self, field_name), allowed_model): + setattr(self, '%s_content_type' % field_name, ContentType.objects.get_for_model(allowed_model)) + break + + # check is generic fields has correct content_type + allowed_ct_ids = [ct.id for ct in ContentType.objects.get_for_models(Group, User).values()] + if self.wall_owner_content_type.id not in allowed_ct_ids: + raise ValueError("'wall_owner' field should be Group or User instance, not %s" % self.wall_owner_content_type) + if self.author_content_type.id not in allowed_ct_ids: + raise ValueError("'author' field should be Group or User instance, not %s" % self.author_content_type) + + return super(Post, self).save(*args, **kwargs) + + def parse(self, response): + + for field_name in ['comments','likes','reposts']: + if field_name in response and 'count' in response[field_name]: + setattr(self, field_name, response.pop(field_name)['count']) + + # parse over API only for user's walls + self.wall_owner = User.objects.get_or_create(remote_id=response.pop('to_id'))[0] + self.author = User.objects.get_or_create(remote_id=response.pop('from_id'))[0] + + if 'attachment' in response: + response.pop('attachment') + super(Post, self).parse(response) + + self.remote_id = '%s_%s' % (self.wall_owner.remote_id, self.remote_id) + + def fetch_comments(self, *args, **kwargs): + if self.on_group_wall: + return Comment.remote.fetch_group_post(self, *args, **kwargs) + elif self.on_user_wall: + return Comment.remote.fetch_user_post(self, *args, **kwargs) + + def update_likes(self, offset=0): + ''' + Update fields: + * likes - count of likes + * like_users - users, who likes this post + ''' + + post_data = { + 'act':'a_get_members', + 'al': 1, + 'object': 'wall%s' % self.remote_id, + 'only_content': 1, + 'published': 0, + 'tab': 0, + 'offset': offset, + } + parser = VkontakteWallParser().request('/like.php', data=post_data) + + if offset == 0: + title = parser.content_bs.find('h4') + if title: + try: + self.likes = int(title.text.split(' ')[1]) + self.save() + except: + log.warning('Strange format of h4 container: "%s"' % title.text) + self.like_users.clear() + + avatars = parser.content_bs.findAll('div', {'class': 'liked_box_row'}) + for avatar in avatars: + user = User.remote.get_by_slug(avatar.findAll('a')[1]['href'][1:]) + if user: + user.first_name = avatar.findAll('a')[1].text + user.photo = avatar.find('img')['src'] + user.save() + self.like_users.add(user) + + if len(avatars) == 24: + self.update_likes(offset=offset+24) + + def update_reposts(self, offset=0): + ''' + Update fields: + * reposts - count of reposts + * repost_users - users, who repost this post + ''' + post_data = { + 'act':'a_get_members', + 'al': 1, + 'object': 'wall%s' % self.remote_id, + 'only_content': 1, + 'published': 1, + 'tab': 1, + 'offset': offset, + } + parser = VkontakteWallParser().request('/like.php', data=post_data) + + if offset == 0: + title = parser.content_bs.find('h4') + if title: + self.reposts = int(title.text.split(' ')[0]) + self.save() + self.repost_users.clear() + + avatars = parser.content_bs.findAll('div', {'class': 'liked_box_row'}) + for avatar in avatars: + user = User.remote.get_by_slug(avatar.findAll('a')[1]['href'][1:]) + if user: + user.first_name = avatar.findAll('a')[1].text + user.photo = avatar.find('img')['src'] + user.save() + self.repost_users.add(user) + + if len(avatars) == 24: + self.update_reposts(offset=offset+24) + +class Comment(VkontakteWallModel): + class Meta: + db_table = 'vkontakte_wall_comment' + verbose_name = u'Коментарий Вконтакте' + verbose_name_plural = u'Комментарии Вконтакте' + ordering = ['post','-date'] + + remote_pk_field = 'cid' + + remote_id = models.CharField(u'ID', max_length='20', help_text=u'Уникальный идентификатор', unique=True) + + post = models.ForeignKey(Post, verbose_name=u'Пост', related_name='wall_comments') + + # Автор комментария + author_content_type = models.ForeignKey(ContentType, related_name='comments') + author_id = models.PositiveIntegerField() + author = generic.GenericForeignKey('author_content_type', 'author_id') + + from_id = models.IntegerField(null=True) # strange value, seems to be equal to author + + # Это ответ пользователю + reply_for_content_type = models.ForeignKey(ContentType, null=True, related_name='replies') + reply_for_id = models.PositiveIntegerField(null=True) + reply_for = generic.GenericForeignKey('reply_for_content_type', 'reply_for_id') + + reply_to = models.ForeignKey('self', null=True, verbose_name=u'Это ответ на комментарий') + + # abstract field for correct deleting group and user models in admin + group = generic.GenericForeignKey('author_content_type', 'author_id') + user = generic.GenericForeignKey('author_content_type', 'author_id') + group_wall_reply = generic.GenericForeignKey('reply_for_content_type', 'reply_for_id') + user_wall_reply = generic.GenericForeignKey('reply_for_content_type', 'reply_for_id') + + date = models.DateTimeField(u'Время комментария') + text = models.TextField(u'Текст комментария') + + likes = models.PositiveIntegerField(u'Кол-во лайков', default=0) + + objects = models.Manager() + remote = VkontakteCommentsRemoteManager(remote_pk=('remote_id',), methods={ + 'get': 'getComments', + }) + + def save(self, *args, **kwargs): + # it's here, because self.post is not in API response + if '_' not in str(self.remote_id): + self.remote_id = '%s_%s' % (self.post.remote_id.split('_')[0], self.remote_id) + + # TODO: move this checking and other one to universal place + # set exactly right Group or User contentTypes, not a child + for field_name in ['reply_for', 'author']: + for allowed_model in [Group, User]: + if isinstance(getattr(self, field_name), allowed_model): + setattr(self, '%s_content_type' % field_name, ContentType.objects.get_for_model(allowed_model)) + break + + allowed_ct_ids = [ct.id for ct in ContentType.objects.get_for_models(Group, User).values()] + if self.author_content_type.id not in allowed_ct_ids: + raise ValueError("'author' field should be Group or User instance, not %s" % self.author_content_type) + if self.reply_for_content_type and self.reply_for_content_type.id not in allowed_ct_ids: + raise ValueError("'reply_for' field should be Group or User instance, not %s" % self.reply_for_content_type) + + return super(Comment, self).save(*args, **kwargs) + + def parse(self, response): + super(Comment, self).parse(response) + + for field_name in ['likes']: + if field_name in response and 'count' in response[field_name]: + setattr(self, field_name, response.pop(field_name)['count']) + + self.author = User.objects.get_or_create(remote_id=response['uid'])[0] + + if 'reply_to_uid' in response: + self.reply_for = User.objects.get_or_create(remote_id=response['reply_to_uid'])[0] + if 'reply_to_cid' in response: + try: + self.reply_to = Comment.objects.get(remote_id=response['reply_to_cid']) + except: + pass + +Group.add_to_class('wall_posts', generic.GenericRelation(Post, content_type_field='wall_owner_content_type', object_id_field='wall_owner_id', related_name='group_wall', verbose_name=u'Сообщения на стене')) +User.add_to_class('wall_posts', generic.GenericRelation(Post, content_type_field='wall_owner_content_type', object_id_field='wall_owner_id', related_name='user_wall', verbose_name=u'Сообщения на стене')) + +Group.add_to_class('posts', generic.GenericRelation(Post, content_type_field='author_content_type', object_id_field='author_id', related_name='group', verbose_name=u'Сообщения')) +User.add_to_class('posts', generic.GenericRelation(Post, content_type_field='author_content_type', object_id_field='author_id', related_name='user', verbose_name=u'Сообщения')) + +Group.add_to_class('comments', generic.GenericRelation(Comment, content_type_field='author_content_type', object_id_field='author_id', related_name='group', verbose_name=u'Комментарии')) +User.add_to_class('comments', generic.GenericRelation(Comment, content_type_field='author_content_type', object_id_field='author_id', related_name='user', verbose_name=u'Комментарии')) + +Group.add_to_class('replies', generic.GenericRelation(Comment, content_type_field='reply_for_content_type', object_id_field='reply_for_id', related_name='group_wall_reply', verbose_name=u'Ответы на комментарии')) +User.add_to_class('replies', generic.GenericRelation(Comment, content_type_field='reply_for_content_type', object_id_field='reply_for_id', related_name='user_wall_reply', verbose_name=u'Ответы на комментарии')) \ No newline at end of file diff --git a/vkontakte_wall/parser.py b/vkontakte_wall/parser.py new file mode 100644 index 0000000..210de1f --- /dev/null +++ b/vkontakte_wall/parser.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- +from datetime import datetime +from vkontakte_api.parser import VkontakteParser, VkontakteParseError +import re + +def get_object_by_slug(slug): + from vkontakte_users.models import User + from vkontakte_groups.models import Group + instance = User.remote.get_by_slug(slug) + if not instance: + instance = Group.remote.get_by_slug(slug) + return instance + +class VkontakteWallParser(VkontakteParser): + + def parse_container_date(self, container): + + text = container.find('span', {'class': re.compile('^rel_date')}) + if text: + text = text.text + else: + raise VkontakteParseError("Impossible to find date container in %s" % container) + return datetime(1970,1,1) + + return self.parse_date(text) + + def parse_comment(self, content, wall_owner=None): + from models import Comment + + remote_id = content['id'][4:] + try: + instance = Comment.objects.get(remote_id=remote_id) + except Comment.DoesNotExist: + instance = Comment(remote_id=remote_id) + + comment_text = content.find('div', {'class': 'fw_reply_text'}) + if comment_text: + instance.text = comment_text.text + + # date + instance.date = self.parse_container_date(content) + # likes + instance.likes = self.parse_container_likes(content, 'like_count fl_l') + + # author + users = content.findAll('a', {'class': 'fw_reply_author'}) + slug = users[0]['href'][1:] + if wall_owner and wall_owner.screen_name == slug: + instance.author = wall_owner + else: + avatar = content.find('a', {'class': 'fw_reply_thumb'}).find('img')['src'] + name_parts = users[0].text.split(' ') + + user = get_object_by_slug(slug) + if user: + user.first_name = name_parts[0] + if len(name_parts) > 1: + user.last_name = name_parts[1] + user.photo = avatar + user.save() + instance.author = user + + if len(users) == 2: + # this comment is answer + slug = users[1]['href'][1:] + if wall_owner and wall_owner.screen_name == slug: + instance.reply_for = wall_owner + else: + instance.reply_for = get_object_by_slug(slug) + # имя в падеже, аватара нет + # чтобы получть текст и ID родительского коммента нужно отправить: + #http://vk.com/al_wall.php + #act:post_tt + #al:1 + #post:-16297716_126263 + #reply:1 + + instance.fetched = datetime.now() + return instance + + def parse_post(self, content, wall_owner): + from models import Post + from vkontakte_users.models import User + + remote_id = content['id'][4:] + try: + instance = Post.objects.get(remote_id=remote_id) + except Post.DoesNotExist: + instance = Post(remote_id=remote_id) + + post_text = content.find('div', {'class': 'wall_post_text'}) + if post_text: + instance.text = post_text.text + + # date + instance.date = self.parse_container_date(content) + # likes + instance.likes = self.parse_container_likes(content, 'post_like_count fl_l') + + # comments + show_comments = content.find('div', {'class': 'wrh_text'}) + if show_comments: + comments_words = show_comments.text.split(' ') + if len(comments_words) in [3,4]: + # Показать все 95 комментариев + # Показать 91 комментарий + instance.comments = int(comments_words[-2]) + elif len(comments_words) == 6: + # Показать последние 100 комментариев из 170 + instance.comments = int(comments_words[-1]) + else: + raise VkontakteParseError("Error number of words in show all comments message: '%s'" % show_comments.text.encode('utf-8')) + else: + instance.comments = len(content.findAll('div', {'class': 'reply_text'})) + + # author + owner_slug = content.find('a', {'class': 'author'})['href'][1:] + if wall_owner and wall_owner.screen_name == owner_slug: + instance.author = wall_owner + else: + # author is someone else, + # possible user, becouse the group can post only on it's own wall, where wall_owner is defined + avatar = content.find('a', {'class': 'post_image'}).find('img')['src'] + name_parts = content.find('a', {'class': 'author'}).text.split(' ') + + user = get_object_by_slug(owner_slug) + if user: + user.first_name = name_parts[0] + if len(name_parts) > 1: + user.last_name = name_parts[1] + user.photo = avatar + user.save() + instance.author = user + + instance.fetched = datetime.now() + if wall_owner: + instance.wall_owner = wall_owner + + return instance \ No newline at end of file diff --git a/vkontakte_wall/tests.py b/vkontakte_wall/tests.py new file mode 100644 index 0000000..ee9fcdd --- /dev/null +++ b/vkontakte_wall/tests.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +from django.test import TestCase +from models import Post, Comment +from factories import PostFactory, UserFactory, GroupFactory +from vkontakte_users.models import User +from datetime import datetime +import simplejson as json + +USER_ID = 18658732 +POST_ID = '18658732_2019' +GROUP_ID = 16297716 +GROUP_SCREEN_NAME = 'cocacola' +GROUP_POST_ID = '-16297716_126261' +OPEN_WALL_GROUP_ID = '19391365' +OPEN_WALL_GROUP_SCREEN_NAME = 'nokia' + + +class VkontakteWallTest(TestCase): + + def test_fetch_user_wall(self): + + owner = UserFactory.create(remote_id=USER_ID) + + self.assertEqual(Post.objects.count(), 0) + + posts = owner.fetch_posts() + + self.assertTrue(len(posts) > 0) + self.assertEqual(Post.objects.count(), len(posts)) + self.assertEqual(posts[0].wall_owner, owner) + + def test_fetch_group_wall(self): + + group = GroupFactory.create(remote_id=GROUP_ID, screen_name=GROUP_SCREEN_NAME) + + self.assertEqual(Post.objects.count(), 0) + + posts = group.fetch_posts(count=10) + + self.assertTrue(len(posts), 10) + self.assertEqual(Post.objects.count(), 10) + self.assertEqual(posts[0].wall_owner, group) + self.assertTrue(isinstance(posts[0].date, datetime)) + self.assertTrue(posts[0].likes + posts[1].likes > 0) + self.assertTrue(posts[0].comments + posts[1].comments > 0) + self.assertTrue(len(posts[0].text) > 0) + + def test_fetch_group_open_wall(self): + + group = GroupFactory.create(remote_id=OPEN_WALL_GROUP_ID, screen_name=OPEN_WALL_GROUP_SCREEN_NAME) + + self.assertEqual(Post.objects.count(), 0) + self.assertEqual(User.objects.count(), 0) + + count = 10 + posts = group.fetch_posts(own=0, count=count) + + self.assertEqual(len(posts), count) + self.assertEqual(Post.objects.count(), count) + self.assertTrue(User.objects.count() > 0) + self.assertTrue(Post.objects.exclude(author_id=None).count() > 0) + + def test_fetch_user_post_comments(self): + + owner = UserFactory.create(remote_id=USER_ID) + post = PostFactory.create(remote_id=POST_ID, wall_owner=owner, author=owner) + self.assertEqual(Comment.objects.count(), 0) + + comments = post.fetch_comments() + + self.assertTrue(len(comments) > 0) + self.assertEqual(Comment.objects.count(), len(comments)) + self.assertEqual(comments[0].post, post) + + def test_fetch_group_post_comments(self): + + group = GroupFactory.create(remote_id=GROUP_ID, screen_name=GROUP_SCREEN_NAME) + post = PostFactory.create(remote_id=GROUP_POST_ID, wall_owner=group) + self.assertEqual(Comment.objects.count(), 0) + + comments = post.fetch_comments() + + self.assertTrue(len(comments) > 0) + self.assertEqual(Comment.objects.count(), len(comments)) + self.assertEqual(comments[0].post, post) + self.assertEqual(post.comments, len(comments)) + +# def test_fetch_group_post_comments_after(self): +# +# group = GroupFactory.create(remote_id=GROUP_ID, screen_name=GROUP_SCREEN_NAME) +# post = PostFactory.create(remote_id=GROUP_POST_ID, wall_owner=group) +# self.assertEqual(Comment.objects.count(), 0) +# +# comments = post.fetch_comments(after=datetime(2012,7,23,0,0)) +# +# self.assertTrue(len(comments) > 10) +# self.assertEqual(Comment.objects.count(), len(comments)) +# self.assertEqual(comments[0].post, post) +# self.assertEqual(post.comments, len(comments)) + + def test_update_post_reposts(self): + + post = PostFactory.create(remote_id=GROUP_POST_ID) + + self.assertEqual(post.reposts, 0) + self.assertEqual(post.repost_users.count(), 0) + post.update_reposts() + self.assertNotEqual(post.reposts, 0) + self.assertNotEqual(post.repost_users.count(), 0) + + def test_update_post_likes(self): + + post = PostFactory.create(remote_id=GROUP_POST_ID) + + self.assertEqual(post.likes, 0) + self.assertEqual(post.like_users.count(), 0) + post.update_likes() + self.assertNotEqual(post.likes, 0) + self.assertNotEqual(post.like_users.count(), 0) + self.assertTrue(post.like_users.count() > 24) + + def test_parse_post(self): + + response = '''{"comments": {"can_post": 0, "count": 4}, + "date": 1298365200, + "from_id": 55555, + "geo": {"coordinates": "55.6745689498 37.8724562529", + "place": {"city": "Moskovskaya oblast", + "country": "Russian Federation", + "title": "Shosseynaya ulitsa, Moskovskaya oblast"}, + "type": "point"}, + "id": 465, + "likes": {"can_like": 1, "can_publish": 1, "count": 10, "user_likes": 0}, + "online": 1, + "post_source": {"type": "api"}, + "reply_count": 0, + "reposts": {"count": 3, "user_reposted": 0}, + "text": "qwerty", + "to_id": 2462759} + ''' + instance = Post() + owner = UserFactory.create(remote_id=2462759) + author = UserFactory.create(remote_id=55555) + instance.parse(json.loads(response)) + instance.save() + + self.assertEqual(instance.remote_id, '2462759_465') + self.assertEqual(instance.wall_owner, owner) + self.assertEqual(instance.author, author) + self.assertEqual(instance.reply_count, 0) + self.assertEqual(instance.likes, 10) + self.assertEqual(instance.reposts, 3) + self.assertEqual(instance.comments, 4) + self.assertEqual(instance.text, 'qwerty') + self.assertEqual(instance.date, datetime(2011,2,22,12,0,0)) + + def test_parse_comments(self): + + response = '''{"response":[6, + {"cid":2505,"uid":16271479,"date":1298365200,"text":"Добрый день , кароче такая идея когда опросы создаешь вместо статуса - можно выбрать аудитории опрашиваемых, например только женский или мужской пол могут участвовать (то бишь голосовать в опросе)."}, + {"cid":2507,"uid":16271479,"date":1286105582,"text":"Это уже не практично, имхо.
Для этого делайте группу и там опрос, а в группу принимайте тех, кого нужно.","reply_to_uid":16271479,"reply_to_cid":2505}, + {"cid":2547,"uid":2943,"date":1286218080,"text":"Он будет только для групп благотворительных организаций."}]} + ''' + post = PostFactory(remote_id='1_0') + instance = Comment(post=post) + author = UserFactory.create(remote_id=16271479) + instance.parse(json.loads(response)['response'][1]) + instance.save() + + self.assertEqual(instance.remote_id, '1_2505') + self.assertEqual(instance.text, u'Добрый день , кароче такая идея когда опросы создаешь вместо статуса - можно выбрать аудитории опрашиваемых, например только женский или мужской пол могут участвовать (то бишь голосовать в опросе).') + self.assertEqual(instance.date, datetime(2011,2,22,12,0,0)) + self.assertEqual(instance.author, author) + + instance.parse(json.loads(response)['response'][2]) + instance.save() + + self.assertEqual(instance.remote_id, '1_2507') + self.assertEqual(instance.reply_for.remote_id, 16271479) +# self.assertEqual(instance.reply_to.remote_id, '...2505') \ No newline at end of file