Disclaimer

Phonotheque is a web app built using the framework Django and other technologies. The front end design is loosely based on TemplateMo's beautiful and free Catalog-Z HTML templateas well as on Bootstrap 5. Please note, the project is still under development and although all effort has been made to deliver an aesthetically pleasing, functional and reliable product there is no guarantee this will be the case. If anything does not look/work quite right I would love to hear from you.

Introduction

Phonotheque is a basic student project created using the Python-based framework Django.
It is intended to combine social media, online forum, data scraping and storage features. All these are implemented at a very rudimentary level and Phonotheque should not be seen as anything but an experiment.
At the same time, the potential for expanding it by adding new services and components, improving its design, security and functionalities is virtually unlimited.
In brief: After successful registration and logging in, the users are given the opportunity to extend their profile by entering additional data.
They can then add their favourite albums to the database. To achieve this the website attempts to fetch information from Wikipedia. If successful the album data will be stored in the DB and will be linked to the relevant user (many-to-many relationship using intermediary model).
Artists' names (one-to-many relationship) will also be retrieved and stored.
Logged in users will be able to view the profiles of other users, their shared albums and, if they wish, add someone else's shared album to their own favourites.
They can also post comments related to any album from the DB (one-to-many relationship).
Inappropriate content can be disabled or deleted by the admins, who also have privileges to disable or delete profiles, either by accessing the admin panel or via the website's own GUI. The superusers will have full privileges to manipulate the DB.

Entity Relationship Diagram

ERD-phonotheque

Accounts App

Phonotheque uses the default Django User Model. There are different schools of thought on what is the best way to customize the user model (creating a custom User model via AbstractBaseUser if the intention is to change the authentication procedure or via AbstractUser if additional info about the user needs to be collected/stored being popular choices).
I decided instead to stick to the other recommended strategy of extending the default Django User model with a One-to-One relationship to a Profile model as I am perfectly happy with the Django User defaults - everything works out of the box, is compatible with all third party modules while at the same time the Profile model provides extendability and flexibility.

Registration

When an anonymous user wishes to create an account they are presented with the UserRegistrationForm. This form takes care of the front end formatting and apart from the default Django validation performs validation of the first name and second name entries – both should be with a length of between 2 and 35 characters and must be satisfying the condition VALID_NAME_REGEX = r"^([ \u00c0-\u01ffa-zA-Z'\-])+$" which is a complicated way to say that our user can have a name like Jérémie O'Conor-IVANOVäüïöëÿâçéèêîïôčšžñáéíóúübut cannot be named R2D2, number1, st*r or f### #ff.
Once an user is created with the Django-default fields, a corresponding entry will be created in the Profiletable as well (with null values for photo_URL, date_of_birth, gender, description) by just linking (via the Primary key) the newly created user to their profile.
In other words, since in our Profile model we have the corresponding user as a primary key:
class Profile(models.Model):
  user = models.OneToOneField
(settings.AUTH_USER_MODEL,
  on_delete=models.CASCADE,
  primary_key=True)

in register_user_create_profileview we just create a profile with relevant pk:
new_profile = Profile.objects.create(user=new_user)

View other users' profiles

Logged in users can view profiles of other registered user. It is debatable whether this functionality is better suited for accounts app or the main app, but I concluded this was the more appropriate place for it. The relevant DetailView is named ProfileDetailView and by using LoginRequiredMixinwe make sure only logged-in users can view the profile info of others. By overriding def get_context_data(self, **kwargs)method data for the searched_user is added to the context variable and can then be passed to the relevant template. The logged-in user's profile will be listed first in turquoise background, so that it could be distinguishable from the rest.)

Profile Editing

