diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..5f9bb7d6d4966cde2b06f26d5f668b449a925dcb --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +.idea/ +.DS_Store +*.sqlite3 + +__pycache__ +# dependencies +*/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build +*/build + +# misc +.env.local +.env.development.local +.env.test.local +.env.production.local +.env + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +*.eslintcache +*/media/account/images/ + +solar/django_static +pypi/*/ diff --git a/Pipfile b/Pipfile index 71e4f7cbf92007f3a0f0ef426b670c974a6d7e7c..8e60571b6ab19789cf8ddfbda397be30e2485890 100644 --- a/Pipfile +++ b/Pipfile @@ -4,6 +4,11 @@ verify_ssl = true name = "pypi" [packages] +django = "*" +django-cors-headers = "*" +djangorestframework = "*" +pillow = "*" +uwsgi = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000000000000000000000000000000000000..9d8d6a14c906b3b26240170a8390306e493429f0 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,122 @@ +{ + "_meta": { + "hash": { + "sha256": "f1ffc1e60d515d604136eec07eea7e16c697c4e5529414b3b41eb9a672871223" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.9" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "asgiref": { + "hashes": [ + "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9", + "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214" + ], + "markers": "python_version >= '3.6'", + "version": "==3.4.1" + }, + "django": { + "hashes": [ + "sha256:51284300f1522ffcdb07ccbdf676a307c6678659e1284f0618e5a774127a6a08", + "sha256:e22c9266da3eec7827737cde57694d7db801fedac938d252bf27377cec06ed1b" + ], + "index": "pypi", + "version": "==3.2.9" + }, + "django-cors-headers": { + "hashes": [ + "sha256:cba6e99659abb0e47cc4aaabb8fcde03f193e6bb3b92ba47c5185ec4cedc5d9e", + "sha256:cd6f4360f5246569c149dc1c40c907c191f1ec45551e10d2a2e2e68512652f78" + ], + "index": "pypi", + "version": "==3.10.0" + }, + "djangorestframework": { + "hashes": [ + "sha256:6d1d59f623a5ad0509fe0d6bfe93cbdfe17b8116ebc8eda86d45f6e16e819aaf", + "sha256:f747949a8ddac876e879190df194b925c177cdeb725a099db1460872f7c0a7f2" + ], + "index": "pypi", + "version": "==3.12.4" + }, + "pillow": { + "hashes": [ + "sha256:066f3999cb3b070a95c3652712cffa1a748cd02d60ad7b4e485c3748a04d9d76", + "sha256:0a0956fdc5defc34462bb1c765ee88d933239f9a94bc37d132004775241a7585", + "sha256:0b052a619a8bfcf26bd8b3f48f45283f9e977890263e4571f2393ed8898d331b", + "sha256:1394a6ad5abc838c5cd8a92c5a07535648cdf6d09e8e2d6df916dfa9ea86ead8", + "sha256:1bc723b434fbc4ab50bb68e11e93ce5fb69866ad621e3c2c9bdb0cd70e345f55", + "sha256:244cf3b97802c34c41905d22810846802a3329ddcb93ccc432870243211c79fc", + "sha256:25a49dc2e2f74e65efaa32b153527fc5ac98508d502fa46e74fa4fd678ed6645", + "sha256:2e4440b8f00f504ee4b53fe30f4e381aae30b0568193be305256b1462216feff", + "sha256:3862b7256046fcd950618ed22d1d60b842e3a40a48236a5498746f21189afbbc", + "sha256:3eb1ce5f65908556c2d8685a8f0a6e989d887ec4057326f6c22b24e8a172c66b", + "sha256:3f97cfb1e5a392d75dd8b9fd274d205404729923840ca94ca45a0af57e13dbe6", + "sha256:493cb4e415f44cd601fcec11c99836f707bb714ab03f5ed46ac25713baf0ff20", + "sha256:4acc0985ddf39d1bc969a9220b51d94ed51695d455c228d8ac29fcdb25810e6e", + "sha256:5503c86916d27c2e101b7f71c2ae2cddba01a2cf55b8395b0255fd33fa4d1f1a", + "sha256:5b7bb9de00197fb4261825c15551adf7605cf14a80badf1761d61e59da347779", + "sha256:5e9ac5f66616b87d4da618a20ab0a38324dbe88d8a39b55be8964eb520021e02", + "sha256:620582db2a85b2df5f8a82ddeb52116560d7e5e6b055095f04ad828d1b0baa39", + "sha256:62cc1afda735a8d109007164714e73771b499768b9bb5afcbbee9d0ff374b43f", + "sha256:70ad9e5c6cb9b8487280a02c0ad8a51581dcbbe8484ce058477692a27c151c0a", + "sha256:72b9e656e340447f827885b8d7a15fc8c4e68d410dc2297ef6787eec0f0ea409", + "sha256:72cbcfd54df6caf85cc35264c77ede902452d6df41166010262374155947460c", + "sha256:792e5c12376594bfcb986ebf3855aa4b7c225754e9a9521298e460e92fb4a488", + "sha256:7b7017b61bbcdd7f6363aeceb881e23c46583739cb69a3ab39cb384f6ec82e5b", + "sha256:81f8d5c81e483a9442d72d182e1fb6dcb9723f289a57e8030811bac9ea3fef8d", + "sha256:82aafa8d5eb68c8463b6e9baeb4f19043bb31fefc03eb7b216b51e6a9981ae09", + "sha256:84c471a734240653a0ec91dec0996696eea227eafe72a33bd06c92697728046b", + "sha256:8c803ac3c28bbc53763e6825746f05cc407b20e4a69d0122e526a582e3b5e153", + "sha256:93ce9e955cc95959df98505e4608ad98281fff037350d8c2671c9aa86bcf10a9", + "sha256:9a3e5ddc44c14042f0844b8cf7d2cd455f6cc80fd7f5eefbe657292cf601d9ad", + "sha256:a4901622493f88b1a29bd30ec1a2f683782e57c3c16a2dbc7f2595ba01f639df", + "sha256:a5a4532a12314149d8b4e4ad8ff09dde7427731fcfa5917ff16d0291f13609df", + "sha256:b8831cb7332eda5dc89b21a7bce7ef6ad305548820595033a4b03cf3091235ed", + "sha256:b8e2f83c56e141920c39464b852de3719dfbfb6e3c99a2d8da0edf4fb33176ed", + "sha256:c70e94281588ef053ae8998039610dbd71bc509e4acbc77ab59d7d2937b10698", + "sha256:c8a17b5d948f4ceeceb66384727dde11b240736fddeda54ca740b9b8b1556b29", + "sha256:d82cdb63100ef5eedb8391732375e6d05993b765f72cb34311fab92103314649", + "sha256:d89363f02658e253dbd171f7c3716a5d340a24ee82d38aab9183f7fdf0cdca49", + "sha256:d99ec152570e4196772e7a8e4ba5320d2d27bf22fdf11743dd882936ed64305b", + "sha256:ddc4d832a0f0b4c52fff973a0d44b6c99839a9d016fe4e6a1cb8f3eea96479c2", + "sha256:e3dacecfbeec9a33e932f00c6cd7996e62f53ad46fbe677577394aaa90ee419a", + "sha256:eb9fc393f3c61f9054e1ed26e6fe912c7321af2f41ff49d3f83d05bacf22cc78" + ], + "index": "pypi", + "version": "==8.4.0" + }, + "pytz": { + "hashes": [ + "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c", + "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326" + ], + "version": "==2021.3" + }, + "sqlparse": { + "hashes": [ + "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae", + "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d" + ], + "markers": "python_version >= '3.5'", + "version": "==0.4.2" + }, + "uwsgi": { + "hashes": [ + "sha256:88ab9867d8973d8ae84719cf233b7dafc54326fcaec89683c3f9f77c002cdff9" + ], + "index": "pypi", + "version": "==2.0.20" + } + }, + "develop": {} +} diff --git a/the_social_network/core/__init__.py b/the_social_network/core/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f709468b08cb08b3724248bc7b5b3485e59807c5 --- /dev/null +++ b/the_social_network/core/__init__.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class Operations(Enum): + """ + This enum is the distribute all possible operations globally in this app. + """ + REGISTER = "register" + LOGIN = "login" + LOGOUT = "logout" diff --git a/the_social_network/core/admin.py b/the_social_network/core/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..ef201f8551aaad861f919057223b1c10b3fb9800 --- /dev/null +++ b/the_social_network/core/admin.py @@ -0,0 +1,78 @@ +from django.contrib import admin + +from .models import * + + +class RelationshipInline(admin.StackedInline): + """ + This is the stackable inline representation of the relationships. + It will use the from_account as an foreign key. + It will not display any more then relationships then necessary (extra = 0) + """ + model = Relationship + fk_name = 'from_account' + extra = 0 + + +class StatementsInline(admin.StackedInline): + """ + This is the stackable inline representation of the statements. + It will not display any more then statements then necessary (extra = 0) + """ + model = Statement + extra = 0 + + +class AccountAdmin(admin.ModelAdmin): + """ + This is the admin for the accounts. + This will add the relationships of an account to admin interface. + This will add content of an account to admin interface. + """ + inlines = [RelationshipInline, StatementsInline] + + +class StatementHashtagInline(admin.StackedInline): + """ + This is the stackable representation of the tagging of an statement with an hashtag. + """ + model = HashtagTagging + fk_name = 'statement' + extra = 0 + + +class StatementAccountInline(admin.StackedInline): + """ + This is the stackable representation of the mentioning of an account within an statement. + """ + model = AccountTagging + fk_name = 'statement' + extra = 0 + + +class StatementReactionInline(admin.StackedInline): + """ + This is the stackable representation of the reaction relation between statements. + """ + model = Reaction + fk_name = 'parent' + extra = 0 + + +class StatementAdmin(admin.ModelAdmin): + """ + This ist the admin for the statements. + It will add the tagging of statements with hashtags to the admin interface. + """ + inlines = [StatementHashtagInline, + StatementAccountInline, StatementReactionInline] + + +admin.site.register(Account, AccountAdmin) +admin.site.register(Relationship) + +admin.site.register(Hashtag) +admin.site.register(HashtagTagging) +admin.site.register(AccountTagging) +admin.site.register(Reaction) +admin.site.register(Statement, StatementAdmin) diff --git a/the_social_network/core/apps.py b/the_social_network/core/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..d180014e8bb2febb22415aaca9e2fe95bc6c5eec --- /dev/null +++ b/the_social_network/core/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AuthenticationConfig(AppConfig): + name = 'core' diff --git a/the_social_network/core/migrations/0001_initial.py b/the_social_network/core/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..7ac9b1f12f78275b221441e1079c533471a2ca89 --- /dev/null +++ b/the_social_network/core/migrations/0001_initial.py @@ -0,0 +1,113 @@ +# Generated by Django 3.1.2 on 2020-12-02 11:13 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='Account', + fields=[ + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='auth.user')), + ('image', models.ImageField(default='account/default/Argunaut.png', upload_to='account/images')), + ('biography', models.CharField(default='Hey there, nice to meet you!', max_length=1000)) + ], + ), + migrations.CreateModel( + name='Relationship', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True, db_index=True)), + ('from_account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='from_account', to='core.account')), + ('to_account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='to_account', to='core.account')), + ], + options={ + 'ordering': ('-created',), + }, + ), + migrations.AddField( + model_name='account', + name='related_to', + field=models.ManyToManyField(blank=True, default=None, related_name='related_by', through='core.Relationship', to='core.Account'), + ), + migrations.CreateModel( + name='Statement', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content', models.CharField(max_length=120)), + ('created', models.DateTimeField(auto_now_add=True, db_index=True)), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.account')), + ], + options={ + 'ordering': ('-created',), + }, + ), + migrations.CreateModel( + name='Hashtag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('tag', models.CharField(max_length=30)), + ('created', models.DateTimeField(auto_now_add=True, db_index=True)), + ], + ), + migrations.CreateModel( + name='HashtagTagging', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True, db_index=True)), + ('hashtag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='hashtag', to='core.hashtag')), + ('statement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.statement')), + ], + options={ + 'ordering': ('-created',), + }, + ), + migrations.AddField( + model_name='statement', + name='tagged', + field=models.ManyToManyField(blank=True, default=None, related_name='tags', through='core.HashtagTagging', to='core.Hashtag'), + ), + migrations.CreateModel( + name='AccountTagging', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True, db_index=True)), + ('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='account', to='core.account')), + ('statement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.statement')), + ], + options={ + 'ordering': ('-created',), + }, + ), + migrations.AddField( + model_name='statement', + name='mentioned', + field=models.ManyToManyField(blank=True, default=None, related_name='mentions', through='core.AccountTagging', to='core.Account'), + ), + migrations.CreateModel( + name='Reaction', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True, db_index=True)), + ('vote', models.PositiveSmallIntegerField(choices=[(1, 'like'), (2, 'dislike')], default=1)), + ('child', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='child', to='core.statement')), + ('parent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='parent', to='core.statement')), + ], + options={ + 'ordering': ('-created',), + }, + ), + migrations.AddField( + model_name='statement', + name='reactions', + field=models.ManyToManyField(blank=True, default=None, related_name='reaction_of', through='core.Reaction', to='core.Statement'), + ), + ] diff --git a/the_social_network/core/migrations/0002_auto_20211111_1220.py b/the_social_network/core/migrations/0002_auto_20211111_1220.py new file mode 100644 index 0000000000000000000000000000000000000000..4eed0fd51fd2f2f37d8d8e36b874c37792e02882 --- /dev/null +++ b/the_social_network/core/migrations/0002_auto_20211111_1220.py @@ -0,0 +1,43 @@ +# Generated by Django 3.2.9 on 2021-11-11 12:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='accounttagging', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='hashtag', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='hashtagtagging', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='reaction', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='relationship', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='statement', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/the_social_network/core/migrations/__init__.py b/the_social_network/core/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/the_social_network/core/models.py b/the_social_network/core/models.py new file mode 100644 index 0000000000000000000000000000000000000000..34ef3619b4ca82310e325dca043cdc3509c68808 --- /dev/null +++ b/the_social_network/core/models.py @@ -0,0 +1,418 @@ +import logging +from typing import List, Optional, Tuple +import re + +from django.contrib.auth.models import User +from django.core.files.uploadedfile import InMemoryUploadedFile +from django.db import models +from django.apps import apps + +logger = logging.getLogger(__name__) + + +class Statement(models.Model): + """ + This model represents an statement of an specific account. + """ + author = models.ForeignKey('core.Account', on_delete=models.CASCADE) + content = models.CharField(max_length=120, blank=False) + created = models.DateTimeField(auto_now_add=True, db_index=True) + # Add an hashtag between statements and hashtags over the tagging model + tagged = models.ManyToManyField('Hashtag', + blank=True, + through='HashtagTagging', + symmetrical=False, + related_name='tags', + default=None) + mentioned = models.ManyToManyField('core.Account', + blank=True, + through='AccountTagging', + symmetrical=False, + related_name='mentions', + default=None) + reactions = models.ManyToManyField('self', + blank=True, + through='Reaction', + symmetrical=False, + related_name='reaction_of', + default=None) + + def __str__(self): + return "{author} says: {content}".format(author=self.author.user.username, content=self.content) + + class Meta: + ordering = ('-created',) + + def save(self, *args, **kwargs) -> None: + """ + This method adds hashtags relations after the statement is saved. + Save is ran after update or create. + + :param args: Not used. + :param kwargs: Not used. + :return: None + """ + super(Statement, self).save(*args, **kwargs) + # resolve hashtags after saving the statement + used_hashtags: List[str] = self.__extract_hashtags() + for used_hashtag in used_hashtags: + result: Tuple[Hashtag, bool] = Hashtag.objects.get_or_create(tag=used_hashtag) + hashtag: Hashtag = result[0] + self.add_hashtag(hashtag=hashtag) + used_mentions: List[str] = self.__extract_mentioning() + # resolve mentions after saving the statement + for used_mention in used_mentions: + account: 'core.Account' = apps.get_model("core", "Account").objects.filter( + user__username=used_mention).first() + if account: + self.add_mentioning(account=account) + + def add_reaction(self, reaction_statement: 'Statement', vote: int) -> Tuple['Reaction', bool]: + """ + This method adds an reaction to the calling statement. + :param reaction_statement: The statement to be added as an reaction. + :param vote: The vote of the reaction regarding the parent element. (Like 1, Dislike 0) + :return: The reaction as well as the status if the reaction was already existing. + """ + reaction, created = Reaction.objects.get_or_create( + parent=self, + child=reaction_statement, + vote=vote + ) + return reaction, created + + def remove_as_reaction(self) -> bool: + """ + This method removes the calling statement as an reaction for the parent. + :return: Status if the reaction was deleted or not. + """ + deleted, _ = Reaction.objects.filter(child=self).delete() + return deleted + + def get_reactions(self) -> List['Reaction']: + """ + This method returns all reaction of the calling statement. + :return: List of all reactions to the calling statement + """ + return list(Reaction.objects.filter(parent=self)) + + def get_reaction_to_parent(self) -> Optional[List['Reaction']]: + """ + This method returns the reaction relation to the parent if this statement is used as an reaction. + :return: Reaction to the parent if there is one. Else none. + """ + reaction: List[Reaction] = Reaction.objects.filter(child=self) + if len(reaction) == 0: + return None + # todo: is there a way to use the instance and not a list in the reaction serialize, if so, also change frontend + return reaction + + def get_parent(self) -> Optional['Statement']: + """ + This method is for getting the parent if of an statement if this statement is an reaction. + :return: The parent statement of the reaction. If there is no parent then None is returned. + """ + parents: List['Statement'] = self.reaction_of.all() + if len(parents) == 0: + return None + return parents[0] + + def __extract_hashtags(self) -> List[str]: + """ + This method extracts the hashtag of the content. + Hashtags are alpha numeric words. + + :return: List of all hashtags used in the content of the statement. + """ + return re.findall(r"#(\w+)", self.content) + + def add_hashtag(self, hashtag: 'Hashtag'): + """ + This method is for adding an hashtag to the corresponding statement. + :param hashtag: The hashtag to be added. + :return: True if the hashtag was created, false otherwise. + """ + tagging, created = HashtagTagging.objects.get_or_create(statement=self, hashtag=hashtag) + return created + + def get_hashtags(self) -> List['Hashtag']: + """ + This method is to get all hashtags of the calling statement. + + :return: List of all hashtags of the calling statement. + """ + return list(self.tagged.all()) + + def remove_hashtag(self, hashtag: 'Hashtag'): + """ + This method is used to delete an specific hashtag for the calling statement. + + :param hashtag: The hashtag to be deleted. + :return: True if the hashtag was deleted, false else. + """ + deleted: bool = HashtagTagging.objects.filter( + statement=self, + hashtag=hashtag + ).delete() + return deleted + + def __extract_mentioning(self) -> List['core.Account']: + """ + This method extracts the mentions of accounts in the calling statement. + Accounts names are alpha numeric words. + + :return: List of all accounts mentioned in the calling statement. + """ + return re.findall(r"@(\w+)", self.content) + + def add_mentioning(self, account: 'core.Account'): + """ + This method is for adding an mention of an account to the corresponding statement. + :param account: The account to be mentioned. + :return: True if the mention was created, false otherwise. + """ + mentioning, created = AccountTagging.objects.get_or_create(statement=self, account=account) + return created + + def get_mentioning(self) -> List['core.Account']: + """ + This method is to get all accounts mentioned by the calling statement. + + :return: List of all accounts mentioned by the calling statement. + """ + return list(self.mentioned.all()) + + def remove_mentioning(self, account: 'core.Account'): + """ + This method is used to delete an specific mentioning of an account for the calling statement. + + :param account: The account to be unmentioned. + :return: True if the hashtag was deleted, false else. + """ + deleted: bool = AccountTagging.objects.filter( + statement=self, + account=account + ).delete() + return deleted + + + + + +class Hashtag(models.Model): + """ + This model represents an hashtag which can be added to specific contents. + """ + tag = models.CharField(max_length=30, blank=False) + created = models.DateTimeField(auto_now_add=True, db_index=True) + + def __str__(self): + return "#{tag}".format(tag=self.tag) + + +class Tagging(models.Model): + """ + This model represents the relation between any type of content and an hashtag + """ + # What is the corresponding statement? + statement = models.ForeignKey(Statement, on_delete=models.CASCADE) + # When was this tagging created? + created = models.DateTimeField(auto_now_add=True, db_index=True) + # The default manager + objects = models.Manager() + + class Meta: + abstract = True + + +class HashtagTagging(Tagging): + """ + This model is to represent the tagging of an statement with an hashtag. + """ + # Which hashtag should be tagged? + hashtag = models.ForeignKey(Hashtag, related_name='hashtag', on_delete=models.CASCADE) + + class Meta: + ordering = ('-created',) + + def __str__(self): + return "{statement} tagged with {hashtag}".format(statement=self.statement, hashtag=self.hashtag) + + +class AccountTagging(Tagging): + """ + This model is to represent the mention of an account within an statement. + """ + # Which account should be mentioned? + account = models.ForeignKey('core.Account', related_name='account', on_delete=models.CASCADE) + + class Meta: + ordering = ('-created',) + + def __str__(self): + return "{statement} mentioned {account}".format(statement=self.statement, account=self.account) + + +class Reaction(models.Model): + # Who is the parent element of the reaction + parent = models.ForeignKey(Statement, related_name='parent', on_delete=models.CASCADE) + # Who is the reaction to the parent + child = models.ForeignKey(Statement, related_name='child', on_delete=models.CASCADE) + # When was this reaction created? + created = models.DateTimeField(auto_now_add=True, db_index=True) + # The relation must have an clear vote + vote = models.PositiveSmallIntegerField( + choices=[ + (1, "like"), + (2, "dislike") + ], + default=1 + ) + # The default manager + objects = models.Manager() + + class Meta: + ordering = ('-created',) + + def __str__(self): + return "{child_author} {reaction}s >>{parent_content}<< of {parent_author} because >>{child_content}<<".format( + child_author=self.child.author.user.username, + child_content=self.child.content, + reaction=self.get_vote_display(), + parent_content=self.parent.content, + parent_author=self.parent.author.user.username, + ) + +class Account(models.Model): + """ + This model is for handling user accounts. + Therefore all data regarding an user is stored in this model. + The account is separated from the user since the default user model is used. + """ + # Link the account to an user + user: User = models.OneToOneField(to=User, + on_delete=models.CASCADE, + primary_key=True) + # Add an relationship between accounts over the relationship model + related_to = models.ManyToManyField('self', + blank=True, + through='Relationship', + symmetrical=False, + related_name='related_by', + default=None) + # This is the image of the account + image = models.ImageField(upload_to='account/images', + default='account/default/Argunaut.png') + + biography = models.CharField(blank=False, + max_length=1000, + default="Hey there, nice to meet you!".format(user)) + + # The default manager + objects = models.Manager() + + def __str__(self): + return "{username}".format(username=self.user.username) + + def add_relationship(self, account: 'Account') -> bool: + """ + This method adds an relationship for an instance. + + :param account: Who should be added to an relation with the instance. + :return: True if the relationship was created, false otherwise. + """ + relationship, created = Relationship.objects.get_or_create( + from_account=self, + to_account=account) + return created + + def remove_relationship(self, account: 'Account'): + """ + This method deletes the relationship to an other Account. + + :param account: The user with whom the relationship is to be terminated. + :return: True if the relationship was deleted, false otherwise. + """ + deleted: bool = Relationship.objects.filter( + from_account=self, + to_account=account).delete() + return deleted + + def get_related_to(self) -> List['Account']: + """ + This method returns all accounts related to the calling instance of this method. + Therefore this returns accounts the calling accounts relates to. + + :return: All related accounts of the calling instance. + """ + return list(self.related_to.filter(to_account__from_account=self)) + + def get_related_by(self) -> List['Account']: + """ + This method returns all accounts who relates with the calling account. + + :return: All accounts who relates to the calling account. + """ + return list(self.related_by.filter(from_account__to_account=self)) + + def get_statements(self) -> List['Statement']: + """ + This method returns all all statements made by the calling account. + + :return: All statements made by the calling account. + """ + return list(self.statement_set.all()) + + def add_statement(self, content: str) -> Statement: + """ + This methods add a statement for the calling account. + + :param content: The content of the statement. + :return: The added statement. + """ + statement: Statement = Statement(author=self, content=content) + self.statement_set.add(statement, bulk=False) + return statement + + def update_image(self, new_image: InMemoryUploadedFile): + """ + This method overwrites the image of an account. + If the account uses the default image the image will not be deleted. + :param new_image: The new image to be added for the account. + :return: Nothing + """ + if self.image != "account/default/Argunaut.png": + self.image.delete(save=True) + self.image = new_image + self.save() + + def update_biography(self, new_biography: str): + """ + This method overwrites the biography of an account if it is not none. + :param new_biography: The new biography to be added for the account. + :return: Nothing + """ + if new_biography and self.biography != new_biography: + self.biography = new_biography + self.save() + + +class Relationship(models.Model): + """ + This model handles relations between users. + By using this model it is possible to create more detailed relationships. + """ + # Who wants to have an relation? + from_account = models.ForeignKey(Account, related_name='from_account', on_delete=models.CASCADE) + # To whom should a relationship be established? + to_account = models.ForeignKey(Account, related_name='to_account', on_delete=models.CASCADE) + # When was this relation created? + created = models.DateTimeField(auto_now_add=True, db_index=True) + # The default manager + objects = models.Manager() + + class Meta: + ordering = ('-created',) + + def __str__(self): + return "{from_user} related to {to_user}".format(from_user=self.from_account, to_user=self.to_account) \ No newline at end of file diff --git a/the_social_network/core/serializers/accountSerializers.py b/the_social_network/core/serializers/accountSerializers.py new file mode 100644 index 0000000000000000000000000000000000000000..95724791e0802b4da840e94c3a38d37fe9a407da --- /dev/null +++ b/the_social_network/core/serializers/accountSerializers.py @@ -0,0 +1,80 @@ +from rest_framework import serializers + +from ..models import Account +from ..serializers.authenticationSerializers import UserPublicSerializer, UserOwnSerializer +from ..serializers.contentSerializers import StatementSerializer + + +class AccountSerializer(serializers.ModelSerializer): + """ + This serializer serializes the accounts and their data. + It can be used to serialize accounts. + """ + user = UserPublicSerializer() + + class Meta: + model = Account + fields = ('user', 'image',) + + +class AccountPublicSerializer(serializers.ModelSerializer): + """ + This serializer serializes the public data of accounts. + It can be used to get all public data of the accounts. + """ + # this is the parent account + user = UserPublicSerializer() + # these are the related (child) accounts + related_to = serializers.ListField(source='get_related_to', child=AccountSerializer()) + # these are all statements of the account + statements = serializers.ListField(source='get_statements', child=StatementSerializer()) + # check if the calling account knows the account as friend. + is_friend = serializers.SerializerMethodField('_is_friend') + # this field is to check if the one calls his own public data. + self_request = serializers.SerializerMethodField('_self_request') + + def _self_request(self, obj: Account): + return self.context["calling_account"].user.id == obj.user.id + + def _is_friend(self, obj: Account): + """ + This intern method checks for the calling account if the requested account is a friend or not! + + :param obj: The requested account. + :return: True if obj is a friend of the calling account. + """ + account: Account = self.context["calling_account"] + return obj in account.get_related_to() + + class Meta: + model = Account + fields = ('user', 'image', 'biography', 'related_to', 'statements', 'is_friend', 'self_request',) + + +class AccountTinySerializer(AccountPublicSerializer): + """ + This serializer serializes the public data of accounts but it is shorter. + It can be used to get all shortened public data of the accounts. + """ + # this is the parent account + user = UserPublicSerializer() + + class Meta: + model = Account + fields = ('user', 'image', 'related_to') + + +class AccountOwnSerializer(AccountPublicSerializer): + """ + This serializer is for the representation of an own account. + It shows more information to the user then the public serializer. + Todo: Make statements use the StatementSerializer. + """ + # this is the parent account, it overwrites the field of AccountPublicSerializer + user = UserOwnSerializer() + # to see how follows the own account + related_by = serializers.ListField(source='get_related_by', child=AccountSerializer()) + + class Meta: + model = Account + fields = ('user', 'image', 'biography', 'related_by', 'related_to', 'statements') diff --git a/the_social_network/core/serializers/authenticationSerializers.py b/the_social_network/core/serializers/authenticationSerializers.py new file mode 100644 index 0000000000000000000000000000000000000000..aec3ee52f925304a57fe6e7249e6769e2b032457 --- /dev/null +++ b/the_social_network/core/serializers/authenticationSerializers.py @@ -0,0 +1,100 @@ +import logging +from typing import OrderedDict + +from django.contrib.auth import authenticate +from django.contrib.auth.models import User +from django.core.validators import RegexValidator +from rest_framework import serializers +from rest_framework.validators import UniqueValidator + +logger = logging.getLogger(__name__) + + +class UserDefaultSerializer(serializers.ModelSerializer): + """ + This is the default serializer to get users and validate their data. + """ + + username = serializers.CharField() + + def validate(self, data: OrderedDict): + """ + This method validates the provided data to check if there is an user existing. + + :param data: Data describing the user. + :return: The validated data. + """ + username: str = data.get("username", None) + password: str = data.get("password", None) + + if not username: + raise serializers.ValidationError("This username is missing", code='blank') + + if not password: + raise serializers.ValidationError("This password is missing", code='blank') + + user: User = authenticate(username=username, password=password) + if not user: + raise serializers.ValidationError({"user": "There is no user like this"}, code='invalid') + + return data + + class Meta: + model = User + fields = ['username', 'password'] + + +class UserRegisterSerializer(serializers.ModelSerializer): + """ + This serializer is used for user registration and their validation. + A user should have a unique valid and non-empty username, email and password. + """ + + username = serializers.CharField( + required=True, + validators=[ + UniqueValidator(queryset=User.objects.all()), + RegexValidator(r'^[0-9a-zA-Z_]*$', 'Only aA-zZ, 0-9, _ are allowed.') + ] + ) + email = serializers.EmailField( + required=True, + validators=[ + UniqueValidator(queryset=User.objects.all()) + ] + ) + + def create(self, validated_data): + """ + This method creates a new user by the validated data. + + :param validated_data: The validated data providing all information to create an user. + :return: The created user. + """ + user: User = User.objects.create_user(**validated_data) + return user + + class Meta: + model = User + fields = ['username', 'email', 'password'] + + +class UserPublicSerializer(serializers.ModelSerializer): + """ + This serializer is for the public representation of the user. + It only shows the username. + """ + + class Meta: + model = User + fields = ('id', 'username',) + + +class UserOwnSerializer(serializers.ModelSerializer): + """ + This serializer is for the own representation of the user. + """ + + class Meta: + model = User + fields = ('id', 'username', 'email', 'date_joined',) diff --git a/the_social_network/core/serializers/contentSerializers.py b/the_social_network/core/serializers/contentSerializers.py new file mode 100644 index 0000000000000000000000000000000000000000..cb936f0f2c594e54c940f2169f0ae6c007c2fda9 --- /dev/null +++ b/the_social_network/core/serializers/contentSerializers.py @@ -0,0 +1,124 @@ +from typing import OrderedDict + +from django.apps import apps +from django.db.models import QuerySet +from rest_framework import serializers + +from ..models import Account, Statement, Hashtag, Reaction, HashtagTagging +from ..serializers.authenticationSerializers import UserPublicSerializer + + +class HashtagSerializer(serializers.ModelSerializer): + """ + This serializer can be used to serialize hashtags. + """ + + class Meta: + model = Hashtag + fields = ("id", "tag",) + + +class TrendingHashtagSerializer(HashtagSerializer): + """ + This serializer is for the serialization of trending hashtags. + In addition to the serialization of the hashtags this serializer adds the usage and participants. + This serializer needs an context. The context must include: + - counted: Dict with the hashtag id as key and the usage as value. + - calling_user: Id of the calling user. + """ + count = serializers.SerializerMethodField('_count') + participants = serializers.SerializerMethodField('_participants') + + def _count(self, obj: Hashtag) -> int: + """ + This method adds the precalculated usage of the hashtag. + :param obj: The current hashtag. + :return: The amount of usage of the specific hashtag. + """ + return self.context["counted"][obj.id] + + def _participants(self, obj: Hashtag) -> OrderedDict: + """ + This method takes all usage of the hashtag in combination ith an statements and returns the authors. + Therefore one can get the participants of an conversation regarding this hashtag. + The calling account is excluded from the results. + :param obj: The current hashtag. + :return: The participants of an hashtag with out the requesting account. + """ + tagged: QuerySet[HashtagTagging] = HashtagTagging.objects.filter(hashtag=obj.id) + tagged = tagged.exclude(statement__author=self.context["calling_user"]) + + authors: QuerySet[Account] = Account.objects.filter( + user__id__in=tagged.values_list('statement__author', flat=True).distinct() + ) + serializer: AccountSerializer = AccountSerializer(instance=authors, many=True) + return serializer.data + + class Meta: + model = Hashtag + fields = HashtagSerializer.Meta.fields + ('count', 'participants') + + +class AccountSerializer(serializers.ModelSerializer): + """ + This serializer serializes the accounts and their data. + It can be used to serialize accounts. + Todo: Replace the account mentioning with users to remove this dependencies. + """ + user = UserPublicSerializer() + + class Meta: + model = apps.get_model("core", "Account") + fields = ('user', 'image',) + + +class SimpleStatementSerializer(serializers.ModelSerializer): + """ + This serializer serializes the statements and their content. + It can be used to serialize content of a statement. + """ + author = AccountSerializer() + tagged = serializers.ListField(source='get_hashtags', child=HashtagSerializer()) + mentioned = serializers.ListField(source='get_mentioning', child=AccountSerializer()) + + class Meta: + model = Statement + fields = ('id', 'author', 'content', 'tagged', 'mentioned', 'created') + + +class ReactionSerializer(serializers.ModelSerializer): + """ + This serializer is for reactions. + It will return the serialized reaction and shows the id, vote and the child statement. + """ + child = SimpleStatementSerializer() + parent = SimpleStatementSerializer() + + class Meta: + model = Reaction + fields = ('id', 'vote', 'child', 'parent') + + +class StatementSerializer(SimpleStatementSerializer): + """ + This is more then the simple statement serializer. + With this serializer one can also get information regarding the connection to the parent. + Todo: Is there a way to combine each statement with parent and child information and shorten the frontend? + """ + relation_to_parent = serializers.ListField(source='get_reaction_to_parent', child=ReactionSerializer()) + + class Meta: + model = Statement + fields = SimpleStatementSerializer.Meta.fields + ('relation_to_parent',) + + +class StatementObservationSerializer(StatementSerializer): + """ + This serializer is for the statement observation. + Therefore the reactions are extended in the fields. + """ + reactions = serializers.ListField(source='get_reactions', child=ReactionSerializer()) + + class Meta: + model = Statement + fields = StatementSerializer.Meta.fields + ('reactions',) diff --git a/the_social_network/core/tests.py b/the_social_network/core/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..26f67fa7b58bf203bfb2bcd8e03e2bf530a47cbe --- /dev/null +++ b/the_social_network/core/tests.py @@ -0,0 +1,707 @@ +from typing import List + +from django.contrib.auth import authenticate +from django.contrib.auth.models import User +from django.test import TestCase +from rest_framework.authtoken.models import Token +from rest_framework.response import Response +from rest_framework.test import APIClient, APITestCase +from rest_framework import status + +from .models import Account, Statement, Hashtag, Reaction + +class TestAccounts(TestCase): + + def setUp(self): + self.user_bernd = User.objects.create_user(username="Bernd", email="Bernd@Brot.de", password="Brot") + self.user_beate = User.objects.create_user(username="Beate", email="Rote@Beate.de", password="Rote") + + self.account_bernd: Account = Account.objects.create(user=self.user_bernd) + self.account_beate: Account = Account.objects.create(user=self.user_beate) + + def clear_up_users(self, users: 'list[User]'): + for user in users: + user.delete() + try: + user_exists = User.objects.get(username=user.username) + except Exception as exception: + user_exists = None + self.assertIsNone(user_exists) + # check if the accounts are also removed if the user is deleted + for user in users: + try: + account_exists = Account.objects.get(user=user) + except Exception as exception: + account_exists = None + self.assertIsNone(account_exists) + + def tearDown(self): + self.clear_up_users([self.user_beate, self.user_bernd]) + + def test_account_can_follow_accounts(self): + self.assertIsNotNone(self.account_bernd) + self.assertIsNotNone(self.account_beate) + self.assertFalse(self.account_bernd.related_to.all().exists()) + self.assertFalse(self.account_beate.related_to.all().exists()) + + created: bool = self.account_beate.add_relationship(self.account_bernd) + self.assertTrue(created) + # Beate should have an relationship to Bernd + beates_relations = self.account_beate.related_to.all() + self.assertEqual(beates_relations[0], self.account_bernd) + # But Bernd should not have an relationship to Beate + self.assertFalse(self.account_bernd.related_to.all().exists()) + + def test_account_can_unfollow_accounts(self): + self.test_account_can_follow_accounts() + deleted: bool = self.account_beate.remove_relationship(self.account_bernd) + # Beate can delete her relationships + self.assertTrue(deleted) + self.assertFalse(self.account_beate.related_to.all().exists()) + + def test_accounts_provide_related_accounts(self): + self.test_account_can_follow_accounts() + related_accounts_of_beate: List[Account] = self.account_beate.get_related_to() + related_accounts_of_bernd: List[Account] = self.account_bernd.get_related_to() + # there are results for the relationships for beate but not for bernd + self.assertIsNotNone(related_accounts_of_beate) + self.assertIsNotNone(related_accounts_of_bernd) + self.assertEqual(related_accounts_of_bernd, []) + # Beate has relation to Bernd + self.assertEqual(related_accounts_of_beate[0], self.account_bernd) + + def test_accounts_can_provides_accounts_who_relates_with_them(self): + self.test_account_can_follow_accounts() + accounts_relating_to_beate: List[Account] = self.account_beate.get_related_by() + accounts_relating_to_bernd: List[Account] = self.account_bernd.get_related_by() + # Beate only relates to Bernd + self.assertEqual([], accounts_relating_to_beate) + self.assertNotEqual([], accounts_relating_to_bernd) + + +class TestGetAccount(APITestCase): + def setUp(self): + self.client = APIClient() + self.user_bernd = User.objects.create_user(username="Bernd", email="Bernd@Brot.de", password="Brot") + self.user_beate = User.objects.create_user(username="Beate", email="Rote@Beate.de", password="Rote") + self.token_bernd = Token.objects.create(user=self.user_bernd) + self.token_beate = Token.objects.create(user=self.user_beate) + + self.account_bernd: Account = Account.objects.create(user=self.user_bernd) + self.account_beate: Account = Account.objects.create(user=self.user_beate) + + def test_own_account_provides_own_data(self): + + created: bool = self.account_beate.add_relationship(self.account_bernd) + self.assertTrue(created) + # Beate should have an relationship to Bernd + beates_relations = self.account_beate.related_to.all() + self.assertEqual(beates_relations[0], self.account_bernd) + # But Bernd should not have an relationship to Beate + self.assertFalse(self.account_bernd.related_to.all().exists()) + + self.client.credentials(HTTP_AUTHORIZATION='Token ' + str(self.token_beate)) + response: Response = self.client.get(path="/accounts/show/own/") + + # public information about beate + self.assertEqual(response.data[0]["user"]["username"], self.user_beate.username) + self.assertEqual(response.data[0]["user"]["id"], self.user_beate.id) + + # public information about the account she is relates to + self.assertEqual(response.data[0]["related_to"][0]["user"]["username"], self.user_bernd.username) + self.assertEqual(response.data[0]["related_to"][0]["user"]["id"], self.user_bernd.id) + + # public information about the account who relates to her + self.assertEqual(response.data[0]["related_by"], []) + + # what do we know about bernd + self.client.credentials(HTTP_AUTHORIZATION='Token ' + str(self.token_bernd)) + response: Response = self.client.get(path="/accounts/show/own/") + + # public information about bernd + self.assertEqual(response.data[0]["user"]["username"], self.user_bernd.username) + self.assertEqual(response.data[0]["user"]["id"], self.user_bernd.id) + + # public information about the accounts who relates to Bernd + self.assertEqual(response.data[0]["related_by"][0]["user"]["username"], self.user_beate.username) + self.assertEqual(response.data[0]["related_by"][0]["user"]["id"], self.user_beate.id) + + # public information about the account Bernd relates to + self.assertEqual(response.data[0]["related_to"], []) + + # Bernd adds an statement + self.account_bernd.add_statement("I like Beate") + # what do we know about Bernd + response: Response = self.client.get(path="/accounts/show/{}/".format(self.user_bernd.id)) + self.assertEqual(response.data[0]["statements"][0]["content"], "I like Beate") + + def test_account_provide_public_data(self): + created: bool = self.account_beate.add_relationship(self.account_bernd) + self.assertTrue(created) + # Beate should have an relationship to Bernd + beates_relations = self.account_beate.related_to.all() + self.assertEqual(beates_relations[0], self.account_bernd) + # But Bernd should not have an relationship to Beate + self.assertFalse(self.account_bernd.related_to.all().exists()) + + self.client.credentials(HTTP_AUTHORIZATION='Token ' + str(self.token_bernd)) + response: Response = self.client.get(path="/accounts/show/{}/".format(self.user_beate.id)) + + # public information about beate + self.assertEqual(response.data[0]["user"]["username"], self.user_beate.username) + self.assertEqual(response.data[0]["user"]["id"], self.user_beate.id) + + # public information about the account she is relates to + self.assertEqual(response.data[0]["related_to"][0]["user"]["username"], self.user_bernd.username) + self.assertEqual(response.data[0]["related_to"][0]["user"]["id"], self.user_bernd.id) + self.assertFalse(response.data[0]["is_friend"]) + + # what do we know about bernd + self.client.credentials(HTTP_AUTHORIZATION='Token ' + str(self.token_beate)) + response: Response = self.client.get(path="/accounts/show/{}/".format(self.user_bernd.id)) + + # public information about bernd + self.assertEqual(response.data[0]["user"]["username"], self.user_bernd.username) + self.assertEqual(response.data[0]["user"]["id"], self.user_bernd.id) + + # public information about the account Bernd relates to + self.assertEqual(response.data[0]["related_to"], []) + self.assertTrue(response.data[0]["is_friend"]) + + # Bernd adds an statement + self.account_bernd.add_statement("I like Beate") + # what do we know about Bernd + self.client.credentials(HTTP_AUTHORIZATION='Token ' + str(self.token_beate)) + response: Response = self.client.get(path="/accounts/show/{}/".format(self.user_bernd.id)) + self.assertEqual(response.data[0]["statements"][0]["content"], "I like Beate") + + def clear_up_users(self, users: 'list[User]'): + for user in users: + user.delete() + try: + user_exists = User.objects.get(username=user.username) + except Exception as exception: + user_exists = None + self.assertIsNone(user_exists) + # check if the accounts are also removed if the user is deleted + for user in users: + try: + account_exists = Account.objects.get(user=user) + except Exception as exception: + account_exists = None + self.assertIsNone(account_exists) + + def tearDown(self): + self.clear_up_users([self.user_beate, self.user_bernd]) + + +class TestObtainingAToken(APITestCase): + def setUp(self): + self.client = APIClient() + self.user = User.objects.create_user(username="Rudiger", email="Rudiger@Dog.com", password="Rudiger") + self.user.save() + + def tearDown(self): + self.user.delete() + try: + user = User.objects.get(username="Rudiger") + except Exception as exception: + user = None + self.assertIsNone(user) + + def test_valid_user_obtains_token(self): + response: Response = self.client.post(path="/authentication/obtain/", data={ + "username": "Rudiger", + "password": "Rudiger" + }) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue("token" in response.data.keys()) + self.assertIsNotNone(response.data["token"]) + + def test_invalid_user_does_not_obtains_token(self): + response: Response = self.client.post(path="/authentication/obtain/", data={ + "username": "Klaus", + "password": "Klaus" + }) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + +class TestValidateAToken(APITestCase): + def setUp(self): + self.client = APIClient() + self.user = User.objects.create_user(username="Rudiger", email="Rudiger@Dog.com", password="Rudiger") + self.user_token = Token.objects.create(user=self.user) + + def tearDown(self): + self.user.delete() + try: + user = User.objects.get(username="Rudiger") + except Exception as exception: + user = None + self.assertIsNone(user) + + def test_invalid_user_cant_validate_token(self): + self.client.credentials(HTTP_AUTHORIZATION='Token ' + 'Invalid Token') + response: Response = self.client.get(path="/authentication/validate/") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_valid_user_can_validate_token(self): + self.client.credentials(HTTP_AUTHORIZATION='Token ' + str(self.user_token)) + response: Response = self.client.get(path="/authentication/validate/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + +class TestRegistration(APITestCase): + def setUp(self): + self.client = APIClient() + + def test_everything_is_missing(self): + response: Response = self.client.post(path="/authentication/register/", data={}) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data["username"][0].code, "required") + self.assertEqual(response.data["password"][0].code, "required") + self.assertEqual(response.data["email"][0].code, "required") + + def test_everything_is_empty(self): + response: Response = self.client.post(path="/authentication/register/", data={ + "username": "", + "email": "", + "password": "" + }) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data["username"][0].code, "blank") + self.assertEqual(response.data["password"][0].code, "blank") + self.assertEqual(response.data["email"][0].code, "blank") + + def test_user_and_email_already_exists(self): + user = User.objects.create_user(username="Peter", password="password", email="e@mail.de") + + response: Response = self.client.post(path="/authentication/register/", data={ + "username": "Peter", + "password": "password", + "email": "e@mail.de" + }) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data["username"][0].code, "unique") + self.assertEqual(response.data["email"][0].code, "unique") + + def test_username_is_not_alphanumeric(self): + response: Response = self.client.post(path="/authentication/register/", data={ + "username": "NoWay%&/\/8)(?=!"'``´', + "password": "password", + "email": "e@mail.de" + }) + self.assertEqual(response.data["username"][0].code, "invalid") + + def test_register_valid_user(self): + response: Response = self.client.post(path="/authentication/register/", data={ + "username": "another_user100", + "password": "another_password", + "email": "another_e@mail.de" + }) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + token_user: User = Token.objects.get(key=response.data["token"]).user + user: User = User.objects.get(username="another_user100") + account: Account = Account.objects.get(user=user) + self.assertIsNotNone(account) + self.assertEqual(account.user, user) + self.assertEqual(token_user, user) + + +class TestLogin(APITestCase): + def setUp(self): + self.client = APIClient() + self.user = User.objects.create_user(username="Bernd", email="Bernd@Brot.com", password="Brot") + self.user.save() + + def test_valid_user_can_login(self): + response: Response = self.client.post(path="/authentication/login/", data={ + "username": "Bernd", "password": "Brot" + }) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + user: User = Token.objects.get(key=response.data["token"]).user + self.assertEqual(self.user, user) + user: User = authenticate(username="Bernd", password="Brot") + self.assertIsNotNone(user) + + def test_token_gets_refreshed_after_new_login(self): + response: Response = self.client.post(path="/authentication/login/", data={ + "username": "Bernd", "password": "Brot" + }) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + token_old: Token = Token.objects.get(key=response.data["token"]) + self.assertIsNotNone(token_old) + + response: Response = self.client.post(path="/authentication/login/", data={ + "username": "Bernd", "password": "Brot" + }) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + token_new: Token = Token.objects.get(key=response.data["token"]) + self.assertIsNotNone(token_new) + + self.assertNotEqual(token_old, token_new) + + def test_user_can_not_login_with_wrong_username(self): + response: Response = self.client.post(path="/authentication/login/", data={ + "username": "Berndy", "password": "Brot" + }) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data["user"][0].code, "invalid") + + def test_user_can_not_login_with_wrong_password(self): + response: Response = self.client.post(path="/authentication/login/", data={ + "username": "Bernd", "password": "Brötchen" + }) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data["user"][0].code, "invalid") + + def test_user_can_not_login_without_username(self): + response: Response = self.client.post(path="/authentication/login/", data={ + "password": "Brot" + }) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data["username"][0].code, "required") + + def test_user_can_not_login_with_empty_username(self): + response: Response = self.client.post(path="/authentication/login/", data={ + "username": "", "password": "Brot" + }) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data["username"][0].code, "blank") + + def test_user_can_not_login_without_password(self): + response: Response = self.client.post(path="/authentication/login/", data={ + "username": "Bernd" + }) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data["password"][0].code, "required") + + def test_user_can_not_login_with_empty_password(self): + response: Response = self.client.post(path="/authentication/login/", data={ + "username": "Bernd", "password": "" + }) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data["password"][0].code, "blank") + + def tearDown(self): + self.user.delete() + try: + user = User.objects.get(username="Bernd") + except Exception as exception: + user = None + self.assertIsNone(user) + + +class TestLogout(APITestCase): + def setUp(self): + self.client = APIClient() + self.user = User.objects.create_user(username="Beate", email="Rote@Beate.com", password="Rote") + self.user.save() + + def test_valid_user_can_logout(self): + response: Response = self.client.post(path="/authentication/login/", data={ + "username": "Beate", "password": "Rote" + }) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + token: Token = Token.objects.get(key=response.data["token"]) + user: User = token.user + self.assertEqual(self.user, user) + self.client.credentials(HTTP_AUTHORIZATION='Token ' + str(token)) + response: Response = self.client.post(path="/authentication/logout/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + token: Token = Token.objects.filter(user=user).first() + self.assertIsNone(token) + + def test_valid_user_can_not_logout_twice(self): + response: Response = self.client.post(path="/authentication/login/", data={ + "username": "Beate", "password": "Rote" + }) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + user: User = Token.objects.get(key=response.data["token"]).user + token: Token = Token.objects.get(key=response.data["token"]) + self.assertEqual(self.user, user) + + self.client.credentials(HTTP_AUTHORIZATION='Token ' + str(token)) + response: Response = self.client.post(path="/authentication/logout/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + token: Token = Token.objects.filter(user=user).first() + self.assertIsNone(token) + + self.client.credentials(HTTP_AUTHORIZATION='Token ' + str(token)) + response: Response = self.client.post(path="/authentication/logout/") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def tearDown(self): + self.user.delete() + try: + user = User.objects.get(username="Rote") + except Exception as exception: + user = None + self.assertIsNone(user) + + +class TestStatement(TestCase): + def setUp(self): + self.user_bernd = User.objects.create_user(username="Bernd", email="Bernd@Brot.de", password="Brot") + self.account_bernd: Account = Account.objects.create(user=self.user_bernd) + + self.user_beate = User.objects.create_user(username="Beate", email="Rote@Beate.de", password="Beate") + self.account_beate: Account = Account.objects.create(user=self.user_beate) + + self.statement: Statement = Statement.objects.create(author=self.account_bernd, content="I like Beate") + self.statement_with_hashtags_and_mentioning: Statement = Statement.objects.create(author=self.account_bernd, + content="I like #eating the #/&%! whopper #Burger_King2020 with @Beate @NoOne") + self.hashtag: Hashtag = Hashtag.objects.create(tag="Burger_King2020") + + def test_statement_can_mention_account(self): + created = self.statement.add_mentioning(self.account_beate) + self.assertTrue(created) + mentions = self.statement.get_mentioning() + self.assertNotEqual(mentions, []) + self.assertEqual(mentions[0], self.account_beate) + deleted = self.statement.remove_mentioning(self.account_beate) + self.assertTrue(deleted) + mentions = self.statement.get_mentioning() + self.assertEqual(mentions, []) + + def test_account_has_statement_from_database(self): + statements: List[Statement] = self.account_bernd.get_statements() + self.assertNotEqual(statements, []) + self.assertEqual(statements[1], self.statement) + + def test_account_can_add_statement(self): + self.account_bernd.add_statement("I <3 burgers") + self.assertEqual(len(self.account_bernd.get_statements()), 3) + self.assertEqual(self.account_bernd.get_statements()[0].content, "I <3 burgers") + + def test_statement_resolves_hashtags(self): + hashtags = self.statement_with_hashtags_and_mentioning.get_hashtags() + self.assertEqual(len(hashtags), 2) + self.assertEqual(hashtags[0].tag, "eating") + self.assertEqual(hashtags[1].tag, "Burger_King2020") + + def test_statement_resolves_mentions(self): + mentions = self.statement_with_hashtags_and_mentioning.get_mentioning() + self.assertNotEqual(mentions, []) + self.assertEqual(len(mentions), 1) + self.assertEqual(mentions[0], self.account_beate) + + def test_statement_can_add_hashtag(self): + created = self.statement.add_hashtag(self.hashtag) + self.assertTrue(created) + + hashtags = self.statement.get_hashtags() + self.assertNotEqual(hashtags, []) + self.assertEqual(self.hashtag, hashtags[0]) + + deleted = self.statement.remove_hashtag(self.hashtag) + self.assertTrue(deleted) + + hashtags = self.statement.get_hashtags() + self.assertEqual(hashtags, []) + + def test_statement_can_add_reaction(self): + created = self.statement.add_reaction(self.statement_with_hashtags_and_mentioning, 1) + self.assertTrue(created) + reactions: List[Reaction] = self.statement.get_reactions() + self.assertEqual(len(reactions), 1) + reaction: Reaction = reactions[0] + self.assertEqual(reaction.get_vote_display(), "like") + self.assertEqual(reaction.child, self.statement_with_hashtags_and_mentioning) + deleted = self.statement_with_hashtags_and_mentioning.remove_as_reaction() + self.assertTrue(deleted) + + def tearDown(self): + self.user_bernd.delete() + try: + user_exists = User.objects.get(username=self.user_bernd.username) + except Exception as exception: + user_exists = None + self.assertIsNone(user_exists) + # check if bernds account is deleted + try: + account_exists = Account.objects.get(user=self.user_bernd) + except Exception as exception: + account_exists = None + self.assertIsNone(account_exists) + # check if bernds statements are deleted + try: + statements_exists = Statement.objects.get(user=self.user_bernd) + except Exception as exception: + statements_exists = None + self.assertIsNone(statements_exists) + try: + hashtag_exists = Statement.objects.get(user=self.hashtag) + except Exception as exception: + hashtag_exists = None + self.assertIsNone(hashtag_exists) + + +class TestGetStatement(APITestCase): + def setUp(self): + self.client = APIClient() + self.user_bernd = User.objects.create_user(username="Bernd", email="Bernd@Brot.de", password="Brot") + self.account_bernd: Account = Account.objects.create(user=self.user_bernd) + self.token_bernd = Token.objects.create(user=self.user_bernd) + self.statement_1: Statement = Statement.objects.create(author=self.account_bernd, content="I like @Bernd #Foo") + self.statement_2: Statement = Statement.objects.create(author=self.account_bernd, content="I like Beate") + + def test_statement_provides_data(self): + self.statement_1.add_reaction(self.statement_2, 2) + self.client.credentials(HTTP_AUTHORIZATION='Token ' + str(self.token_bernd)) + response: Response = self.client.get(path="/contents/statements/get/{id}/".format(id=self.statement_1.id)) + self.assertEqual(response.data[0]["id"], self.statement_1.id) + self.assertEqual(len(response.data[0]["mentioned"]), 1) + self.assertEqual(len(response.data[0]["tagged"]), 1) + self.assertEqual(len(response.data[0]["reactions"]), 1) + reaction = response.data[0]["reactions"][0] + self.assertEqual(reaction["vote"], 2) + self.assertEqual(reaction["child"]["id"], self.statement_2.id) + self.assertEqual(self.statement_2.get_parent(), self.statement_1) + self.assertIsNone(self.statement_1.get_parent()) + self.assertEqual(self.statement_2.get_reaction_to_parent()[0].vote, 2) + + def test_statements_with_hashtags_can_be_provided(self): + self.client.credentials(HTTP_AUTHORIZATION='Token ' + str(self.token_bernd)) + response: Response = self.client.get(path="/contents/statements/with/hashtag/?q=Foo") + self.assertTrue(len(response.data) != 0) + + self.assertEqual(response.data[0]["id"], self.statement_1.id) + + +class TestGetStatementFeed(APITestCase): + def setUp(self): + self.client = APIClient() + self.user_bernd = User.objects.create_user(username="Bernd", email="Bernd@Brot.de", password="Brot") + self.account_bernd: Account = Account.objects.create(user=self.user_bernd) + + self.user_beate = User.objects.create_user(username="Beate", email="Rote@Beate.de", password="Beate") + self.account_beate: Account = Account.objects.create(user=self.user_beate) + self.account_beate.add_relationship(self.account_bernd) + self.token_beate = Token.objects.create(user=self.user_beate) + + self.statement_1: Statement = Statement.objects.create(author=self.account_bernd, content="I like @Bernd #Foo") + self.statement_2: Statement = Statement.objects.create(author=self.account_bernd, content="I like Beate") + self.statement_3: Statement = Statement.objects.create(author=self.account_beate, content="I like Bernd") + self.statement_1.add_reaction(self.statement_2, 2) + + def test_feed_contains_correct_data(self): + self.statement_1.add_reaction(self.statement_2, 2) + self.client.credentials(HTTP_AUTHORIZATION='Token ' + str(self.token_beate)) + response: Response = self.client.get(path="/contents/statements/feed/") + result = response.data + self.assertTrue(len(result), 3) + print(result[0].get("id")) + print(result[1].get("id")) + print(result[2].get("id")) + print(self.statement_1.id) + print(self.statement_2.id) + print(self.statement_3.id) + self.assertEqual(result[0].get("id"), self.statement_3.id) + self.assertEqual(result[1].get("id"), self.statement_2.id) + self.assertEqual(result[2].get("id"), self.statement_1.id) + self.assertTrue(result[0].get("id") > result[1].get("id")) + self.assertTrue(result[0].get("created") > result[1].get("created")) + self.assertTrue(result[1].get("id") > result[2].get("id")) + self.assertTrue(result[1].get("created") > result[2].get("created")) + + +class TestTrendingHashtag(APITestCase): + def setUp(self): + self.client = APIClient() + self.user_bernd = User.objects.create_user(username="Bernd", email="Bernd@Brot.de", password="Brot") + self.account_bernd: Account = Account.objects.create(user=self.user_bernd) + + self.user_beate = User.objects.create_user(username="Beate", email="Rote@Beate.de", password="Beate") + self.account_beate: Account = Account.objects.create(user=self.user_beate) + self.token_beate = Token.objects.create(user=self.user_beate) + + def test_trending_hashtag_is_empty(self): + self.client.credentials(HTTP_AUTHORIZATION='Token ' + str(self.token_beate)) + response: Response = self.client.get(path="/contents/trending/hashtag/") + self.assertEqual(response.data, []) + + def test_trending_hashtag_not_empty(self): + self.statement_1: Statement = Statement.objects.create(author=self.account_bernd, content="#Foo#Bar!") + self.statement_2: Statement = Statement.objects.create(author=self.account_beate, content="#Foo #Bar#Baz") + self.statement_3: Statement = Statement.objects.create(author=self.account_beate, + content="I like @Beate#Foo#Baz#Bizz!") + self.statement_1.add_reaction(self.statement_3, 2) + self.client.credentials(HTTP_AUTHORIZATION='Token ' + str(self.token_beate)) + response: Response = self.client.get(path="/contents/trending/hashtag/") + self.assertEqual(response.data[0]["count"], 3) + self.assertEqual(response.data[0]["tag"], "Foo") + self.assertEqual(len(response.data[0]["participants"]), 1) + self.assertEqual(response.data[0]["participants"][0]["user"]["id"], self.user_bernd.id) + self.assertEqual(response.data[1]["count"], 2) + self.assertEqual(response.data[1]["tag"], "Bar") + self.assertEqual(len(response.data[1]["participants"]), 1) + self.assertEqual(response.data[1]["participants"][0]["user"]["id"], self.user_bernd.id) + self.assertTrue(response.data[2]["count"], 2) + self.assertEqual(response.data[2]["tag"], "Baz") + self.assertEqual(len(response.data[2]["participants"]), 0) + + +class TestSearch(APITestCase): + def setUp(self): + self.client = APIClient() + self.user_bernd = User.objects.create_user(username="Bernd", email="Bernd@Brot.de", password="Brot") + self.token_bernd = Token.objects.create(user=self.user_bernd) + self.account_bernd: Account = Account.objects.create(user=self.user_bernd) + self.hashtag: Hashtag = Hashtag.objects.create(tag="Berg") + + def clear_up_users(self, users: 'list[User]'): + for user in users: + user.delete() + try: + user_exists = User.objects.get(username=user.username) + except Exception as exception: + user_exists = None + self.assertIsNone(user_exists) + # check if the accounts are also removed if the user is deleted + for user in users: + try: + account_exists = Account.objects.get(user=user) + except Exception as exception: + account_exists = None + self.assertIsNone(account_exists) + + def tearDown(self): + self.clear_up_users([self.user_bernd]) + self.hashtag.delete() + + def test_user_can_search(self): + self.client.credentials(HTTP_AUTHORIZATION='Token ' + str(self.token_bernd)) + response: Response = self.client.get(path="/search/?q=Ber") + self.assertEqual(len(response.data["accounts"]), 1) + self.assertEqual(response.data["accounts"][0]["user"]["username"], self.user_bernd.username) + self.assertEqual(len(response.data["hashtags"]), 1) + self.assertEqual(response.data["hashtags"][0]["tag"], self.hashtag.tag) + + def test_user_can_filter_search(self): + self.client.credentials(HTTP_AUTHORIZATION='Token ' + str(self.token_bernd)) + response: Response = self.client.get(path="/search/?q=Ber&filter=account") + self.assertEqual(len(response.data["accounts"]), 1) + self.assertEqual(response.data["accounts"][0]["user"]["username"], self.user_bernd.username) + self.assertTrue("hashtags" not in response.data.keys()) + response: Response = self.client.get(path="/search/?q=Ber&filter=hashtag") + self.assertEqual(len(response.data["hashtags"]), 1) + self.assertEqual(response.data["hashtags"][0]["tag"], self.hashtag.tag) + self.assertTrue("accounts" not in response.data.keys()) diff --git a/the_social_network/core/urls/accountUrls.py b/the_social_network/core/urls/accountUrls.py new file mode 100644 index 0000000000000000000000000000000000000000..31f663ff0feb7354b5aa558224d1937799de4918 --- /dev/null +++ b/the_social_network/core/urls/accountUrls.py @@ -0,0 +1,13 @@ +from django.urls import path + +from ..views.accountViews import * + +urlpatterns = [ + path('show/<int:id>/', PublicAccounts.as_view(), name='show'), + path('show/all/', AllPublicAccounts.as_view(), name='showAll'), + path('show/own/', OwnAccount.as_view(), name='own'), + path('update/', OwnAccountUpdate.as_view(), name='update'), + path('follow/<int:id>/', OwnAccountFollow.as_view(), name='follow'), + path('unfollow/<int:id>/', OwnAccountUnfollow.as_view(), name='unfollow'), + path('operation/add/statement/', AddStatement.as_view(), name='addStatement'), +] \ No newline at end of file diff --git a/the_social_network/core/urls/authenticationUrls.py b/the_social_network/core/urls/authenticationUrls.py new file mode 100644 index 0000000000000000000000000000000000000000..dfe496d721ea8974d1207e38dab10dde26dc0a00 --- /dev/null +++ b/the_social_network/core/urls/authenticationUrls.py @@ -0,0 +1,12 @@ +from django.conf.urls import url +from rest_framework.authtoken.views import obtain_auth_token + +from ..views.authenticationViews import * + +urlpatterns = [ + url(r'obtain/', obtain_auth_token, name='obtain'), + url(r'register/', Register.as_view(), name='register'), + url(r'login/', Login.as_view(), name='login'), + url(r'logout/', Logout.as_view(), name='logout'), + url(r'validate/', TokenValidation.as_view(), name='validate'), +] diff --git a/the_social_network/core/urls/contentUrls.py b/the_social_network/core/urls/contentUrls.py new file mode 100644 index 0000000000000000000000000000000000000000..575325426a1d3e8cb10242c357789588c9e17233 --- /dev/null +++ b/the_social_network/core/urls/contentUrls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from ..views.contentViews import * + +urlpatterns = [ + path('statements/get/<int:id>/', ShowStatement.as_view(), name="show_statement"), + path('statements/with/hashtag/', ShowStatementsWithHashtag.as_view(), name="show_statement_with_hashtag"), + path('statements/feed/', ShowStatementFeed.as_view(), name="show_statement_feed"), + path('trending/hashtag/', ShowTrendingHashtag.as_view(), name="show_trending_hashtags"), +] \ No newline at end of file diff --git a/the_social_network/core/urls/searchUrls.py b/the_social_network/core/urls/searchUrls.py new file mode 100644 index 0000000000000000000000000000000000000000..d382879525dbb3c48edf98353649acc310239f3a --- /dev/null +++ b/the_social_network/core/urls/searchUrls.py @@ -0,0 +1,7 @@ +from django.urls import path + +from ..views.searchViews import * + +urlpatterns = [ + path('', Search.as_view(), name='search'), +] \ No newline at end of file diff --git a/the_social_network/core/validation.py b/the_social_network/core/validation.py new file mode 100644 index 0000000000000000000000000000000000000000..8e11ba7f535fe5d7cfd2cde323b02993f0e2a710 --- /dev/null +++ b/the_social_network/core/validation.py @@ -0,0 +1,34 @@ +import logging + +from rest_framework import status +from rest_framework.request import Request +from rest_framework.response import Response + +from . import Operations +from .serializers.authenticationSerializers import UserRegisterSerializer, UserDefaultSerializer + +logger = logging.getLogger(__name__) + + +def validate_request_data_for(operation: Operations, request: Request) -> Response: + """ + This method validates the a request regarding user its user information. + + :param operation: Which operation was used? This influences which serializer is used and which fields are checked. + :param request: The request regarding an user. + :return: 400_BAD_REQUEST if the provided data is sparse or one of the values is empty or there are duplicated values. + 201_CREATE if the new user is created. + 200_OK if the user is valid. + """ + serializer = UserDefaultSerializer(data=request.data) + + if operation is Operations.REGISTER: + serializer = UserRegisterSerializer(data=request.data) + + if serializer.is_valid(): + if operation is Operations.REGISTER: + serializer.save() # this calls update or create based on the existence of the corresponding instance + return Response(data=serializer.validated_data, status=status.HTTP_201_CREATED) + return Response(data=serializer.validated_data, status=status.HTTP_200_OK) + else: + return Response(data=serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/the_social_network/core/views/accountViews.py b/the_social_network/core/views/accountViews.py new file mode 100644 index 0000000000000000000000000000000000000000..a1df217cbd69453e92ff8093a9cabfaa727daea6 --- /dev/null +++ b/the_social_network/core/views/accountViews.py @@ -0,0 +1,204 @@ +# Create your views here. +import logging +from io import BytesIO + +from PIL import Image +from django.contrib.auth.models import User +from django.core.files.uploadedfile import InMemoryUploadedFile +from django.db.models import Q +from rest_framework import status +from rest_framework.authentication import TokenAuthentication +from rest_framework.permissions import IsAuthenticated +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.views import APIView + +from ..models import Account, Statement +from ..serializers.accountSerializers import AccountPublicSerializer, AccountOwnSerializer +from ..serializers.contentSerializers import ReactionSerializer, StatementSerializer + +logger = logging.getLogger(__name__) + + +class PublicAccounts(APIView): + """ + This view is used to represent the accounts. + It can be used to get public information about accounts from the perspective of the calling account. + To get this information the calling account must use its token. + """ + authentication_classes = [TokenAuthentication] + permission_classes = (IsAuthenticated,) + + def get(self, request: Request, *args, **kwargs): + """ + This method is used to get all public information regarding a specific account. + :param request: Not used. + :param args: Not used. + :param kwargs: Should have the id of the requested account (see view.py) + :return: + """ + calling_account: Account = Account.objects.filter(user=request.user).first() + account: Account = Account.objects.filter(user=int(kwargs.get("id"))) + serializer: AccountPublicSerializer = AccountPublicSerializer(instance=account, + many=True, + context={"calling_account": calling_account}) + return Response(data=serializer.data, status=status.HTTP_200_OK) + + +class AllPublicAccounts(APIView): + """ + This view is for showing all public accounts. + It requires the user token to see which account is calling the overview. + The calling account is then removed from the result. + """ + authentication_classes = [TokenAuthentication] + permission_classes = (IsAuthenticated,) + + def get(self, request: Request): + """ + This method gets all available public accounts. + Furthermore the calling account is excluded from the result. + :param request: Used to get the calling account. + :return: + """ + calling_account: Account = Account.objects.filter(user=request.user).first() + accounts: Account = Account.objects.filter(~Q(user=request.user)) + serializer: AccountPublicSerializer = AccountPublicSerializer(instance=accounts, + many=True, + context={"calling_account": calling_account}) + return Response(data=serializer.data, status=status.HTTP_200_OK) + + +class OwnAccount(APIView): + """ + This view is used to represent the private account of an user. + To access the information one have to use its token. + """ + authentication_classes = [TokenAuthentication] + permission_classes = (IsAuthenticated,) + + def get(self, request: Request): + """ + This method is used to get the own account data. + :param request: To access the user from its token. + :return: Data regarding the own account. + """ + account: Account = Account.objects.filter(user=request.user) + serializer: AccountOwnSerializer = AccountOwnSerializer(instance=account, many=True) + return Response(data=serializer.data, status=status.HTTP_200_OK) + + +class OwnAccountFollow(APIView): + """ + This view is for adding a follow relation from the calling account to the targeted one. + To access this view the requesting account has to use its token. + """ + authentication_classes = [TokenAuthentication] + permission_classes = (IsAuthenticated,) + + def post(self, request: Request, *args, **kwargs): + own_account: Account = Account.objects.filter(user=request.user).first() + foreign_account: Account = Account.objects.filter(user=kwargs.get("id")).first() + if not foreign_account: + return Response(status=status.HTTP_409_CONFLICT) + created: bool = own_account.add_relationship(foreign_account) + if created: + return Response(status=status.HTTP_200_OK) + return Response(status=status.HTTP_409_CONFLICT) + + +class OwnAccountUnfollow(APIView): + """ + This view is for deleting a follow relation from the calling account to the targeted one. + To access this view the requesting account has to use its token. + """ + authentication_classes = [TokenAuthentication] + permission_classes = (IsAuthenticated,) + + def post(self, request: Request, *args, **kwargs): + own_account: Account = Account.objects.filter(user=request.user).first() + foreign_account: Account = Account.objects.filter(user=kwargs.get("id")).first() + if not foreign_account: + return Response(status=status.HTTP_409_CONFLICT) + deleted: bool = own_account.remove_relationship(foreign_account) + if deleted: + return Response(status=status.HTTP_200_OK) + return Response(status=status.HTTP_409_CONFLICT) + + +class OwnAccountUpdate(APIView): + """ + This view is for updating the account data. + It will update an image. + """ + authentication_classes = [TokenAuthentication] + permission_classes = (IsAuthenticated,) + + @staticmethod + def put(request: Request): + """ + This method updates the image as well as the biography. + :param request: The request send by the user. + :return: A response with an 200 status. + """ + user: User = request.user + account: Account = Account.objects.filter(user=user).first() + + if "biography" in request.data.keys(): + biography: str = request.data["biography"] + account.update_biography(biography) + if "file" in request.FILES.keys(): + file: InMemoryUploadedFile = request.FILES["file"] + file.name = "{name}.{extension}".format(name="account{user}{secret}".format(user=user, secret=hash(user)), + extension="jpeg") + # compress image + image: Image = Image.open(file) + image = image.convert('RGB') + io_stream = BytesIO() + image.save(io_stream, format="JPEG", quality=50, optimize=True) + compressed_image: InMemoryUploadedFile = InMemoryUploadedFile(file=io_stream, + field_name=None, + name=file.name, + content_type="image/jpeg", + size=io_stream.tell(), + charset=None) + # update with compressed image + account.update_image(compressed_image) + return Response(status=status.HTTP_200_OK) + + +class AddStatement(APIView): + """ + This view serves to add statements for an specific user. + The user must be authenticated. + """ + authentication_classes = [TokenAuthentication] + permission_classes = (IsAuthenticated,) + + @staticmethod + def post(request: Request): + """ + This method handles the post of an new statement. + :param request: The request to be handled, containing the input. + :return: Response with an 200 OK if everything is okay. 400 if there is no statement input. + If there is an reaction then this will return 200 and the serialized reaction. + This is necessary, because the frontend must only add this element to the overview of reaction and needs the + analysis regarding the mentions and tags by the backend. + todo: Make it possible for single statement + + """ + account: Account = Account.objects.filter(user=request.user).first() + statement: str = request.data.get("input", None) + if not statement: + return Response(status=status.HTTP_400_BAD_REQUEST) + statement: Statement = account.add_statement(statement) + # if there is an reaction given then is must be added to the parent element. + reaction: dict = request.data.get("reaction", None) + if reaction: + parent: Statement = Statement.objects.get(id=reaction.get("to")) + vote: int = 1 if reaction.get("relation") == "support" else 2 + reaction, _ = parent.add_reaction(reaction_statement=statement, vote=vote) + serializer: ReactionSerializer = ReactionSerializer(instance=reaction, many=False) + return Response(status=status.HTTP_200_OK, data=serializer.data) + serializer: StatementSerializer = StatementSerializer(instance=statement, many=False) + return Response(status=status.HTTP_200_OK, data=serializer.data) diff --git a/the_social_network/core/views/authenticationViews.py b/the_social_network/core/views/authenticationViews.py new file mode 100644 index 0000000000000000000000000000000000000000..e74d31879a263744e39213244e264c2775988800 --- /dev/null +++ b/the_social_network/core/views/authenticationViews.py @@ -0,0 +1,154 @@ +import logging + +from django.contrib.auth import login, logout +from django.contrib.auth.models import User +from rest_framework import status +from rest_framework.authentication import TokenAuthentication +from rest_framework.authtoken.models import Token +from rest_framework.exceptions import ValidationError +from rest_framework.permissions import IsAuthenticated +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.views import APIView + +from ..models import Account +from .. import Operations +from ..validation import validate_request_data_for + +logger = logging.getLogger(__name__) + + +class Register(APIView): + """ + This APIView takes care of the registration of new users. + It checks if the new user has entered all fields for registration. + When a new user logs in, he is also directly logged in and receives a token. + """ + + @staticmethod + def validate(request: Request) -> Response: + """ + This method validates the data provided for the new user. + It validates if the username, email and the password is set and they are not empty. + + :param request: The request which should be validated. + :return: 400_BAD_REQUEST if the provided data is sparse or one of the values is empty. + 201_CREATED otherwise. + """ + return validate_request_data_for(Operations.REGISTER, request) + + def post(self, request: Request) -> Response: + """ + This method handles the actual POST-request of the new user. + First the data send is checked for mistakes, missing or empty values. + Afterwards the User is created and logged in. + + :param request: The request of the new user containing all necessary information for an registration. + :return: 400_BAD_REQUEST if the provided data is sparse or one of the values is empty or the user existing. + 201_CREATE if the new user is created (Contains the token in the data-section). + """ + valid: Response = self.validate(request) + if valid.status_code != 201: + return valid + + username: str = valid.data["username"] + user: User = User.objects.filter(username=username).first() + account: Account = Account.objects.create(user=user) + login(request, account.user) + return Response(status=valid.status_code, data={"token": Token.objects.create(user=account.user).__str__()}) + + +class Login(APIView): + """ + This APIView takes care of the login of a requesting users. + It checks if the user has entered all fields for login. + When a user logs in, he receives a token. + """ + + @staticmethod + def validate(request: Request) -> Response: + """ + This method validates the data provided for the requesting user. + It validates if the username and the password is set and they are not empty. + + :param request: The data provided by the requesting user. + :return: 400_BAD_REQUEST if the provided data is sparse or one of the values is empty. + 200_OK otherwise. + """ + return validate_request_data_for(Operations.LOGIN, request) + + def post(self, request): + """ + This method handles the actual POST-request of the requesting user. + First the data send is checked for mistakes, missing or empty values. + Afterwards the User is authenticated and logged in. + + :param request: The request of the user containing all necessary information for an login. + :return: 400_BAD_REQUEST if the provided data is sparse or one of the values is empty or wrong. + 200_OK if the user is authenticated (Contains the token in the data-section). + """ + valid: Response = self.validate(request) + if valid.status_code != 200: + return valid + + username: str = valid.data["username"] + user: User = User.objects.filter(username=username).first() + token, operation_was_create = Token.objects.get_or_create(user=user) + + if not operation_was_create: + # refresh the token if there was a previous token detected + token.delete() + token = Token.objects.create(user=user) + login(request, user) + + return Response(status=valid.status_code, data={"token": token.key.__str__()}) + + +class Logout(APIView): + """ + This APIView takes care of the logout of a requesting users. + It checks if the user has entered all fields for logout. + When a user logs out, his token will be destroyed. + """ + + authentication_classes = [TokenAuthentication] + permission_classes = (IsAuthenticated,) + + @staticmethod + def post(request): + """ + This method handles the actual POST-request of the requesting user. + First the data send is checked for mistakes, missing or empty values. + Afterwards the User is authenticated and logged out. + + :param request: The request of the user containing all necessary information for an logout. + :return: 400_BAD_REQUEST if the provided data is sparse or one of the values is empty or wrong or the user is not logged in. + 200_OK if the user is authenticated (will destroy the users token). + """ + user: User = request.user + token: Token = Token.objects.filter(user=user).first() + if token: + token.delete() + logout(request) + return Response(status=status.HTTP_200_OK) + + +class TokenValidation(APIView): + """ + This view can be used to validate a token of an user. + Therefore it can be used for the frontend to validate if the token has expired e.g. if the user has logged in + on another device. + + """ + authentication_classes = [TokenAuthentication] + permission_classes = (IsAuthenticated,) + + def get(self, request) -> Response: + """ + This method is allways called if the token is valid. + Otherwise the permission_classes will return 401 indication an invalid token. + + :param request: Unused + :return: Status 200 Ok if the token is authorized. + """ + return Response(status=status.HTTP_200_OK) diff --git a/the_social_network/core/views/contentViews.py b/the_social_network/core/views/contentViews.py new file mode 100644 index 0000000000000000000000000000000000000000..dfc9760721e0cc22094c91e49d1ec118490912b2 --- /dev/null +++ b/the_social_network/core/views/contentViews.py @@ -0,0 +1,128 @@ +import logging +# Create your views here. +from typing import Optional, List, Dict + +from django.db.models import QuerySet, Count +from rest_framework import status +from rest_framework.authentication import TokenAuthentication +from rest_framework.permissions import IsAuthenticated +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.views import APIView + +from ..models import Account, Statement, Hashtag, HashtagTagging +from ..serializers.contentSerializers import StatementObservationSerializer, StatementSerializer, TrendingHashtagSerializer + +logger = logging.getLogger(__name__) + + +class ShowStatement(APIView): + """ + This view can be used to get an specific statement and all correlated data by the statements id. + To get information about an statement one must provide an valid token for identification. + """ + authentication_classes = [TokenAuthentication] + permission_classes = (IsAuthenticated,) + + @staticmethod + def get(request: Request, *args, **kwargs): + """ + This method returns all information about an specific statement. + :param request: Not used. + :param args: Not used. + :param kwargs: Additional information to get the id of the requested statement. + :return: + """ + statement: Statement = Statement.objects.filter(id=int(kwargs.get("id"))) + serializer: StatementObservationSerializer = StatementObservationSerializer(instance=statement, many=True) + return Response(status=status.HTTP_200_OK, data=serializer.data) + + +class ShowStatementsWithHashtag(APIView): + """ + This view is representative for the hashtag view. + It can be used to get all statements with containing an specific hashtag. + To get the information one must have an valid token. + """ + authentication_classes = [TokenAuthentication] + permission_classes = (IsAuthenticated,) + + @staticmethod + def get(request: Request): + """ + This method returns all statements containing the given hashtag string. + If there is no data or if there is no hashtag one get 200. + If there is data one get data and 200. + If the request is wrong one get 400. + + :param request: Request with the parameter q, which is the string representation of the hashtag. + :return: 200 if there are statements with the hashtag or if there is no data, 400 if the request is invalid. + """ + query: str = request.query_params.get('q', None) + if not query: + return Response(status=status.HTTP_400_BAD_REQUEST) + hashtag: Optional[Hashtag] = Hashtag.objects.filter(tag=query).first() + if not hashtag: + return Response(status=status.HTTP_200_OK) + statement: List[Statement] = Statement.objects.filter(tagged=hashtag) + serializer: StatementSerializer = StatementSerializer(instance=statement, many=True) + return Response(status=status.HTTP_200_OK, data=serializer.data) + + +class ShowStatementFeed(APIView): + """ + This view is for querying the feed for an calling account. + The feed is generated by the accounts the calling account is following. + To get the feed the calling account must be authenticated. + """ + authentication_classes = [TokenAuthentication] + permission_classes = (IsAuthenticated,) + + @staticmethod + def get(request: Request): + """ + This is for getting the feed. + Todo: Add pagination for infinite scrolling. + :param request: The request containing the token to identify the calling user. + :return: Feed for the calling user based on the actions of those the calling account follows. + """ + account: Account = Account.objects.get(user=request.user) + following: List[Account] = account.get_related_to() + [account] + feed: QuerySet[Statement] = Statement.objects.filter(author__in=following) + serializer: StatementSerializer = StatementSerializer(instance=feed, many=True) + return Response(status=status.HTTP_200_OK, data=serializer.data) + + +class ShowTrendingHashtag(APIView): + """ + This view is for getting the five most trending hashtags. + The calling user must be authenticated. + Also the calling user is not included as an participant of the hashtag, + since those others are for recommendation. + """ + authentication_classes = [TokenAuthentication] + permission_classes = (IsAuthenticated,) + + @staticmethod + def get(request: Request): + """ + This method handles the request for trending hashtags. + Therefore the tagging of hashtags are counted and turned into an trending hashtag representation. + The TrendingHashtagSerializer adds all needed information like the count of uses and other participants. + The calling account is excluded from the participants. + :param request: Request containing the the token for identification. + :return: 200 OK with empty or not empty data section. The data section is empty if there are not hashtags. + """ + hashtags: QuerySet[Dict] = HashtagTagging.objects.values('hashtag') + hashtags_counted: QuerySet[Dict] = hashtags.annotate( + the_count=Count('hashtag') + ).order_by("-the_count") + counted: Dict = {item["hashtag"]: item["the_count"] for item in hashtags_counted} + hashtags: QuerySet[Hashtag] = Hashtag.objects.filter(id__in=counted.keys()) + if not hashtags_counted: + return Response(status=status.HTTP_200_OK, data=[]) + serializer: TrendingHashtagSerializer = TrendingHashtagSerializer( + instance=hashtags, + many=True, + context={"counted": counted, "calling_user": request.user.id}) + return Response(status=status.HTTP_200_OK, data=serializer.data[:3]) diff --git a/the_social_network/core/views/searchViews.py b/the_social_network/core/views/searchViews.py new file mode 100644 index 0000000000000000000000000000000000000000..650f9fb798c029c381f778a256f494d8bc8080b7 --- /dev/null +++ b/the_social_network/core/views/searchViews.py @@ -0,0 +1,57 @@ +# Create your views here. +import logging + +from rest_framework import status +from rest_framework.authentication import TokenAuthentication +from rest_framework.permissions import IsAuthenticated +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.views import APIView + +from ..models import Account, Hashtag +from ..serializers.accountSerializers import AccountSerializer +from ..serializers.contentSerializers import HashtagSerializer + +logger = logging.getLogger(__name__) + +class Search(APIView): + """ + This view is for searching hashtags and accounts. + The search requires an user registration. + """ + authentication_classes = [TokenAuthentication] + permission_classes = (IsAuthenticated,) + + @staticmethod + def get(request: Request, *args, **kwargs) -> Response: + """ + This method searches for matching hashtags and account for a given search query. + Then the results are returned in an corresponding composed dict. + + :param request: The request send by the user. + :param args: Not used. + :param kwargs: Additional arguments which provides the search parameter q. + :return: Composed dict of results in the Response with status code 200, otherwise if no query q is given it will + return an empty Response with status code 400. + The results can also be filtered for single categories. Those categories are: account and hashtag. + """ + query: str = request.query_params.get('q', None) + limit: int = 5 + if not query: + return Response(status=status.HTTP_400_BAD_REQUEST) + result_filter: str = request.query_params.get('filter', None) + + result: dict = {} + + if result_filter == "account" or not result_filter: + accounts: Account = Account.objects.filter(user__username__contains=query)[:limit] + result["accounts"] = AccountSerializer(instance=accounts, many=True).data + + if result_filter == "hashtag" or not result_filter: + hashtags: Hashtag = Hashtag.objects.filter(tag__contains=query)[:limit] + result["hashtags"] = HashtagSerializer(instance=hashtags, many=True).data + + if result: + return Response(data=result, status=status.HTTP_200_OK) + + return Response(status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file diff --git a/the_social_network/manage.py b/the_social_network/manage.py new file mode 100755 index 0000000000000000000000000000000000000000..f7f55f928f7d3ac725a2ead5ea368033dd57a52f --- /dev/null +++ b/the_social_network/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'the_social_network.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/the_social_network/the_social_network/__init__.py b/the_social_network/the_social_network/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/the_social_network/the_social_network/asgi.py b/the_social_network/the_social_network/asgi.py new file mode 100644 index 0000000000000000000000000000000000000000..5fea914abe523144f7d1c154e3a0c3da2990c303 --- /dev/null +++ b/the_social_network/the_social_network/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for the_social_network project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'the_social_network.settings') + +application = get_asgi_application() diff --git a/the_social_network/the_social_network/settings.py b/the_social_network/the_social_network/settings.py new file mode 100644 index 0000000000000000000000000000000000000000..f19f1ca7142d95bd8b298c2155864d72f63a29f7 --- /dev/null +++ b/the_social_network/the_social_network/settings.py @@ -0,0 +1,137 @@ +""" +Django settings for the_social_network project. + +Generated by 'django-admin startproject' using Django 3.2.9. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.2/ref/settings/ +""" +import os +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-hpjug(8bb2&mx*lv-$h!a6ried17*-34or7ngwee*1x#f6kak^' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + +# Application definition + + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'corsheaders', + 'rest_framework', + 'rest_framework.authtoken', + 'core', +] + +MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'the_social_network.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'the_social_network.wsgi.application' + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + +# Password validation +# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +# Internationalization +# https://docs.djangoproject.com/en/3.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +STATIC_URL = '/django_static/' +STATIC_ROOT = os.path.join(BASE_DIR, "django_static/") + +# REST Framework settings +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.TokenAuthentication', + ], +} + +# Folder to store media data +MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/the_social_network/the_social_network/urls.py b/the_social_network/the_social_network/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..1acc893cfeb1de6fb679a7bedcf40ccf3f608500 --- /dev/null +++ b/the_social_network/the_social_network/urls.py @@ -0,0 +1,28 @@ +"""the_social_network URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.conf import settings +from django.conf.urls import url +from django.conf.urls.static import static +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path('admin/', admin.site.urls), + url(r'^authentication/', include('core.urls.authenticationUrls')), + url(r'^accounts/', include('core.urls.accountUrls')), + url(r'^search/', include('core.urls.searchUrls')), + url(r'^contents/', include('core.urls.contentUrls')), + ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/the_social_network/the_social_network/wsgi.py b/the_social_network/the_social_network/wsgi.py new file mode 100644 index 0000000000000000000000000000000000000000..21d6cfa778697bf5ead31733e197c347f0d7e97f --- /dev/null +++ b/the_social_network/the_social_network/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for the_social_network project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'the_social_network.settings') + +application = get_wsgi_application()