Spaces:
Sleeping
Sleeping
Commit
·
bd98c1d
1
Parent(s):
bd52915
Add social authentication and enhance database configuration
Browse files- Integrated social_django for Google OAuth2 authentication.
- Updated database settings to include MySQL alongside SQLite.
- Implemented logout functionality and adjusted URL routing.
- Enhanced author admin display with additional fields.
- Added search hierarchy functionality in views.
- Created a new home page template with user authentication checks.
- Established API endpoint for search functionality.
- Implemented cursor pagination in OpenAlex data scraping.
- Added database routing logic for core app.
- BridgeMentor/routers.py +21 -0
- BridgeMentor/settings.py +34 -3
- BridgeMentor/urls.py +14 -0
- core/admin.py +1 -1
- core/templates/home.html +48 -0
- core/urls.py +7 -0
- core/views.py +27 -1
- populate_user.py +2 -2
- requirements.txt +2 -1
- scrap_openalex.py +21 -12
- sql_test.py +27 -0
BridgeMentor/routers.py
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
class AppRouter:
|
2 |
+
def db_for_read(self, model, **hints):
|
3 |
+
if model._meta.app_label == 'core':
|
4 |
+
return 'sqlite'
|
5 |
+
return 'default'
|
6 |
+
|
7 |
+
def db_for_write(self, model, **hints):
|
8 |
+
if model._meta.app_label == 'core':
|
9 |
+
return 'sqlite'
|
10 |
+
return 'default'
|
11 |
+
|
12 |
+
def allow_relation(self, obj1, obj2, **hints):
|
13 |
+
# Allow relations if both models are in the same DB
|
14 |
+
db_obj1 = self.db_for_read(obj1)
|
15 |
+
db_obj2 = self.db_for_read(obj2)
|
16 |
+
return db_obj1 == db_obj2
|
17 |
+
|
18 |
+
def allow_migrate(self, db, app_label, model_name=None, **hints):
|
19 |
+
if app_label == 'core':
|
20 |
+
return db == 'sqlite'
|
21 |
+
return db == 'default'
|
BridgeMentor/settings.py
CHANGED
@@ -39,6 +39,7 @@ INSTALLED_APPS = [
|
|
39 |
'django.contrib.staticfiles',
|
40 |
'django_filters',
|
41 |
'graphene_django',
|
|
|
42 |
'core',
|
43 |
]
|
44 |
|
@@ -57,15 +58,19 @@ ROOT_URLCONF = 'BridgeMentor.urls'
|
|
57 |
TEMPLATES = [
|
58 |
{
|
59 |
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
60 |
-
'DIRS': [],
|
61 |
'APP_DIRS': True,
|
62 |
'OPTIONS': {
|
63 |
'context_processors': [
|
|
|
|
|
64 |
'django.template.context_processors.debug',
|
65 |
'django.template.context_processors.request',
|
66 |
'django.contrib.auth.context_processors.auth',
|
67 |
'django.contrib.messages.context_processors.messages',
|
|
|
68 |
],
|
|
|
69 |
},
|
70 |
},
|
71 |
]
|
@@ -77,12 +82,31 @@ WSGI_APPLICATION = 'BridgeMentor.wsgi.application'
|
|
77 |
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
78 |
|
79 |
DATABASES = {
|
80 |
-
'
|
81 |
'ENGINE': 'django.db.backends.sqlite3',
|
82 |
'NAME': BASE_DIR / 'db.sqlite3',
|
83 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
84 |
}
|
85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
86 |
|
87 |
# Password validation
|
88 |
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
@@ -128,3 +152,10 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
|
128 |
GRAPHENE = {
|
129 |
"SCHEMA": "core.schema.schema" # Adjust to match your project
|
130 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
'django.contrib.staticfiles',
|
40 |
'django_filters',
|
41 |
'graphene_django',
|
42 |
+
'social_django',
|
43 |
'core',
|
44 |
]
|
45 |
|
|
|
58 |
TEMPLATES = [
|
59 |
{
|
60 |
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
61 |
+
'DIRS': ['core/templates'],
|
62 |
'APP_DIRS': True,
|
63 |
'OPTIONS': {
|
64 |
'context_processors': [
|
65 |
+
'social_django.context_processors.backends',
|
66 |
+
'social_django.context_processors.login_redirect',
|
67 |
'django.template.context_processors.debug',
|
68 |
'django.template.context_processors.request',
|
69 |
'django.contrib.auth.context_processors.auth',
|
70 |
'django.contrib.messages.context_processors.messages',
|
71 |
+
|
72 |
],
|
73 |
+
|
74 |
},
|
75 |
},
|
76 |
]
|
|
|
82 |
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
83 |
|
84 |
DATABASES = {
|
85 |
+
'sqlite': {
|
86 |
'ENGINE': 'django.db.backends.sqlite3',
|
87 |
'NAME': BASE_DIR / 'db.sqlite3',
|
88 |
+
},
|
89 |
+
'default': {
|
90 |
+
'ENGINE': 'django.db.backends.mysql',
|
91 |
+
'NAME': 'cybersan_bridgementor',
|
92 |
+
'USER': 'cybersan_bridgementor',
|
93 |
+
'PASSWORD': 'bridgementor',
|
94 |
+
'HOST': '192.250.235.27', # or your MariaDB host
|
95 |
+
'PORT': '3306', # default MariaDB port
|
96 |
+
'OPTIONS': {
|
97 |
+
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
|
98 |
+
},
|
99 |
+
},
|
100 |
}
|
101 |
|
102 |
+
DATABASE_ROUTERS = ['BridgeMentor.routers.AppRouter']
|
103 |
+
|
104 |
+
AUTHENTICATION_BACKENDS = (
|
105 |
+
'social_core.backends.open_id.OpenIdAuth',
|
106 |
+
'social_core.backends.google.GoogleOAuth2',
|
107 |
+
'social_core.backends.twitter.TwitterOAuth',
|
108 |
+
'django.contrib.auth.backends.ModelBackend',
|
109 |
+
)
|
110 |
|
111 |
# Password validation
|
112 |
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
|
|
152 |
GRAPHENE = {
|
153 |
"SCHEMA": "core.schema.schema" # Adjust to match your project
|
154 |
}
|
155 |
+
|
156 |
+
# SOCIAL_AUTH_REQUIRE_POST = True
|
157 |
+
SOCIAL_AUTH_URL_NAMESPACE = 'social'
|
158 |
+
|
159 |
+
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = '556457523-2d180pkqq6srm7rvg5veidjteqat3mvs.apps.googleusercontent.com'
|
160 |
+
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = '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'
|
161 |
+
LOGIN_REDIRECT_URL = '/'
|
BridgeMentor/urls.py
CHANGED
@@ -16,10 +16,24 @@ Including another URLconf
|
|
16 |
"""
|
17 |
from graphene_django.views import GraphQLView
|
18 |
from django.contrib import admin
|
|
|
19 |
from django.urls import path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
|
21 |
urlpatterns = [
|
22 |
path('admin/', admin.site.urls),
|
23 |
path("graphql/", GraphQLView.as_view(graphiql=True)),
|
|
|
|
|
|
|
|
|
|
|
24 |
|
25 |
]
|
|
|
16 |
"""
|
17 |
from graphene_django.views import GraphQLView
|
18 |
from django.contrib import admin
|
19 |
+
from django.contrib.auth import logout
|
20 |
from django.urls import path
|
21 |
+
from django.urls import include
|
22 |
+
from django.shortcuts import redirect
|
23 |
+
|
24 |
+
|
25 |
+
def logout_view(request):
|
26 |
+
logout(request)
|
27 |
+
return redirect('/')
|
28 |
+
|
29 |
|
30 |
urlpatterns = [
|
31 |
path('admin/', admin.site.urls),
|
32 |
path("graphql/", GraphQLView.as_view(graphiql=True)),
|
33 |
+
path("", include('social_django.urls', namespace="social")),
|
34 |
+
path('', include('core.urls')), # routes '' to your home app
|
35 |
+
# routes '' to your home app
|
36 |
+
path('logout', logout_view, name='logout'),
|
37 |
+
|
38 |
|
39 |
]
|
core/admin.py
CHANGED
@@ -20,7 +20,7 @@ class InstitutionAdmin(admin.ModelAdmin):
|
|
20 |
@admin.register(Author)
|
21 |
class AuthorAdmin(admin.ModelAdmin):
|
22 |
list_display = ('id', 'name', 'orcid', 'h_index',
|
23 |
-
'i10_index', 'cited_by_count', 'works_count')
|
24 |
search_fields = ('name', 'orcid')
|
25 |
list_filter = ('h_index',)
|
26 |
|
|
|
20 |
@admin.register(Author)
|
21 |
class AuthorAdmin(admin.ModelAdmin):
|
22 |
list_display = ('id', 'name', 'orcid', 'h_index',
|
23 |
+
'i10_index', 'cited_by_count', 'works_count', 'updated_at', 'created_at')
|
24 |
search_fields = ('name', 'orcid')
|
25 |
list_filter = ('h_index',)
|
26 |
|
core/templates/home.html
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!-- home/templates/home/home.html -->
|
2 |
+
<!DOCTYPE html>
|
3 |
+
<html>
|
4 |
+
<head>
|
5 |
+
<title>My Home Page</title>
|
6 |
+
</head>
|
7 |
+
|
8 |
+
<body>
|
9 |
+
{% if user.is_authenticated %}
|
10 |
+
<span>Welcome, {{ user.first_name }}!</span> <a href="{% url 'logout' %}">Logout</a>
|
11 |
+
{% else %}
|
12 |
+
<a href="{% url 'social:begin' 'google-oauth2' %}">Login With Google</a>
|
13 |
+
{% endif %}
|
14 |
+
|
15 |
+
<h2>Search Domains</h2>
|
16 |
+
<input type="text" id="search-input" placeholder="Type something..." autocomplete="off" />
|
17 |
+
<ul id="results-list"></ul>
|
18 |
+
|
19 |
+
<script>
|
20 |
+
const input = document.getElementById('search-input')
|
21 |
+
const resultsList = document.getElementById('results-list')
|
22 |
+
|
23 |
+
input.addEventListener('input', async () => {
|
24 |
+
const query = input.value.trim()
|
25 |
+
if (!query) {
|
26 |
+
resultsList.innerHTML = ''
|
27 |
+
return
|
28 |
+
}
|
29 |
+
|
30 |
+
const res = await fetch(`/api/search/?q=${encodeURIComponent(query)}`)
|
31 |
+
const data = await res.json()
|
32 |
+
|
33 |
+
resultsList.innerHTML = ''
|
34 |
+
data.results.forEach((item) => {
|
35 |
+
const li = document.createElement('li')
|
36 |
+
|
37 |
+
const link = document.createElement('a')
|
38 |
+
link.href = `/${item.type.toLowerCase()}/${item.id}/`
|
39 |
+
link.textContent = item.text
|
40 |
+
link.style.textDecoration = 'none'
|
41 |
+
link.style.color = 'black'
|
42 |
+
li.appendChild(link)
|
43 |
+
resultsList.appendChild(li)
|
44 |
+
})
|
45 |
+
})
|
46 |
+
</script>
|
47 |
+
</body>
|
48 |
+
</html>
|
core/urls.py
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from django.urls import path
|
2 |
+
from .views import home_page, search_hierarchy
|
3 |
+
|
4 |
+
urlpatterns = [
|
5 |
+
path('', home_page, name='home'),
|
6 |
+
path("api/search/", search_hierarchy, name="search-hierarchy"),
|
7 |
+
]
|
core/views.py
CHANGED
@@ -1,3 +1,29 @@
|
|
|
|
|
|
1 |
from django.shortcuts import render
|
2 |
|
3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from .models import Domain, Field, Subfield, Topic
|
2 |
+
from django.http import JsonResponse
|
3 |
from django.shortcuts import render
|
4 |
|
5 |
+
|
6 |
+
def home_page(request):
|
7 |
+
return render(request, 'home.html')
|
8 |
+
|
9 |
+
|
10 |
+
def search_hierarchy(request):
|
11 |
+
q = request.GET.get("q", "").strip()
|
12 |
+
if not q:
|
13 |
+
return JsonResponse({"results": []})
|
14 |
+
|
15 |
+
results = []
|
16 |
+
|
17 |
+
results += [{"id": d.id, "text": d.name, "type": "Domain"}
|
18 |
+
for d in Domain.objects.filter(name__icontains=q)]
|
19 |
+
|
20 |
+
results += [{"id": f.id, "text": f"{f.domain.name} > {f.name}", "type": "Field"}
|
21 |
+
for f in Field.objects.select_related("domain").filter(name__icontains=q)]
|
22 |
+
|
23 |
+
results += [{"id": s.id, "text": f"{s.field.domain.name} > {s.field.name} > {s.name}", "type": "Subfield"}
|
24 |
+
for s in Subfield.objects.select_related("field__domain").filter(name__icontains=q)]
|
25 |
+
|
26 |
+
results += [{"id": t.id, "text": f"{t.subfield.field.domain.name} > {t.subfield.field.name} > {t.subfield.name} > {t.name}", "type": "Topic"}
|
27 |
+
for t in Topic.objects.select_related("subfield__field__domain").filter(name__icontains=q)]
|
28 |
+
|
29 |
+
return JsonResponse({"results": sorted(results, key=lambda x: x["text"].lower())[:10]})
|
populate_user.py
CHANGED
@@ -20,7 +20,7 @@ def add_author(user_info, updated_date):
|
|
20 |
author, _ = Author.objects.update_or_create(
|
21 |
id=author_id,
|
22 |
defaults={
|
23 |
-
"name": max(user_info['display_name_alternatives'], key=lambda name: len(name)),
|
24 |
"orcid": parse_id_from_url(user_info["orcid"]) if user_info.get("orcid") else None,
|
25 |
"h_index": user_info["summary_stats"]["h_index"],
|
26 |
"i10_index": user_info["summary_stats"]["i10_index"],
|
@@ -42,7 +42,7 @@ def add_institution(inst_data):
|
|
42 |
"name": inst_data["display_name"],
|
43 |
"ror_id": parse_id_from_url(inst_data["ror"]),
|
44 |
"country_code": 'N/A' or inst_data.get("country_code"),
|
45 |
-
"institution_type": inst_data
|
46 |
}
|
47 |
)
|
48 |
return inst
|
|
|
20 |
author, _ = Author.objects.update_or_create(
|
21 |
id=author_id,
|
22 |
defaults={
|
23 |
+
"name": max(user_info['display_name_alternatives'] + [user_info['display_name']], key=lambda name: len(name)),
|
24 |
"orcid": parse_id_from_url(user_info["orcid"]) if user_info.get("orcid") else None,
|
25 |
"h_index": user_info["summary_stats"]["h_index"],
|
26 |
"i10_index": user_info["summary_stats"]["i10_index"],
|
|
|
42 |
"name": inst_data["display_name"],
|
43 |
"ror_id": parse_id_from_url(inst_data["ror"]),
|
44 |
"country_code": 'N/A' or inst_data.get("country_code"),
|
45 |
+
"institution_type": 'N/A' or inst_data.get("institution_type")
|
46 |
}
|
47 |
)
|
48 |
return inst
|
requirements.txt
CHANGED
@@ -1,3 +1,4 @@
|
|
1 |
django==4.2.20
|
2 |
graphene-django
|
3 |
-
django-filter
|
|
|
|
1 |
django==4.2.20
|
2 |
graphene-django
|
3 |
+
django-filter
|
4 |
+
mysqlclient?
|
scrap_openalex.py
CHANGED
@@ -7,32 +7,41 @@ from time import sleep
|
|
7 |
BASE_URL = "https://api.openalex.org/authors"
|
8 |
FILTER = "last_known_institutions.country_code:NO,x_concepts.id:C41008148"
|
9 |
PER_PAGE = 200
|
10 |
-
TOTAL_RESULTS = 86500
|
11 |
-
TOTAL_PAGES = (TOTAL_RESULTS + PER_PAGE - 1) // PER_PAGE # Ceiling division
|
12 |
|
13 |
-
# Output directory
|
14 |
OUTPUT_DIR = "C41008148_authors"
|
15 |
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
16 |
|
17 |
-
#
|
18 |
-
|
19 |
-
|
|
|
|
|
|
|
20 |
try:
|
21 |
-
print(f"Fetching page {
|
22 |
response = requests.get(url)
|
23 |
response.raise_for_status()
|
24 |
data = response.json()
|
25 |
|
26 |
-
filename = os.path.join(OUTPUT_DIR, f"{
|
27 |
-
# skip if exists
|
28 |
if os.path.exists(filename):
|
29 |
print(f"File {filename} already exists, skipping...")
|
|
|
|
|
30 |
continue
|
|
|
31 |
with open(filename, 'w', encoding='utf-8') as f:
|
32 |
json.dump(data, f, ensure_ascii=False, indent=2)
|
33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
except Exception as e:
|
35 |
-
print(f"Error on page {
|
36 |
break
|
37 |
|
38 |
-
print("Download complete.")
|
|
|
7 |
BASE_URL = "https://api.openalex.org/authors"
|
8 |
FILTER = "last_known_institutions.country_code:NO,x_concepts.id:C41008148"
|
9 |
PER_PAGE = 200
|
|
|
|
|
10 |
|
|
|
11 |
OUTPUT_DIR = "C41008148_authors"
|
12 |
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
13 |
|
14 |
+
# Initialize cursor
|
15 |
+
cursor = "*"
|
16 |
+
page_count = 1 # Track page numbers for saving files
|
17 |
+
|
18 |
+
while cursor:
|
19 |
+
url = f"{BASE_URL}?filter={FILTER}&per-page={PER_PAGE}&cursor={cursor}"
|
20 |
try:
|
21 |
+
print(f"Fetching page {page_count} with cursor...")
|
22 |
response = requests.get(url)
|
23 |
response.raise_for_status()
|
24 |
data = response.json()
|
25 |
|
26 |
+
filename = os.path.join(OUTPUT_DIR, f"{page_count:010}.json")
|
|
|
27 |
if os.path.exists(filename):
|
28 |
print(f"File {filename} already exists, skipping...")
|
29 |
+
cursor = data.get("meta", {}).get("next_cursor")
|
30 |
+
page_count += 1
|
31 |
continue
|
32 |
+
|
33 |
with open(filename, 'w', encoding='utf-8') as f:
|
34 |
json.dump(data, f, ensure_ascii=False, indent=2)
|
35 |
+
|
36 |
+
cursor = data.get("meta", {}).get("next_cursor")
|
37 |
+
if not cursor:
|
38 |
+
print("No more results.")
|
39 |
+
break
|
40 |
+
|
41 |
+
page_count += 1
|
42 |
+
sleep(1) # Rate-limiting
|
43 |
except Exception as e:
|
44 |
+
print(f"Error on page {page_count}: {e}")
|
45 |
break
|
46 |
|
47 |
+
print("Download complete using cursor pagination.")
|
sql_test.py
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pymysql
|
2 |
+
|
3 |
+
try:
|
4 |
+
print("Connecting to MariaDB...")
|
5 |
+
conn = pymysql.connect(
|
6 |
+
host="192.250.235.27", # or your DB host, e.g. "127.0.0.1"
|
7 |
+
port=3306,
|
8 |
+
user="cybersan_bridgementor", # replace with your MySQL username
|
9 |
+
password="bridgementor", # replace with your MySQL password
|
10 |
+
database="cybersan_bridgementor", # replace with your database name
|
11 |
+
)
|
12 |
+
print("Connected successfully.")
|
13 |
+
|
14 |
+
cursor = conn.cursor()
|
15 |
+
print("Fetching table list...")
|
16 |
+
cursor.execute("SHOW TABLES")
|
17 |
+
|
18 |
+
print("Tables in the database:")
|
19 |
+
for (table_name,) in cursor.fetchall():
|
20 |
+
print(f"- {table_name}")
|
21 |
+
|
22 |
+
cursor.close()
|
23 |
+
conn.close()
|
24 |
+
print("Connection closed.")
|
25 |
+
|
26 |
+
except pymysql.MySQLError as e:
|
27 |
+
print(f"MariaDB connection error: {e}")
|