The profile is editable (names and email can be changed, username cannot) and additional data can be added if the user wishes to do so – gender (selecting from pre-defined options), description, date of birth (must be in range between today and 1920) and photo (the development version was based on uploading actual image files but deploying it with media files seemed a bit too much hassle, e.g. submitting credit card details for verification, therefore the live version expects an URL for a profile photo instead).
Essentially, at this point the newly created user can actually create a meaningful profile as upto this point their profile data contains nothing (apart from reference to the relevant user entry).
The profile editing functionality is accessible from the Profileslink in the main menu. Clicking it redirects to a gallery-style list of all non-staff users. The logged-in user's profile will be displayed first, with turquoise background and with 'view', 'edit' and 'delete' buttons.
The edit_user_and_profile (name could have been more appropriate, I guess…) presents the user with one visible form to fill which actually consists of 2 model forms - user_form = UserEditForm and profile_form = ProfileEditForm. By using the @login_required decorator the view makes sure only the logged-in user can access this page. At the same time the verification if user_instance != request.user and not request.user.is_staff:ensures anyone who is not the logged-in user or isn't member of staff cannot access the page even if they try to manipulate the url - they will be redirected to the profiles-list page and a message "Yo! Are you trying to edit someone else's profile? Tut-tut..." will be displayed.
If the validation is passed successfully , the verified data is being saved in the relevant columns in the Userand Profiletables.
The user is then redirected to their own profile details page where they can view the updated data; a confirmation message is being displayed too.
Staff users can edit regular users' profiles and have an additional button appearing above the currently viewed user's data - if the users is if request.user.is_staff:they can switch between DE-activating or RE-activating the relevant profile with user_to_edit.is_active = not user_to_edit.is_active (This functionality is implemented in a dedicated deactivate_or_reactivate_userview). Deactivated users will no longer be able to Sign In to the system and their profiles will be visible to staff (displayed with reddish background) but will remain invisible to regular users.

Profile Deletion

Profile deletion functionality is being accessed in pretty much the same way (via Profileslink). Logged-in user's own profile (first in list and with turquoise background) has a Deletebutton displayed. If they click it they will view their profile data and be asked if they wanted to proceed with the deletion.
Staff users with the appropriate permissions have the ability to not only edit but also delete regular users' profiles. (A two-category separation of duties applies also to staff permissions to edit or edit AND delete comments - see the Admin section below.)
Again, if an unauthorised user attempts to delete another user's profile by messing up with the url, they will be prevented from doing so and will get a message "Ooopsy! User {instance.username} won't be happy if you delete their account. Luckily you can't do it.)"
If authorised the user will see the UserAndProfileDeleteFormModelForm which will display relevant data about the profile (username and date joined amongst others) and ask if the user is sure they wish to proceed with the deletion.

Logging In

Logging in functionality relies on the default Django CBV LoginView. By overwriting its def __init__(self, *args, **kwargs)method we ensure that the login form is displayed with the required styling – this approach seems more straightforward than creating a custom AuthenticationForm. The success URL is the logged in user’s dashboard (will be explained later).
Once logged in the Register button in the main manu will be replaced with a Profile button where users will be able to edit their profiles.

Logging Out

Logging out also relies on the default LogoutView. Once logged out, the user will be redirected to the index page by using next_page = 'index_page'while overriding its def dispatch(self, request, *args, **kwargs) method will add appropriate messages (“You have successfully logged out from your account.” and “You can either close this tab or Sign In again”) to be displayed as well

Password change

Logged in users can change their passwords by inheriting the PasswordChangeView CBV from django.contrib.auth.views. Again, the relevant form is displayed with the required styling by overwriting its def __init__(self, *args, **kwargs)method.

Main App

The main app allows users to share their favourite albums as well as to discuss albums already shared on the platform with other users. Obviously, an album is a very specific object (not only in the programming sense of the word) – it is not comparable to sharing someone’s personal experiences or ideas on a blog site or even listing items for sale on an online shop. A music album is an unique item – it has properties which remain constant for everyone, anywhere and anytime - its cover image, date of issue or musicians involved won't change. We need to somehow ensure our website can identify the correct object>and make it available for sharing, liking or whatever else is required.
Therefore, for this scenario to be meaningful it is essential to gather our data from a reliable source and luckily Wikipedia (God bless everyone involved in the project!) provide great API which interacts perfectly with Django/Python.
Basically, the general idea is to retrieve album data (I won’t call our basic searches ‘web scraping’ but to some degree this is what we are doing here.) from Wikipedia’s API which has been imported in the project (alongside some other modules which seem to have mutual dependencies).
Once obtained, the album data is being displayed and if the user wishes they can add it to their personal Collection albums (see ERD for clarification).
By doing this they also create a record for the Album in the DB as well as for the Artist but only if these are not already present there.
Then can then view their favourite albums in their personal dashboard and for each album they can view additional info like comments from users and which other users, if any, have liked this album (displayed in chronological order).
As a side note, I would say this particular part of the application provides significant potential and incentive to continue working on it as there is a wide range of possibilities to add new features – for example IM system, tagging system, automatic recommendations based on custom criteria, recommendations from other users, creation of interest groups, ‘friendship’ FB-style…

Home Page/Index

The index page is accessible for both anonymous and logged-in users. The difference is that the former will see a short message explaining the raison d'etre of this web page. Logged-in users presumably will not need this.
The index page functionality is based on Django’s default ListView. It implements pagination and the template will display a gallery-style list of all albums shared on the platform (ordered chronologically, newest first) as well as the number of users who have liked each of them.

Album Details

AlbumDetailView(views.DetailView)is also visible to anonymous and logged-in users. Anonymous users can view the stored album information and click a link to the full Wikipedia article (opens in new tab). However, logged-in users will be able to:

  • delete the album from their collection if it is present there
  • add the album to their collection if it is not present there
  • view comments posted by users
  • post a comment
  • see which users have liked the same album
  • access the profile pages of these users by being redirected to the relevant URL

To achieve all that the view performs a series of queries which are then added to the context (with get_context_data(self, **kwargs)) alongside the Comment form and sent to the template. The template then has access to variables like others_who_liked_it, album_to_unlike_pk, liked_by_current_user.

Artist Album List

The artist name will be clickable in the album details template and will redirect to another ListViewwhose template will display a page which looks visually very similar to the index one but with albums related to the selected artist only. This is being achieved by overriding the methods:
def get_queryset(self): # get queryset/albums_list for this specific artist
return super(ArtistDiscographyView, self).get_queryset() \
.filter(artist_id=self.kwargs['pk'])

def get_context_data(self, *, object_list=None, **kwargs):
context = super(ArtistDiscographyView, self).get_context_data()
artist = Artist.objects.get(pk=self.kwargs['pk'])
context['artist_name'] = artist.name
return context
Since the Album table only stores artist_id as foreign key we need to get the name of the artist and add it to the context.

Comments

The class CommentCreateViewinherits from views.CreateViewand PermissionRequiredMixin, i.e. only logged-in users will be able to view comments and post new ones. If an user crates a new comment via its related ModelFormit will be saved in the DB by creating record of its text content as well as the user and album id’s it is related to via its FK.
def form_valid(self, form):   comment = form.save(commit=False)   comment.user = self.request.user   comment.album = Album.objects.get(wiki_id=self.kwargs['album_wiki_id'])   return super().form_valid(form) The view will then redirect again to the same album info page where the user’s newly published comment will be visible at the top as comments are displayed by their creation time, most recent first.

Dashboard

Logged-in users have access to their personalized dashboard. Actually this is the page they are being redirected to as soon as they are logged in.
It lists, again gallery-style and with pagination (4 items on a page) their favourite albums while the two forms below initiate the Wikipedia search - these are actually are the bottom of the main logic of the webpage – titled ‘Search for an album by artist and title…’ (uses forms.Form) and ‘... or paste album's Wikipedia link below’.
They both redirect to the relevant URLs and, subsequently, to either find_album_by_title_and_artist(request)or find_album_by_url(request)
Again, this section of the website could be extended by adding all sorts of functionalities - any sort of data a proper music lover might get from or about like-minded people could be displayed there.

Retrieving the data from Wikipedia

Each of these views calls a separate function which does the Wikipedia search for us.
1) def get_wiki_info_by_album_name(search_term)will initially retrieve a wikipedia.page objectfrom the top result from Wikipeadia’s API with the search_term argument (please note: the search term provided by find_album_by_title_and_artist(request)will initially be only the album name only, if unsuccessful it will search with artist name added too.)
If the string 'album' is not present in the summary of the Wikipedia article returned to us it will retrieve the first 8 results, will look for the string ‘album’ in these as well and if again it finds nothing will return None, None (for album and artist) to the view, the dashboard page will reload with an appropriate message. If successful though it will send the wikipadia.page object to a function called assign_values(page_object).

2) def get_wiki_info_from_url(album_url)works in a similar and maybe not the optimal manner - there might be a more intelligent method to get wikipedia info from a wikipedia link rather than obtain a string from the link itself and then use it to search the website. It will strip everything from the link provided apart from the actual article name (https://en.wikipedia.org/wiki/Ride_the_Lightning becomes just Ride_the_Lightning), will replace most of the undesired escaped characters (Ride_the_Lightning becomes Ride the Lightning) and will invoke the same assign_values(page_object) function with the wikipadia.page object if found, if not it will return None, None (for album and artist) to the view.
The assign_values(page_object)function will create a dictionary with all album data contained in the page_object, removing everything contained in round brackets from article names (Animals (Pink Floyd album) becomes just Animals), including an album cover URL.
The artist name will be retrieved from a particular section in the Wikipedia’s article html – each Wikipedia album page has a side section titled something like ‘Studio album by Massive Attack’. By searching the raw html with if 'album</a> by'in raw_html and with some slicing, splitting, cutting and torturing we can hopefully get the artist name and return it to the relevant function alongside the album data.
Then the above-mentioned functions will return themselves the wiki_info, artist to the corresponding views.

Saving the data from Wikipedia

As mentioned before the data from Wikipedia will be returned to either find_album_by_title_and_artist(request) viewor find_album_by_url(request) view. The first one basically prepares the search term based on the user input and invokes the above-mentioned function with album_wiki_info, artist_name = get_wiki_info_by_album_name(search_term).
If None is returned the view will redirect back to dashboard with the message We couldn't find an album called {album_name} by {searched_artist}.
If successful it will send the data to the template called album_found.htmland, crucially, will ‘attach’ the retrieved data to the session- request.session['data'] = artist_name, album_wiki_infoso that it could be used/saved later if the user confirms in album_found.htmlthat they wish to save this album. The find_album_by_url(request)view works in a very similar way.
If the user confirms the displayed album is indeed what they were looking for, they will be redirected to save_artist_album_data(request). Then we follow the following consequence of steps to save the data:
1) Unpack the additional data we stored in the session with artist_name, album_wiki_info = request.session['data']
2) Check if artist already in DB, if not create record (we will need that later if we have need to save the album in our DB).
3) We try to get the album with this wiki_id from DB.
4) If it is there (no exception is raised) we check if this album is already in users collection and if yes redirect to dashboard again with the message The Album {album.title} by {album.artist} is already in your favourites'.
5) If getting the album from the DB fails with ObjectDoesNotExist exception we will create a new Album object adding the artist_object as FK too.
6) Once we make sure artist name is stored in DB we get it in case we need it as a FK.
7) Then we can it to the users own collection with Collection.objects.create(user=user, album=album) and display message 'The album {album.title} by {album.artist} has been saved in your favourites'

Admin Panel/Functionalities

The permissions system of Phonotheque is based on 3 staff groups with different level of authorisation to manage the DB. Obviously, the Super Users group will have full add, delete, change, view privileges.
A group of lower level moderators can de-activate comments and users (if for example an inappropriate content has been detected) and another group will be able to delete these comments and users if required.

There is also a GUI covering the Comment model where this de-activation/deletion can be made.
If an user logs in as moderator they can perform the aforementioned activities and unlike regular users they will be able to see inactive comments as well.
In other words users will be able to view only active comments unless their permission level is_staff is True – they will then be able to view the de-activated ones too.